Skip to content

Commit

Permalink
back to using env for local dependencies
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
timbertson committed Aug 26, 2021
1 parent 58fb84e commit 34befa0
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 95 deletions.
53 changes: 47 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,61 @@ There's also the `fix` attribute, which is the (not stable but handy) [./mainten

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

There's no good workflow for this built into dhall, so `dhall-render` provides a utility script (`dhall/local`) for this purpose.
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.

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.
To conditionally import either the local or public version of a library, you can use this pattern:

Then, to use it:
```dhall
let Dependency =
(\(local : Bool) -> ../local-directory/package.dhall ? local "import failed")
env:DHALL_LOCAL
? https://example.org/package.dhall
```

And then run whatever you need as a subcommand of `dhall/local`:

```
./dhall/local ./dhall/render
```

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).
This will run the command with `DHALL_LOCAL` set to `True`.

**Use in libraries**:

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

```dhall
let Dependency =
(\(local : Bool) -> ../local-directory/package.dhall ? local "import failed")
env:DHALL_LOCAL_MYLIB
? https://example.org/package.dhall
```

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.:

```
./dhall/local -s mylib ./dhall/render
```

Multiple scopes should be comma-separated in a single argument, e.g. `./dhall/local -s foo,bar,baz ./dhall/render`

**Error reporting**:

Note that if the local import fails to load (a missing file, type error, etc), you will unfortunately
not get the full error, you will only get:

