annotate vext.sml @ 303:523f8f1789b4

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