Revision 70:2d3e1d1f99c0

View differences:

.appveyor.yml
6 6
  - x86
7 7
  - x64
8 8

  
9
install:
10
  - cinst --allow-empty-checksums smlnj
11
  - ps: '"[hostfingerprints]" | Out-File -Encoding "ASCII" -Append $env:USERPROFILE\mercurial.ini'
12
  - ps: '"code.soundsoftware.ac.uk = 66:ef:e2:0e:e3:55:93:9a:33:aa:2a:e9:fe:be:21:c2:a2:8d:4f:f1" | Out-File -Encoding "ASCII" -Append $env:USERPROFILE\mercurial.ini'
13
  - ps: '"[hostsecurity]" | Out-File -Encoding "ASCII" -Append $env:USERPROFILE\mercurial.ini'
14
  - ps: '"code.soundsoftware.ac.uk = code.soundsoftware.ac.uk:fingerprints=sha256:64:75:f6:47:15:de:b4:51:ea:96:e2:f4:8a:f5:53:a5:11:c8:dd:82:73:5d:bd:54:18:cb:c8:9d:10:37:28:85" | Out-File -Encoding "ASCII" -Append $env:USERPROFILE\mercurial.ini'
15

  
16
before_build:
17
  - set PATH=%PATH%;C:\Program Files (x86)\SMLNJ\bin
18
  - vext install
19

  
9 20
build:
10 21
  project: build\vamp-plugin-tester.sln
11 22

  
.hgignore
12 12
*.VC.db
13 13
*.VC.opendb
14 14
*.filters
15
glob:.vext-*.bin
.hgsub
1
vamp-plugin-sdk = https://code.soundsoftware.ac.uk/hg/vamp-plugin-sdk
.hgsubstate
1
5d9af3140f050e99c9ac6c99290dcb55014baf37 vamp-plugin-sdk
Makefile
34 34
	TestDefaults.o \
35 35
	TestInitialise.o
36 36

  
37
vamp-plugin-tester:	$(OBJECTS) $(VAMP_OBJECTS)
37
vamp-plugin-tester:	vamp-plugin-sdk/README $(OBJECTS) $(VAMP_OBJECTS)
38 38
	$(CXX) $(OBJECTS) -o $@ $(LDFLAGS)
39 39

  
40
vamp-plugin-sdk/README:
41
	./vext install
42

  
40 43
clean:
41 44
	rm -f $(OBJECTS) $(VAMP_OBJECTS)
42 45

  
vext
1
#!/bin/bash
2

  
3
# Disable shellcheck warnings for useless-use-of-cat. UUOC is good
4
# practice, not bad: clearer, safer, less error-prone.
5
# shellcheck disable=SC2002
6

  
7
sml="$VEXT_SML"
8

  
9
set -eu
10

  
11
mydir=$(dirname "$0")
12
program="$mydir/vext.sml"
13

  
14
hasher=
15
local_install=
16
if [ -w "$mydir" ]; then
17
    if echo | sha256sum >/dev/null 2>&1 ; then
18
	hasher=sha256sum
19
        local_install=true
20
    elif echo | shasum >/dev/null 2>&1 ; then
21
	hasher=shasum
22
	local_install=true
23
    else
24
        echo "WARNING: sha256sum or shasum program not found" 1>&2
25
    fi
26
fi
27

  
28
if [ -n "$local_install" ]; then
29
    hash=$(echo "$sml" | cat "$program" - | $hasher | cut -c1-16)
30
    gen_sml=$mydir/.vext-$hash.sml
31
    gen_out=$mydir/.vext-$hash.bin
32
    trap 'rm -f $gen_sml' 0
33
else
34
    gen_sml=$(mktemp /tmp/vext-XXXXXXXX.sml)
35
    gen_out=$(mktemp /tmp/vext-XXXXXXXX.bin)
36
    trap 'rm -f $gen_sml $gen_out' 0
37
fi
38

  
39
if [ -x "$gen_out" ]; then
40
    exec "$gen_out" "$@"
41
fi
42

  
43
# We need one of Poly/ML, SML/NJ, or MLton. Since we're running a
44
# single-file SML program as if it were a script, our order of
45
# preference is based on startup speed, except in the local_install
46
# case where we retain a persistent binary.
47

  
48
if [ -z "$sml" ]; then
49
    if [ -n "$local_install" ] && mlton 2>&1 | grep -q 'MLton'; then
50
	sml="mlton"
51
    elif sml -h 2>&1 | grep -q 'Standard ML of New Jersey'; then
52
	sml="smlnj"
53
    # We would prefer Poly/ML to SML/NJ, except that Poly v5.7 has a
54
    # nasty bug that occasionally causes it to deadlock on startup.
55
    # That appears to be fixed in their repo, so we could promote it
56
    # up the order again at some point in future
57
    elif echo | poly -v 2>/dev/null | grep -q 'Poly/ML'; then
58
	sml="poly"
59
    elif mlton 2>&1 | grep -q 'MLton'; then
60
	sml="mlton"
61
    else cat 1>&2 <<EOF
62

  
63
ERROR: No supported SML compiler or interpreter found       
64
EOF
65
	cat 1>&2 <<EOF
66

  
67
  The Vext external source code manager needs a Standard ML (SML)
68
  compiler or interpreter to run.
69

  
70
  Please ensure you have one of the following SML implementations
71
  installed and present in your PATH, and try again.
72

  
73
    1. Standard ML of New Jersey
74
       - often found in a distribution package called: smlnj
75
       - executable name: sml
76

  
77
    2. Poly/ML
78
       - often found in a distribution package called: polyml
79
       - executable name: poly
80

  
81
    3. MLton
82
       - often found in a distribution package called: mlton
83
       - executable name: mlton
84

  
85
EOF
86
	exit 2
87
    fi
88
fi
89

  
90
arglist=""
91
for arg in "$@"; do
92
    if [ -n "$arglist" ]; then arglist="$arglist,"; fi
93
    if echo "$arg" | grep -q '["'"'"']' ; then
94
	arglist="$arglist\"usage\""
95
    else
96
	arglist="$arglist\"$arg\""
97
    fi
98
done
99

  
100
case "$sml" in
101
    poly)
102
        if [ -n "$local_install" ] && polyc --help >/dev/null 2>&1 ; then
103
            if [ ! -x "$gen_out" ]; then
104
                polyc -o "$gen_out" "$program"
105
            fi
106
	    "$gen_out" "$@"
107
        else
108
            echo 'use "'"$program"'"; vext ['"$arglist"'];' |
109
                poly -q --error-exit
110
        fi ;;
111
    mlton)
112
        if [ ! -x "$gen_out" ]; then
113
	    echo "[Precompiling Vext binary...]" 1>&2
114
	    echo "val _ = main ()" | cat "$program" - > "$gen_sml"