```
Error: Not a function
local "import failed"
```

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`.


**Use outside the terminal**:

Upon termination, it restores the original `*.dhall` files, so it should result in no actual changes to your workspace.
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.
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).

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

Expand Down
108 changes: 21 additions & 87 deletions maintenance/local
Original file line number Diff line number Diff line change
@@ -1,94 +1,28 @@
#!/usr/bin/env bash
set -eu
# with-local will temporarily install all `.dhall.local` files in place of the
# corresponding `.dhall` files, for the duration of a single command.
#
# Upon exit, the script will attempt to restore the original versions.
# If this fails, the following invariants will hold, which should allow the
# script to be run again and self-correct:
# This script enables local imports using the following pattern:
#
# Invariants:
# - there is always a .local version
# - if there is no explicit .remote file, then there must be a plain file (which is the logical "remote" version)
# - if there is a .remote file, then
# - there may be a plain file, which is a symlink to .local

if [ "${DEBUG:-0}" = 1 ]; then
# set -x
function log {
echo >&2 "$@"
}
else
function log {
true
}
fi

function local_version {
echo "$1.local"
}

function remote_version {
echo "$1.remote"
}

function install_local {
path="$1"
remote_path="$(remote_version "$1")"
local_path="$(local_version "$1")"
if [ ! -e "$remote_path" ]; then
if [ -L "$path" ]; then
log "Error: $remote_path doesn't exist but $path is a symlink"
exit 1
fi
# backup to remote_path
log "moving to $remote_path"
mv "$path" "$remote_path"
fi
echo >&2 "[ using $local_path ... ]"
ln -s "$(basename "$local_path")" "$path"
}

function restore_remote {
# put remote version back in place
path="$1"
remote_path="$(remote_version "$1")"
if [ -e "$remote_path" ]; then
log "restoring $remote_path"
mv "$remote_path" "$path"
# let Dependency =
# (\(local : Bool) -> ../dependency/package.dhall ? local "import failed")
# env:DHALL_LOCAL
# ? https://(...)
#
# See the dhall-render readme for full details:
# https://github.com/timbertson/dhall-render#readme

SCOPES=()
while [ "$#" -gt 0 ]; do
if [ "x$1" = "x-s" ]; then
IFS=',' read -r -a SCOPES <<< "$(echo "$2" | tr '[:lower:]' '[:upper:]')"
shift 2
else
log "Error: $remote_path is not present"
exit 1
break
fi
}

function find_all {
base_dir="$(dirname "$0")"
base_dir="${DHALL_LOCAL_ROOT:-$base_dir}"
log "base_dir: $base_dir"
find "$base_dir" -name '*.dhall.local' | while read f; do
path="$(dirname "$f")/$(basename "$f" .local)"
log "processing $path"
echo "$path"
done
}

function restore_all {
find_all | while read f; do
restore_remote "$f"
done
}

function install_all {
find_all | while read f; do
install_local "$f"
done
}

if [ "$#" -eq 0 ]; then
echo >&2 "Usage: "$0" COMMAND"
fi
done

trap restore_all EXIT
install_all
"$@"
export DHALL_LOCAL=True
for scope in ${SCOPES[@]+${SCOPES[@]}}; do
eval "export DHALL_LOCAL_$scope=True"
done
exec "$@"
28 changes: 26 additions & 2 deletions test/lib/utest.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
# itty bitty test libby
def assert_equal(a,b)

require 'ostruct'
require 'open3'

def assert_equal(a, b, desc = nil)
if a != b
raise "AssertionError, expected: #{b.inspect}, got: #{a.inspect}"
suffix = desc ? " (#{desc})" : ""
raise "AssertionError, expected: #{b.inspect}, got: #{a.inspect}#{suffix}"
end
end

def assert_matches(a,b)
if !b.match?(a)
raise "AssertionError, expected: #{a.inspect} to match #{b.inspect}"
end
end

def test(desc)
puts("# #{desc} ...")
yield
end

def run(*cmd)
run?(*cmd).success or raise "Command failed: #{cmd.join(' ')}"
end

def run?(*cmd)
puts(" + #{cmd.join(' ')}")
Open3.popen2e(*cmd) do |stdin, out_and_err, wait|
output = out_and_err.read
code = wait.value
OpenStruct.new({ output: output, success: wait.value == 0 })
end
end
1 change: 1 addition & 0 deletions test/local/local-show.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
\(x : Natural) -> "number: ${Natural/show x}"
8 changes: 8 additions & 0 deletions test/local/missing-scoped.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
let show =
( \(local : Bool) ->
./local-show-impl-not-present.dhall ? local "import failed"
)
env:DHALL_LOCAL_SHOW
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21

in show 1
12 changes: 12 additions & 0 deletions test/local/missing-semi-scoped.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
let show =
-- This is a bit arduous, but it is possible to have a local implementation
-- which is attempted if `DHALL_LOCAL` is set, but required if DHALL_LOCAL_SHOW
-- is set. It's probably too verbose unless you want maximal flexibility.
(\(local : Bool) -> ./local-show-impl-not-present.dhall) env:DHALL_LOCAL
? ( \(local : Bool) ->
./local-show-impl-not-present.dhall ? local "import failed"
)
env:DHALL_LOCAL_SHOW
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21

in show 1
5 changes: 5 additions & 0 deletions test/local/missing.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let show =
(\(local : Bool) -> ./local-show-impl-not-present.dhall) env:DHALL_LOCAL
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21

in show 1
6 changes: 6 additions & 0 deletions test/local/present-scoped.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
let show =
(\(local : Bool) -> ./local-show.dhall ? local "import failed")
env:DHALL_LOCAL_SHOW
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21

in show 1
5 changes: 5 additions & 0 deletions test/local/present.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let show =
(\(local : Bool) -> ./local-show.dhall) env:DHALL_LOCAL
? https://prelude.dhall-lang.org/v20.0.0/Natural/show.dhall sha256:684ed560ad86f438efdea229eca122c29e8e14f397ed32ec97148d578ca5aa21

in show 1
74 changes: 74 additions & 0 deletions test/test-local.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env ruby
require_relative 'lib/utest'

load "maintenance/fix"

[
{
desc: "local",
file: 'test/local/present.dhall',
output: "number: 1"
},
{
desc: "scope selected",
args: ['-s', 'show'],
file: 'test/local/present-scoped.dhall',
output: "number: 1"
},
{
desc: "scope not selected",
file: 'test/local/present-scoped.dhall',
output: "1"
},

{
desc: "missing: lax",
file: 'test/local/missing.dhall',
output: "1"
},

{
desc: "missing: scope not selected",
file: 'test/local/missing-scoped.dhall',
output: "1"
},

{
desc: "missing: semi-scope not selected",
file: 'test/local/missing-semi-scoped.dhall',
output: "1"
},

{
desc: "missing: scope selected",
args: ['-s', 'show'],
file: 'test/local/missing-scoped.dhall',
success: false,
output: /local "import failed"/
},

{
desc: "missing: semi-scope selected",
args: ['-s', 'show'],
file: 'test/local/missing-semi-scoped.dhall',
success: false,
output: /local "import failed"/
},
].each do |test_case|
local_args = test_case.fetch(:args, [])
file = test_case.fetch(:file)
full_args = local_args + [file]
test("#{test_case[:desc]} (#{full_args})") do
result = run?(
'./maintenance/local', *local_args,
'dhall', 'text', '--file', file
)
assert_equal(result.success, test_case.fetch(:success, true), "process with output:\n#{result.output}")
expected_output = test_case.fetch(:output)
if expected_output.is_a?(Regexp)
assert_matches(result.output, expected_output)
else
assert_equal(result.output, expected_output)
end
end
end

0 comments on commit 34befa0

Please sign in to comment.