Skip to content

Commit 2f9792e

Browse files
authored
Fix paths in env scripts in nested shells (#13)
When starting a shell from an existing shell where the shell config has already been sourced, or when sourcing a shell config for a second time within the same shell session, any opam setup in the shell config will re-insert the bin directory from the current opam switch at the beginning of PATH. Dune's setup logic doesn't re-insert directories already present in PATH as dune may share its bin directory with other programs reliant on the current PATH order. Thus opam's re-initialization will result in any dune executable in the current opam switch taking precedence over the binary distro. To prevent this, the __dune_env function detects when it's called subsequent times in a shell session, and if it succeeded the first time in correctly setting up the environment for the dune binary distribution will remove any opam directories from PATH which precede the first instance of location of the dune binary distro in PATH. Opam bin directories following the location of the binary distro in PATH, as well as all other directories in PATH are left unchanged. Signed-off-by: Stephen Sherratt <stephen@sherra.tt>
1 parent c754950 commit 2f9792e

File tree

5 files changed

+371
-53
lines changed

5 files changed

+371
-53
lines changed

.github/workflows/test.yml

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ name: test
22
on: [push, pull_request]
33
jobs:
44

5-
test-make-tarball:
6-
name: "Test that we can build the binary distro and package it as a tarball"
5+
test-env-scripts:
6+
name: "Test the env scripts packaged in the dune binary distro"
77
runs-on: ${{ matrix.os }}
88
strategy:
99
matrix:
@@ -33,12 +33,56 @@ jobs:
3333
run: ./make_tarball.sh $NAME ${{ matrix.output }}
3434
- name: "Extract the tarball"
3535
run: tar xf $NAME.tar.gz
36-
- name: "Test that we can env scripts and the dune executable is in the expected place"
36+
- name: "Test that we can run the env scripts and the dune executable is in the expected place"
3737
run: |
3838
bash -c '. $NAME/share/dune/env/env.bash; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
3939
zsh -c '. $NAME/share/dune/env/env.zsh; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
40-
fish -c '. $NAME/share/dune/env/env.fish; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
4140
sh -c '. $NAME/share/dune/env/env.sh; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
41+
fish -c '. $NAME/share/dune/env/env.fish; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
42+
- name: "Exercise that running __dune_env twice is benign since some logic only runs the second time they are sourced"
43+
run: |
44+
bash -c '. $NAME/share/dune/env/env.bash; __dune_env $PWD/$NAME; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
45+
zsh -c '. $NAME/share/dune/env/env.zsh; __dune_env $PWD/$NAME; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
46+
sh -c '. $NAME/share/dune/env/env.sh; __dune_env $PWD/$NAME; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
47+
fish -c '. $NAME/share/dune/env/env.fish; __dune_env $PWD/$NAME; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
48+
- name: "Make a fake opam switch and add it to PATH in between calls to __dune_env simulating a shell config with both opam and dune installed"
49+
run: |
50+
# Make a fake opam installation of dune
51+
mkdir -p ~/.opam/default/bin
52+
printf '%s\necho "%s"' '#!/bin/sh' 'fake dune' > ~/.opam/default/bin/dune
53+
chmod a+x ~/.opam/default/bin/dune
54+
55+
# Test adding the fake opam switch to the path after __dune_env
56+
# causes opam's dune to take precedence, and that calling __dune_env
57+
# a second time causes the binary distro's dune to take precedence.
58+
bash -c '. $NAME/share/dune/env/env.bash; __dune_env $PWD/$NAME; export PATH=~/.opam/default/bin:$PATH; test $(which dune) = ~/.opam/default/bin/dune'
59+
bash -c '. $NAME/share/dune/env/env.bash; __dune_env $PWD/$NAME; export PATH=~/.opam/default/bin:$PATH; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
60+
zsh -c '. $NAME/share/dune/env/env.zsh; __dune_env $PWD/$NAME; export PATH=~/.opam/default/bin:$PATH; test $(which dune) = ~/.opam/default/bin/dune'
61+
zsh -c '. $NAME/share/dune/env/env.zsh; __dune_env $PWD/$NAME; export PATH=~/.opam/default/bin:$PATH; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
62+
sh -c '. $NAME/share/dune/env/env.sh; __dune_env $PWD/$NAME; export PATH=~/.opam/default/bin:$PATH; test $(which dune) = ~/.opam/default/bin/dune'
63+
sh -c '. $NAME/share/dune/env/env.sh; __dune_env $PWD/$NAME; export PATH=~/.opam/default/bin:$PATH; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
64+
fish -c '. $NAME/share/dune/env/env.fish; __dune_env $PWD/$NAME; fish_add_path --prepend --move --path ~/.opam/default/bin; test $(which dune) = ~/.opam/default/bin/dune'
65+
fish -c '. $NAME/share/dune/env/env.fish; __dune_env $PWD/$NAME; fish_add_path --prepend --move --path ~/.opam/default/bin; __dune_env $PWD/$NAME; test $(which dune) = $PWD/$NAME/bin/dune'
66+
67+
- name: "Test that opam paths later in the PATH variable are preserved by multiple calls to __dune_env"
68+
run: |
69+
printf '%s\necho "%s"' '#!/bin/sh' 'only in opam bin' > ~/.opam/default/bin/only-in-opam-bin
70+
chmod a+x ~/.opam/default/bin/only-in-opam-bin
71+
bash -c '. $NAME/share/dune/env/env.bash; export PATH=~/.opam/default/bin:$PATH; __dune_env $PWD/$NAME; __dune_env $PWD/$NAME; only-in-opam-bin'
72+
zsh -c '. $NAME/share/dune/env/env.zsh; export PATH=~/.opam/default/bin:$PATH; __dune_env $PWD/$NAME; __dune_env $PWD/$NAME; only-in-opam-bin'
73+
sh -c '. $NAME/share/dune/env/env.sh; export PATH=~/.opam/default/bin:$PATH; __dune_env $PWD/$NAME; __dune_env $PWD/$NAME; only-in-opam-bin'
74+
fish -c '. $NAME/share/dune/env/env.fish; fish_add_path --prepend --move --path ~/.opam/default/bin; __dune_env $PWD/$NAME; __dune_env $PWD/$NAME; only-in-opam-bin'
75+
76+
- name: "Test that non-opam changes to PATH between two calls to __dune_env have their effect preserved"
77+
run: |
78+
# Simulate the user setting up a custom version of dune (e.g. what a dune developer might do to try out local changes)
79+
mkdir -p ~/bin
80+
printf '%s\necho "%s"' '#!/bin/sh' 'fake dune' > ~/bin/dune
81+
chmod a+x ~/bin/dune
82+
bash -c '. $NAME/share/dune/env/env.bash; __dune_env $PWD/$NAME; export PATH=~/bin:$PATH; __dune_env $PWD/$NAME; test $(which dune) = ~/bin/dune'
83+
zsh -c '. $NAME/share/dune/env/env.zsh ; __dune_env $PWD/$NAME; export PATH=~/bin:$PATH; __dune_env $PWD/$NAME; test $(which dune) = ~/bin/dune'
84+
sh -c '. $NAME/share/dune/env/env.sh; __dune_env $PWD/$NAME; export PATH=~/bin:$PATH; __dune_env $PWD/$NAME; test $(which dune) = ~/bin/dune'
85+
fish -c '. $NAME/share/dune/env/env.fish; __dune_env $PWD/$NAME; fish_add_path --prepend --move --path ~/bin; __dune_env $PWD/$NAME; test $(which dune) = ~/bin/dune'
4286
4387
4488
test-make-tarball-shellcheck:
@@ -65,5 +109,4 @@ jobs:
65109
sudo apt-get install -y shellcheck
66110
- name: "Run shellcheck on the shell env script"
67111
run: |
68-
# exclude warning for sourcing missing external file
69112
shellcheck extra/share/dune/env/env.sh extra/share/dune/env/env.bash

extra/share/dune/env/env.bash

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,105 @@
11
#!/usr/bin/env bash
22

3+
# Equivalent to `which dune`, but `which` might not be installed. Bash's `type`
4+
# supports -P which acts like `which` but is guaranteed to be installed, but
5+
# older versions of bash might not support -P so we'll err on the side of
6+
# caution and implement it here in the interest of portability.
7+
__dune_which() {
8+
echo "$PATH" | \
9+
tr ':' '\n' |\
10+
while read -r entry; do
11+
if test -x "$entry/dune"; then
12+
echo "$entry/dune"
13+
return 0
14+
fi
15+
done
16+
return 1
17+
}
18+
19+
export __DUNE_SETUP_STATE="${__DUNE_SETUP_STATE:-incomplete}"
20+
321
__dune_env() {
422
if [ "$#" != "1" ]; then
523
echo "__dune_env expected 1 argument, got $#"
624
return
725
fi
826
local ROOT="$1"
927

10-
# Add dune to PATH unless it's already present.
11-
# Affix colons on either side of $PATH to simplify matching (based on
12-
# rustup's env script).
13-
case :"$PATH": in
14-
*:"$ROOT/bin":*)
15-
# Do nothing since the bin directory is already in PATH.
28+
case "$__DUNE_SETUP_STATE" in
29+
incomplete)
30+
# This is the first time __dune_env has been called, so attempt to set up
31+
# the environment for dune and record in the global variable
32+
# __DUNE_SETUP_STATE whether or not it succeeded.
33+
34+
# Add dune to PATH unless it's already present.
35+
# Affix colons on either side of $PATH to simplify matching (based on
36+
# rustup's env script).
37+
case :"$PATH": in
38+
*:"$ROOT/bin":*)
39+
# Do nothing since the bin directory is already in PATH.
40+
;;
41+
*)
42+
export PATH="$ROOT/bin:$PATH"
43+
;;
44+
esac
45+
46+
if [ "$(__dune_which)" = "$ROOT/bin/dune" ]; then
47+
export __DUNE_SETUP_STATE=success
48+
49+
# Only load completions if the shell is interactive.
50+
if [ -t 0 ]; then
51+
# Load bash completions for dune.
52+
# Suppress warning from shellcheck as it can't see the completion script.
53+
# shellcheck disable=SC1091
54+
. "$ROOT"/share/bash-completion/completions/dune
55+
fi
56+
else
57+
# Despite modifying the environment, running `dune` would resolve to
58+
# the wrong dune instance. This can happen if the bin directory
59+
# containing our dune was already present in PATH behind the bin
60+
# directory of the default opam switch.
61+
# TODO Possibly print a warning/hint in this case to help users fix
62+
# their environment.
63+
export __DUNE_SETUP_STATE=failure
64+
fi
65+
;;
66+
success)
67+
# This is at least the second time this function was called in the
68+
# current environment (possibly by the user sourcing their shell config
69+
# or nesting shell sessions), and the previous time it was called it
70+
# successfully modified the environment to give precedence to our dune
71+
# executable. check that our dune still has precedence, and attempt to
72+
# undo any opam-specific path shenanigans that have taken place since the
73+
# last time this function was called.
74+
if [ "$(__dune_which)" != "$ROOT/bin/dune" ]; then
75+
case :"$PATH": in
76+
*:"$ROOT/bin":*)
77+
# Remove all opam bin directories from the PATH variable
78+
# between the start of the PATH variable and the first
79+
# occurrence of the dune binary distro's bin directory.
80+
PATH_MAYBE_FIXED=$(echo "$PATH" | \
81+
tr ':' '\n' |\
82+
sed "1,\#^$ROOT/bin\$# { \#^$HOME/.opam#d; }" |\
83+
paste -sd ':' -)
84+
# Only commit the change if it actually fixed the problem.
85+
if [ "$(PATH=$PATH_MAYBE_FIXED __dune_which)" = "$ROOT/bin/dune" ]; then
86+
export PATH="$PATH_MAYBE_FIXED"
87+
else
88+
# The attempt to fix the PATH variable failed, so give up.
89+
export __DUNE_SETUP_STATE=failure
90+
fi
91+
;;
92+
*)
93+
# The dune binary distro is no longer in the PATH variable at
94+
# all, so give up.
95+
export __DUNE_SETUP_STATE=failure
96+
;;
97+
esac
98+
fi
1699
;;
17-
*)
18-
# Prepending path in case a system-installed dune needs to be overridden
19-
export PATH="$ROOT/bin:$PATH"
100+
failure)
101+
# A previous attempt at modifying the environment failed, so don't
102+
# attempt further environment modifications here.
20103
;;
21104
esac
22-
23-
# Only load completions if the shell is interactive.
24-
if [ -t 0 ]; then
25-
# Load bash completions for dune.
26-
# Suppress warning from shellcheck as it can't see the completion script.
27-
# shellcheck disable=SC1091
28-
. "$ROOT"/share/bash-completion/completions/dune
29-
fi
30105
}

