Skip to content

Commit 34befa0

Browse files
committed
back to using env for local dependencies
This supercedes the changes in f53b604 Thanks to dhall-lang/dhall-haskell#2203 (to be released in dhall 1.40.0), the `?` operator now only falls back on import not found, but not type errors etc. There's still some chance for accidental fallback, but that can now be worked around by using an intentional type error to prevent unwanted fallbacks.
1 parent 58fb84e commit 34befa0

10 files changed

+205
-95
lines changed

README.md

+47-6
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,61 @@ There's also the `fix` attribute, which is the (not stable but handy) [./mainten
7878

7979
When working on dhall libraries, it's common to want to develop against a local copy while making changes.
8080

81-
There's no good workflow for this built into dhall, so `dhall-render` provides a utility script (`dhall/local`) for this purpose.
81+
There's no good workflow for this built into dhall, so `dhall-render` provides a utility script (`dhall/local`) for this purpose. You should use dhall > 1.40.0, as prior versions will silently fall back to the public version if there is a type error in your local version.
8282

83-
To set up a local version of a `Foo.dhall` file, create a `Foo.dhall.local` file next to it, with the same structure as `Foo.dhall` but using local imports.
83+
To conditionally import either the local or public version of a library, you can use this pattern:
8484

85-
Then, to use it:
85+
```dhall
86+
let Dependency =
87+
(\(local : Bool) -> ../local-directory/package.dhall ? local "import failed")
88+
env:DHALL_LOCAL
89+
? https://example.org/package.dhall
90+
```
91+
92+
And then run whatever you need as a subcommand of `dhall/local`:
8693

8794
```
8895
./dhall/local ./dhall/render
8996
```
9097

91-
This finds all `*.dhall.local` files, and _temporarily_ replaces the corresponding `*.dhall` file, then runs the supplied command (in this case `dhall/render`, but it could be anything).
98+
This will run the command with `DHALL_LOCAL` set to `True`.
99+
100+
**Use in libraries**:
101+
102+
If you're using this pattern in a library, you may prefer to scope this code (so that someone can use `local` to import your code, without also needing your dependencies available locally):
103+
104+
```dhall
105+
let Dependency =
106+
(\(local : Bool) -> ../local-directory/package.dhall ? local "import failed")
107+
env:DHALL_LOCAL_MYLIB
108+
? https://example.org/package.dhall
109+
```
110+
111+
The `DHALL_LOCAL_MYLIB` environment variable will only be set if the user opts in by passing the `mylib` scope to the `local` script, i.e.:
112+
113+
```
114+
./dhall/local -s mylib ./dhall/render
115+
```
116+
117+
Multiple scopes should be comma-separated in a single argument, e.g. `./dhall/local -s foo,bar,baz ./dhall/render`
118+
119+
**Error reporting**:
120+
121+
Note that if the local import fails to load (a missing file, type error, etc), you will unfortunately
122+
not get the full error, you will only get:
123+
124+
```
125+
Error: Not a function
126+
127+
local "import failed"
128+
```
129+
130+
When this happens you'll need to manually try to evaluate the local version in isolation to see the real error, e.g. `dhall ../local-directory/package.dhall`.
131+
132+
133+
**Use outside the terminal**:
92134

93-
Upon termination, it restores the original `*.dhall` files, so it should result in no actual changes to your workspace.
94-
Note that it does actually move files around on disk so the effects will be observed by all running processes, not just the one you specify.
135+
If you need to integrate this into your editor or other environment, you can manually export `DHALL_LOCAL=True` (or `DHALL_LOCAL_<SCOPENAME>=True` for specific scopes).
95136

96137
## How do I get more details about type errors?
97138

maintenance/local

