To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Revision:

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 ())