extra/share/dune/env/env.fish

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,63 @@
11
#!/usr/bin/env fish
22

3+
if ! set -q __DUNE_SETUP_STATE
4+
set --export __DUNE_SETUP_STATE "incomplete"
5+
end
6+
37
function __dune_env
4-
set dune_bin_path "$argv[1]/bin"
5-
if ! contains "$dune_bin_path" $PATH;
6-
fish_add_path --prepend --path "$dune_bin_path"
8+
if [ "$(count $argv)" != "1" ]
9+
echo "__dune_env expected 1 argument, got $(count $argv)"
10+
end
11+
12+
set --local dune_root "$argv[1]"
13+
set --local dune_bin "$dune_root/bin"
14+
15+
switch "$__DUNE_SETUP_STATE"
16+
case incomplete
17+
if ! contains "$dune_bin" $PATH; and [ -d "$dune_bin" ]
18+
fish_add_path --prepend --path "$dune_bin"
19+
end
20+
if [ "$(type -P dune)" = "$dune_bin/dune" ]
21+
set --export __DUNE_SETUP_STATE "success"
22+
else
23+
# Despite modifying the environment, running `dune` would resolve to
24+
# the wrong dune instance. This can happen if the bin directory
25+
# containing our dune was already present in PATH behind the bin
26+
# directory of the default opam switch.
27+
# TODO Possibly print a warning/hint in this case to help users fix
28+
# their environment.
29+
set --export __DUNE_SETUP_STATE "failure"
30+
end
31+
case success
32+
# This is at least the second time this function was called in the
33+
# current environment (possibly by the user sourcing their shell config
34+
# or nesting shell sessions), and the previous time it was called it
35+
# successfully modified the environment to give precedence to our dune
36+
# executable. check that our dune still has precedence, and attempt to
37+
# undo any opam-specific path shenanigans that have taken place since the
38+
# last time this function was called.
39+
if [ "$(type -P dune)" != "$dune_bin/dune" ]
40+
if contains "$dune_bin" $PATH
41+
# Remove all opam bin directories from the PATH variable
42+
# between the start of the PATH variable and the first
43+
# occurrence of the dune binary distro's bin directory.
44+
set --local PATH_maybe_fixed $(printf "%s\n" $PATH | \
45+
sed "1,\#^$dune_bin\$# { \#^$HOME/.opam#d; }")
46+
# Only commit the change if it actually fixed the problem.
47+
if [ "$(PATH=$PATH_maybe_fixed type -P dune)" = "$dune_bin/dune" ]
48+
set --export PATH $PATH_maybe_fixed
49+
else
50+
# The attempt to fix the PATH variable failed, so give up.
51+
set --export __DUNE_SETUP_STATE "failure"
52+
end
53+
else
54+
# The dune binary distro is no longer in the PATH variable at
55+
# all, so give up.
56+
set --export __DUNE_SETUP_STATE "failure"
57+
end
58+
end
59+
case failure
60+
# A previous attempt at modifying the environment failed, so don't
61+
# attempt further environment modifications here.
762
end
863
end

