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