Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/bash32-script-compat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Bash 3.2 script compatibility

on:
push:
branches: [ main ]
paths: &script-paths
- bin/check-bash32
- bin/docker-run
- bin/docker-user-args
- bin/normalize-sdk-modes
- bin/php
- bin/php-in-docker
- bin/sandbox-dotnet
- bin/sandbox-java
- bin/sandbox-node
- bin/sandbox-ruby
- sdks/*/bin/dotnet
- sdks/*/bin/gradle
- sdks/*/bin/node
- sdks/*/bin/php
- sdks/*/bin/php-8
- sdks/*/bin/python
- sdks/*/bin/ruby
- sdks/*/run-build
- .github/workflows/bash32-script-compat.yml
pull_request:
branches: [ main ]
paths: *script-paths

permissions:
contents: read

jobs:
bash32:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Repo
uses: actions/checkout@v3

- name: Check SDK scripts on Bash 3.2
run: ./bin/check-bash32
258 changes: 258 additions & 0 deletions bin/check-bash32
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
#!/usr/bin/env bash

# macOS ships Bash 3.2. SDK Docker wrappers must keep running there.
# This re-executes under the bash:3.2 image, parses the wrappers, rejects
# Bash 4 syntax that `bash -n` cannot see, and runs them with docker stubbed.

set -euo pipefail

ROOT=$(cd "$(dirname "$0")/.." && pwd)

if [[ "${CHECK_BASH32_INNER:-}" != "1" ]]; then
exec docker run --rm \
-v "${ROOT}:/work" \
-w /work \
-e CHECK_BASH32_INNER=1 \
"${BASH32_IMAGE:-bash:3.2}" \
bash bin/check-bash32
fi

if [[ "${BASH_VERSINFO[0]}" -ne 3 || "${BASH_VERSINFO[1]}" -ne 2 ]]; then
printf 'expected Bash 3.2, found %s\n' "${BASH_VERSION}" >&2
exit 1
fi

# Bash 4+ tokens. `bash -n` accepts `mapfile` on Bash 3.2 because it is
# just a command name; the shell only fails when that line runs.
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]+\]|&>>|[|][&]'

scripts=(
bin/docker-run
bin/docker-user-args
bin/normalize-sdk-modes
bin/php
bin/php-in-docker
bin/sandbox-dotnet
bin/sandbox-java
bin/sandbox-node
bin/sandbox-ruby
sdks/dotnet/bin/dotnet
sdks/dotnet/bin/php
sdks/dotnet/run-build
sdks/java-v1/bin/gradle
sdks/java-v1/bin/php
sdks/java-v1/run-build
sdks/java-v2/bin/gradle
sdks/java-v2/bin/php
sdks/java-v2/run-build
sdks/node/bin/node
sdks/node/bin/php
sdks/node/run-build
sdks/php/bin/php
sdks/php/bin/php-8
sdks/php/run-build
sdks/python/bin/php
sdks/python/bin/python
sdks/python/run-build
sdks/ruby/bin/php
sdks/ruby/bin/ruby
sdks/ruby/run-build
)

reject_bash4() {
local script="$1"
local line
line=$(grep -nE "${bash4_pattern}" "${script}" | grep -vE '^[0-9]+:[[:space:]]*#' || true)
if [[ -n "${line}" ]]; then
printf 'Bash 4+ syntax in %s:\n%s\n' "${script}" "${line}" >&2
return 1
fi
}

self_test_denylist() {
local tmp
tmp=$(mktemp)
printf '#!/usr/bin/env bash\n# mapfile stays in this comment\necho ok\n' > "${tmp}"
reject_bash4 "${tmp}"
printf '#!/usr/bin/env bash\nmapfile -t xs < /dev/null\n' > "${tmp}"
if reject_bash4 "${tmp}" 2>/dev/null; then
rm -f "${tmp}"
printf 'denylist did not reject mapfile\n' >&2
exit 1
fi
rm -f "${tmp}"
}

assert_log_contains() {
local needle="$1"
if ! grep -Fxq -e "${needle}" "${DOCKER_RUN_LOG}"; then
printf 'expected docker argument <%s>\nactual:\n' "${needle}" >&2
cat "${DOCKER_RUN_LOG}" >&2
exit 1
fi
}

assert_log_excludes() {
local needle="$1"
if grep -Fxq -e "${needle}" "${DOCKER_RUN_LOG}"; then
printf 'unexpected docker argument <%s>\nactual:\n' "${needle}" >&2
cat "${DOCKER_RUN_LOG}" >&2
exit 1
fi
}

run_docker_wrapper() {
: > "${DOCKER_RUN_LOG}"
if ! "$@"; then
printf 'command failed:' >&2
printf ' %s' "$@" >&2
printf '\n' >&2
exit 1
fi
}

self_test_denylist

for script in "${scripts[@]}"; do
if ! bash -n "${script}"; then
printf 'Bash 3.2 rejected %s\n' "${script}" >&2
exit 1
fi
reject_bash4 "${script}"
done

work=$(mktemp -d)
stubdir=$(mktemp -d)
trap 'rm -rf "${work}" "${stubdir}"' EXIT

for script in "${scripts[@]}"; do
mkdir -p "${work}/$(dirname "${script}")"
cp "${script}" "${work}/${script}"
done

cat > "${stubdir}/docker" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" == "info" ]]; then
if [[ -n "${DOCKER_INFO_SECURITY:-}" ]]; then
printf '%s\n' "${DOCKER_INFO_SECURITY}"
fi
exit 0
fi
if [[ "${1:-}" == "run" ]]; then
printf '%s\n' "$@" >> "${DOCKER_RUN_LOG:?}"
exit 0
fi
printf 'unexpected docker command: %s\n' "$*" >&2
exit 1
EOF
chmod +x "${stubdir}/docker"

