annotate vext.sml @ 529:2cc8700975db

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