115
	    mlton -output "$gen_out" "$gen_sml"
116
        fi
117
	"$gen_out" "$@" ;;
118
    smlnj)
119
	cat "$program" | (
120
	    cat <<EOF
121
val smlrun__cp = 
122
    let val x = !Control.Print.out in
123
        Control.Print.out := { say = fn _ => (), flush = fn () => () };
124
        x
125
    end;
126
val smlrun__prev = ref "";
127
Control.Print.out := { 
128
    say = fn s => 
129
        (if String.isSubstring " Error" s
130
         then (Control.Print.out := smlrun__cp;
131
               (#say smlrun__cp) (!smlrun__prev);
132
               (#say smlrun__cp) s)
133
         else (smlrun__prev := s; ())),
134
    flush = fn s => ()
135
};
136
EOF
137
	    cat -
138
	    cat <<EOF
139
val _ = vext [$arglist];
140
val _ = OS.Process.exit (OS.Process.success);
141
EOF
142
            ) > "$gen_sml"
143
	CM_VERBOSE=false sml "$gen_sml" ;;
144
    *)
145
	echo "ERROR: Unknown SML implementation name: $sml" 1>&2;
146
	exit 2 ;;
147
esac
148
       
vext-lock.json
1
{
2
  "libraries": {
3
    "vamp-plugin-sdk": {
4
      "pin": "d129bf797f24"
5
    }
6
  }
7
}
vext-project.json
1
{
2
    "config": {
3
        "extdir": "."
4
    },
5
    "services": {
6
	"soundsoftware": {
7
	    "vcs": ["hg", "git"],
8
	    "anonymous": "https://code.soundsoftware.ac.uk/{vcs}/{repository}",
9
	    "authenticated": "https://{account}@code.soundsoftware.ac.uk/{vcs}/{repository}"
10
	}
11
    },
12
    "libraries": {
13
        "vamp-plugin-sdk": {
14
            "vcs": "hg",
15
            "service": "soundsoftware"
16
        }
17
    }
18
}
19

  
vext.bat
1
@echo off
2
PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dpn0.ps1' %*";
3

  
vext.ps1
1
<#
2

  
3
.SYNOPSIS
4
A simple manager for third-party source code dependencies.
5
Run "vext help" for more documentation.
6

  
7
#>
8

  
9
Set-StrictMode -Version 2.0
10
$ErrorActionPreference = "Stop"
11

  
12
$sml = $env:VEXT_SML
13

  
14
$mydir = Split-Path $MyInvocation.MyCommand.Path -Parent
15
$program = "$mydir/vext.sml"
16

  
17
# We need either Poly/ML or SML/NJ. No great preference as to which.
18

  
19
if (!$sml) {
20
    if (Get-Command "sml" -ErrorAction SilentlyContinue) {
21
       $sml = "smlnj"
22
    } elseif (Get-Command "polyml" -ErrorAction SilentlyContinue) {
23
       $sml = "poly"
24
    } else {
25
       echo @"
26

  
27
ERROR: No supported SML compiler or interpreter found       
28

  
29
  The Vext external source code manager needs a Standard ML (SML)
30
  compiler or interpreter to run.
31

  
32
  Please ensure you have one of the following SML implementations
33
  installed and present in your PATH, and try again.
34

  
35
    1. Standard ML of New Jersey
36
       - executable name: sml
37

  
38
    2. Poly/ML
39
       - executable name: polyml
40

  
41
"@
42
       exit 1
43
    }
44
}
45

  
46
if ($args -match "'""") {
47
    $arglist = '["usage"]'
48
} else {
49
    $arglist = '["' + ($args -join '","') + '"]'
50
}
51

  
52
if ($sml -eq "poly") {
53

  
54
    $program = $program -replace "\\","\\\\"
55
    echo "use ""$program""; vext $arglist" | polyml -q --error-exit | Out-Host
56

  
57
    if (-not $?) {
58
        exit $LastExitCode
59
    }
60

  
61
} elseif ($sml -eq "smlnj") {
62

  
63
    $lines = @(Get-Content $program)
64
    $lines = $lines -notmatch "val _ = main ()"
65

  
66
    $intro = @"
67
val smlrun__cp = 
68
    let val x = !Control.Print.out in
69
        Control.Print.out := { say = fn _ => (), flush = fn () => () };
70
        x
71
    end;
72
val smlrun__prev = ref "";
73
Control.Print.out := { 
74
    say = fn s => 
75
        (if String.isSubstring "Error" s orelse String.isSubstring "Fail" s
76
         then (Control.Print.out := smlrun__cp;
77
               (#say smlrun__cp) (!smlrun__prev);
78
               (#say smlrun__cp) s)
79
         else (smlrun__prev := s; ())),
80
    flush = fn s => ()
81
};
82
"@ -split "[\r\n]+"
83

  
84
    $outro = @"
85
val _ = vext $arglist;
86
val _ = OS.Process.exit (OS.Process.success);
87
"@ -split "[\r\n]+"
88

  
89
    $script = @()
90
    $script += $intro
91
    $script += $lines
92
    $script += $outro
93

  
94
    $tmpfile = ([System.IO.Path]::GetTempFileName()) -replace "[.]tmp",".sml"
95

  
96
    $script | Out-File -Encoding "ASCII" $tmpfile
97

  
98
    $env:CM_VERBOSE="false"
99

  
100
    sml $tmpfile
101

  
102
    if (-not $?) {
103
        del $tmpfile
104
        exit $LastExitCode
105
    }
106

  
107
    del $tmpfile
108

  
109
} else {
110

  
111
    "Unknown SML implementation name: $sml"
112
    exit 2
113
}
vext.sml
1
(*
2
    DO NOT EDIT THIS FILE.
3
    This file is automatically generated from the individual
4
    source files in the Vext repository.
5
*)
6

  
7
(* 
8
    Vext
9

  
10
    A simple manager for third-party source code dependencies
11

  
12
    Copyright 2018 Chris Cannam, Particular Programs Ltd,
13
    and Queen Mary, University of London
14

  
15
    Permission is hereby granted, free of charge, to any person
16
    obtaining a copy of this software and associated documentation
17
    files (the "Software"), to deal in the Software without
18
    restriction, including without limitation the rights to use, copy,
19
    modify, merge, publish, distribute, sublicense, and/or sell copies
20
    of the Software, and to permit persons to whom the Software is
21
    furnished to do so, subject to the following conditions:
22

  
23
    The above copyright notice and this permission notice shall be
24
    included in all copies or substantial portions of the Software.
25

  
26
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28
    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
30
    ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
31
    CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
32
    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33

  
34
    Except as contained in this notice, the names of Chris Cannam,
35
    Particular Programs Ltd, and Queen Mary, University of London
36
    shall not be used in advertising or otherwise to promote the sale,
37
    use or other dealings in this Software without prior written
38
    authorization.
39
*)
40

  
41
val vext_version = "0.9.95"
42

  
43

  
44
datatype vcs =
45
         HG |
46
         GIT |
47
         SVN
48

  
49
datatype source =
50
         URL_SOURCE of string |
51
         SERVICE_SOURCE of {
52
             service : string,
53
             owner : string option,
54
             repo : string option
55
         }
56

  
57
type id_or_tag = string
58

  
59
datatype pin =
60
         UNPINNED |
61
         PINNED of id_or_tag
62

  
63
datatype libstate =
64
         ABSENT |
65
         CORRECT |
66
         SUPERSEDED |
67
         WRONG
68

  
69
datatype localstate =
70
         MODIFIED |
71
         LOCK_MISMATCHED |
72
         CLEAN
73

  
74
datatype branch =
75
         BRANCH of string |
76
         DEFAULT_BRANCH
77
             
78
(* If we can recover from an error, for example by reporting failure
79
   for this one thing and going on to the next thing, then the error
80
   should usually be returned through a result type rather than an
81
   exception. *)
82
             
83
datatype 'a result =
84
         OK of 'a |
85
         ERROR of string
86

  
87
type libname = string
88

  
89
type libspec = {
90
    libname : libname,
91
    vcs : vcs,
92
    source : source,
93
    branch : branch,
94
    project_pin : pin,
95
    lock_pin : pin
96
}
97

  
98
type lock = {
99
    libname : libname,
100
    id_or_tag : id_or_tag
101
}
102

  
103
type remote_spec = {
104
    anon : string option,
105
    auth : string option
106
}
107

  
108
type provider = {
109
    service : string,
110
    supports : vcs list,
111
    remote_spec : remote_spec
112
}
113

  
114
type account = {
115
    service : string,
116
    login : string
117
}
118
                    
119
type context = {
120
    rootpath : string,
121
    extdir : string,
122
    providers : provider list,
123
    accounts : account list
124
}
125

  
126
type userconfig = {
127
    providers : provider list,
128
    accounts : account list
129
}
130
                   
131
type project = {
132
    context : context,
133
    libs : libspec list
134
}
135

  
136
structure VextFilenames = struct
137
    val project_file = "vext-project.json"
138
    val project_lock_file = "vext-lock.json"
139
    val user_config_file = ".vext.json"
140
    val archive_dir = ".vext-archive"
141
end
142
                   
143
signature VCS_CONTROL = sig
144

  
145
    (** Test whether the library is present locally at all *)
146
    val exists : context -> libname -> bool result
147
                                            
148
    (** Return the id (hash) of the current revision for the library *)
149
    val id_of : context -> libname -> id_or_tag result
150

  
151
    (** Test whether the library is at the given id *)
152
    val is_at : context -> libname * id_or_tag -> bool result
153

  
154
    (** Test whether the library is on the given branch, i.e. is at
155
        the branch tip or an ancestor of it *)
156
    val is_on_branch : context -> libname * branch -> bool result
157

  
158
    (** Test whether the library is at the newest revision for the
159
        given branch. False may indicate that the branch has advanced
160
        or that the library is not on the branch at all. This function
161
        may use the network to check for new revisions *)
162
    val is_newest : context -> libname * source * branch -> bool result
163

  
164
    (** Test whether the library is at the newest revision available
165
        locally for the given branch. False may indicate that the
166
        branch has advanced or that the library is not on the branch
167
        at all. This function must not use the network *)
168
    val is_newest_locally : context -> libname * branch -> bool result
169

  
170
    (** Test whether the library has been modified in the local
171
        working copy *)
172
    val is_modified_locally : context -> libname -> bool result
173

  
174
    (** Check out, i.e. clone a fresh copy of, the repo for the given
175
        library on the given branch *)
176
    val checkout : context -> libname * source * branch -> unit result
177

  
178
    (** Update the library to the given branch tip. Assumes that a
179
        local copy of the library already exists *)
180
    val update : context -> libname * source * branch -> unit result
181

  
182
    (** Update the library to the given specific id or tag *)
183
    val update_to : context -> libname * source * id_or_tag -> unit result
184

  
185
    (** Return a URL from which the library can be cloned, given that
186
        the local copy already exists. For a DVCS this can be the
187
        local copy, but for a centralised VCS it will have to be the
188
        remote repository URL. Used for archiving *)
189
    val copy_url_for : context -> libname -> string result
190
end
191

  
192
signature LIB_CONTROL = sig
193
    val review : context -> libspec -> (libstate * localstate) result
194
    val status : context -> libspec -> (libstate * localstate) result
195
    val update : context -> libspec -> unit result
196
    val id_of : context -> libspec -> id_or_tag result
197
end
198

  
199
structure FileBits :> sig
200
    val extpath : context -> string
201
    val libpath : context -> libname -> string
202
    val subpath : context -> libname -> string -> string
203
    val command_output : context -> libname -> string list -> string result
204
    val command : context -> libname -> string list -> unit result
205
    val file_url : string -> string
206
    val file_contents : string -> string
207
    val mydir : unit -> string
208
    val homedir : unit -> string
209
    val mkpath : string -> unit result
210
    val rmpath : string -> unit result
211
    val nonempty_dir_exists : string -> bool
212
    val project_spec_path : string -> string
213
    val project_lock_path : string -> string
214
    val verbose : unit -> bool
215
end = struct
216

  
217
    fun verbose () =
218
        case OS.Process.getEnv "VEXT_VERBOSE" of
219
            SOME "0" => false
220
          | SOME _ => true
221
          | NONE => false
222

  
223
    fun split_relative path desc =
224
        case OS.Path.fromString path of
225
            { isAbs = true, ... } => raise Fail (desc ^ " may not be absolute")
226
          | { arcs, ... } => arcs
227
                        
228
    fun extpath ({ rootpath, extdir, ... } : context) =
229
        let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
230
        in OS.Path.toString {
231
                isAbs = isAbs,
232
                vol = vol,
233
                arcs = arcs @
234
                       split_relative extdir "extdir"
235
            }
236
        end
237
    
238
    fun subpath ({ rootpath, extdir, ... } : context) libname remainder =
239
        (* NB libname is allowed to be a path fragment, e.g. foo/bar *)
240
        let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
241
        in OS.Path.toString {
242
                isAbs = isAbs,
243
                vol = vol,
244
                arcs = arcs @
245
                       split_relative extdir "extdir" @
246
                       split_relative libname "library path" @
247
                       split_relative remainder "subpath"
248
            }
249
        end
250

  
251
    fun libpath context "" =
252
        extpath context
253
      | libpath context libname =
254
        subpath context libname ""
255

  
256
    fun project_file_path rootpath filename =
257
        let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
258
        in OS.Path.toString {
259
                isAbs = isAbs,
260
                vol = vol,
261
                arcs = arcs @ [ filename ]
262
            }
263
        end
264
                
265
    fun project_spec_path rootpath =
266
        project_file_path rootpath (VextFilenames.project_file)
267

  
268
    fun project_lock_path rootpath =
269
        project_file_path rootpath (VextFilenames.project_lock_file)
270

  
271
    fun trim str =
272
        hd (String.fields (fn x => x = #"\n" orelse x = #"\r") str)
273
            
274
    fun file_url path =
275
        let val forward_path = 
276
                String.translate (fn #"\\" => "/" |
277
                                  c => Char.toString c)
278
                                 (OS.Path.mkCanonical path)
279
        in
280
            (* Path is expected to be absolute already, but if it
281
               starts with a drive letter, we'll need an extra slash *)
282
            case explode forward_path of
283
                #"/"::rest => "file:///" ^ implode rest
284
              | _ => "file:///" ^ forward_path
285
        end
286
        
287
    fun file_contents filename =
288
        let val stream = TextIO.openIn filename
289
            fun read_all str acc =
290
                case TextIO.inputLine str of
291
                    SOME line => read_all str (trim line :: acc)
292
                  | NONE => rev acc
293
            val contents = read_all stream []
294
            val _ = TextIO.closeIn stream
295
        in
296
            String.concatWith "\n" contents
297
        end
298

  
299
    fun expand_commandline cmdlist =
300
        (* We are quite [too] strict about what we accept here, except
301
           for the first element in cmdlist which is assumed to be a
302
           known command location rather than arbitrary user input. NB
303
           only ASCII accepted at this point. *)
304
        let open Char
305
            fun quote arg =
306
                if List.all
307
                       (fn c => isAlphaNum c orelse c = #"-" orelse c = #"_")
308
                       (explode arg)
309
                then arg
310
                else "\"" ^ arg ^ "\""
311
            fun check arg =
312
                let val valid = explode " /#:;?,._-{}@=+"
313
                in
314
                    app (fn c =>
315
                            if isAlphaNum c orelse
316
                               List.exists (fn v => v = c) valid orelse
317
                               c > chr 127
318
                            then ()
319
                            else raise Fail ("Invalid character '" ^
320
                                             (Char.toString c) ^
321
                                             "' in command list"))
322
                        (explode arg);
323
                    arg
324
                end
325
        in
326
            String.concatWith " "
327
                              (map quote
328
                                   (hd cmdlist :: map check (tl cmdlist)))
329
        end
330

  
331
    val tick_cycle = ref 0
332
    val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
333

  
334
    fun tick libname cmdlist =
335
        let val n = Vector.length tick_chars
336
            fun pad_to n str =
337
                if n <= String.size str then str
338
                else pad_to n (str ^ " ")
339
            val name = if libname <> "" then libname
340
                       else if cmdlist = nil then ""
341
                       else hd (rev cmdlist)
342
        in
343
            print ("  " ^
344
                   Vector.sub(tick_chars, !tick_cycle) ^ " " ^
345
                   pad_to 70 name ^
346
                   "\r");
347
            tick_cycle := (if !tick_cycle = n - 1 then 0 else 1 + !tick_cycle)
348
        end
349
            
350
    fun run_command context libname cmdlist redirect =
351
        let open OS
352
            val dir = libpath context libname
353
            val cmd = expand_commandline cmdlist
354
            val _ = if verbose ()
355
                    then print ("\n=== " ^ dir ^ "\n<<< " ^ cmd ^ "\n")
356
                    else tick libname cmdlist
357
            val _ = FileSys.chDir dir
358
            val status = case redirect of
359
                             NONE => Process.system cmd
360
                           | SOME file => Process.system (cmd ^ ">" ^ file)
361
        in
362
            if Process.isSuccess status
363
            then OK ()
364
            else ERROR ("Command failed: " ^ cmd ^ " (in dir " ^ dir ^ ")")
365
        end
366
        handle ex => ERROR ("Unable to run command: " ^ exnMessage ex)
367

  
368
    fun command context libname cmdlist =
369
        run_command context libname cmdlist NONE
370
            
371
    fun command_output context libname cmdlist =
372
        let open OS
373
            val tmpFile = FileSys.tmpName ()
374
            val result = run_command context libname cmdlist (SOME tmpFile)
375
            val contents = file_contents tmpFile
376
            val _ = if verbose ()
377
                    then print (">>> \"" ^ contents ^ "\"\n")
378
                    else ()
379
        in
380
            FileSys.remove tmpFile handle _ => ();
381
            case result of
382
                OK () => OK contents
383
              | ERROR e => ERROR e
384
        end
385

  
386
    fun mydir () =
387
        let open OS
388
            val { dir, file } = Path.splitDirFile (CommandLine.name ())
389
        in
390
            FileSys.realPath
391
                (if Path.isAbsolute dir
392
                 then dir
393
                 else Path.concat (FileSys.getDir (), dir))
394
        end
395

  
396
    fun homedir () =
397
        (* Failure is not routine, so we use an exception here *)
398
        case (OS.Process.getEnv "HOME",
399
              OS.Process.getEnv "HOMEPATH") of
400
            (SOME home, _) => home
401
          | (NONE, SOME home) => home
402
          | (NONE, NONE) =>
403
            raise Fail "Failed to look up home directory from environment"
404

  
405
    fun mkpath' path =
406
        if OS.FileSys.isDir path handle _ => false
407
        then OK ()
408
        else case OS.Path.fromString path of
409
                 { arcs = nil, ... } => OK ()
410
               | { isAbs = false, ... } => ERROR "mkpath requires absolute path"
411
               | { isAbs, vol, arcs } => 
412
                 case mkpath' (OS.Path.toString {      (* parent *)
413
                                    isAbs = isAbs,
414
                                    vol = vol,
415
                                    arcs = rev (tl (rev arcs)) }) of
416
                     ERROR e => ERROR e
417
                   | OK () => ((OS.FileSys.mkDir path; OK ())
418
                               handle OS.SysErr (e, _) =>
419
                                      ERROR ("Directory creation failed: " ^ e))
420

  
421
    fun mkpath path =
422
        mkpath' (OS.Path.mkCanonical path)
423

  
424
    fun dir_contents dir =
425
        let open OS
426
            fun files_from dirstream =
427
                case FileSys.readDir dirstream of
428
                    NONE => []
429
                  | SOME file =>
430
                    (* readDir is supposed to filter these, 
431
                       but let's be extra cautious: *)
432
                    if file = Path.parentArc orelse file = Path.currentArc
433
                    then files_from dirstream
434
                    else file :: files_from dirstream
435
            val stream = FileSys.openDir dir
436
            val files = map (fn f => Path.joinDirFile
437
                                         { dir = dir, file = f })
438
                            (files_from stream)
439
            val _ = FileSys.closeDir stream
440
        in
441
            files
442
        end
443

  
444
    fun rmpath' path =
445
        let open OS
446
            fun remove path =
447
                if FileSys.isLink path (* dangling links bother isDir *)
448
                then FileSys.remove path
449
                else if FileSys.isDir path
450
                then (app remove (dir_contents path); FileSys.rmDir path)
451
                else FileSys.remove path
452
        in
453
            (remove path; OK ())
454
            handle SysErr (e, _) => ERROR ("Path removal failed: " ^ e)
455
        end
456

  
457
    fun rmpath path =
458
        rmpath' (OS.Path.mkCanonical path)
459

  
460
    fun nonempty_dir_exists path =
461
        let open OS.FileSys
462
        in
463
            (not (isLink path) andalso
464
             isDir path andalso
465
             dir_contents path <> [])
466
            handle _ => false
467
        end                                        
468
                
469
end
470
                                         
471
functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
472

  
473
    (* Valid states for unpinned libraries:
474

  
475
       - CORRECT: We are on the right branch and are up-to-date with
476
         it as far as we can tell. (If not using the network, this
477
         should be reported to user as "Present" rather than "Correct"
478
         as the remote repo may have advanced without us knowing.)
479

  
480
       - SUPERSEDED: We are on the right branch but we can see that
481
         there is a newer revision either locally or on the remote (in
482
         Git terms, we are at an ancestor of the desired branch tip).
483

  
484
       - WRONG: We are on the wrong branch (in Git terms, we are not
485
         at the desired branch tip or any ancestor of it).
486

  
487
       - ABSENT: Repo doesn't exist here at all.
488

  
489
       Valid states for pinned libraries:
490

  
491
       - CORRECT: We are at the pinned revision.
492

  
493
       - WRONG: We are at any revision other than the pinned one.
494

  
495
       - ABSENT: Repo doesn't exist here at all.
496
    *)
497

  
498
    fun check with_network context
499
              ({ libname, source, branch,
500
                 project_pin, lock_pin, ... } : libspec) =
501
        let fun check_unpinned () =
502
                let val newest =
503
                        if with_network
504
                        then V.is_newest context (libname, source, branch)
505
                        else V.is_newest_locally context (libname, branch)
506
                in
507
                    case newest of
508
                         ERROR e => ERROR e
509
                       | OK true => OK CORRECT
510
                       | OK false =>
511
                         case V.is_on_branch context (libname, branch) of
512
                             ERROR e => ERROR e
513
                           | OK true => OK SUPERSEDED
514
                           | OK false => OK WRONG
515
                end
516
            fun check_pinned target =
517
                case V.is_at context (libname, target) of
518
                    ERROR e => ERROR e
519
                  | OK true => OK CORRECT
520
                  | OK false => OK WRONG
521
            fun check_remote () =
522
                case project_pin of
523
                    UNPINNED => check_unpinned ()
524
                  | PINNED target => check_pinned target
525
            fun check_local () =
526
                case V.is_modified_locally context libname of
527
                    ERROR e => ERROR e
528
                  | OK true  => OK MODIFIED
529
                  | OK false => 
530
                    case lock_pin of
531
                        UNPINNED => OK CLEAN
532
                      | PINNED target =>
533
                        case V.is_at context (libname, target) of
534
                            ERROR e => ERROR e
535
                          | OK true => OK CLEAN
536
                          | OK false => OK LOCK_MISMATCHED
537
        in
538
            case V.exists context libname of
539
                ERROR e => ERROR e
540
              | OK false => OK (ABSENT, CLEAN)
541
              | OK true =>
542
                case (check_remote (), check_local ()) of
543
                    (ERROR e, _) => ERROR e
544
                  | (_, ERROR e) => ERROR e
545
                  | (OK r, OK l) => OK (r, l)
546
        end
547

  
548
    val review = check true
549
    val status = check false
550

  
551
    fun update context
552
               ({ libname, source, branch,
553
                  project_pin, lock_pin, ... } : libspec) =
554
        let fun update_unpinned () =
555
                case V.is_newest context (libname, source, branch) of
556
                    ERROR e => ERROR e
557
                  | OK true => OK ()
558
                  | OK false => V.update context (libname, source, branch)
559
            fun update_pinned target =
560
                case V.is_at context (libname, target) of
561
                    ERROR e => ERROR e
562
                  | OK true => OK ()
563
                  | OK false => V.update_to context (libname, source, target)
564
            fun update' () =
565
                case lock_pin of
566
                    PINNED target => update_pinned target
567
                  | UNPINNED =>
568
                    case project_pin of
569
                        PINNED target => update_pinned target
570
                      | UNPINNED => update_unpinned ()
571
        in
572
            case V.exists context libname of
573
                ERROR e => ERROR e
574
              | OK true => update' ()
575
              | OK false =>
576
                case V.checkout context (libname, source, branch) of
577
                    ERROR e => ERROR e
578
                  | OK () => update' ()
579
        end
580

  
581
    fun id_of context ({ libname, ... } : libspec) =
582
        V.id_of context libname
583
                
584
end
585

  
586
(* Simple Standard ML JSON parser
587
   https://bitbucket.org/cannam/sml-simplejson
588
   Copyright 2017 Chris Cannam. BSD licence.
589
   Parts based on the JSON parser in the Ponyo library by Phil Eaton.
590
*)
591

  
592
signature JSON = sig
593

  
594
    datatype json = OBJECT of (string * json) list
595
                  | ARRAY of json list
596
                  | NUMBER of real
597
                  | STRING of string
598
                  | BOOL of bool
599
                  | NULL
600

  
601
    datatype 'a result = OK of 'a
602
                       | ERROR of string
603

  
604
    val parse : string -> json result
605
    val serialise : json -> string
606
    val serialiseIndented : json -> string
607

  
608
end
609

  
610
structure Json :> JSON = struct
611

  
612
    datatype json = OBJECT of (string * json) list
613
                  | ARRAY of json list
614
                  | NUMBER of real
615
                  | STRING of string
616
                  | BOOL of bool
617
                  | NULL
618

  
619
    datatype 'a result = OK of 'a
620
                       | ERROR of string
621

  
622
    structure T = struct
623
        datatype token = NUMBER of char list
624
                       | STRING of string
625
                       | BOOL of bool
626
                       | NULL
627
                       | CURLY_L
628
                       | CURLY_R
629
                       | SQUARE_L
630
                       | SQUARE_R
631
                       | COLON
632
                       | COMMA
633

  
634
        fun toString t =
635
            case t of NUMBER digits => implode digits
636
                    | STRING s => s
637
                    | BOOL b => Bool.toString b
638
                    | NULL => "null"
639
                    | CURLY_L => "{"
640
                    | CURLY_R => "}"
641
                    | SQUARE_L => "["
642
                    | SQUARE_R => "]"
643
                    | COLON => ":"
644
                    | COMMA => ","
645
    end
646

  
647
    fun bmpToUtf8 cp =  (* convert a codepoint in Unicode BMP to utf8 bytes *)
648
        let open Word
649
	    infix 6 orb andb >>
650
        in
651
            map (Char.chr o toInt)
652
                (if cp < 0wx80 then
653
                     [cp]
654
                 else if cp < 0wx800 then
655
                     [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)]
656
                 else if cp < 0wx10000 then
657
                     [0wxe0 orb (cp >> 0w12),
658
                      0wx80 orb ((cp >> 0w6) andb 0wx3f),
659
		      0wx80 orb (cp andb 0wx3f)]
660
                 else raise Fail ("Invalid BMP point " ^ (Word.toString cp)))
661
        end
662
                      
663
    fun error pos text = ERROR (text ^ " at character position " ^
664
                                Int.toString (pos - 1))
665
    fun token_error pos = error pos ("Unexpected token")
666

  
667
    fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
668
        lex (pos + 3) (T.NULL :: acc) xs
669
      | lexNull pos acc _ = token_error pos
670

  
671
    and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
672
        lex (pos + 3) (T.BOOL true :: acc) xs
673
      | lexTrue pos acc _ = token_error pos
674

  
675
    and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
676
        lex (pos + 4) (T.BOOL false :: acc) xs
677
      | lexFalse pos acc _ = token_error pos
678

  
679
    and lexChar tok pos acc xs =
680
        lex pos (tok :: acc) xs
681
        
682
    and lexString pos acc cc =
683
        let datatype escaped = ESCAPED | NORMAL
684
            fun lexString' pos text ESCAPED [] =
685
                error pos "End of input during escape sequence"
686
              | lexString' pos text NORMAL [] = 
687
                error pos "End of input during string"
688
              | lexString' pos text ESCAPED (x :: xs) =
689
                let fun esc c = lexString' (pos + 1) (c :: text) NORMAL xs
690
                in case x of
691
                       #"\"" => esc x
692
                     | #"\\" => esc x
693
                     | #"/"  => esc x
694
                     | #"b"  => esc #"\b"
695
                     | #"f"  => esc #"\f"
696
                     | #"n"  => esc #"\n"
697
                     | #"r"  => esc #"\r"
698
                     | #"t"  => esc #"\t"
699
                     | _     => error pos ("Invalid escape \\" ^
700
                                           Char.toString x)
701
                end
702
              | lexString' pos text NORMAL (#"\\" :: #"u" ::a::b::c::d:: xs) =
703
                if List.all Char.isHexDigit [a,b,c,d]
704
                then case Word.fromString ("0wx" ^ (implode [a,b,c,d])) of
705
                         SOME w => (let val utf = rev (bmpToUtf8 w) in
706
                                        lexString' (pos + 6) (utf @ text)
707
                                                   NORMAL xs
708
                                    end
709
                                    handle Fail err => error pos err)
710
                       | NONE => error pos "Invalid Unicode BMP escape sequence"
711
                else error pos "Invalid Unicode BMP escape sequence"
712
              | lexString' pos text NORMAL (x :: xs) =
713
                if Char.ord x < 0x20
714
                then error pos "Invalid unescaped control character"
715
                else
716
                    case x of
717
                        #"\"" => OK (rev text, xs, pos + 1)
718
                      | #"\\" => lexString' (pos + 1) text ESCAPED xs
719
                      | _     => lexString' (pos + 1) (x :: text) NORMAL xs
720
        in
721
            case lexString' pos [] NORMAL cc of
722
                OK (text, rest, newpos) =>
723
                lex newpos (T.STRING (implode text) :: acc) rest
724
              | ERROR e => ERROR e
725
        end
726

  
727
    and lexNumber firstChar pos acc cc =
728
        let val valid = explode ".+-e"
729
            fun lexNumber' pos digits [] = (rev digits, [], pos)
730
              | lexNumber' pos digits (x :: xs) =
731
                if x = #"E" then lexNumber' (pos + 1) (#"e" :: digits) xs
732
                else if Char.isDigit x orelse List.exists (fn c => x = c) valid
733
                then lexNumber' (pos + 1) (x :: digits) xs
734
                else (rev digits, x :: xs, pos)
735
            val (digits, rest, newpos) =
736
                lexNumber' (pos - 1) [] (firstChar :: cc)
737
        in
738
            case digits of
739
                [] => token_error pos
740
              | _ => lex newpos (T.NUMBER digits :: acc) rest
741
        end
742
                                           
743
    and lex pos acc [] = OK (rev acc)
744
      | lex pos acc (x::xs) = 
745
        (case x of
746
             #" "  => lex
747
           | #"\t" => lex
748
           | #"\n" => lex
749
           | #"\r" => lex
750
           | #"{"  => lexChar T.CURLY_L
751
           | #"}"  => lexChar T.CURLY_R
752
           | #"["  => lexChar T.SQUARE_L
753
           | #"]"  => lexChar T.SQUARE_R
754
           | #":"  => lexChar T.COLON
755
           | #","  => lexChar T.COMMA
756
           | #"\"" => lexString
757
           | #"t"  => lexTrue
758
           | #"f"  => lexFalse
759
           | #"n"  => lexNull
760
           | x     => lexNumber x) (pos + 1) acc xs
761

  
762
    fun show [] = "end of input"
763
      | show (tok :: _) = T.toString tok
764

  
765
    fun parseNumber digits =
766
        (* Note lexNumber already case-insensitised the E for us *)
767
        let open Char
768

  
769
            fun okExpDigits [] = false
770
              | okExpDigits (c :: []) = isDigit c
771
              | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
772

  
773
            fun okExponent [] = false
774
              | okExponent (#"+" :: cs) = okExpDigits cs
775
              | okExponent (#"-" :: cs) = okExpDigits cs
776
              | okExponent cc = okExpDigits cc
777

  
778
            fun okFracTrailing [] = true
779
              | okFracTrailing (c :: cs) =
780
                (isDigit c andalso okFracTrailing cs) orelse
781
                (c = #"e" andalso okExponent cs)
782

  
783
            fun okFraction [] = false
784
              | okFraction (c :: cs) =
785
                isDigit c andalso okFracTrailing cs
786

  
787
            fun okPosTrailing [] = true
788
              | okPosTrailing (#"." :: cs) = okFraction cs
789
              | okPosTrailing (#"e" :: cs) = okExponent cs
790
              | okPosTrailing (c :: cs) =
791
                isDigit c andalso okPosTrailing cs
792
                                                      
793
            fun okPositive [] = false
794
              | okPositive (#"0" :: []) = true
795
              | okPositive (#"0" :: #"." :: cs) = okFraction cs
796
              | okPositive (#"0" :: #"e" :: cs) = okExponent cs
797
              | okPositive (#"0" :: cs) = false
798
              | okPositive (c :: cs) = isDigit c andalso okPosTrailing cs
799
                    
800
            fun okNumber (#"-" :: cs) = okPositive cs
801
              | okNumber cc = okPositive cc
802
        in
803
            if okNumber digits
804
            then case Real.fromString (implode digits) of
805
                     NONE => ERROR "Number out of range"
806
                   | SOME r => OK r
807
            else ERROR ("Invalid number \"" ^ (implode digits) ^ "\"")
808
        end
809
                                     
810
    fun parseObject (T.CURLY_R :: xs) = OK (OBJECT [], xs)
811
      | parseObject tokens =
812
        let fun parsePair (T.STRING key :: T.COLON :: xs) =
813
                (case parseTokens xs of
814
                     ERROR e => ERROR e
815
                   | OK (j, xs) => OK ((key, j), xs))
816
              | parsePair other =
817
                ERROR ("Object key/value pair expected around \"" ^
818
                       show other ^ "\"")
819
            fun parseObject' acc [] = ERROR "End of input during object"
820
              | parseObject' acc tokens =
821
                case parsePair tokens of
822
                    ERROR e => ERROR e
823
                  | OK (pair, T.COMMA :: xs) =>
824
                    parseObject' (pair :: acc) xs
825
                  | OK (pair, T.CURLY_R :: xs) =>
826
                    OK (OBJECT (rev (pair :: acc)), xs)
827
                  | OK (_, _) => ERROR "Expected , or } after object element"
828
        in
829
            parseObject' [] tokens
830
        end
831

  
832
    and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs)
833
      | parseArray tokens =
834
        let fun parseArray' acc [] = ERROR "End of input during array"
835
              | parseArray' acc tokens =
836
                case parseTokens tokens of
837
                    ERROR e => ERROR e
838
                  | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs
839
                  | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs)
840
                  | OK (_, _) => ERROR "Expected , or ] after array element"
841
        in
842
            parseArray' [] tokens
843
        end
844

  
845
    and parseTokens [] = ERROR "Value expected"
846
      | parseTokens (tok :: xs) =
847
        (case tok of
848
             T.NUMBER d => (case parseNumber d of
849
                                OK r => OK (NUMBER r, xs)
850
                              | ERROR e => ERROR e)
851
           | T.STRING s => OK (STRING s, xs)
852
           | T.BOOL b   => OK (BOOL b, xs)
853
           | T.NULL     => OK (NULL, xs)
854
           | T.CURLY_L  => parseObject xs
855
           | T.SQUARE_L => parseArray xs
856
           | _ => ERROR ("Unexpected token " ^ T.toString tok ^
857
                         " before " ^ show xs))
858
                                   
859
    fun parse str =
860
        case lex 1 [] (explode str) of
861
           ERROR e => ERROR e
862
         | OK tokens => case parseTokens tokens of
863
                            OK (value, []) => OK value
864
                          | OK (_, _) => ERROR "Extra data after input"
865
                          | ERROR e => ERROR e
866

  
867
    fun stringEscape s =
868
        let fun esc x = [x, #"\\"]
869
            fun escape' acc [] = rev acc
870
              | escape' acc (x :: xs) =
871
                escape' (case x of
872
                             #"\"" => esc x @ acc
873
                           | #"\\" => esc x @ acc
874
                           | #"\b" => esc #"b" @ acc
875
                           | #"\f" => esc #"f" @ acc
876
                           | #"\n" => esc #"n" @ acc
877
                           | #"\r" => esc #"r" @ acc
878
                           | #"\t" => esc #"t" @ acc
879
                           | _ =>
880
                             let val c = Char.ord x
881
                             in
882
                                 if c < 0x20
883
                                 then let val hex = Word.toString (Word.fromInt c)
884
                                      in (rev o explode) (if c < 0x10
885
                                                          then ("\\u000" ^ hex)
886
                                                          else ("\\u00" ^ hex))
887
                                      end @ acc
888
                                 else 
889
                                     x :: acc
890
                             end)
891
                        xs
892
        in
893
            implode (escape' [] (explode s))
894
        end
895
        
896
    fun serialise json =
897
        case json of
898
            OBJECT pp => "{" ^ String.concatWith
899
                                   "," (map (fn (key, value) =>
900
                                                serialise (STRING key) ^ ":" ^
901
                                                serialise value) pp) ^
902
                         "}"
903
          | ARRAY arr => "[" ^ String.concatWith "," (map serialise arr) ^ "]"
904
          | NUMBER n => implode (map (fn #"~" => #"-" | c => c) 
905
                                     (explode (Real.toString n)))
906
          | STRING s => "\"" ^ stringEscape s ^ "\""
907
          | BOOL b => Bool.toString b
908
          | NULL => "null"
909
        
910
    fun serialiseIndented json =
911
        let fun indent 0 = ""
912
              | indent i = "  " ^ indent (i - 1)
913
            fun serialiseIndented' i json =
914
                let val ser = serialiseIndented' (i + 1)
915
                in
916
                    case json of
917
                        OBJECT [] => "{}"
918
                      | ARRAY [] => "[]"
919
                      | OBJECT pp => "{\n" ^ indent (i + 1) ^
920
                                     String.concatWith
921
                                         (",\n" ^ indent (i + 1))
922
                                         (map (fn (key, value) =>
923
                                                  ser (STRING key) ^ ": " ^
924
                                                  ser value) pp) ^
925
                                     "\n" ^ indent i ^ "}"
926
                      | ARRAY arr => "[\n" ^ indent (i + 1) ^
927
                                     String.concatWith
928
                                         (",\n" ^ indent (i + 1))
929
                                         (map ser arr) ^
930
                                     "\n" ^ indent i ^ "]"
931
                      | other => serialise other
932
                end
933
        in
934
            serialiseIndented' 0 json ^ "\n"
935
        end
936
                                             
937
end
938

  
939

  
940
structure JsonBits :> sig
941
    val load_json_from : string -> Json.json (* filename -> json *)
942
    val save_json_to : string -> Json.json -> unit
943
    val lookup_optional : Json.json -> string list -> Json.json option
944
    val lookup_optional_string : Json.json -> string list -> string option
945
    val lookup_mandatory : Json.json -> string list -> Json.json
946
    val lookup_mandatory_string : Json.json -> string list -> string
947
end = struct
948

  
949
    fun load_json_from filename =
950
        case Json.parse (FileBits.file_contents filename) of
951
            Json.OK json => json
952
          | Json.ERROR e => raise Fail ("Failed to parse file: " ^ e)
953

  
954
    fun save_json_to filename json =
955
        (* using binary I/O to avoid ever writing CR/LF line endings *)
956
        let val jstr = Json.serialiseIndented json
957
            val stream = BinIO.openOut filename
958
        in
959
            BinIO.output (stream, Byte.stringToBytes jstr);
960
            BinIO.closeOut stream
961
        end
962
                                  
963
    fun lookup_optional json kk =
964
        let fun lookup key =
965
                case json of
966
                    Json.OBJECT kvs =>
967
                    (case List.find (fn (k, v) => k = key) kvs of
968
                         SOME (k, v) => SOME v
969
                       | NONE => NONE)
970
                  | _ => raise Fail "Object expected"
971
        in
972
            case kk of
973
                [] => NONE
974
              | key::[] => lookup key
975
              | key::kk => case lookup key of
976
                               NONE => NONE
977
                             | SOME j => lookup_optional j kk
978
        end
979
                       
980
    fun lookup_optional_string json kk =
981
        case lookup_optional json kk of
982
            SOME (Json.STRING s) => SOME s
983
          | SOME _ => raise Fail ("Value (if present) must be string: " ^
984
                                  (String.concatWith " -> " kk))
985
          | NONE => NONE
986

  
987
    fun lookup_mandatory json kk =
988
        case lookup_optional json kk of
989
            SOME v => v
990
          | NONE => raise Fail ("Value is mandatory: " ^
991
                                (String.concatWith " -> " kk) ^ " in json: " ^
992
                                (Json.serialise json))
993
                          
994
    fun lookup_mandatory_string json kk =
995
        case lookup_optional json kk of
996
            SOME (Json.STRING s) => s
997
          | _ => raise Fail ("Value must be string: " ^
998
                             (String.concatWith " -> " kk))
999
end
1000

  
1001
structure Provider :> sig
1002
    val load_providers : Json.json -> provider list
1003
    val load_more_providers : provider list -> Json.json -> provider list
1004
    val remote_url : context -> vcs -> source -> libname -> string
1005
end = struct
1006

  
1007
    val known_providers : provider list =
1008
        [ {
1009
            service = "bitbucket",
1010
            supports = [HG, GIT],
1011
            remote_spec = {
1012
                anon = SOME "https://bitbucket.org/{owner}/{repository}",
1013
                auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repository}"
1014
            }
1015
          },
1016
          {
1017
            service = "github",
1018
            supports = [GIT],
1019
            remote_spec = {
1020
                anon = SOME "https://github.com/{owner}/{repository}",
1021
                auth = SOME "ssh://{vcs}@github.com/{owner}/{repository}"
1022
            }
1023
          }
1024
        ]
1025

  
1026
    fun vcs_name vcs =
1027
        case vcs of HG => "hg"
1028
                  | GIT => "git"
1029
                  | SVN => "svn"
1030
                                             
1031
    fun vcs_from_name name =
1032
        case name of "hg" => HG
1033
                   | "git" => GIT 
1034
                   | "svn" => SVN
1035
                   | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"")
1036

  
1037
    fun load_more_providers previously_loaded json =
1038
        let open JsonBits
1039
            fun load pjson pname : provider =
1040
                {
1041
                  service = pname,
1042
                  supports =
1043
                  case lookup_mandatory pjson ["vcs"] of
1044
                      Json.ARRAY vv =>
1045
                      map (fn (Json.STRING v) => vcs_from_name v
1046
                          | _ => raise Fail "Strings expected in vcs array")
1047
                          vv
1048
                    | _ => raise Fail "Array expected for vcs",
1049
                  remote_spec = {
1050
                      anon = lookup_optional_string pjson ["anonymous"],
1051
                      auth = lookup_optional_string pjson ["authenticated"]
1052
                  }
1053
                }
1054
            val loaded = 
1055
                case lookup_optional json ["services"] of
1056
                    NONE => []
1057
                  | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl
1058
                  | _ => raise Fail "Object expected for services in config"
1059
            val newly_loaded =
1060
                List.filter (fn p => not (List.exists (fn pp => #service p =
1061
                                                                #service pp)
1062
                                                      previously_loaded))
1063
                            loaded
1064
        in
1065
            previously_loaded @ newly_loaded
1066
        end
1067

  
1068
    fun load_providers json =
1069
        load_more_providers known_providers json
1070
                                                    
1071
    fun expand_spec spec { vcs, service, owner, repo } login =
1072
        (* ugly *)
1073
        let fun replace str = 
1074
                case str of
1075
                    "vcs" => vcs_name vcs
1076
                  | "service" => service
1077
                  | "owner" =>
1078
                    (case owner of
1079
                         SOME ostr => ostr
1080
                       | NONE => raise Fail ("Owner not specified for service " ^
1081
                                             service))
1082
                  | "repository" => repo
1083
                  | "account" =>
1084
                    (case login of
1085
                         SOME acc => acc
1086
                       | NONE => raise Fail ("Account not given for service " ^
1087
                                             service))
1088
                  | other => raise Fail ("Unknown variable \"" ^ other ^
1089
                                         "\" in spec for service " ^ service)
1090
            fun expand' acc sstr =
1091
                case Substring.splitl (fn c => c <> #"{") sstr of
1092
                    (pfx, sfx) =>
1093
                    if Substring.isEmpty sfx
1094
                    then rev (pfx :: acc)
1095
                    else 
1096
                        case Substring.splitl (fn c => c <> #"}") sfx of
1097
                            (tok, remainder) =>
1098
                            if Substring.isEmpty remainder
1099
                            then rev (tok :: pfx :: acc)
1100
                            else let val replacement =
1101
                                         replace
1102
                                             (* tok begins with "{": *)
1103
                                             (Substring.string
1104
                                                  (Substring.triml 1 tok))
1105
                                 in
1106
                                     expand' (Substring.full replacement ::
1107
                                              pfx :: acc)
1108
                                             (* remainder begins with "}": *)
1109
                                             (Substring.triml 1 remainder)
1110
                                 end
1111
        in
1112
            Substring.concat (expand' [] (Substring.full spec))
1113
        end
1114
        
1115
    fun provider_url req login providers =
1116
        case providers of
1117
            [] => raise Fail ("Unknown service \"" ^ (#service req) ^
1118
                              "\" for vcs \"" ^ (vcs_name (#vcs req)) ^ "\"")
1119
          | ({ service, supports, remote_spec : remote_spec } :: rest) =>
1120
            if service <> (#service req) orelse
1121
               not (List.exists (fn v => v = (#vcs req)) supports)
1122
            then provider_url req login rest
1123
            else
1124
                case (login, #auth remote_spec, #anon remote_spec) of
1125
                    (SOME _, SOME auth, _) => expand_spec auth req login
1126
                  | (SOME _, _, SOME anon) => expand_spec anon req NONE
1127
                  | (NONE,   _, SOME anon) => expand_spec anon req NONE
1128
                  | _ => raise Fail ("No suitable anonymous or authenticated " ^
1129
                                     "URL spec provided for service \"" ^
1130
                                     service ^ "\"")
1131

  
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff