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