+21-87
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,28 @@
11
#!/usr/bin/env bash
22
set -eu
3-
# with-local will temporarily install all `.dhall.local` files in place of the
4-
# corresponding `.dhall` files, for the duration of a single command.
53
#
6-
# Upon exit, the script will attempt to restore the original versions.
7-
# If this fails, the following invariants will hold, which should allow the
8-
# script to be run again and self-correct:
4+
# This script enables local imports using the following pattern:
95
#
10-
# Invariants:
11-
# - there is always a .local version
12-
# - if there is no explicit .remote file, then there must be a plain file (which is the logical "remote" version)
13-
# - if there is a .remote file, then
14-
# - there may be a plain file, which is a symlink to .local
15-
16-
if [ "${DEBUG:-0}" = 1 ]; then
17-
# set -x
18-
function log {
19-
echo >&2 "$@"
20-
}
21-
else
22-
function log {
23-
true
24-
}
25-
fi
26-
27-
function local_version {
28-
echo "$1.local"
29-
}
30-
31-
function remote_version {
32-
echo "$1.remote"
33-
}
34-
35-
function install_local {
36-
path="$1"
37-
remote_path="$(remote_version "$1")"
38-
local_path="$(local_version "$1")"
39-
if [ ! -e "$remote_path" ]; then
40-
if [ -L "$path" ]; then
41-
log "Error: $remote_path doesn't exist but $path is a symlink"
42-
exit 1
43-
fi
44-
# backup to remote_path
45-
log "moving to $remote_path"
46-
mv "$path" "$remote_path"
47-
fi
48-
echo >&2 "[ using $local_path ... ]"
49-
ln -s "$(basename "$local_path")" "$path"
50-
}
51-
52-
function restore_remote {
53-
# put remote version back in place
54-
path="$1"
55-
remote_path="$(remote_version "$1")"
56-
if [ -e "$remote_path" ]; then
57-
log "restoring $remote_path"
58-
mv "$remote_path" "$path"
6+
# let Dependency =
7+
# (\(local : Bool) -> ../dependency/package.dhall ? local "import failed")
8+
# env:DHALL_LOCAL
9+
# ? https://(...)
10+
#
11+
# See the dhall-render readme for full details:
12+
# https://github.com/timbertson/dhall-render#readme
13+
14+
SCOPES=()
15+
while [ "$#" -gt 0 ]; do
16+
if [ "x$1" = "x-s" ]; then
17+
IFS=',' read -r -a SCOPES <<< "$(echo "$2" | tr '[:lower:]' '[:upper:]')"
18+
shift 2
5919
else
60-
log "Error: $remote_path is not present"
61-
exit 1
20+
break
6221
fi
63-
}
64-
65-
function find_all {
66-
base_dir="$(dirname "$0")"
67-
base_dir="${DHALL_LOCAL_ROOT:-$base_dir}"
68-
log "base_dir: $base_dir"
69-
find "$base_dir" -name '*.dhall.local' | while read f; do
70-
path="$(dirname "$f")/$(basename "$f" .local)"
71-
log "processing $path"
72-
echo "$path"
73-
done
74-
}
75-
76-
function restore_all {
77-
find_all | while read f; do
78-
restore_remote "$f"
79-
done
80-
}
81-
82-
function install_all {
83-
find_all | while read f; do
84-
install_local "$f"
85-
done
86-
}
87-
88-
if [ "$#" -eq 0 ]; then
89-
echo >&2 "Usage: "$0" COMMAND"
90-
fi
22+
done
9123

92-
trap restore_all EXIT
93-
install_all
94-
"$@"
24+
export DHALL_LOCAL=True
25+
for scope in ${SCOPES[@]+${SCOPES[@]}}; do
26+
eval "export DHALL_LOCAL_$scope=True"
27+
done
28+
exec "$@"

test/lib/utest.rb

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,35 @@
11
# itty bitty test libby
2-
def assert_equal(a,b)
2+
3+
require 'ostruct'
4+
require 'open3'
5+
6+
def assert_equal(a, b, desc = nil)
37
if a != b
4-
raise "AssertionError, expected: #{b.inspect}, got: #{a.inspect}"
8+
suffix = desc ? " (#{desc})" : ""
9+
raise "AssertionError, expected: #{b.inspect}, got: #{a.inspect}#{suffix}"
10+
end
11+
end
12+
13+
def assert_matches(a,b)
14+
if !b.match?(a)
15+
raise "AssertionError, expected: #{a.inspect} to match #{b.inspect}"
516
end
617
end
718