export PATH="${stubdir}:${PATH}"
DOCKER_RUN_LOG=$(mktemp)
export DOCKER_RUN_LOG
unset DOCKER_RUN_AS_ROOT DOCKER_INFO_SECURITY PHP_DOCKER_PLATFORM PHP_USE_NATIVE GITHUB_ACTIONS

host_user="$(id -u):$(id -g)"

user_args=$("${work}/bin/docker-user-args")
expected=$(printf '%s\n' --user "${host_user}")
if [[ "${user_args}" != "${expected}" ]]; then
printf 'rootful docker-user-args returned:\n%s\n' "${user_args}" >&2
exit 1
fi

user_args=$(DOCKER_RUN_AS_ROOT=1 "${work}/bin/docker-user-args")
expected=$(printf '%s\n' --user root:root)
if [[ "${user_args}" != "${expected}" ]]; then
printf 'DOCKER_RUN_AS_ROOT docker-user-args returned:\n%s\n' "${user_args}" >&2
exit 1
fi

user_args=$(DOCKER_INFO_SECURITY=name=rootless "${work}/bin/docker-user-args")
if [[ "${user_args}" != "${expected}" ]]; then
printf 'rootless docker-user-args returned:\n%s\n' "${user_args}" >&2
exit 1
fi

run_docker_wrapper "${work}/bin/docker-run" echo ok
assert_log_contains --rm
assert_log_contains --user
assert_log_contains "${host_user}"
assert_log_contains echo
assert_log_contains ok

DOCKER_RUN_AS_ROOT=1 run_docker_wrapper "${work}/bin/docker-run" echo ok
assert_log_contains root:root

runtime=(
bin/php
sdks/dotnet/bin/dotnet
sdks/dotnet/bin/php
sdks/java-v1/bin/gradle
sdks/java-v1/bin/php
sdks/java-v2/bin/gradle
sdks/java-v2/bin/php
sdks/node/bin/node
sdks/node/bin/php
sdks/php/bin/php
sdks/php/bin/php-8
sdks/python/bin/php
sdks/python/bin/python
sdks/ruby/bin/php
sdks/ruby/bin/ruby
)

for script in "${runtime[@]}"; do
run_docker_wrapper "${work}/${script}" true
assert_log_contains --user
assert_log_contains "${host_user}"
assert_log_contains true
done

run_docker_wrapper "${work}/bin/php" composer install
assert_log_excludes --platform
assert_log_contains jtreminio/php-cli:7.4

run_docker_wrapper "${work}/sdks/node/bin/php" composer install
assert_log_excludes --platform

run_docker_wrapper "${work}/sdks/php/bin/php" php -v
assert_log_contains --platform
assert_log_contains linux/amd64
assert_log_contains jtreminio/php-cli:8.1

run_docker_wrapper "${work}/sdks/php/bin/php-8" php -v
assert_log_contains --platform
assert_log_contains jtreminio/php:8.1

run_docker_wrapper "${work}/sdks/dotnet/bin/dotnet" dotnet test
assert_log_contains mcr.microsoft.com/dotnet/sdk:10.0

: > "${DOCKER_RUN_LOG}"
PHP_USE_NATIVE=1 "${work}/sdks/php/bin/php" true
if [[ -s "${DOCKER_RUN_LOG}" ]]; then
printf 'PHP_USE_NATIVE=1 invoked docker:\n' >&2
cat "${DOCKER_RUN_LOG}" >&2
exit 1
fi

sample=$(mktemp)
printf 'x\n' > "${sample}"
chmod 755 "${sample}"
"${work}/bin/normalize-sdk-modes" "${sample}"
mode=$(stat -c %a "${sample}")
if [[ "${mode}" != "644" ]]; then
printf 'normalize-sdk-modes left mode %s\n' "${mode}" >&2
exit 1
fi
chmod 755 "${sample}"
GITHUB_ACTIONS=1 "${work}/bin/normalize-sdk-modes" "${sample}"
mode=$(stat -c %a "${sample}")
if [[ "${mode}" != "755" ]]; then
printf 'normalize-sdk-modes changed mode to %s on GitHub Actions\n' "${mode}" >&2
exit 1
fi
rm -f "${sample}"

printf 'Bash %s accepted the SDK Docker scripts\n' "${BASH_VERSION}"
5 changes: 4 additions & 1 deletion bin/docker-run
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
set -euo pipefail

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

exec docker run --rm "${DOCKER_USER_ARGS[@]}" "$@"
21 changes: 11 additions & 10 deletions bin/php-in-docker
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ shift 3

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

platform=()
# Keep this array non-empty. Bash 3.2 with set -u errors on an empty
# "${array[@]}", and that is the Bash macOS ships.
docker_args=(
-v "${ROOT_DIR}:${WORKING_DIR}"
-v "${ROOT_DIR}/.docker/composer:/tmp/composer"
-e COMPOSER_HOME=/tmp/composer
-w "${WORKING_DIR}"
"${IMAGE}"
)
if [[ -n "${PHP_DOCKER_PLATFORM:-}" ]]; then
platform=(--platform "${PHP_DOCKER_PLATFORM}")
docker_args=(--platform "${PHP_DOCKER_PLATFORM}" "${docker_args[@]}")
fi

exec "${DIR}/docker-run" \
"${platform[@]}" \
-v "${ROOT_DIR}:${WORKING_DIR}" \
-v "${ROOT_DIR}/.docker/composer:/tmp/composer" \
-e COMPOSER_HOME=/tmp/composer \
-w "${WORKING_DIR}" \
"${IMAGE}" \
"$@"
exec "${DIR}/docker-run" "${docker_args[@]}" "$@"