Revision 125:34e428693f5d

View differences:

.hgignore
4 4
REPORTS
5 5
PACKAGES
6 6
.vext*.bin
7
.repoint*
SCRIPTS/process.sh
30 30
    exit 1
31 31
fi
32 32

  
33
# Get errors from Vext about SML implementation early
34
( cd "$mydir/.." ; ./vext version >/dev/null )
33
# Get errors from Repoint about SML implementation early
34
( cd "$mydir/.." ; ./repoint version >/dev/null )
35 35

  
36 36
do_rebuild=""
37 37
do_speedy=""
......
192 192

  
193 193
repodir="$mydir/../REPOS"
194 194

  
195
( cd "$mydir/.." ; ./vext install )
195
( cd "$mydir/.." ; ./repoint install )
196 196

  
197 197
cd "$repodir"
198 198

  
repoint
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="$REPOINT_SML"
8

  
9
set -eu
10

  
11
# avoid gussying up output
12
export HGPLAIN=true
13

  
14
mydir=$(dirname "$0")
15
program="$mydir/repoint.sml"
16

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

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

  
42
if [ -x "$gen_out" ]; then
43
    exec "$gen_out" "$@"
44
fi
45

  
46
# We need one of Poly/ML, SML/NJ, MLton, or MLKit. Since we're running
47
# a single-file SML program as if it were a script, our order of
48
# preference is usually based on startup speed. An exception is the
49
# local_install case, where we retain a persistent binary
50

  
51
if [ -z "$sml" ]; then
52
    if [ -n "$local_install" ] && mlton 2>&1 | grep -q 'MLton'; then
53
	sml="mlton"
54
    elif sml -h 2>&1 | grep -q 'Standard ML of New Jersey'; then
55
	sml="smlnj"
56
    # We would prefer Poly/ML to SML/NJ, except that Poly v5.7 has a
57
    # nasty bug that occasionally causes it to deadlock on startup.
58
    # That is fixed in v5.7.1, so we could promote it up the order
59
    # again at some point in future
60
    elif echo | poly -v 2>/dev/null | grep -q 'Poly/ML'; then
61
	sml="polyml"
62
    elif mlton 2>&1 | grep -q 'MLton'; then
63
	sml="mlton"
64
    # MLKit is at the bottom because it leaves compiled files around
65
    # in an MLB subdir in the current directory
66
    elif mlkit 2>&1 | grep -q 'MLKit'; then
67
	sml="mlkit"
68
    else cat 1>&2 <<EOF
69

  
70
ERROR: No supported SML compiler or interpreter found       
71
EOF
72
	cat 1>&2 <<EOF
73

  
74
  The Repoint external source code manager needs a Standard ML (SML)
75
  compiler or interpreter to run.
76

  
77
  Please ensure you have one of the following SML implementations
78
  installed and present in your PATH, and try again.
79

  
80
    1. Standard ML of New Jersey
81
       - may be found in a distribution package called: smlnj
82
       - executable name: sml
83

  
84
    2. Poly/ML
85
       - may be found in a distribution package called: polyml
86
       - executable name: poly
87

  
88
    3. MLton
89
       - may be found in a distribution package called: mlton
90
       - executable name: mlton
91

  
92
    4. MLKit
93
       - may be found in a distribution package called: mlkit
94
       - executable name: mlkit
95

  
96
EOF
97
	exit 2
98
    fi
99
fi
100

  
101
arglist=""
102
for arg in "$@"; do
103
    if [ -n "$arglist" ]; then arglist="$arglist,"; fi
104
    if echo "$arg" | grep -q '["'"'"']' ; then
105
	arglist="$arglist\"usage\""
106
    else
107
	arglist="$arglist\"$arg\""
108
    fi
109
done
110

  
111
case "$sml" in
112
    polyml)
113
        if [ -n "$local_install" ] && polyc --help >/dev/null 2>&1 ; then
114
            if [ ! -x "$gen_out" ]; then
115
                polyc -o "$gen_out" "$program"
116
            fi
117
	    "$gen_out" "$@"
118
        else
119
            echo 'use "'"$program"'"; repoint ['"$arglist"'];' |
120
                poly -q --error-exit
121
        fi ;;
122
    mlton)
123
        if [ ! -x "$gen_out" ]; then
124
	    echo "[Precompiling Repoint binary...]" 1>&2
125
	    echo "val _ = main ()" | cat "$program" - > "$gen_sml"