819
def test(desc)
920
puts("# #{desc} ...")
1021
yield
1122
end
23+
24+
def run(*cmd)
25+
run?(*cmd).success or raise "Command failed: #{cmd.join(' ')}"
26+
end
27+
28+
def run?(*cmd)
29+
puts(" + #{cmd.join(' ')}")
30+
Open3.popen2e(*cmd) do |stdin, out_and_err, wait|
31+
output = out_and_err.read
32+
code = wait.value
33+
OpenStruct.new({ output: output, success: wait.value == 0 })
34+
end
35+
end

test/local/local-show.dhall

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
\(x : Natural) -> "number: ${Natural/show x}"

test/local/missing-scoped.dhall

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
let show =
2+
( \(local : Bool) ->
3+
./local-show-impl-not-present.dhall ? local "import failed"
4+
)
5+
env:DHALL_LOCAL_SHOW
6+
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21
7+
8+
in show 1

test/local/missing-semi-scoped.dhall

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
let show =
2+
-- This is a bit arduous, but it is possible to have a local implementation
3+
-- which is attempted if `DHALL_LOCAL` is set, but required if DHALL_LOCAL_SHOW
4+
-- is set. It's probably too verbose unless you want maximal flexibility.
5+
(\(local : Bool) -> ./local-show-impl-not-present.dhall) env:DHALL_LOCAL
6+
? ( \(local : Bool) ->
7+
./local-show-impl-not-present.dhall ? local "import failed"
8+
)
9+
env:DHALL_LOCAL_SHOW
10+
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21
11+
12+
in show 1

test/local/missing.dhall

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
let show =
2+
(\(local : Bool) -> ./local-show-impl-not-present.dhall) env:DHALL_LOCAL
3+
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21
4+
5+
in show 1

test/local/present-scoped.dhall

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
let show =
2+
(\(local : Bool) -> ./local-show.dhall ? local "import failed")
3+
env:DHALL_LOCAL_SHOW
4+
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21
5+
6+
in show 1

test/local/present.dhall

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
let show =
2+
(\(local : Bool) -> ./local-show.dhall) env:DHALL_LOCAL
3+
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21
4+
5+
in show 1

test/test-local.rb

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env ruby
2+
require_relative 'lib/utest'
3+
4+
load "maintenance/fix"
5+
6+
[
7+
{
8+
desc: "local",
9+
file: 'test/local/present.dhall',
10+
output: "number: 1"
11+
},
12+
{
13+
desc: "scope selected",
14+
args: ['-s', 'show'],
15+
file: 'test/local/present-scoped.dhall',
16+
output: "number: 1"
17+
},
18+
{
19+
desc: "scope not selected",
20+
file: 'test/local/present-scoped.dhall',
21+
output: "1"
22+
},
23+
24+
{
25+
desc: "missing: lax",
26+
file: 'test/local/missing.dhall',
27+
output: "1"
28+
},
29+
30+
{
31+
desc: "missing: scope not selected",
32+
file: 'test/local/missing-scoped.dhall',
33+
output: "1"
34+
},
35+
36+
{
37+
desc: "missing: semi-scope not selected",
38+
file: 'test/local/missing-semi-scoped.dhall',
39+
output: "1"
40+
},
41+
42+
{
43+
desc: "missing: scope selected",
44+
args: ['-s', 'show'],
45+
file: 'test/local/missing-scoped.dhall',
46+
success: false,
47+
output: /local "import failed"/
48+
},
49+
50+
{
51+
desc: "missing: semi-scope selected",
52+
args: ['-s', 'show'],
53+
file: 'test/local/missing-semi-scoped.dhall',
54+
success: false,
55+
output: /local "import failed"/
56+
},
57+
].each do |test_case|
58+
local_args = test_case.fetch(:args, [])
59+
file = test_case.fetch(:file)
60+
full_args = local_args + [file]
61+
test("#{test_case[:desc]} (#{full_args})") do
62+
result = run?(
63+
'./maintenance/local', *local_args,
64+
'dhall', 'text', '--file', file
65+
)
66+
assert_equal(result.success, test_case.fetch(:success, true), "process with output:\n#{result.output}")
67+
expected_output = test_case.fetch(:output)
68+
if expected_output.is_a?(Regexp)
69+
assert_matches(result.output, expected_output)
70+
else
71+
assert_equal(result.output, expected_output)
72+
end
73+
end
74+
end

0 commit comments

Comments
 (0)