Skip to content

Commit 23bfd20

Browse files
Check SDK Docker scripts on Bash 3.2 (#590)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b05da88 commit 23bfd20

4 files changed

Lines changed: 315 additions & 11 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Bash 3.2 script compatibility
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths: &script-paths
7+
- bin/check-bash32
8+
- bin/docker-run
9+
- bin/docker-user-args
10+
- bin/normalize-sdk-modes
11+
- bin/php
12+
- bin/php-in-docker
13+
- bin/sandbox-dotnet
14+
- bin/sandbox-java
15+
- bin/sandbox-node
16+
- bin/sandbox-ruby
17+
- sdks/*/bin/dotnet
18+
- sdks/*/bin/gradle
19+
- sdks/*/bin/node
20+
- sdks/*/bin/php
21+
- sdks/*/bin/php-8
22+
- sdks/*/bin/python
23+
- sdks/*/bin/ruby
24+
- sdks/*/run-build
25+
- .github/workflows/bash32-script-compat.yml
26+
pull_request:
27+
branches: [ main ]
28+
paths: *script-paths
29+
30+
permissions:
31+
contents: read
32+
33+
jobs:
34+
bash32:
35+
runs-on: ubuntu-latest
36+
timeout-minutes: 10
37+
steps:
38+
- name: Checkout Repo
39+
uses: actions/checkout@v3
40+
41+
- name: Check SDK scripts on Bash 3.2
42+
run: ./bin/check-bash32

‎bin/check-bash32‎

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#!/usr/bin/env bash
2+
3+
# macOS ships Bash 3.2. SDK Docker wrappers must keep running there.
4+
# This re-executes under the bash:3.2 image, parses the wrappers, rejects
5+
# Bash 4 syntax that `bash -n` cannot see, and runs them with docker stubbed.
6+
7+
set -euo pipefail
8+
9+
ROOT=$(cd "$(dirname "$0")/.." && pwd)
10+
11+
if [[ "${CHECK_BASH32_INNER:-}" != "1" ]]; then
12+
exec docker run --rm \
13+
-v "${ROOT}:/work" \
14+
-w /work \
15+
-e CHECK_BASH32_INNER=1 \
16+
"${BASH32_IMAGE:-bash:3.2}" \
17+
bash bin/check-bash32
18+
fi
19+
20+
if [[ "${BASH_VERSINFO[0]}" -ne 3 || "${BASH_VERSINFO[1]}" -ne 2 ]]; then
21+
printf 'expected Bash 3.2, found %s\n' "${BASH_VERSION}" >&2
22+
exit 1
23+
fi
24+
25+
# Bash 4+ tokens. `bash -n` accepts `mapfile` on Bash 3.2 because it is
26+
# just a command name; the shell only fails when that line runs.
27+
bash4_pattern='(^|[[:space:]])(mapfile|readarray|coproc)([[:space:]]|$)|declare[[:space:]]+-A|\[\[[[:space:]]+-v[[:space:]]|\$\{[A-Za-z_][A-Za-z0-9_]*(,,|\^\^)|\$\{[A-Za-z_][A-Za-z0-9_]*@[A-Za-z]|\$\{[^}]*\[-[0-9]+\]|&>>|[|][&]'
28+
29+
scripts=(
30+
bin/docker-run
31+
bin/docker-user-args
32+
bin/normalize-sdk-modes
33+
bin/php
34+
bin/php-in-docker
35+
bin/sandbox-dotnet
36+
bin/sandbox-java
37+
bin/sandbox-node
38+
bin/sandbox-ruby
39+
sdks/dotnet/bin/dotnet
40+
sdks/dotnet/bin/php
41+
sdks/dotnet/run-build
42+
sdks/java-v1/bin/gradle
43+
sdks/java-v1/bin/php
44+
sdks/java-v1/run-build
45+
sdks/java-v2/bin/gradle
46+
sdks/java-v2/bin/php
47+
sdks/java-v2/run-build
48+
sdks/node/bin/node
49+
sdks/node/bin/php
50+
sdks/node/run-build
51+
sdks/php/bin/php
52+
sdks/php/bin/php-8
53+
sdks/php/run-build
54+
sdks/python/bin/php
55+
sdks/python/bin/python
56+
sdks/python/run-build
57+
sdks/ruby/bin/php
58+
sdks/ruby/bin/ruby
59+
sdks/ruby/run-build
60+
)
61+
62+
reject_bash4() {
63+
local script="$1"
64+
local line
65+
line=$(grep -nE "${bash4_pattern}" "${script}" | grep -vE '^[0-9]+:[[:space:]]*#' || true)
66+
if [[ -n "${line}" ]]; then
67+
printf 'Bash 4+ syntax in %s:\n%s\n' "${script}" "${line}" >&2
68+
return 1
69+
fi
70+
}
71+
72+
self_test_denylist() {
73+
local tmp
74+
tmp=$(mktemp)
75+
printf '#!/usr/bin/env bash\n# mapfile stays in this comment\necho ok\n' > "${tmp}"
76+
reject_bash4 "${tmp}"
77+
printf '#!/usr/bin/env bash\nmapfile -t xs < /dev/null\n' > "${tmp}"
78+
if reject_bash4 "${tmp}" 2>/dev/null; then
79+
rm -f "${tmp}"
80+
printf 'denylist did not reject mapfile\n' >&2
81+
exit 1
82+
fi
83+
rm -f "${tmp}"
84+
}
85+
86+
assert_log_contains() {
87+
local needle="$1"
88+
if ! grep -Fxq -e "${needle}" "${DOCKER_RUN_LOG}"; then
89+
printf 'expected docker argument <%s>\nactual:\n' "${needle}" >&2
90+
cat "${DOCKER_RUN_LOG}" >&2
91+
exit 1
92+
fi
93+
}
94+
95+
assert_log_excludes() {
96+
local needle="$1"
97+
if grep -Fxq -e "${needle}" "${DOCKER_RUN_LOG}"; then
98+
printf 'unexpected docker argument <%s>\nactual:\n' "${needle}" >&2
99+
cat "${DOCKER_RUN_LOG}" >&2
100+
exit 1
101+
fi
102+
}
103+
104+
run_docker_wrapper() {
105+
: > "${DOCKER_RUN_LOG}"
106+
if ! "$@"; then
107+
printf 'command failed:' >&2
108+
printf ' %s' "$@" >&2
109+
printf '\n' >&2
110+
exit 1
111+
fi
112+
}
113+
114+
self_test_denylist
115+
116+
for script in "${scripts[@]}"; do
117+
if ! bash -n "${script}"; then
118+
printf 'Bash 3.2 rejected %s\n' "${script}" >&2
119+
exit 1
120+
fi
121+
reject_bash4 "${script}"
122+
done
123+
124+
work=$(mktemp -d)
125+
stubdir=$(mktemp -d)
126+
trap 'rm -rf "${work}" "${stubdir}"' EXIT
127+
128+
for script in "${scripts[@]}"; do
129+
mkdir -p "${work}/$(dirname "${script}")"
130+
cp "${script}" "${work}/${script}"
131+
done
132+
133+
cat > "${stubdir}/docker" <<'EOF'
134+
#!/usr/bin/env bash
135+
set -euo pipefail
136+
if [[ "${1:-}" == "info" ]]; then
137+
if [[ -n "${DOCKER_INFO_SECURITY:-}" ]]; then
138+
printf '%s\n' "${DOCKER_INFO_SECURITY}"
139+
fi
140+
exit 0
141+
fi
142+
if [[ "${1:-}" == "run" ]]; then
143+
printf '%s\n' "$@" >> "${DOCKER_RUN_LOG:?}"
144+
exit 0
145+
fi
146+
printf 'unexpected docker command: %s\n' "$*" >&2
147+
exit 1
148+
EOF
149+
chmod +x "${stubdir}/docker"
150+
151+
export PATH="${stubdir}:${PATH}"
152+
DOCKER_RUN_LOG=$(mktemp)
153+
export DOCKER_RUN_LOG
154+
unset DOCKER_RUN_AS_ROOT DOCKER_INFO_SECURITY PHP_DOCKER_PLATFORM PHP_USE_NATIVE GITHUB_ACTIONS
155+
156+
host_user="$(id -u):$(id -g)"
157+
158+
user_args=$("${work}/bin/docker-user-args")
159+
expected=$(printf '%s\n' --user "${host_user}")
160+
if [[ "${user_args}" != "${expected}" ]]; then
161+
printf 'rootful docker-user-args returned:\n%s\n' "${user_args}" >&2
162+
exit 1
163+
fi
164+
165+
user_args=$(DOCKER_RUN_AS_ROOT=1 "${work}/bin/docker-user-args")
166+
expected=$(printf '%s\n' --user root:root)
167+
if [[ "${user_args}" != "${expected}" ]]; then
168+
printf 'DOCKER_RUN_AS_ROOT docker-user-args returned:\n%s\n' "${user_args}" >&2
169+
exit 1
170+
fi
171+
172+
user_args=$(DOCKER_INFO_SECURITY=name=rootless "${work}/bin/docker-user-args")
173+
if [[ "${user_args}" != "${expected}" ]]; then
174+
printf 'rootless docker-user-args returned:\n%s\n' "${user_args}" >&2
175+
exit 1
176+
fi
177+
178+
run_docker_wrapper "${work}/bin/docker-run" echo ok
179+
assert_log_contains --rm
180+
assert_log_contains --user
181+
assert_log_contains "${host_user}"
182+
assert_log_contains echo
183+
assert_log_contains ok
184+
185+
DOCKER_RUN_AS_ROOT=1 run_docker_wrapper "${work}/bin/docker-run" echo ok
186+
assert_log_contains root:root
187+
188+
runtime=(
189+
bin/php
190+
sdks/dotnet/bin/dotnet
191+
sdks/dotnet/bin/php
192+
sdks/java-v1/bin/gradle
193+
sdks/java-v1/bin/php
194+
sdks/java-v2/bin/gradle
195+
sdks/java-v2/bin/php
196+
sdks/node/bin/node
197+
sdks/node/bin/php
198+
sdks/php/bin/php
199+
sdks/php/bin/php-8
200+
sdks/python/bin/php
201+
sdks/python/bin/python
202+
sdks/ruby/bin/php
203+
sdks/ruby/bin/ruby
204+
)
205+
206+
for script in "${runtime[@]}"; do
207+
run_docker_wrapper "${work}/${script}" true
208+
assert_log_contains --user
209+
assert_log_contains "${host_user}"
210+
assert_log_contains true
211+
done
212+
213+
run_docker_wrapper "${work}/bin/php" composer install
214+
assert_log_excludes --platform
215+
assert_log_contains jtreminio/php-cli:7.4
216+
217+
run_docker_wrapper "${work}/sdks/node/bin/php" composer install
218+
assert_log_excludes --platform
219+
220+
run_docker_wrapper "${work}/sdks/php/bin/php" php -v
221+
assert_log_contains --platform
222+
assert_log_contains linux/amd64
223+
assert_log_contains jtreminio/php-cli:8.1
224+
225+
run_docker_wrapper "${work}/sdks/php/bin/php-8" php -v
226+
assert_log_contains --platform
227+
assert_log_contains jtreminio/php:8.1
228+
229+
run_docker_wrapper "${work}/sdks/dotnet/bin/dotnet" dotnet test
230+
assert_log_contains mcr.microsoft.com/dotnet/sdk:10.0
231+
232+
: > "${DOCKER_RUN_LOG}"
233+
PHP_USE_NATIVE=1 "${work}/sdks/php/bin/php" true
234+
if [[ -s "${DOCKER_RUN_LOG}" ]]; then
235+
printf 'PHP_USE_NATIVE=1 invoked docker:\n' >&2
236+
cat "${DOCKER_RUN_LOG}" >&2
237+
exit 1
238+
fi
239+
240+
sample=$(mktemp)
241+
printf 'x\n' > "${sample}"
242+
chmod 755 "${sample}"
243+
"${work}/bin/normalize-sdk-modes" "${sample}"
244+
mode=$(stat -c %a "${sample}")
245+
if [[ "${mode}" != "644" ]]; then
246+
printf 'normalize-sdk-modes left mode %s\n' "${mode}" >&2
247+
exit 1
248+
fi
249+
chmod 755 "${sample}"
250+
GITHUB_ACTIONS=1 "${work}/bin/normalize-sdk-modes" "${sample}"
251+
mode=$(stat -c %a "${sample}")
252+
if [[ "${mode}" != "755" ]]; then
253+
printf 'normalize-sdk-modes changed mode to %s on GitHub Actions\n' "${mode}" >&2
254+
exit 1
255+
fi
256+
rm -f "${sample}"
257+
258+
printf 'Bash %s accepted the SDK Docker scripts\n' "${BASH_VERSION}"

‎bin/docker-run‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
set -euo pipefail
77

88
DIR=$(cd "$(dirname "$0")" && pwd)
9-
mapfile -t DOCKER_USER_ARGS < <("${DIR}/docker-user-args")
9+
DOCKER_USER_ARGS=()
10+
while IFS= read -r arg; do
11+
DOCKER_USER_ARGS+=("$arg")
12+
done < <("${DIR}/docker-user-args")
1013

1114
exec docker run --rm "${DOCKER_USER_ARGS[@]}" "$@"

‎bin/php-in-docker‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@ shift 3
1414

1515
mkdir -p "${ROOT_DIR}/.docker/composer"
1616

17-
platform=()
17+
# Keep this array non-empty. Bash 3.2 with set -u errors on an empty
18+
# "${array[@]}", and that is the Bash macOS ships.
19+
docker_args=(
20+
-v "${ROOT_DIR}:${WORKING_DIR}"
21+
-v "${ROOT_DIR}/.docker/composer:/tmp/composer"
22+
-e COMPOSER_HOME=/tmp/composer
23+
-w "${WORKING_DIR}"
24+
"${IMAGE}"
25+
)
1826
if [[ -n "${PHP_DOCKER_PLATFORM:-}" ]]; then
19-
platform=(--platform "${PHP_DOCKER_PLATFORM}")
27+
docker_args=(--platform "${PHP_DOCKER_PLATFORM}" "${docker_args[@]}")
2028
fi
2129

22-
exec "${DIR}/docker-run" \
23-
"${platform[@]}" \
24-
-v "${ROOT_DIR}:${WORKING_DIR}" \
25-
-v "${ROOT_DIR}/.docker/composer:/tmp/composer" \
26-
-e COMPOSER_HOME=/tmp/composer \
27-
-w "${WORKING_DIR}" \
28-
"${IMAGE}" \
29-
"$@"
30+
exec "${DIR}/docker-run" "${docker_args[@]}" "$@"

0 commit comments

Comments
 (0)