extra/share/dune/env/env.sh

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,94 @@
11
#!/bin/sh
22

3+
# Equivalent to `which dune`, but `which` might not be installed.
4+
__dune_which() {
5+
echo "$PATH" | \
6+
tr ':' '\n' |\
7+
while read -r entry; do
8+
if test -x "$entry/dune"; then
9+
echo "$entry/dune"
10+
return 0
11+
fi
12+
done
13+
return 1
14+
}
15+
16+
export __DUNE_SETUP_STATE="${__DUNE_SETUP_STATE:-incomplete}"
17+
318
__dune_env() {
419
if [ "$#" != "1" ]; then
520
echo "__dune_env expected 1 argument, got $#"
621
return
722
fi
8-
__dune_root="$1"
23+
__DUNE_ROOT="$1"
924

10-
# Add dune to PATH unless it's already present.
11-
# Affix colons on either side of $PATH to simplify matching (based on
12-
# rustup's env script).
13-
case :"$PATH": in
14-
*:"$__dune_root/bin":*)
15-
# Do nothing since the bin directory is already in PATH.
25+
case "$__DUNE_SETUP_STATE" in
26+
incomplete)
27+
# This is the first time __dune_env has been called, so attempt to set up
28+
# the environment for dune and record in the global variable
29+
# __DUNE_SETUP_STATE whether or not it succeeded.
30+
31+
# Add dune to PATH unless it's already present.
32+
# Affix colons on either side of $PATH to simplify matching (based on
33+
# rustup's env script).
34+
case :"$PATH": in
35+
*:"$__DUNE_ROOT/bin":*)
36+
# Do nothing since the bin directory is already in PATH.
37+
;;
38+
*)
39+
export PATH="$__DUNE_ROOT/bin:$PATH"
40+
;;
41+
esac
42+
43+
if [ "$(__dune_which)" = "$__DUNE_ROOT/bin/dune" ]; then
44+
export __DUNE_SETUP_STATE=success
45+
else
46+
# Despite modifying the environment, running `dune` would resolve to
47+
# the wrong dune instance. This can happen if the bin directory
48+
# containing our dune was already present in PATH behind the bin
49+
# directory of the default opam switch.
50+
# TODO Possibly print a warning/hint in this case to help users fix
51+
# their environment.
52+
export __DUNE_SETUP_STATE=failure
53+
fi
54+
;;
55+
success)
56+
# This is at least the second time this function was called in the
57+
# current environment (possibly by the user sourcing their shell config
58+
# or nesting shell sessions), and the previous time it was called it
59+
# successfully modified the environment to give precedence to our dune
60+
# executable. check that our dune still has precedence, and attempt to
61+
# undo any opam-specific path shenanigans that have taken place since the
62+
# last time this function was called.
63+
if [ "$(__dune_which)" != "$__DUNE_ROOT/bin/dune" ]; then
64+
case :"$PATH": in
65+
*:"$__DUNE_ROOT/bin":*)
66+
# Remove all opam bin directories from the PATH variable
67+
# between the start of the PATH variable and the first
68+
# occurrence of the dune binary distro's bin directory.
69+
PATH_MAYBE_FIXED=$(echo "$PATH" | \
70+
tr ':' '\n' |\
71+
sed "1,\#^$__DUNE_ROOT/bin\$# { \#^$HOME/.opam#d; }" |\
72+
paste -sd ':' -)
73+
# Only commit the change if it actually fixed the problem.
74+
if [ "$(PATH=$PATH_MAYBE_FIXED __dune_which)" = "$__DUNE_ROOT/bin/dune" ]; then
75+
export PATH="$PATH_MAYBE_FIXED"
76+
else
77+
# The attempt to fix the PATH variable failed, so give up.
78+
export __DUNE_SETUP_STATE=failure
79+
fi
80+
;;
81+
*)
82+
# The dune binary distro is no longer in the PATH variable at
83+
# all, so give up.
84+
export __DUNE_SETUP_STATE=failure
85+
;;
86+
esac
87+
fi
1688
;;
17-
*)
18-
# Prepending path in case a system-installed dune needs to be overridden
19-
export PATH="$__dune_root/bin:$PATH"
89+
failure)
90+
# A previous attempt at modifying the environment failed, so don't
91+
# attempt further environment modifications here.
2092
;;
2193
esac
2294
}

0 commit comments

Comments
 (0)