126
	    mlton -output "$gen_out" "$gen_sml"
127
        fi
128
	"$gen_out" "$@" ;;
129
    mlkit)
130
        if [ ! -x "$gen_out" ]; then
131
	    echo "[Precompiling Repoint binary...]" 1>&2
132
	    echo "val _ = main ()" | cat "$program" - > "$gen_sml"
133
	    mlkit -output "$gen_out" "$gen_sml"
134
        fi
135
	"$gen_out" "$@" ;;
136
    smlnj)
137
	cat "$program" | (
138
	    cat <<EOF
139
val smlrun__cp = 
140
    let val x = !Control.Print.out in
141
        Control.Print.out := { say = fn _ => (), flush = fn () => () };
142
        x
143
    end;
144
val smlrun__prev = ref "";
145
Control.Print.out := { 
146
    say = fn s => 
147
        (if String.isSubstring " Error" s
148
         then (Control.Print.out := smlrun__cp;
149
               (#say smlrun__cp) (!smlrun__prev);
150
               (#say smlrun__cp) s)
151
         else (smlrun__prev := s; ())),
152
    flush = fn s => ()
153
};
154
EOF
155
	    cat -
156
	    cat <<EOF
157
val _ = repoint [$arglist];
158
val _ = OS.Process.exit (OS.Process.success);
159
EOF
160
            ) > "$gen_sml"
161
	CM_VERBOSE=false sml "$gen_sml" ;;
162
    *)
163
	echo "ERROR: Unknown SML implementation name: $sml" 1>&2;
164
	exit 2 ;;
165
esac
166
       
repoint-lock.json
1
{
2
  "libraries": {
3
    "vamp-plugin-sdk": {
4
      "pin": "5d9af3140f05"
5
    },
6
    "vamp-plugin-tester": {
7
      "pin": "63d6776904df"
8
    },
9
    "vamp-aubio-plugins": {
10
      "pin": "be4c729d6549"
11
    },
12
    "mazurka-plugins": {
13
      "pin": "ac2e3562731e015321e1d959bb356f32ac6b2c7c"
14
    },
15
    "qm-vamp-plugins": {
16
      "pin": "50042f3189ca"
17
    },
18
    "segmentino": {
19
      "pin": "a0b1f4810e4e"
20
    },
21
    "vamp-libxtract-plugins": {
22
      "pin": "81146373f024"
23
    },
24
    "vamp-onsetsds-plugin": {
25
      "pin": "a55de4e9d7e1"
26
    },
27
    "vamp-test-plugin": {
28
      "pin": "96cb7ef3cc24"
29
    },
30
    "match-vamp": {
31
      "pin": "4b272c839f7e"
32
    },
33
    "vampy": {
34
      "pin": "e2bb3cf7adf1"
35
    },
36
    "nnls-chroma": {
37
      "pin": "6bfce96d87b4"
38
    },
39
    "pyin": {
40
      "pin": "550d5f186abb"
41
    },
42
    "silvet": {
43
      "pin": "426ce52ef096"
44
    },
45
    "constant-q-cpp": {
46
      "pin": "445f94d0cb4e"
47
    },
48
    "vamp-tempogram": {
49
      "pin": "02721bb9c4f0"
50
    },
51
    "vamp-simple-cepstrum": {
52
      "pin": "f021dc97da29"
53
    },
54
    "cepstral-pitchtracker": {
55
      "pin": "759ea6e988fe"
56
    },
57
    "tempest": {
58
      "pin": "f6597489acf5"
59
    },
60
    "miredu": {
61
      "pin": "7fc6a5b6585417e4bd1bd5a87c4e18d912b1ea50"
62
    },
63
    "ua-vamp-plugins": {
64
      "pin": "d2a57e002f5a3a0cf5311e1f77a8b21bf932f7bb"
65
    },
66
    "tipic": {
67
      "pin": "cbebb8a657ea"
68
    },
69
    "btrack": {
70
      "pin": "bf7501e5ee0c033e4b19dd3f9246c43e3b1b8e6c"
71
    }
72
  }
73
}
repoint-project.json
1
{
2
    "config": {
3
        "extdir": "REPOS"
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
        "vamp-plugin-tester": {
18
            "vcs": "hg",
19
            "service": "soundsoftware"
20
        },
21
        "vamp-aubio-plugins": {
22
            "vcs": "hg",
23
            "service": "soundsoftware"
24
        },
25
        "mazurka-plugins": {
26
            "vcs": "git",
27
            "service": "github",
28
            "owner": "craigsapp",
29
            "repository": "MazurkaPlugins"
30
        },
31
        "qm-vamp-plugins": {
32
            "vcs": "hg",
33
            "service": "soundsoftware"
34
        },
35
        "segmentino": {
36
            "vcs": "hg",
37
            "service": "soundsoftware",
38
            "repository": "segmenter-vamp-plugin"
39
        },
40
        "vamp-libxtract-plugins": {
41
            "vcs": "hg",
42
            "service": "soundsoftware"
43
        },
44
        "vamp-onsetsds-plugin": {
45
            "vcs": "hg",
46
            "service": "soundsoftware"
47
        },
48
        "vamp-test-plugin": {
49
            "vcs": "hg",
50
            "service": "soundsoftware"
51
        },
52
        "match-vamp": {
53
            "vcs": "hg",
54
            "service": "soundsoftware"
55
        },
56
        "vampy": {
57
            "vcs": "hg",
58
            "service": "soundsoftware"
59
        },
60
        "nnls-chroma": {
61
            "vcs": "hg",
62
            "service": "soundsoftware"
63
        },
64
        "pyin": {
65
            "vcs": "hg",
66
            "service": "soundsoftware"
67
        },
68
        "silvet": {
69
            "vcs": "hg",
70
            "service": "soundsoftware"
71
        },
72
        "constant-q-cpp": {
73
            "vcs": "hg",
74
            "service": "soundsoftware"
75
        },
76
        "vamp-tempogram": {
77
            "vcs": "hg",
78
            "service": "soundsoftware"
79
        },
80
        "vamp-simple-cepstrum": {
81
            "vcs": "hg",
82
            "service": "soundsoftware"
83
        },
84
        "cepstral-pitchtracker": {
85
            "vcs": "hg",
86
            "service": "soundsoftware"
87
        },
88
        "tempest": {
89
            "vcs": "hg",
90
            "service": "soundsoftware"
91
        },
92
        "miredu": {
93
            "vcs": "git",
94
            "service": "github",
95
            "owner": "MTG"
96
        },
97
        "ua-vamp-plugins": {
98
            "vcs": "git",
99
            "service": "github",
100
            "owner": "pertusa",
101
            "repository": "UAVampPlugins"
102
        },
103
        "tipic": {
104
            "vcs": "hg",
105
            "service": "soundsoftware"
106
        },
107
        "btrack": {
108
            "vcs": "git",
109
            "service": "github",
110
            "owner": "adamstark",
111
            "repository": "BTrack"
112
        }
113
    }
114
}
repoint.bat
1
@echo off
2
PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dpn0.ps1' %*";
3

  
repoint.ps1
1
<#
2

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

  
7
#>
8

  
9
Set-StrictMode -Version 2.0
10
$ErrorActionPreference = "Stop"
11
$env:HGPLAIN = "true"
12

  
13
$sml = $env:REPOINT_SML
14

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

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

  
20
# Typical locations
21
$env:PATH = "$env:PATH;C:\Program Files (x86)\SMLNJ\bin;C:\Program Files\Poly ML;C:\Program Files (x86)\Poly ML"
22

  
23
if (!$sml) {
24
    if (Get-Command "sml" -ErrorAction SilentlyContinue) {
25
       $sml = "smlnj"
26
    } elseif (Get-Command "polyml" -ErrorAction SilentlyContinue) {
27
       $sml = "poly"
28
    } else {
29
       echo @"
30

  
31
ERROR: No supported SML compiler or interpreter found       
32

  
33
  The Repoint external source code manager needs a Standard ML (SML)
34
  compiler or interpreter to run.
35

  
36
  Please ensure you have one of the following SML implementations
37
  installed and present in your PATH, and try again.
38

  
39
    1. Standard ML of New Jersey
40
       - executable name: sml
41

  
42
    2. Poly/ML
43
       - executable name: polyml
44

  
45
"@
46
       exit 1
47
    }
48
}
49

  
50
if ($args -match "'""") {
51
    $arglist = '["usage"]'
52
} else {
53
    $arglist = '["' + ($args -join '","') + '"]'
54
}
55

  
56
if ($sml -eq "poly") {
57

  
58
    $program = $program -replace "\\","\\\\"
59
    echo "use ""$program""; repoint $arglist" | polyml -q --error-exit | Out-Host
60

  
61
    if (-not $?) {
62
        exit $LastExitCode
63
    }
64

  
65
} elseif ($sml -eq "smlnj") {
66

  
67
    $lines = @(Get-Content $program)
68
    $lines = $lines -notmatch "val _ = main ()"
69

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

  
88
    $outro = @"
89
val _ = repoint $arglist;
90
val _ = OS.Process.exit (OS.Process.success);
91
"@ -split "[\r\n]+"
92

  
93
    $script = @()
94
    $script += $intro
95
    $script += $lines
96
    $script += $outro
97

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

  
100
    $script | Out-File -Encoding "ASCII" $tmpfile
101

  
102
    $env:CM_VERBOSE="false"
103

  
104
    sml $tmpfile
105

  
106
    if (-not $?) {
107
        del $tmpfile
108
        exit $LastExitCode
109
    }
110

  
111
    del $tmpfile
112

  
113
} else {
114

  
115
    "Unknown SML implementation name: $sml"
116
    exit 2
117
}
repoint.sml
1
(*
2
    DO NOT EDIT THIS FILE.
3
    This file is automatically generated from the individual
4
    source files in the Repoint repository.
5
*)
6

  
7
(* 
8
    Repoint
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 repoint_version = "1.0"
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 RepointFilenames = struct
137
    val project_file = "repoint-project.json"
138
    val project_lock_file = "repoint-lock.json"
139
    val user_config_file = ".repoint.json"
140
    val archive_dir = ".repoint-archive"
141
end
142
                   
143
signature VCS_CONTROL = sig
144

  
145
    (** Check whether the given VCS is installed and working *)
146
    val is_working : context -> bool result
147
    
148
    (** Test whether the library is present locally at all *)
149
    val exists : context -> libname -> bool result
150
                                            
151
    (** Return the id (hash) of the current revision for the library *)
152
    val id_of : context -> libname -> id_or_tag result
153

  
154
    (** Test whether the library is at the given id *)
155
    val is_at : context -> libname * id_or_tag -> bool result
156

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

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

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

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

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

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

  
185
    (** Update the library to the given specific id or tag *)
186
    val update_to : context -> libname * source * id_or_tag -> unit result
187

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

  
195
signature LIB_CONTROL = sig
196
    val review : context -> libspec -> (libstate * localstate) result
197
    val status : context -> libspec -> (libstate * localstate) result
198
    val update : context -> libspec -> unit result
199
    val id_of : context -> libspec -> id_or_tag result
200
    val is_working : context -> vcs -> bool result
201
end
202

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

  
221
    fun verbose () =
222
        case OS.Process.getEnv "REPOINT_VERBOSE" of
223
            SOME "0" => false
224
          | SOME _ => true
225
          | NONE => false
226

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

  
255
    fun libpath context "" =
256
        extpath context
257
      | libpath context libname =
258
        subpath context libname ""
259

  
260
    fun project_file_path rootpath filename =
261
        let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
262
        in OS.Path.toString {
263
                isAbs = isAbs,
264
                vol = vol,
265
                arcs = arcs @ [ filename ]
266
            }
267
        end
268
                
269
    fun project_spec_path rootpath =
270
        project_file_path rootpath (RepointFilenames.project_file)
271

  
272
    fun project_lock_path rootpath =
273
        project_file_path rootpath (RepointFilenames.project_lock_file)
274

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

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

  
334
    val tick_cycle = ref 0
335
    val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
336

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

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

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

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

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

  
424
    fun mkpath path =
425
        mkpath' (OS.Path.mkCanonical path)
426

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

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

  
460
    fun rmpath path =
461
        rmpath' (OS.Path.mkCanonical path)
462

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

  
476
    (* Valid states for unpinned libraries:
477

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

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

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

  
490
       - ABSENT: Repo doesn't exist here at all.
491

  
492
       Valid states for pinned libraries:
493

  
494
       - CORRECT: We are at the pinned revision.
495

  
496
       - WRONG: We are at any revision other than the pinned one.
497

  
498
       - ABSENT: Repo doesn't exist here at all.
499
    *)
500

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

  
551
    val review = check true
552
    val status = check false
553

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

  
584
    fun id_of context ({ libname, ... } : libspec) =
585
        V.id_of context libname
586

  
587
    fun is_working context vcs =
588
        V.is_working context
589
                
590
end
591

  
592
(* Simple Standard ML JSON parser
593
   https://bitbucket.org/cannam/sml-simplejson
594
   Copyright 2017 Chris Cannam. BSD licence.
595
   Parts based on the JSON parser in the Ponyo library by Phil Eaton.
596
*)
597

  
598
signature JSON = sig
599

  
600
    datatype json = OBJECT of (string * json) list
601
                  | ARRAY of json list
602
                  | NUMBER of real
603
                  | STRING of string
604
                  | BOOL of bool
605
                  | NULL
606

  
607
    datatype 'a result = OK of 'a
608
                       | ERROR of string
609

  
610
    val parse : string -> json result
611
    val serialise : json -> string
612
    val serialiseIndented : json -> string
613

  
614
end
615

  
616
structure Json :> JSON = struct
617

  
618
    datatype json = OBJECT of (string * json) list
619
                  | ARRAY of json list
620
                  | NUMBER of real
621
                  | STRING of string
622
                  | BOOL of bool
623
                  | NULL
624

  
625
    datatype 'a result = OK of 'a
626
                       | ERROR of string
627

  
628
    structure T = struct
629
        datatype token = NUMBER of char list
630
                       | STRING of string
631
                       | BOOL of bool
632
                       | NULL
633
                       | CURLY_L
634
                       | CURLY_R
635
                       | SQUARE_L
636
                       | SQUARE_R
637
                       | COLON
638
                       | COMMA
639

  
640
        fun toString t =
641
            case t of NUMBER digits => implode digits
642
                    | STRING s => s
643
                    | BOOL b => Bool.toString b
644
                    | NULL => "null"
645
                    | CURLY_L => "{"
646
                    | CURLY_R => "}"
647
                    | SQUARE_L => "["
648
                    | SQUARE_R => "]"
649
                    | COLON => ":"
650
                    | COMMA => ","
651
    end
652

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

  
673
    fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
674
        lex (pos + 3) (T.NULL :: acc) xs
675
      | lexNull pos acc _ = token_error pos
676

  
677
    and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
678
        lex (pos + 3) (T.BOOL true :: acc) xs
679
      | lexTrue pos acc _ = token_error pos
680

  
681
    and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
682
        lex (pos + 4) (T.BOOL false :: acc) xs
683
      | lexFalse pos acc _ = token_error pos
684

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

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

  
768
    fun show [] = "end of input"
769
      | show (tok :: _) = T.toString tok
770

  
771
    fun parseNumber digits =
772
        (* Note lexNumber already case-insensitised the E for us *)
773
        let open Char
774

  
775
            fun okExpDigits [] = false
776
              | okExpDigits (c :: []) = isDigit c
777
              | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
778

  
779
            fun okExponent [] = false
780
              | okExponent (#"+" :: cs) = okExpDigits cs
781
              | okExponent (#"-" :: cs) = okExpDigits cs
782
              | okExponent cc = okExpDigits cc
783

  
784
            fun okFracTrailing [] = true
785
              | okFracTrailing (c :: cs) =
786
                (isDigit c andalso okFracTrailing cs) orelse
787
                (c = #"e" andalso okExponent cs)
788

  
789
            fun okFraction [] = false
790
              | okFraction (c :: cs) =
791
                isDigit c andalso okFracTrailing cs
792

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

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

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

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

  
945

  
946
structure JsonBits :> sig
947
    exception Config of string
948
    val load_json_from : string -> Json.json (* filename -> json *)
949
    val save_json_to : string -> Json.json -> unit
950
    val lookup_optional : Json.json -> string list -> Json.json option
951
    val lookup_optional_string : Json.json -> string list -> string option
952
    val lookup_mandatory : Json.json -> string list -> Json.json
953
    val lookup_mandatory_string : Json.json -> string list -> string
954
end = struct
955

  
956
    exception Config of string
957

  
958
    fun load_json_from filename =
959
        case Json.parse (FileBits.file_contents filename) of
960
            Json.OK json => json
961
          | Json.ERROR e => raise Config ("Failed to parse file: " ^ e)
962

  
963
    fun save_json_to filename json =
964
        (* using binary I/O to avoid ever writing CR/LF line endings *)
965
        let val jstr = Json.serialiseIndented json
966
            val stream = BinIO.openOut filename
967
        in
968
            BinIO.output (stream, Byte.stringToBytes jstr);
969
            BinIO.closeOut stream
970
        end
971
                                  
972
    fun lookup_optional json kk =
973
        let fun lookup key =
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff