annotate vext.sml @ 1708:975dee07ff5c vext

Update Vext
author Chris Cannam
date Wed, 07 Jun 2017 09:56:57 +0100
parents d60b30ea9b80
children e64981b88943
rev   line source
Chris@1706 1 (* This file is automatically generated from the individual
Chris@1706 2 source files in the Vext repository. *)
Chris@1706 3
Chris@1706 4 (*
Chris@1706 5 Vext
Chris@1706 6
Chris@1706 7 A simple manager for third-party source code dependencies
Chris@1706 8
Chris@1706 9 Copyright 2017 Chris Cannam.
Chris@1706 10
Chris@1706 11 Permission is hereby granted, free of charge, to any person
Chris@1706 12 obtaining a copy of this software and associated documentation
Chris@1706 13 files (the "Software"), to deal in the Software without
Chris@1706 14 restriction, including without limitation the rights to use, copy,
Chris@1706 15 modify, merge, publish, distribute, sublicense, and/or sell copies
Chris@1706 16 of the Software, and to permit persons to whom the Software is
Chris@1706 17 furnished to do so, subject to the following conditions:
Chris@1706 18
Chris@1706 19 The above copyright notice and this permission notice shall be
Chris@1706 20 included in all copies or substantial portions of the Software.
Chris@1706 21
Chris@1706 22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
Chris@1706 23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
Chris@1706 24 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Chris@1706 25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
Chris@1706 26 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
Chris@1706 27 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
Chris@1706 28 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Chris@1706 29
Chris@1706 30 Except as contained in this notice, the names of Chris Cannam and
Chris@1706 31 Particular Programs Ltd shall not be used in advertising or
Chris@1706 32 otherwise to promote the sale, use or other dealings in this
Chris@1706 33 Software without prior written authorization.
Chris@1706 34 *)
Chris@1706 35
Chris@1706 36 val vext_version = "0.0.1"
Chris@1706 37
Chris@1706 38
Chris@1706 39 datatype vcs =
Chris@1706 40 HG |
Chris@1706 41 GIT
Chris@1706 42
Chris@1706 43 datatype source =
Chris@1706 44 URL of string |
Chris@1706 45 PROVIDER of {
Chris@1706 46 service : string,
Chris@1706 47 owner : string option,
Chris@1706 48 repo : string option
Chris@1706 49 }
Chris@1706 50
Chris@1706 51 datatype pin =
Chris@1706 52 UNPINNED |
Chris@1706 53 PINNED of string
Chris@1706 54
Chris@1706 55 datatype libstate =
Chris@1706 56 ABSENT |
Chris@1706 57 CORRECT |
Chris@1706 58 SUPERSEDED |
Chris@1706 59 WRONG
Chris@1706 60
Chris@1706 61 datatype localstate =
Chris@1706 62 MODIFIED |
Chris@1706 63 UNMODIFIED
Chris@1706 64
Chris@1706 65 datatype branch =
Chris@1706 66 BRANCH of string |
Chris@1706 67 DEFAULT_BRANCH
Chris@1706 68
Chris@1706 69 (* If we can recover from an error, for example by reporting failure
Chris@1706 70 for this one thing and going on to the next thing, then the error
Chris@1706 71 should usually be returned through a result type rather than an
Chris@1706 72 exception. *)
Chris@1706 73
Chris@1706 74 datatype 'a result =
Chris@1706 75 OK of 'a |
Chris@1706 76 ERROR of string
Chris@1706 77
Chris@1706 78 type libname = string
Chris@1706 79
Chris@1706 80 type id_or_tag = string
Chris@1706 81
Chris@1706 82 type libspec = {
Chris@1706 83 libname : libname,
Chris@1706 84 vcs : vcs,
Chris@1706 85 source : source,
Chris@1706 86 branch : branch,
Chris@1706 87 pin : pin
Chris@1706 88 }
Chris@1706 89
Chris@1706 90 type lock = {
Chris@1706 91 libname : libname,
Chris@1706 92 id_or_tag : id_or_tag
Chris@1706 93 }
Chris@1706 94
Chris@1706 95 type remote_spec = {
Chris@1706 96 anon : string option,
Chris@1706 97 auth : string option
Chris@1706 98 }
Chris@1706 99
Chris@1706 100 type provider = {
Chris@1706 101 service : string,
Chris@1706 102 supports : vcs list,
Chris@1706 103 remote_spec : remote_spec
Chris@1706 104 }
Chris@1706 105
Chris@1706 106 type account = {
Chris@1706 107 service : string,
Chris@1706 108 login : string
Chris@1706 109 }
Chris@1706 110
Chris@1706 111 type context = {
Chris@1706 112 rootpath : string,
Chris@1706 113 extdir : string,
Chris@1706 114 providers : provider list,
Chris@1706 115 accounts : account list
Chris@1706 116 }
Chris@1706 117
Chris@1706 118 type userconfig = {
Chris@1706 119 providers : provider list,
Chris@1706 120 accounts : account list
Chris@1706 121 }
Chris@1706 122
Chris@1706 123 type project = {
Chris@1706 124 context : context,
Chris@1706 125 libs : libspec list
Chris@1706 126 }
Chris@1706 127
Chris@1706 128 structure VextFilenames = struct
Chris@1706 129 val project_file = "vext-project.json"
Chris@1706 130 val project_lock_file = "vext-lock.json"
Chris@1706 131 val user_config_file = ".vext.json"
Chris@1706 132 end
Chris@1706 133
Chris@1706 134 signature VCS_CONTROL = sig
Chris@1706 135
Chris@1706 136 (** Test whether the library is present locally at all *)
Chris@1706 137 val exists : context -> libname -> bool result
Chris@1706 138
Chris@1706 139 (** Return the id (hash) of the current revision for the library *)
Chris@1706 140 val id_of : context -> libname -> id_or_tag result
Chris@1706 141
Chris@1706 142 (** Test whether the library is at the given id *)
Chris@1706 143 val is_at : context -> libname * id_or_tag -> bool result
Chris@1706 144
Chris@1706 145 (** Test whether the library is on the given branch, i.e. is at
Chris@1706 146 the branch tip or an ancestor of it *)
Chris@1706 147 val is_on_branch : context -> libname * branch -> bool result
Chris@1706 148
Chris@1706 149 (** Test whether the library is at the newest revision for the
Chris@1706 150 given branch. False may indicate that the branch has advanced
Chris@1706 151 or that the library is not on the branch at all. This function
Chris@1706 152 may use the network to check for new revisions *)
Chris@1706 153 val is_newest : context -> libname * branch -> bool result
Chris@1706 154
Chris@1706 155 (** Test whether the library is at the newest revision available
Chris@1706 156 locally for the given branch. False may indicate that the
Chris@1706 157 branch has advanced or that the library is not on the branch
Chris@1706 158 at all. This function must not use the network *)
Chris@1706 159 val is_newest_locally : context -> libname * branch -> bool result
Chris@1706 160
Chris@1706 161 (** Test whether the library has been modified in the local
Chris@1706 162 working copy *)
Chris@1706 163 val is_modified_locally : context -> libname -> bool result
Chris@1706 164
Chris@1706 165 (** Check out, i.e. clone a fresh copy of, the repo for the given
Chris@1706 166 library on the given branch *)
Chris@1706 167 val checkout : context -> libname * source * branch -> unit result
Chris@1706 168
Chris@1706 169 (** Update the library to the given branch tip *)
Chris@1706 170 val update : context -> libname * branch -> id_or_tag result
Chris@1706 171
Chris@1706 172 (** Update the library to the given specific id or tag *)
Chris@1706 173 val update_to : context -> libname * id_or_tag -> id_or_tag result
Chris@1706 174 end
Chris@1706 175
Chris@1706 176 signature LIB_CONTROL = sig
Chris@1706 177 val review : context -> libspec -> (libstate * localstate) result
Chris@1706 178 val status : context -> libspec -> (libstate * localstate) result
Chris@1706 179 val update : context -> libspec -> id_or_tag result
Chris@1706 180 end
Chris@1706 181
Chris@1706 182 structure FileBits :> sig
Chris@1706 183 val extpath : context -> string
Chris@1706 184 val libpath : context -> libname -> string
Chris@1706 185 val subpath : context -> libname -> string -> string
Chris@1706 186 val command_output : context -> libname -> string list -> string result
Chris@1706 187 val command : context -> libname -> string list -> unit result
Chris@1706 188 val file_contents : string -> string
Chris@1706 189 val mydir : unit -> string
Chris@1706 190 val homedir : unit -> string
Chris@1706 191 val mkpath : string -> unit result
Chris@1706 192 val project_spec_path : string -> string
Chris@1706 193 val project_lock_path : string -> string
Chris@1706 194 val verbose : unit -> bool
Chris@1706 195 end = struct
Chris@1706 196
Chris@1706 197 fun verbose () =
Chris@1706 198 case OS.Process.getEnv "VEXT_VERBOSE" of
Chris@1706 199 SOME "0" => false
Chris@1706 200 | SOME _ => true
Chris@1706 201 | NONE => false
Chris@1706 202
Chris@1706 203 fun extpath ({ rootpath, extdir, ... } : context) =
Chris@1706 204 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
Chris@1706 205 in OS.Path.toString {
Chris@1706 206 isAbs = isAbs,
Chris@1706 207 vol = vol,
Chris@1706 208 arcs = arcs @ [ extdir ]
Chris@1706 209 }
Chris@1706 210 end
Chris@1706 211
Chris@1706 212 fun subpath ({ rootpath, extdir, ... } : context) libname remainder =
Chris@1706 213 (* NB libname is allowed to be a path fragment, e.g. foo/bar *)
Chris@1706 214 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
Chris@1706 215 val split = String.fields (fn c => c = #"/")
Chris@1706 216 in OS.Path.toString {
Chris@1706 217 isAbs = isAbs,
Chris@1706 218 vol = vol,
Chris@1706 219 arcs = arcs @ [ extdir ] @ split libname @ split remainder
Chris@1706 220 }
Chris@1706 221 end
Chris@1706 222
Chris@1706 223 fun libpath context "" =
Chris@1706 224 extpath context
Chris@1706 225 | libpath context libname =
Chris@1706 226 subpath context libname ""
Chris@1706 227
Chris@1706 228 fun project_file_path rootpath filename =
Chris@1706 229 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
Chris@1706 230 in OS.Path.toString {
Chris@1706 231 isAbs = isAbs,
Chris@1706 232 vol = vol,
Chris@1706 233 arcs = arcs @ [ filename ]
Chris@1706 234 }
Chris@1706 235 end
Chris@1706 236
Chris@1706 237 fun project_spec_path rootpath =
Chris@1706 238 project_file_path rootpath (VextFilenames.project_file)
Chris@1706 239
Chris@1706 240 fun project_lock_path rootpath =
Chris@1706 241 project_file_path rootpath (VextFilenames.project_lock_file)
Chris@1706 242
Chris@1706 243 fun trim str =
Chris@1706 244 hd (String.fields (fn x => x = #"\n" orelse x = #"\r") str)
Chris@1706 245
Chris@1706 246 fun file_contents filename =
Chris@1706 247 let val stream = TextIO.openIn filename
Chris@1706 248 fun read_all str acc =
Chris@1706 249 case TextIO.inputLine str of
Chris@1706 250 SOME line => read_all str (trim line :: acc)
Chris@1706 251 | NONE => rev acc
Chris@1706 252 val contents = read_all stream []
Chris@1706 253 val _ = TextIO.closeIn stream
Chris@1706 254 in
Chris@1706 255 String.concatWith "\n" contents
Chris@1706 256 end
Chris@1706 257
Chris@1706 258 fun expand_commandline cmdlist =
Chris@1706 259 (* We are quite [too] strict about what we accept here, except
Chris@1706 260 for the first element in cmdlist which is assumed to be a
Chris@1706 261 known command location rather than arbitrary user input. NB
Chris@1706 262 only ASCII accepted at this point. *)
Chris@1706 263 let open Char
Chris@1706 264 fun quote arg =
Chris@1706 265 if List.all
Chris@1706 266 (fn c => isAlphaNum c orelse c = #"-" orelse c = #"_")
Chris@1706 267 (explode arg)
Chris@1706 268 then arg
Chris@1706 269 else "\"" ^ arg ^ "\""
Chris@1706 270 fun check arg =
Chris@1706 271 let val valid = explode " /#:;?,._-{}@="
Chris@1706 272 in
Chris@1706 273 app (fn c =>
Chris@1706 274 if isAlphaNum c orelse
Chris@1706 275 List.exists (fn v => v = c) valid
Chris@1706 276 then ()
Chris@1706 277 else raise Fail ("Invalid character '" ^
Chris@1706 278 (Char.toString c) ^
Chris@1706 279 "' in command list"))
Chris@1706 280 (explode arg);
Chris@1706 281 arg
Chris@1706 282 end
Chris@1706 283 in
Chris@1706 284 String.concatWith " "
Chris@1706 285 (map quote
Chris@1706 286 (hd cmdlist :: map check (tl cmdlist)))
Chris@1706 287 end
Chris@1706 288
Chris@1706 289 val tick_cycle = ref 0
Chris@1706 290 val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
Chris@1706 291
Chris@1706 292 fun tick name =
Chris@1706 293 let val n = Vector.length tick_chars
Chris@1706 294 fun pad_to n str =
Chris@1706 295 if n <= String.size str then str
Chris@1706 296 else pad_to n (str ^ " ")
Chris@1706 297 in
Chris@1706 298 print ("\r " ^
Chris@1706 299 Vector.sub(tick_chars, !tick_cycle) ^ " " ^
Chris@1708 300 pad_to 24 name);
Chris@1706 301 tick_cycle := (if !tick_cycle = n - 1 then 0 else 1 + !tick_cycle)
Chris@1706 302 end
Chris@1706 303
Chris@1706 304 fun run_command context libname cmdlist redirect =
Chris@1706 305 let open OS
Chris@1706 306 val dir = libpath context libname
Chris@1706 307 val cmd = expand_commandline cmdlist
Chris@1706 308 val _ = if verbose ()
Chris@1706 309 then print ("Running: " ^ cmd ^
Chris@1706 310 " (in dir " ^ dir ^ ")...\n")
Chris@1706 311 else tick libname
Chris@1706 312 val _ = FileSys.chDir dir
Chris@1706 313 val status = case redirect of
Chris@1706 314 NONE => Process.system cmd
Chris@1706 315 | SOME file => Process.system (cmd ^ ">" ^ file)
Chris@1706 316 in
Chris@1706 317 if Process.isSuccess status
Chris@1706 318 then OK ()
Chris@1706 319 else ERROR ("Command failed: " ^ cmd ^ " (in dir " ^ dir ^ ")")
Chris@1706 320 end
Chris@1706 321 handle ex => ERROR ("Unable to run command: " ^ exnMessage ex)
Chris@1706 322
Chris@1706 323 fun command context libname cmdlist =
Chris@1706 324 run_command context libname cmdlist NONE
Chris@1706 325
Chris@1706 326 fun command_output context libname cmdlist =
Chris@1706 327 let open OS
Chris@1706 328 val tmpFile = FileSys.tmpName ()
Chris@1706 329 val result = run_command context libname cmdlist (SOME tmpFile)
Chris@1706 330 val contents = file_contents tmpFile
Chris@1706 331 in
Chris@1706 332 FileSys.remove tmpFile handle _ => ();
Chris@1706 333 case result of
Chris@1706 334 OK () => OK contents
Chris@1706 335 | ERROR e => ERROR e
Chris@1706 336 end
Chris@1706 337
Chris@1706 338 fun mydir () =
Chris@1706 339 let open OS
Chris@1706 340 val { dir, file } = Path.splitDirFile (CommandLine.name ())
Chris@1706 341 in
Chris@1706 342 FileSys.realPath
Chris@1706 343 (if Path.isAbsolute dir
Chris@1706 344 then dir
Chris@1706 345 else Path.concat (FileSys.getDir (), dir))
Chris@1706 346 end
Chris@1706 347
Chris@1706 348 fun homedir () =
Chris@1706 349 (* Failure is not routine, so we use an exception here *)
Chris@1706 350 case (OS.Process.getEnv "HOME",
Chris@1706 351 OS.Process.getEnv "HOMEPATH") of
Chris@1706 352 (SOME home, _) => home
Chris@1706 353 | (NONE, SOME home) => home
Chris@1706 354 | (NONE, NONE) =>
Chris@1706 355 raise Fail "Failed to look up home directory from environment"
Chris@1706 356
Chris@1706 357 fun mkpath path =
Chris@1706 358 if OS.FileSys.isDir path handle _ => false
Chris@1706 359 then OK ()
Chris@1706 360 else case OS.Path.fromString path of
Chris@1706 361 { arcs = nil, ... } => OK ()
Chris@1706 362 | { isAbs = false, ... } => ERROR "mkpath requires absolute path"
Chris@1706 363 | { isAbs, vol, arcs } =>
Chris@1706 364 case mkpath (OS.Path.toString { (* parent *)
Chris@1706 365 isAbs = isAbs,
Chris@1706 366 vol = vol,
Chris@1706 367 arcs = rev (tl (rev arcs)) }) of
Chris@1706 368 ERROR e => ERROR e
Chris@1706 369 | OK () => ((OS.FileSys.mkDir path; OK ())
Chris@1706 370 handle OS.SysErr (e, _) =>
Chris@1706 371 ERROR ("Directory creation failed: " ^ e))
Chris@1706 372 end
Chris@1706 373
Chris@1706 374 functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
Chris@1706 375
Chris@1706 376 (* Valid states for unpinned libraries:
Chris@1706 377
Chris@1706 378 - CORRECT: We are on the right branch and are up-to-date with
Chris@1706 379 it as far as we can tell. (If not using the network, this
Chris@1706 380 should be reported to user as "Present" rather than "Correct"
Chris@1706 381 as the remote repo may have advanced without us knowing.)
Chris@1706 382
Chris@1706 383 - SUPERSEDED: We are on the right branch but we can see that
Chris@1706 384 there is a newer revision either locally or on the remote (in
Chris@1706 385 Git terms, we are at an ancestor of the desired branch tip).
Chris@1706 386
Chris@1706 387 - WRONG: We are on the wrong branch (in Git terms, we are not
Chris@1706 388 at the desired branch tip or any ancestor of it).
Chris@1706 389
Chris@1706 390 - ABSENT: Repo doesn't exist here at all.
Chris@1706 391
Chris@1706 392 Valid states for pinned libraries:
Chris@1706 393
Chris@1706 394 - CORRECT: We are at the pinned revision.
Chris@1706 395
Chris@1706 396 - WRONG: We are at any revision other than the pinned one.
Chris@1706 397
Chris@1706 398 - ABSENT: Repo doesn't exist here at all.
Chris@1706 399 *)
Chris@1706 400
Chris@1706 401 fun check with_network context ({ libname, branch, pin, ... } : libspec) =
Chris@1706 402 let fun check_unpinned () =
Chris@1706 403 let val is_newest = if with_network
Chris@1706 404 then V.is_newest
Chris@1706 405 else V.is_newest_locally
Chris@1706 406 in
Chris@1706 407 case is_newest context (libname, branch) of
Chris@1706 408 ERROR e => ERROR e
Chris@1706 409 | OK true => OK CORRECT
Chris@1706 410 | OK false =>
Chris@1706 411 case V.is_on_branch context (libname, branch) of
Chris@1706 412 ERROR e => ERROR e
Chris@1706 413 | OK true => OK SUPERSEDED
Chris@1706 414 | OK false => OK WRONG
Chris@1706 415 end
Chris@1706 416 fun check_pinned target =
Chris@1706 417 case V.is_at context (libname, target) of
Chris@1706 418 ERROR e => ERROR e
Chris@1706 419 | OK true => OK CORRECT
Chris@1706 420 | OK false => OK WRONG
Chris@1706 421 fun check' () =
Chris@1706 422 case pin of
Chris@1706 423 UNPINNED => check_unpinned ()
Chris@1706 424 | PINNED target => check_pinned target
Chris@1706 425 in
Chris@1706 426 case V.exists context libname of
Chris@1706 427 ERROR e => ERROR e
Chris@1706 428 | OK false => OK (ABSENT, UNMODIFIED)
Chris@1706 429 | OK true =>
Chris@1706 430 case (check' (), V.is_modified_locally context libname) of
Chris@1706 431 (ERROR e, _) => ERROR e
Chris@1706 432 | (_, ERROR e) => ERROR e
Chris@1706 433 | (OK state, OK true) => OK (state, MODIFIED)
Chris@1706 434 | (OK state, OK false) => OK (state, UNMODIFIED)
Chris@1706 435 end
Chris@1706 436
Chris@1706 437 val review = check true
Chris@1706 438 val status = check false
Chris@1706 439
Chris@1706 440 fun update context ({ libname, source, branch, pin, ... } : libspec) =
Chris@1706 441 let fun update_unpinned () =
Chris@1706 442 case V.is_newest context (libname, branch) of
Chris@1706 443 ERROR e => ERROR e
Chris@1706 444 | OK true => V.id_of context libname
Chris@1706 445 | OK false => V.update context (libname, branch)
Chris@1706 446 fun update_pinned target =
Chris@1706 447 case V.is_at context (libname, target) of
Chris@1706 448 ERROR e => ERROR e
Chris@1706 449 | OK true => OK target
Chris@1706 450 | OK false => V.update_to context (libname, target)
Chris@1706 451 fun update' () =
Chris@1706 452 case pin of
Chris@1706 453 UNPINNED => update_unpinned ()
Chris@1706 454 | PINNED target => update_pinned target
Chris@1706 455 in
Chris@1706 456 case V.exists context libname of
Chris@1706 457 ERROR e => ERROR e
Chris@1706 458 | OK true => update' ()
Chris@1706 459 | OK false =>
Chris@1706 460 case V.checkout context (libname, source, branch) of
Chris@1706 461 ERROR e => ERROR e
Chris@1706 462 | OK () => update' ()
Chris@1706 463 end
Chris@1706 464 end
Chris@1706 465
Chris@1706 466 (* Simple Standard ML JSON parser
Chris@1706 467 ==============================
Chris@1706 468
Chris@1706 469 https://bitbucket.org/cannam/sml-simplejson
Chris@1706 470
Chris@1706 471 An RFC-compliant JSON parser in one SML file with no dependency
Chris@1706 472 on anything outside the Basis library. Also includes a simple
Chris@1706 473 serialiser.
Chris@1706 474
Chris@1706 475 Tested with MLton, Poly/ML, and SML/NJ compilers.
Chris@1706 476
Chris@1706 477 Parser notes:
Chris@1706 478
Chris@1706 479 * Complies with RFC 7159, The JavaScript Object Notation (JSON)
Chris@1706 480 Data Interchange Format
Chris@1706 481
Chris@1706 482 * Passes all of the JSONTestSuite parser accept/reject tests that
Chris@1706 483 exist at the time of writing, as listed in "Parsing JSON is a
Chris@1706 484 Minefield" (http://seriot.ch/parsing_json.php)
Chris@1706 485
Chris@1706 486 * Two-pass parser using naive exploded strings, therefore not
Chris@1706 487 particularly fast and not suitable for large input files
Chris@1706 488
Chris@1706 489 * Only supports UTF-8 input, not UTF-16 or UTF-32. Doesn't check
Chris@1706 490 that JSON strings are valid UTF-8 -- the caller must do that --
Chris@1706 491 but does handle \u escapes
Chris@1706 492
Chris@1706 493 * Converts all numbers to type "real". If that is a 64-bit IEEE
Chris@1706 494 float type (common but not guaranteed in SML) then we're pretty
Chris@1706 495 standard for a JSON parser
Chris@1706 496
Chris@1706 497 Copyright 2017 Chris Cannam.
Chris@1706 498 Parts based on the JSON parser in the Ponyo library by Phil Eaton.
Chris@1706 499
Chris@1706 500 Permission is hereby granted, free of charge, to any person
Chris@1706 501 obtaining a copy of this software and associated documentation
Chris@1706 502 files (the "Software"), to deal in the Software without
Chris@1706 503 restriction, including without limitation the rights to use, copy,
Chris@1706 504 modify, merge, publish, distribute, sublicense, and/or sell copies
Chris@1706 505 of the Software, and to permit persons to whom the Software is
Chris@1706 506 furnished to do so, subject to the following conditions:
Chris@1706 507
Chris@1706 508 The above copyright notice and this permission notice shall be
Chris@1706 509 included in all copies or substantial portions of the Software.
Chris@1706 510
Chris@1706 511 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
Chris@1706 512 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
Chris@1706 513 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Chris@1706 514 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
Chris@1706 515 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
Chris@1706 516 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
Chris@1706 517 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Chris@1706 518
Chris@1706 519 Except as contained in this notice, the names of Chris Cannam and
Chris@1706 520 Particular Programs Ltd shall not be used in advertising or
Chris@1706 521 otherwise to promote the sale, use or other dealings in this
Chris@1706 522 Software without prior written authorization.
Chris@1706 523 *)
Chris@1706 524
Chris@1706 525 signature JSON = sig
Chris@1706 526
Chris@1706 527 datatype json = OBJECT of (string * json) list
Chris@1706 528 | ARRAY of json list
Chris@1706 529 | NUMBER of real
Chris@1706 530 | STRING of string
Chris@1706 531 | BOOL of bool
Chris@1706 532 | NULL
Chris@1706 533
Chris@1706 534 datatype 'a result = OK of 'a
Chris@1706 535 | ERROR of string
Chris@1706 536
Chris@1706 537 val parse : string -> json result
Chris@1706 538 val serialise : json -> string
Chris@1706 539 val serialiseIndented : json -> string
Chris@1706 540
Chris@1706 541 end
Chris@1706 542
Chris@1706 543 structure Json :> JSON = struct
Chris@1706 544
Chris@1706 545 datatype json = OBJECT of (string * json) list
Chris@1706 546 | ARRAY of json list
Chris@1706 547 | NUMBER of real
Chris@1706 548 | STRING of string
Chris@1706 549 | BOOL of bool
Chris@1706 550 | NULL
Chris@1706 551
Chris@1706 552 datatype 'a result = OK of 'a
Chris@1706 553 | ERROR of string
Chris@1706 554
Chris@1706 555 structure T = struct
Chris@1706 556 datatype token = NUMBER of char list
Chris@1706 557 | STRING of string
Chris@1706 558 | BOOL of bool
Chris@1706 559 | NULL
Chris@1706 560 | CURLY_L
Chris@1706 561 | CURLY_R
Chris@1706 562 | SQUARE_L
Chris@1706 563 | SQUARE_R
Chris@1706 564 | COLON
Chris@1706 565 | COMMA
Chris@1706 566
Chris@1706 567 fun toString t =
Chris@1706 568 case t of NUMBER digits => implode digits
Chris@1706 569 | STRING s => s
Chris@1706 570 | BOOL b => Bool.toString b
Chris@1706 571 | NULL => "null"
Chris@1706 572 | CURLY_L => "{"
Chris@1706 573 | CURLY_R => "}"
Chris@1706 574 | SQUARE_L => "["
Chris@1706 575 | SQUARE_R => "]"
Chris@1706 576 | COLON => ":"
Chris@1706 577 | COMMA => ","
Chris@1706 578 end
Chris@1706 579
Chris@1706 580 fun bmpToUtf8 cp = (* convert a codepoint in Unicode BMP to utf8 bytes *)
Chris@1706 581 let open Word
Chris@1706 582 infix 6 orb andb >>
Chris@1706 583 in
Chris@1706 584 map (Char.chr o toInt)
Chris@1706 585 (if cp < 0wx80 then
Chris@1706 586 [cp]
Chris@1706 587 else if cp < 0wx800 then
Chris@1706 588 [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)]
Chris@1706 589 else if cp < 0wx10000 then
Chris@1706 590 [0wxe0 orb (cp >> 0w12),
Chris@1706 591 0wx80 orb ((cp >> 0w6) andb 0wx3f),
Chris@1706 592 0wx80 orb (cp andb 0wx3f)]
Chris@1706 593 else raise Fail ("Invalid BMP point " ^ (Word.toString cp)))
Chris@1706 594 end
Chris@1706 595
Chris@1706 596 fun error pos text = ERROR (text ^ " at character position " ^
Chris@1706 597 Int.toString (pos - 1))
Chris@1706 598 fun token_error pos = error pos ("Unexpected token")
Chris@1706 599
Chris@1706 600 fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
Chris@1706 601 lex (pos + 3) (T.NULL :: acc) xs
Chris@1706 602 | lexNull pos acc _ = token_error pos
Chris@1706 603
Chris@1706 604 and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
Chris@1706 605 lex (pos + 3) (T.BOOL true :: acc) xs
Chris@1706 606 | lexTrue pos acc _ = token_error pos
Chris@1706 607
Chris@1706 608 and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
Chris@1706 609 lex (pos + 4) (T.BOOL false :: acc) xs
Chris@1706 610 | lexFalse pos acc _ = token_error pos
Chris@1706 611
Chris@1706 612 and lexChar tok pos acc xs =
Chris@1706 613 lex pos (tok :: acc) xs
Chris@1706 614
Chris@1706 615 and lexString pos acc cc =
Chris@1706 616 let datatype escaped = ESCAPED | NORMAL
Chris@1706 617 fun lexString' pos text ESCAPED [] =
Chris@1706 618 error pos "End of input during escape sequence"
Chris@1706 619 | lexString' pos text NORMAL [] =
Chris@1706 620 error pos "End of input during string"
Chris@1706 621 | lexString' pos text ESCAPED (x :: xs) =
Chris@1706 622 let fun esc c = lexString' (pos + 1) (c :: text) NORMAL xs
Chris@1706 623 in case x of
Chris@1706 624 #"\"" => esc x
Chris@1706 625 | #"\\" => esc x
Chris@1706 626 | #"/" => esc x
Chris@1706 627 | #"b" => esc #"\b"
Chris@1706 628 | #"f" => esc #"\f"
Chris@1706 629 | #"n" => esc #"\n"
Chris@1706 630 | #"r" => esc #"\r"
Chris@1706 631 | #"t" => esc #"\t"
Chris@1706 632 | _ => error pos ("Invalid escape \\" ^
Chris@1706 633 Char.toString x)
Chris@1706 634 end
Chris@1706 635 | lexString' pos text NORMAL (#"\\" :: #"u" ::a::b::c::d:: xs) =
Chris@1706 636 if List.all Char.isHexDigit [a,b,c,d]
Chris@1706 637 then case Word.fromString ("0wx" ^ (implode [a,b,c,d])) of
Chris@1706 638 SOME w => (let val utf = rev (bmpToUtf8 w) in
Chris@1706 639 lexString' (pos + 6) (utf @ text)
Chris@1706 640 NORMAL xs
Chris@1706 641 end
Chris@1706 642 handle Fail err => error pos err)
Chris@1706 643 | NONE => error pos "Invalid Unicode BMP escape sequence"
Chris@1706 644 else error pos "Invalid Unicode BMP escape sequence"
Chris@1706 645 | lexString' pos text NORMAL (x :: xs) =
Chris@1706 646 if Char.ord x < 0x20
Chris@1706 647 then error pos "Invalid unescaped control character"
Chris@1706 648 else
Chris@1706 649 case x of
Chris@1706 650 #"\"" => OK (rev text, xs, pos + 1)
Chris@1706 651 | #"\\" => lexString' (pos + 1) text ESCAPED xs
Chris@1706 652 | _ => lexString' (pos + 1) (x :: text) NORMAL xs
Chris@1706 653 in
Chris@1706 654 case lexString' pos [] NORMAL cc of
Chris@1706 655 OK (text, rest, newpos) =>
Chris@1706 656 lex newpos (T.STRING (implode text) :: acc) rest
Chris@1706 657 | ERROR e => ERROR e
Chris@1706 658 end
Chris@1706 659
Chris@1706 660 and lexNumber firstChar pos acc cc =
Chris@1706 661 let val valid = explode ".+-e"
Chris@1706 662 fun lexNumber' pos digits [] = (rev digits, [], pos)
Chris@1706 663 | lexNumber' pos digits (x :: xs) =
Chris@1706 664 if x = #"E" then lexNumber' (pos + 1) (#"e" :: digits) xs
Chris@1706 665 else if Char.isDigit x orelse List.exists (fn c => x = c) valid
Chris@1706 666 then lexNumber' (pos + 1) (x :: digits) xs
Chris@1706 667 else (rev digits, x :: xs, pos)
Chris@1706 668 val (digits, rest, newpos) =
Chris@1706 669 lexNumber' (pos - 1) [] (firstChar :: cc)
Chris@1706 670 in
Chris@1706 671 case digits of
Chris@1706 672 [] => token_error pos
Chris@1706 673 | _ => lex newpos (T.NUMBER digits :: acc) rest
Chris@1706 674 end
Chris@1706 675
Chris@1706 676 and lex pos acc [] = OK (rev acc)
Chris@1706 677 | lex pos acc (x::xs) =
Chris@1706 678 (case x of
Chris@1706 679 #" " => lex
Chris@1706 680 | #"\t" => lex
Chris@1706 681 | #"\n" => lex
Chris@1706 682 | #"\r" => lex
Chris@1706 683 | #"{" => lexChar T.CURLY_L
Chris@1706 684 | #"}" => lexChar T.CURLY_R
Chris@1706 685 | #"[" => lexChar T.SQUARE_L
Chris@1706 686 | #"]" => lexChar T.SQUARE_R
Chris@1706 687 | #":" => lexChar T.COLON
Chris@1706 688 | #"," => lexChar T.COMMA
Chris@1706 689 | #"\"" => lexString
Chris@1706 690 | #"t" => lexTrue
Chris@1706 691 | #"f" => lexFalse
Chris@1706 692 | #"n" => lexNull
Chris@1706 693 | x => lexNumber x) (pos + 1) acc xs
Chris@1706 694
Chris@1706 695 fun show [] = "end of input"
Chris@1706 696 | show (tok :: _) = T.toString tok
Chris@1706 697
Chris@1706 698 fun parseNumber digits =
Chris@1706 699 (* Note lexNumber already case-insensitised the E for us *)
Chris@1706 700 let open Char
Chris@1706 701
Chris@1706 702 fun okExpDigits [] = false
Chris@1706 703 | okExpDigits (c :: []) = isDigit c
Chris@1706 704 | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
Chris@1706 705
Chris@1706 706 fun okExponent [] = false
Chris@1706 707 | okExponent (#"+" :: cs) = okExpDigits cs
Chris@1706 708 | okExponent (#"-" :: cs) = okExpDigits cs
Chris@1706 709 | okExponent cc = okExpDigits cc
Chris@1706 710
Chris@1706 711 fun okFracTrailing [] = true
Chris@1706 712 | okFracTrailing (c :: cs) =
Chris@1706 713 (isDigit c andalso okFracTrailing cs) orelse
Chris@1706 714 (c = #"e" andalso okExponent cs)
Chris@1706 715
Chris@1706 716 fun okFraction [] = false
Chris@1706 717 | okFraction (c :: cs) =
Chris@1706 718 isDigit c andalso okFracTrailing cs
Chris@1706 719
Chris@1706 720 fun okPosTrailing [] = true
Chris@1706 721 | okPosTrailing (#"." :: cs) = okFraction cs
Chris@1706 722 | okPosTrailing (#"e" :: cs) = okExponent cs
Chris@1706 723 | okPosTrailing (c :: cs) =
Chris@1706 724 isDigit c andalso okPosTrailing cs
Chris@1706 725
Chris@1706 726 fun okPositive [] = false
Chris@1706 727 | okPositive (#"0" :: []) = true
Chris@1706 728 | okPositive (#"0" :: #"." :: cs) = okFraction cs
Chris@1706 729 | okPositive (#"0" :: #"e" :: cs) = okExponent cs
Chris@1706 730 | okPositive (#"0" :: cs) = false
Chris@1706 731 | okPositive (c :: cs) = isDigit c andalso okPosTrailing cs
Chris@1706 732
Chris@1706 733 fun okNumber (#"-" :: cs) = okPositive cs
Chris@1706 734 | okNumber cc = okPositive cc
Chris@1706 735 in
Chris@1706 736 if okNumber digits
Chris@1706 737 then case Real.fromString (implode digits) of
Chris@1706 738 NONE => ERROR "Number out of range"
Chris@1706 739 | SOME r => OK r
Chris@1706 740 else ERROR ("Invalid number \"" ^ (implode digits) ^ "\"")
Chris@1706 741 end
Chris@1706 742
Chris@1706 743 fun parseObject (T.CURLY_R :: xs) = OK (OBJECT [], xs)
Chris@1706 744 | parseObject tokens =
Chris@1706 745 let fun parsePair (T.STRING key :: T.COLON :: xs) =
Chris@1706 746 (case parseTokens xs of
Chris@1706 747 ERROR e => ERROR e
Chris@1706 748 | OK (j, xs) => OK ((key, j), xs))
Chris@1706 749 | parsePair other =
Chris@1706 750 ERROR ("Object key/value pair expected around \"" ^
Chris@1706 751 show other ^ "\"")
Chris@1706 752 fun parseObject' acc [] = ERROR "End of input during object"
Chris@1706 753 | parseObject' acc tokens =
Chris@1706 754 case parsePair tokens of
Chris@1706 755 ERROR e => ERROR e
Chris@1706 756 | OK (pair, T.COMMA :: xs) =>
Chris@1706 757 parseObject' (pair :: acc) xs
Chris@1706 758 | OK (pair, T.CURLY_R :: xs) =>
Chris@1706 759 OK (OBJECT (rev (pair :: acc)), xs)
Chris@1706 760 | OK (_, _) => ERROR "Expected , or } after object element"
Chris@1706 761 in
Chris@1706 762 parseObject' [] tokens
Chris@1706 763 end
Chris@1706 764
Chris@1706 765 and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs)
Chris@1706 766 | parseArray tokens =
Chris@1706 767 let fun parseArray' acc [] = ERROR "End of input during array"
Chris@1706 768 | parseArray' acc tokens =
Chris@1706 769 case parseTokens tokens of
Chris@1706 770 ERROR e => ERROR e
Chris@1706 771 | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs
Chris@1706 772 | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs)
Chris@1706 773 | OK (_, _) => ERROR "Expected , or ] after array element"
Chris@1706 774 in
Chris@1706 775 parseArray' [] tokens
Chris@1706 776 end
Chris@1706 777
Chris@1706 778 and parseTokens [] = ERROR "Value expected"
Chris@1706 779 | parseTokens (tok :: xs) =
Chris@1706 780 (case tok of
Chris@1706 781 T.NUMBER d => (case parseNumber d of
Chris@1706 782 OK r => OK (NUMBER r, xs)
Chris@1706 783 | ERROR e => ERROR e)
Chris@1706 784 | T.STRING s => OK (STRING s, xs)
Chris@1706 785 | T.BOOL b => OK (BOOL b, xs)
Chris@1706 786 | T.NULL => OK (NULL, xs)
Chris@1706 787 | T.CURLY_L => parseObject xs
Chris@1706 788 | T.SQUARE_L => parseArray xs
Chris@1706 789 | _ => ERROR ("Unexpected token " ^ T.toString tok ^
Chris@1706 790 " before " ^ show xs))
Chris@1706 791
Chris@1706 792 fun parse str =
Chris@1706 793 case lex 1 [] (explode str) of
Chris@1706 794 ERROR e => ERROR e
Chris@1706 795 | OK tokens => case parseTokens tokens of
Chris@1706 796 OK (value, []) => OK value
Chris@1706 797 | OK (_, _) => ERROR "Extra data after input"
Chris@1706 798 | ERROR e => ERROR e
Chris@1706 799
Chris@1706 800 fun stringEscape s =
Chris@1706 801 let fun esc x = [x, #"\\"]
Chris@1706 802 fun escape' acc [] = rev acc
Chris@1706 803 | escape' acc (x :: xs) =
Chris@1706 804 escape' (case x of
Chris@1706 805 #"\"" => esc x @ acc
Chris@1706 806 | #"\\" => esc x @ acc
Chris@1706 807 | #"\b" => esc #"b" @ acc
Chris@1706 808 | #"\f" => esc #"f" @ acc
Chris@1706 809 | #"\n" => esc #"n" @ acc
Chris@1706 810 | #"\r" => esc #"r" @ acc
Chris@1706 811 | #"\t" => esc #"t" @ acc
Chris@1706 812 | _ =>
Chris@1706 813 let val c = Char.ord x
Chris@1706 814 in
Chris@1706 815 if c < 0x20
Chris@1706 816 then let val hex = Word.toString (Word.fromInt c)
Chris@1706 817 in (rev o explode) (if c < 0x10
Chris@1706 818 then ("\\u000" ^ hex)
Chris@1706 819 else ("\\u00" ^ hex))
Chris@1706 820 end @ acc
Chris@1706 821 else
Chris@1706 822 x :: acc
Chris@1706 823 end)
Chris@1706 824 xs
Chris@1706 825 in
Chris@1706 826 implode (escape' [] (explode s))
Chris@1706 827 end
Chris@1706 828
Chris@1706 829 fun serialise json =
Chris@1706 830 case json of
Chris@1706 831 OBJECT pp => "{" ^ String.concatWith
Chris@1706 832 "," (map (fn (key, value) =>
Chris@1706 833 serialise (STRING key) ^ ":" ^
Chris@1706 834 serialise value) pp) ^
Chris@1706 835 "}"
Chris@1706 836 | ARRAY arr => "[" ^ String.concatWith "," (map serialise arr) ^ "]"
Chris@1706 837 | NUMBER n => implode (map (fn #"~" => #"-" | c => c)
Chris@1706 838 (explode (Real.toString n)))
Chris@1706 839 | STRING s => "\"" ^ stringEscape s ^ "\""
Chris@1706 840 | BOOL b => Bool.toString b
Chris@1706 841 | NULL => "null"
Chris@1706 842
Chris@1706 843 fun serialiseIndented json =
Chris@1706 844 let fun indent 0 = ""
Chris@1706 845 | indent i = " " ^ indent (i - 1)
Chris@1706 846 fun serialiseIndented' i json =
Chris@1706 847 let val ser = serialiseIndented' (i + 1)
Chris@1706 848 in
Chris@1706 849 case json of
Chris@1706 850 OBJECT [] => "{}"
Chris@1706 851 | ARRAY [] => "[]"
Chris@1706 852 | OBJECT pp => "{\n" ^ indent (i + 1) ^
Chris@1706 853 String.concatWith
Chris@1706 854 (",\n" ^ indent (i + 1))
Chris@1706 855 (map (fn (key, value) =>
Chris@1706 856 ser (STRING key) ^ ": " ^
Chris@1706 857 ser value) pp) ^
Chris@1706 858 "\n" ^ indent i ^ "}"
Chris@1706 859 | ARRAY arr => "[\n" ^ indent (i + 1) ^
Chris@1706 860 String.concatWith
Chris@1706 861 (",\n" ^ indent (i + 1))
Chris@1706 862 (map ser arr) ^
Chris@1706 863 "\n" ^ indent i ^ "]"
Chris@1706 864 | other => serialise other
Chris@1706 865 end
Chris@1706 866 in
Chris@1706 867 serialiseIndented' 0 json ^ "\n"
Chris@1706 868 end
Chris@1706 869
Chris@1706 870 end
Chris@1706 871
Chris@1706 872
Chris@1706 873 structure JsonBits :> sig
Chris@1706 874 val load_json_from : string -> Json.json (* filename -> json *)
Chris@1706 875 val save_json_to : string -> Json.json -> unit
Chris@1706 876 val lookup_optional : Json.json -> string list -> Json.json option
Chris@1706 877 val lookup_optional_string : Json.json -> string list -> string option
Chris@1706 878 val lookup_mandatory : Json.json -> string list -> Json.json
Chris@1706 879 val lookup_mandatory_string : Json.json -> string list -> string
Chris@1706 880 end = struct
Chris@1706 881
Chris@1706 882 fun load_json_from filename =
Chris@1706 883 case Json.parse (FileBits.file_contents filename) of
Chris@1706 884 Json.OK json => json
Chris@1706 885 | Json.ERROR e => raise Fail ("Failed to parse file: " ^ e)
Chris@1706 886
Chris@1706 887 fun save_json_to filename json =
Chris@1706 888 let val jstr = Json.serialiseIndented json
Chris@1706 889 val stream = TextIO.openOut filename
Chris@1706 890 in
Chris@1706 891 TextIO.output (stream, jstr);
Chris@1706 892 TextIO.closeOut stream
Chris@1706 893 end
Chris@1706 894
Chris@1706 895 fun lookup_optional json kk =
Chris@1706 896 let fun lookup key =
Chris@1706 897 case json of
Chris@1706 898 Json.OBJECT kvs =>
Chris@1706 899 (case List.find (fn (k, v) => k = key) kvs of
Chris@1706 900 SOME (k, v) => SOME v
Chris@1706 901 | NONE => NONE)
Chris@1706 902 | _ => raise Fail "Object expected"
Chris@1706 903 in
Chris@1706 904 case kk of
Chris@1706 905 [] => NONE
Chris@1706 906 | key::[] => lookup key
Chris@1706 907 | key::kk => case lookup key of
Chris@1706 908 NONE => NONE
Chris@1706 909 | SOME j => lookup_optional j kk
Chris@1706 910 end
Chris@1706 911
Chris@1706 912 fun lookup_optional_string json kk =
Chris@1706 913 case lookup_optional json kk of
Chris@1706 914 SOME (Json.STRING s) => SOME s
Chris@1706 915 | SOME _ => raise Fail ("Value (if present) must be string: " ^
Chris@1706 916 (String.concatWith " -> " kk))
Chris@1706 917 | NONE => NONE
Chris@1706 918
Chris@1706 919 fun lookup_mandatory json kk =
Chris@1706 920 case lookup_optional json kk of
Chris@1706 921 SOME v => v
Chris@1706 922 | NONE => raise Fail ("Value is mandatory: " ^
Chris@1706 923 (String.concatWith " -> " kk) ^ " in json: " ^
Chris@1706 924 (Json.serialise json))
Chris@1706 925
Chris@1706 926 fun lookup_mandatory_string json kk =
Chris@1706 927 case lookup_optional json kk of
Chris@1706 928 SOME (Json.STRING s) => s
Chris@1706 929 | _ => raise Fail ("Value must be string: " ^
Chris@1706 930 (String.concatWith " -> " kk))
Chris@1706 931 end
Chris@1706 932
Chris@1706 933 structure Provider :> sig
Chris@1706 934 val load_providers : Json.json -> provider list
Chris@1706 935 val load_more_providers : provider list -> Json.json -> provider list
Chris@1706 936 val remote_url : context -> vcs -> source -> libname -> string
Chris@1706 937 end = struct
Chris@1706 938
Chris@1706 939 val known_providers : provider list =
Chris@1706 940 [ {
Chris@1706 941 service = "bitbucket",
Chris@1706 942 supports = [HG, GIT],
Chris@1706 943 remote_spec = {
Chris@1706 944 anon = SOME "https://bitbucket.org/{owner}/{repo}",
Chris@1706 945 auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repo}"
Chris@1706 946 }
Chris@1706 947 },
Chris@1706 948 {
Chris@1706 949 service = "github",
Chris@1706 950 supports = [GIT],
Chris@1706 951 remote_spec = {
Chris@1706 952 anon = SOME "https://github.com/{owner}/{repo}",
Chris@1706 953 auth = SOME "ssh://{vcs}@github.com/{owner}/{repo}"
Chris@1706 954 }
Chris@1706 955 }
Chris@1706 956 ]
Chris@1706 957
Chris@1706 958 fun vcs_name vcs =
Chris@1706 959 case vcs of GIT => "git" |
Chris@1706 960 HG => "hg"
Chris@1706 961
Chris@1706 962 fun vcs_from_name name =
Chris@1706 963 case name of "git" => GIT
Chris@1706 964 | "hg" => HG
Chris@1706 965 | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"")
Chris@1706 966
Chris@1706 967 fun load_more_providers previously_loaded json =
Chris@1706 968 let open JsonBits
Chris@1706 969 fun load pjson pname : provider =
Chris@1706 970 {
Chris@1706 971 service = pname,
Chris@1706 972 supports =
Chris@1706 973 case lookup_mandatory pjson ["vcs"] of
Chris@1706 974 Json.ARRAY vv =>
Chris@1706 975 map (fn (Json.STRING v) => vcs_from_name v
Chris@1706 976 | _ => raise Fail "Strings expected in vcs array")
Chris@1706 977 vv
Chris@1706 978 | _ => raise Fail "Array expected for vcs",
Chris@1706 979 remote_spec = {
Chris@1706 980 anon = lookup_optional_string pjson ["anon"],
Chris@1706 981 auth = lookup_optional_string pjson ["auth"]
Chris@1706 982 }
Chris@1706 983 }
Chris@1706 984 val loaded =
Chris@1706 985 case lookup_optional json ["providers"] of
Chris@1706 986 NONE => []
Chris@1706 987 | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl
Chris@1706 988 | _ => raise Fail "Object expected for providers in config"
Chris@1706 989 val newly_loaded =
Chris@1706 990 List.filter (fn p => not (List.exists (fn pp => #service p =
Chris@1706 991 #service pp)
Chris@1706 992 previously_loaded))
Chris@1706 993 loaded
Chris@1706 994 in
Chris@1706 995 previously_loaded @ newly_loaded
Chris@1706 996 end
Chris@1706 997
Chris@1706 998 fun load_providers json =
Chris@1706 999 load_more_providers known_providers json
Chris@1706 1000
Chris@1706 1001 fun expand_spec spec { vcs, service, owner, repo } login =
Chris@1706 1002 (* ugly *)
Chris@1706 1003 let fun replace str =
Chris@1706 1004 case str of
Chris@1706 1005 "vcs" => vcs_name vcs
Chris@1706 1006 | "service" => service
Chris@1706 1007 | "owner" =>
Chris@1706 1008 (case owner of
Chris@1706 1009 SOME ostr => ostr
Chris@1706 1010 | NONE => raise Fail ("Owner not specified for service " ^
Chris@1706 1011 service))
Chris@1706 1012 | "repo" => repo
Chris@1706 1013 | "account" =>
Chris@1706 1014 (case login of
Chris@1706 1015 SOME acc => acc
Chris@1706 1016 | NONE => raise Fail ("Account not given for service " ^
Chris@1706 1017 service))
Chris@1706 1018 | other => raise Fail ("Unknown variable \"" ^ other ^
Chris@1706 1019 "\" in spec for service " ^ service)
Chris@1706 1020 fun expand' acc sstr =
Chris@1706 1021 case Substring.splitl (fn c => c <> #"{") sstr of
Chris@1706 1022 (pfx, sfx) =>
Chris@1706 1023 if Substring.isEmpty sfx
Chris@1706 1024 then rev (pfx :: acc)
Chris@1706 1025 else
Chris@1706 1026 case Substring.splitl (fn c => c <> #"}") sfx of
Chris@1706 1027 (tok, remainder) =>
Chris@1706 1028 if Substring.isEmpty remainder
Chris@1706 1029 then rev (tok :: pfx :: acc)
Chris@1706 1030 else let val replacement =
Chris@1706 1031 replace
Chris@1706 1032 (* tok begins with "{": *)
Chris@1706 1033 (Substring.string
Chris@1706 1034 (Substring.triml 1 tok))
Chris@1706 1035 in
Chris@1706 1036 expand' (Substring.full replacement ::
Chris@1706 1037 pfx :: acc)
Chris@1706 1038 (* remainder begins with "}": *)
Chris@1706 1039 (Substring.triml 1 remainder)
Chris@1706 1040 end
Chris@1706 1041 in
Chris@1706 1042 Substring.concat (expand' [] (Substring.full spec))
Chris@1706 1043 end
Chris@1706 1044
Chris@1706 1045 fun provider_url req login providers =
Chris@1706 1046 case providers of
Chris@1706 1047 [] => raise Fail ("Unknown service \"" ^ (#service req) ^
Chris@1706 1048 "\" for vcs \"" ^ (vcs_name (#vcs req)) ^ "\"")
Chris@1706 1049 | ({ service, supports, remote_spec : remote_spec } :: rest) =>
Chris@1706 1050 if service <> (#service req) orelse
Chris@1706 1051 not (List.exists (fn v => v = (#vcs req)) supports)
Chris@1706 1052 then provider_url req login rest
Chris@1706 1053 else
Chris@1706 1054 case (login, #auth remote_spec, #anon remote_spec) of
Chris@1706 1055 (SOME _, SOME auth, _) => expand_spec auth req login
Chris@1706 1056 | (SOME _, _, SOME anon) => expand_spec anon req NONE
Chris@1706 1057 | (NONE, _, SOME anon) => expand_spec anon req NONE
Chris@1706 1058 | _ => raise Fail ("No suitable anon/auth URL spec " ^
Chris@1706 1059 "provided for service \"" ^ service ^ "\"")
Chris@1706 1060
Chris@1706 1061 fun login_for ({ accounts, ... } : context) service =
Chris@1706 1062 case List.find (fn a => service = #service a) accounts of
Chris@1706 1063 SOME { login, ... } => SOME login
Chris@1706 1064 | NONE => NONE
Chris@1706 1065
Chris@1706 1066 fun remote_url (context : context) vcs source libname =
Chris@1706 1067 case source of
Chris@1706 1068 URL u => u
Chris@1706 1069 | PROVIDER { service, owner, repo } =>
Chris@1706 1070 provider_url { vcs = vcs,
Chris@1706 1071 service = service,
Chris@1706 1072 owner = owner,
Chris@1706 1073 repo = case repo of
Chris@1706 1074 SOME r => r
Chris@1706 1075 | NONE => libname }
Chris@1706 1076 (login_for context service)
Chris@1706 1077 (#providers context)
Chris@1706 1078 end
Chris@1706 1079
Chris@1706 1080 structure HgControl :> VCS_CONTROL = struct
Chris@1706 1081
Chris@1706 1082 type vcsstate = { id: string, modified: bool,
Chris@1706 1083 branch: string, tags: string list }
Chris@1706 1084
Chris@1706 1085 val hg_args = [ "--config", "ui.interactive=true" ]
Chris@1706 1086
Chris@1706 1087 fun hg_command context libname args =
Chris@1706 1088 FileBits.command context libname ("hg" :: hg_args @ args)
Chris@1706 1089
Chris@1706 1090 fun hg_command_output context libname args =
Chris@1706 1091 FileBits.command_output context libname ("hg" :: hg_args @ args)
Chris@1706 1092
Chris@1706 1093 fun exists context libname =
Chris@1706 1094 OK (OS.FileSys.isDir (FileBits.subpath context libname ".hg"))
Chris@1706 1095 handle _ => OK false
Chris@1706 1096
Chris@1706 1097 fun remote_for context (libname, source) =
Chris@1706 1098 Provider.remote_url context HG source libname
Chris@1706 1099
Chris@1706 1100 fun current_state context libname : vcsstate result =
Chris@1706 1101 let fun is_branch text = text <> "" andalso #"(" = hd (explode text)
Chris@1706 1102 and extract_branch b =
Chris@1706 1103 if is_branch b (* need to remove enclosing parens *)
Chris@1706 1104 then (implode o rev o tl o rev o tl o explode) b
Chris@1706 1105 else "default"
Chris@1706 1106 and is_modified id = id <> "" andalso #"+" = hd (rev (explode id))
Chris@1706 1107 and extract_id id =
Chris@1706 1108 if is_modified id (* need to remove trailing "+" *)
Chris@1706 1109 then (implode o rev o tl o rev o explode) id
Chris@1706 1110 else id
Chris@1706 1111 and split_tags tags = String.tokens (fn c => c = #"/") tags
Chris@1706 1112 and state_for (id, branch, tags) =
Chris@1706 1113 OK { id = extract_id id,
Chris@1706 1114 modified = is_modified id,
Chris@1706 1115 branch = extract_branch branch,
Chris@1706 1116 tags = split_tags tags }
Chris@1706 1117 in
Chris@1706 1118 case hg_command_output context libname ["id"] of
Chris@1706 1119 ERROR e => ERROR e
Chris@1706 1120 | OK out =>
Chris@1706 1121 case String.tokens (fn x => x = #" ") out of
Chris@1706 1122 [id, branch, tags] => state_for (id, branch, tags)
Chris@1706 1123 | [id, other] => if is_branch other
Chris@1706 1124 then state_for (id, other, "")
Chris@1706 1125 else state_for (id, "", other)
Chris@1706 1126 | [id] => state_for (id, "", "")
Chris@1706 1127 | _ => ERROR ("Unexpected output from hg id: " ^ out)
Chris@1706 1128 end
Chris@1706 1129
Chris@1706 1130 fun branch_name branch = case branch of
Chris@1706 1131 DEFAULT_BRANCH => "default"
Chris@1706 1132 | BRANCH "" => "default"
Chris@1706 1133 | BRANCH b => b
Chris@1706 1134
Chris@1706 1135 fun id_of context libname =
Chris@1706 1136 case current_state context libname of
Chris@1706 1137 ERROR e => ERROR e
Chris@1706 1138 | OK { id, ... } => OK id
Chris@1706 1139
Chris@1706 1140 fun is_at context (libname, id_or_tag) =
Chris@1706 1141 case current_state context libname of
Chris@1706 1142 ERROR e => ERROR e
Chris@1706 1143 | OK { id, tags, ... } =>
Chris@1706 1144 OK (String.isPrefix id_or_tag id orelse
Chris@1706 1145 String.isPrefix id id_or_tag orelse
Chris@1706 1146 List.exists (fn t => t = id_or_tag) tags)
Chris@1706 1147
Chris@1706 1148 fun is_on_branch context (libname, b) =
Chris@1706 1149 case current_state context libname of
Chris@1706 1150 ERROR e => ERROR e
Chris@1706 1151 | OK { branch, ... } => OK (branch = branch_name b)
Chris@1706 1152
Chris@1706 1153 fun is_newest_locally context (libname, branch) =
Chris@1706 1154 case hg_command_output context libname
Chris@1706 1155 ["log", "-l1",
Chris@1706 1156 "-b", branch_name branch,
Chris@1706 1157 "--template", "{node}"] of
Chris@1706 1158 ERROR e => ERROR e
Chris@1706 1159 | OK newest_in_repo => is_at context (libname, newest_in_repo)
Chris@1706 1160
Chris@1706 1161 fun pull context libname =
Chris@1706 1162 hg_command context libname
Chris@1706 1163 (if FileBits.verbose ()
Chris@1706 1164 then ["pull"]
Chris@1706 1165 else ["pull", "-q"])
Chris@1706 1166
Chris@1706 1167 fun is_newest context (libname, branch) =
Chris@1706 1168 case is_newest_locally context (libname, branch) of
Chris@1706 1169 ERROR e => ERROR e
Chris@1706 1170 | OK false => OK false
Chris@1706 1171 | OK true =>
Chris@1706 1172 case pull context libname of
Chris@1706 1173 ERROR e => ERROR e
Chris@1706 1174 | _ => is_newest_locally context (libname, branch)
Chris@1706 1175
Chris@1706 1176 fun is_modified_locally context libname =
Chris@1706 1177 case current_state context libname of
Chris@1706 1178 ERROR e => ERROR e
Chris@1706 1179 | OK { modified, ... } => OK modified
Chris@1706 1180
Chris@1706 1181 fun checkout context (libname, source, branch) =
Chris@1706 1182 let val url = remote_for context (libname, source)
Chris@1706 1183 in
Chris@1706 1184 case FileBits.mkpath (FileBits.extpath context) of
Chris@1706 1185 ERROR e => ERROR e
Chris@1706 1186 | _ => hg_command context ""
Chris@1706 1187 ["clone", "-u", branch_name branch,
Chris@1706 1188 url, libname]
Chris@1706 1189 end
Chris@1706 1190
Chris@1706 1191 fun update context (libname, branch) =
Chris@1706 1192 let val pull_result = pull context libname
Chris@1706 1193 in
Chris@1706 1194 case hg_command context libname ["update", branch_name branch] of
Chris@1706 1195 ERROR e => ERROR e
Chris@1706 1196 | _ =>
Chris@1706 1197 case pull_result of
Chris@1706 1198 ERROR e => ERROR e
Chris@1706 1199 | _ => id_of context libname
Chris@1706 1200 end
Chris@1706 1201
Chris@1706 1202 fun update_to context (libname, "") =
Chris@1706 1203 ERROR "Non-empty id (tag or revision id) required for update_to"
Chris@1706 1204 | update_to context (libname, id) =
Chris@1706 1205 case hg_command context libname ["update", "-r" ^ id] of
Chris@1706 1206 OK () => id_of context libname
Chris@1706 1207 | ERROR _ =>
Chris@1706 1208 case pull context libname of
Chris@1706 1209 ERROR e => ERROR e
Chris@1706 1210 | _ =>
Chris@1706 1211 case hg_command context libname ["update", "-r" ^ id] of
Chris@1706 1212 ERROR e => ERROR e
Chris@1706 1213 | _ => id_of context libname
Chris@1706 1214
Chris@1706 1215 end
Chris@1706 1216
Chris@1706 1217 structure GitControl :> VCS_CONTROL = struct
Chris@1706 1218
Chris@1706 1219 (* With Git repos we always operate in detached HEAD state. Even
Chris@1706 1220 the master branch is checked out using the remote reference,
Chris@1706 1221 origin/master. *)
Chris@1706 1222
Chris@1706 1223 fun git_command context libname args =
Chris@1706 1224 FileBits.command context libname ("git" :: args)
Chris@1706 1225
Chris@1706 1226 fun git_command_output context libname args =
Chris@1706 1227 FileBits.command_output context libname ("git" :: args)
Chris@1706 1228
Chris@1706 1229 fun exists context libname =
Chris@1706 1230 OK (OS.FileSys.isDir (FileBits.subpath context libname ".git"))
Chris@1706 1231 handle _ => OK false
Chris@1706 1232
Chris@1706 1233 fun remote_for context (libname, source) =
Chris@1706 1234 Provider.remote_url context GIT source libname
Chris@1706 1235
Chris@1706 1236 fun branch_name branch = case branch of
Chris@1706 1237 DEFAULT_BRANCH => "master"
Chris@1706 1238 | BRANCH "" => "master"
Chris@1706 1239 | BRANCH b => b
Chris@1706 1240
Chris@1706 1241 fun remote_branch_name branch = "origin/" ^ branch_name branch
Chris@1706 1242
Chris@1706 1243 fun checkout context (libname, source, branch) =
Chris@1706 1244 let val url = remote_for context (libname, source)
Chris@1706 1245 in
Chris@1706 1246 case FileBits.mkpath (FileBits.extpath context) of
Chris@1706 1247 OK () => git_command context ""
Chris@1706 1248 ["clone", "-b",
Chris@1706 1249 branch_name branch,
Chris@1706 1250 url, libname]
Chris@1706 1251 | ERROR e => ERROR e
Chris@1706 1252 end
Chris@1706 1253
Chris@1706 1254 (* NB git rev-parse HEAD shows revision id of current checkout;
Chris@1706 1255 git rev-list -1 <tag> shows revision id of revision with that tag *)
Chris@1706 1256
Chris@1706 1257 fun id_of context libname =
Chris@1706 1258 git_command_output context libname ["rev-parse", "HEAD"]
Chris@1706 1259
Chris@1706 1260 fun is_at context (libname, id_or_tag) =
Chris@1706 1261 case id_of context libname of
Chris@1706 1262 ERROR e => ERROR e
Chris@1706 1263 | OK id =>
Chris@1706 1264 if String.isPrefix id_or_tag id orelse
Chris@1706 1265 String.isPrefix id id_or_tag
Chris@1706 1266 then OK true
Chris@1706 1267 else
Chris@1706 1268 case git_command_output context libname
Chris@1706 1269 ["rev-list", "-1", id_or_tag] of
Chris@1706 1270 ERROR e => ERROR e
Chris@1706 1271 | OK tid =>
Chris@1706 1272 OK (tid = id andalso
Chris@1706 1273 tid <> id_or_tag) (* else id_or_tag was id not tag *)
Chris@1706 1274
Chris@1706 1275 fun branch_tip context (libname, branch) =
Chris@1706 1276 git_command_output context libname
Chris@1706 1277 ["rev-list", "-1",
Chris@1706 1278 remote_branch_name branch]
Chris@1706 1279
Chris@1706 1280 fun is_newest_locally context (libname, branch) =
Chris@1706 1281 case branch_tip context (libname, branch) of
Chris@1706 1282 ERROR e => ERROR e
Chris@1706 1283 | OK rev => is_at context (libname, rev)
Chris@1706 1284
Chris@1706 1285 fun is_on_branch context (libname, branch) =
Chris@1706 1286 case branch_tip context (libname, branch) of
Chris@1706 1287 ERROR e => ERROR e
Chris@1706 1288 | OK rev =>
Chris@1706 1289 case is_at context (libname, rev) of
Chris@1706 1290 ERROR e => ERROR e
Chris@1706 1291 | OK true => OK true
Chris@1706 1292 | OK false =>
Chris@1706 1293 case git_command context libname
Chris@1706 1294 ["merge-base", "--is-ancestor",
Chris@1706 1295 "HEAD", remote_branch_name branch] of
Chris@1706 1296 ERROR e => OK false (* cmd returns non-zero for no *)
Chris@1706 1297 | _ => OK true
Chris@1706 1298
Chris@1706 1299 fun is_newest context (libname, branch) =
Chris@1706 1300 case is_newest_locally context (libname, branch) of
Chris@1706 1301 ERROR e => ERROR e
Chris@1706 1302 | OK false => OK false
Chris@1706 1303 | OK true =>
Chris@1706 1304 case git_command context libname ["fetch"] of
Chris@1706 1305 ERROR e => ERROR e
Chris@1706 1306 | _ => is_newest_locally context (libname, branch)
Chris@1706 1307
Chris@1706 1308 fun is_modified_locally context libname =
Chris@1706 1309 case git_command_output context libname ["status", "--porcelain"] of
Chris@1706 1310 ERROR e => ERROR e
Chris@1706 1311 | OK "" => OK false
Chris@1706 1312 | OK _ => OK true
Chris@1706 1313
Chris@1706 1314 (* This function updates to the latest revision on a branch rather
Chris@1706 1315 than to a specific id or tag. We can't just checkout the given
Chris@1706 1316 branch, as that will succeed even if the branch isn't up to
Chris@1706 1317 date. We could checkout the branch and then fetch and merge,
Chris@1706 1318 but it's perhaps cleaner not to maintain a local branch at all,
Chris@1706 1319 but instead checkout the remote branch as a detached head. *)
Chris@1706 1320
Chris@1706 1321 fun update context (libname, branch) =
Chris@1706 1322 case git_command context libname ["fetch"] of
Chris@1706 1323 ERROR e => ERROR e
Chris@1706 1324 | _ =>
Chris@1706 1325 case git_command context libname ["checkout", "--detach",
Chris@1706 1326 remote_branch_name branch] of
Chris@1706 1327 ERROR e => ERROR e
Chris@1706 1328 | _ => id_of context libname
Chris@1706 1329
Chris@1706 1330 (* This function is dealing with a specific id or tag, so if we
Chris@1706 1331 can successfully check it out (detached) then that's all we need
Chris@1706 1332 to do. Otherwise we need to fetch and try again *)
Chris@1706 1333
Chris@1706 1334 fun update_to context (libname, "") =
Chris@1706 1335 ERROR "Non-empty id (tag or revision id) required for update_to"
Chris@1706 1336 | update_to context (libname, id) =
Chris@1706 1337 case git_command context libname ["checkout", "--detach", id] of
Chris@1706 1338 OK () => id_of context libname
Chris@1706 1339 | ERROR _ =>
Chris@1706 1340 case git_command context libname ["fetch"] of
Chris@1706 1341 ERROR e => ERROR e
Chris@1706 1342 | _ =>
Chris@1706 1343 case git_command context libname ["checkout", "--detach", id] of
Chris@1706 1344 ERROR e => ERROR e
Chris@1706 1345 | _ => id_of context libname
Chris@1706 1346 end
Chris@1706 1347
Chris@1706 1348 structure AnyLibControl :> LIB_CONTROL = struct
Chris@1706 1349
Chris@1706 1350 structure H = LibControlFn(HgControl)
Chris@1706 1351 structure G = LibControlFn(GitControl)
Chris@1706 1352
Chris@1706 1353 fun review context (spec as { vcs, ... } : libspec) =
Chris@1706 1354 (fn HG => H.review | GIT => G.review) vcs context spec
Chris@1706 1355
Chris@1706 1356 fun status context (spec as { vcs, ... } : libspec) =
Chris@1706 1357 (fn HG => H.status | GIT => G.status) vcs context spec
Chris@1706 1358
Chris@1706 1359 fun update context (spec as { vcs, ... } : libspec) =
Chris@1706 1360 (fn HG => H.update | GIT => G.update) vcs context spec
Chris@1706 1361 end
Chris@1706 1362
Chris@1706 1363 fun load_libspec spec_json lock_json libname : libspec =
Chris@1706 1364 let open JsonBits
Chris@1706 1365 val libobj = lookup_mandatory spec_json ["libs", libname]
Chris@1706 1366 val vcs = lookup_mandatory_string libobj ["vcs"]
Chris@1706 1367 val retrieve = lookup_optional_string libobj
Chris@1706 1368 val service = retrieve ["service"]
Chris@1706 1369 val owner = retrieve ["owner"]
Chris@1706 1370 val repo = retrieve ["repository"]
Chris@1706 1371 val url = retrieve ["url"]
Chris@1706 1372 val branch = retrieve ["branch"]
Chris@1706 1373 val user_pin = retrieve ["pin"]
Chris@1706 1374 val lock_pin = case lookup_optional lock_json ["libs", libname] of
Chris@1706 1375 SOME ll => lookup_optional_string ll ["pin"]
Chris@1706 1376 | NONE => NONE
Chris@1706 1377 in
Chris@1706 1378 {
Chris@1706 1379 libname = libname,
Chris@1706 1380 vcs = case vcs of
Chris@1706 1381 "hg" => HG
Chris@1706 1382 | "git" => GIT
Chris@1706 1383 | other => raise Fail ("Unknown version-control system \"" ^
Chris@1706 1384 other ^ "\""),
Chris@1706 1385 source = case (url, service, owner, repo) of
Chris@1706 1386 (SOME u, NONE, _, _) => URL u
Chris@1706 1387 | (NONE, SOME ss, owner, repo) =>
Chris@1706 1388 PROVIDER { service = ss, owner = owner, repo = repo }
Chris@1706 1389 | _ => raise Fail ("Must have exactly one of service " ^
Chris@1706 1390 "or url string"),
Chris@1706 1391 pin = case lock_pin of
Chris@1706 1392 SOME p => PINNED p
Chris@1706 1393 | NONE =>
Chris@1706 1394 case user_pin of
Chris@1706 1395 SOME p => PINNED p
Chris@1706 1396 | NONE => UNPINNED,
Chris@1706 1397 branch = case branch of
Chris@1706 1398 SOME b => BRANCH b
Chris@1706 1399 | NONE => DEFAULT_BRANCH
Chris@1706 1400 }
Chris@1706 1401 end
Chris@1706 1402
Chris@1706 1403 fun load_userconfig () : userconfig =
Chris@1706 1404 let val home = FileBits.homedir ()
Chris@1706 1405 val conf_json =
Chris@1706 1406 JsonBits.load_json_from
Chris@1706 1407 (OS.Path.joinDirFile {
Chris@1706 1408 dir = home,
Chris@1706 1409 file = VextFilenames.user_config_file })
Chris@1706 1410 handle IO.Io _ => Json.OBJECT []
Chris@1706 1411 in
Chris@1706 1412 {
Chris@1706 1413 accounts = case JsonBits.lookup_optional conf_json ["accounts"] of
Chris@1706 1414 NONE => []
Chris@1706 1415 | SOME (Json.OBJECT aa) =>
Chris@1706 1416 map (fn (k, (Json.STRING v)) =>
Chris@1706 1417 { service = k, login = v }
Chris@1706 1418 | _ => raise Fail
Chris@1706 1419 "String expected for account name")
Chris@1706 1420 aa
Chris@1706 1421 | _ => raise Fail "Array expected for accounts",
Chris@1706 1422 providers = Provider.load_providers conf_json
Chris@1706 1423 }
Chris@1706 1424 end
Chris@1706 1425
Chris@1706 1426 fun load_project (userconfig : userconfig) rootpath use_locks : project =
Chris@1706 1427 let val spec_file = FileBits.project_spec_path rootpath
Chris@1706 1428 val lock_file = FileBits.project_lock_path rootpath
Chris@1706 1429 val _ = if OS.FileSys.access (spec_file, [OS.FileSys.A_READ])
Chris@1706 1430 handle OS.SysErr _ => false
Chris@1706 1431 then ()
Chris@1706 1432 else raise Fail ("Failed to open project spec file " ^
Chris@1706 1433 (VextFilenames.project_file) ^ " in " ^
Chris@1706 1434 rootpath ^
Chris@1706 1435 ".\nPlease ensure the spec file is in the " ^
Chris@1706 1436 "project root and run this from there.")
Chris@1706 1437 val spec_json = JsonBits.load_json_from spec_file
Chris@1706 1438 val lock_json = if use_locks
Chris@1706 1439 then JsonBits.load_json_from lock_file
Chris@1706 1440 handle IO.Io _ => Json.OBJECT []
Chris@1706 1441 else Json.OBJECT []
Chris@1706 1442 val extdir = JsonBits.lookup_mandatory_string spec_json
Chris@1706 1443 ["config", "extdir"]
Chris@1706 1444 val spec_libs = JsonBits.lookup_optional spec_json ["libs"]
Chris@1706 1445 val lock_libs = JsonBits.lookup_optional lock_json ["libs"]
Chris@1706 1446 val providers = Provider.load_more_providers
Chris@1706 1447 (#providers userconfig) spec_json
Chris@1706 1448 val libnames = case spec_libs of
Chris@1706 1449 NONE => []
Chris@1706 1450 | SOME (Json.OBJECT ll) => map (fn (k, v) => k) ll
Chris@1706 1451 | _ => raise Fail "Object expected for libs"
Chris@1706 1452 in
Chris@1706 1453 {
Chris@1706 1454 context = {
Chris@1706 1455 rootpath = rootpath,
Chris@1706 1456 extdir = extdir,
Chris@1706 1457 providers = providers,
Chris@1706 1458 accounts = #accounts userconfig
Chris@1706 1459 },
Chris@1706 1460 libs = map (load_libspec spec_json lock_json) libnames
Chris@1706 1461 }
Chris@1706 1462 end
Chris@1706 1463
Chris@1706 1464 fun save_lock_file rootpath locks =
Chris@1706 1465 let val lock_file = FileBits.project_lock_path rootpath
Chris@1706 1466 open Json
Chris@1706 1467 val lock_json =
Chris@1706 1468 OBJECT [
Chris@1706 1469 ("libs", OBJECT
Chris@1706 1470 (map (fn { libname, id_or_tag } =>
Chris@1706 1471 (libname,
Chris@1706 1472 OBJECT [ ("pin", STRING id_or_tag) ]))
Chris@1706 1473 locks))
Chris@1706 1474 ]
Chris@1706 1475 in
Chris@1706 1476 JsonBits.save_json_to lock_file lock_json
Chris@1706 1477 end
Chris@1706 1478
Chris@1706 1479 fun pad_to n str =
Chris@1706 1480 if n <= String.size str then str
Chris@1706 1481 else pad_to n (str ^ " ")
Chris@1706 1482
Chris@1706 1483 fun hline_to 0 = ""
Chris@1706 1484 | hline_to n = "-" ^ hline_to (n-1)
Chris@1706 1485
Chris@1706 1486 val libname_width = 25
Chris@1706 1487 val libstate_width = 11
Chris@1706 1488 val localstate_width = 9
Chris@1706 1489 val notes_width = 5
Chris@1706 1490 val divider = " | "
Chris@1706 1491
Chris@1706 1492 fun print_status_header () =
Chris@1706 1493 print ("\r" ^ pad_to 80 "" ^ "\n " ^
Chris@1706 1494 pad_to libname_width "Library" ^ divider ^
Chris@1706 1495 pad_to libstate_width "State" ^ divider ^
Chris@1706 1496 pad_to localstate_width "Local" ^ divider ^
Chris@1706 1497 "Notes" ^ "\n " ^
Chris@1706 1498 hline_to libname_width ^ "-+-" ^
Chris@1706 1499 hline_to libstate_width ^ "-+-" ^
Chris@1706 1500 hline_to localstate_width ^ "-+-" ^
Chris@1706 1501 hline_to notes_width ^ "\n")
Chris@1706 1502
Chris@1706 1503 fun print_outcome_header () =
Chris@1706 1504 print ("\r" ^ pad_to 80 "" ^ "\n " ^
Chris@1706 1505 pad_to libname_width "Library" ^ divider ^
Chris@1706 1506 pad_to libstate_width "Outcome" ^ divider ^
Chris@1706 1507 "Notes" ^ "\n " ^
Chris@1706 1508 hline_to libname_width ^ "-+-" ^
Chris@1706 1509 hline_to libstate_width ^ "-+-" ^
Chris@1706 1510 hline_to notes_width ^ "\n")
Chris@1706 1511
Chris@1706 1512 fun print_status with_network (libname, status) =
Chris@1706 1513 let val libstate_str =
Chris@1706 1514 case status of
Chris@1706 1515 OK (ABSENT, _) => "Absent"
Chris@1706 1516 | OK (CORRECT, _) => if with_network then "Correct" else "Present"
Chris@1706 1517 | OK (SUPERSEDED, _) => "Superseded"
Chris@1706 1518 | OK (WRONG, _) => "Wrong"
Chris@1706 1519 | ERROR _ => "Error"
Chris@1706 1520 val localstate_str =
Chris@1706 1521 case status of
Chris@1706 1522 OK (_, MODIFIED) => "Modified"
Chris@1706 1523 | OK (_, UNMODIFIED) => "Clean"
Chris@1706 1524 | _ => ""
Chris@1706 1525 val error_str =
Chris@1706 1526 case status of
Chris@1706 1527 ERROR e => e
Chris@1706 1528 | _ => ""
Chris@1706 1529 in
Chris@1706 1530 print (" " ^
Chris@1706 1531 pad_to libname_width libname ^ divider ^
Chris@1706 1532 pad_to libstate_width libstate_str ^ divider ^
Chris@1706 1533 pad_to localstate_width localstate_str ^ divider ^
Chris@1706 1534 error_str ^ "\n")
Chris@1706 1535 end
Chris@1706 1536
Chris@1706 1537 fun print_update_outcome (libname, outcome) =
Chris@1706 1538 let val outcome_str =
Chris@1706 1539 case outcome of
Chris@1706 1540 OK id => "Ok"
Chris@1706 1541 | ERROR e => "Failed"
Chris@1706 1542 val error_str =
Chris@1706 1543 case outcome of
Chris@1706 1544 ERROR e => e
Chris@1706 1545 | _ => ""
Chris@1706 1546 in
Chris@1706 1547 print (" " ^
Chris@1706 1548 pad_to libname_width libname ^ divider ^
Chris@1706 1549 pad_to libstate_width outcome_str ^ divider ^
Chris@1706 1550 error_str ^ "\n")
Chris@1706 1551 end
Chris@1706 1552
Chris@1706 1553 fun act_and_print action print_header print_line (libs : libspec list) =
Chris@1706 1554 let val lines = map (fn lib => (#libname lib, action lib)) libs
Chris@1706 1555 val _ = print_header ()
Chris@1706 1556 in
Chris@1706 1557 app print_line lines;
Chris@1706 1558 lines
Chris@1706 1559 end
Chris@1708 1560
Chris@1708 1561 fun return_code_for outcomes =
Chris@1708 1562 foldl (fn ((_, result), acc) =>
Chris@1708 1563 case result of
Chris@1708 1564 ERROR _ => OS.Process.failure
Chris@1708 1565 | _ => acc)
Chris@1708 1566 OS.Process.success
Chris@1708 1567 outcomes
Chris@1706 1568
Chris@1706 1569 fun status_of_project ({ context, libs } : project) =
Chris@1708 1570 return_code_for (act_and_print (AnyLibControl.status context)
Chris@1708 1571 print_status_header (print_status false)
Chris@1708 1572 libs)
Chris@1706 1573
Chris@1706 1574 fun review_project ({ context, libs } : project) =
Chris@1708 1575 return_code_for (act_and_print (AnyLibControl.review context)
Chris@1708 1576 print_status_header (print_status true)
Chris@1708 1577 libs)
Chris@1706 1578
Chris@1706 1579 fun update_project ({ context, libs } : project) =
Chris@1706 1580 let val outcomes = act_and_print
Chris@1706 1581 (AnyLibControl.update context)
Chris@1706 1582 print_outcome_header print_update_outcome libs
Chris@1708 1583 val locks =
Chris@1708 1584 List.concat
Chris@1708 1585 (map (fn (libname, result) =>
Chris@1708 1586 case result of
Chris@1708 1587 ERROR _ => []
Chris@1708 1588 | OK id => [{ libname = libname, id_or_tag = id }])
Chris@1708 1589 outcomes)
Chris@1708 1590 val return_code = return_code_for outcomes
Chris@1706 1591 in
Chris@1708 1592 if OS.Process.isSuccess return_code
Chris@1708 1593 then save_lock_file (#rootpath context) locks
Chris@1708 1594 else ();
Chris@1708 1595 return_code
Chris@1706 1596 end
Chris@1706 1597
Chris@1706 1598 fun load_local_project use_locks =
Chris@1706 1599 let val userconfig = load_userconfig ()
Chris@1706 1600 val rootpath = OS.FileSys.getDir ()
Chris@1706 1601 in
Chris@1706 1602 load_project userconfig rootpath use_locks
Chris@1706 1603 end
Chris@1706 1604
Chris@1706 1605 fun with_local_project use_locks f =
Chris@1708 1606 let val return_code = f (load_local_project use_locks)
Chris@1708 1607 handle e =>
Chris@1708 1608 (print ("Failed with exception: " ^
Chris@1708 1609 (exnMessage e) ^ "\n");
Chris@1708 1610 OS.Process.failure)
Chris@1708 1611 val _ = print "\n";
Chris@1708 1612 in
Chris@1708 1613 return_code
Chris@1708 1614 end
Chris@1706 1615
Chris@1706 1616 fun review () = with_local_project false review_project
Chris@1706 1617 fun status () = with_local_project false status_of_project
Chris@1706 1618 fun update () = with_local_project false update_project
Chris@1706 1619 fun install () = with_local_project true update_project
Chris@1706 1620
Chris@1706 1621 fun version () =
Chris@1708 1622 (print ("v" ^ vext_version ^ "\n");
Chris@1708 1623 OS.Process.success)
Chris@1706 1624
Chris@1706 1625 fun usage () =
Chris@1706 1626 (print "\nVext ";
Chris@1706 1627 version ();
Chris@1706 1628 print ("\nA simple manager for third-party source code dependencies.\n\n"
Chris@1706 1629 ^ "Usage:\n\n"
Chris@1706 1630 ^ " vext <command>\n\n"
Chris@1706 1631 ^ "where <command> is one of:\n\n"
Chris@1706 1632 ^ " review check configured libraries against their providers, and report\n"
Chris@1706 1633 ^ " status print quick report on local status only, without using network\n"
Chris@1706 1634 ^ " install update configured libraries according to project specs and lock file\n"
Chris@1706 1635 ^ " update update configured libraries and lock file according to project specs\n"
Chris@1708 1636 ^ " version print the Vext version number and exit\n\n");
Chris@1708 1637 OS.Process.failure)
Chris@1706 1638
Chris@1706 1639 fun vext args =
Chris@1708 1640 let val return_code =
Chris@1708 1641 case args of
Chris@1708 1642 ["review"] => review ()
Chris@1708 1643 | ["status"] => status ()
Chris@1708 1644 | ["install"] => install ()
Chris@1708 1645 | ["update"] => update ()
Chris@1708 1646 | ["version"] => version ()
Chris@1708 1647 | _ => usage ()
Chris@1708 1648 in
Chris@1708 1649 OS.Process.exit return_code;
Chris@1708 1650 ()
Chris@1708 1651 end
Chris@1706 1652
Chris@1706 1653 fun main () =
Chris@1706 1654 vext (CommandLine.arguments ())