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