Skip to content
Draft
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
13 changes: 13 additions & 0 deletions docker/php-ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,16 @@ build identity fields needed by the source reference cross that boundary.
Run `prepare-php-reference-container.sh CONFIG_ID PRIVATE_STATE` to check an
already-inspected image, or the setup script to build the pinned runtime. The
proposed caller fixture preserves the original vector, Cargo and parity commands.

The two-build PHAR reproducibility caller reuses the reviewed PHAR profile
through a separate launcher. Its archived trees and comparison artifacts
live outside PHP's writable temporary data. The host prepares one selected
tree at a time; PHP sees that tree read-only with only vendor, build and the
generated build-info file writable. Other trees and the compared PHAR copy
are not mounted. The original build script's mutable Box cache path is
routed through the existing guard and read-only pinned Box artifact.

The prepared caller is `scripts/ci/verify-php-reproducible-build.sh`; the
ordinary local script is unchanged. Each host-prepared tree receives a
read-only Box configuration with its timestamp set from `SOURCE_DATE_EPOCH`.
This pins PHAR metadata as well as the original input-file normalization.
11 changes: 11 additions & 0 deletions rust/parity/test_php_repro_container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Discover Reproducibility launcher controls in the existing host-Python test lane."""
import importlib.util
from pathlib import Path


def load_tests(loader, _tests, _pattern):
source = Path(__file__).resolve().parents[2]/'scripts/ci/test_php_repro_container.py'
spec = importlib.util.spec_from_file_location('php_repro_controls', source)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return loader.loadTestsFromModule(module)
18 changes: 18 additions & 0 deletions scripts/ci/prepare-php-repro-container.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
image="${1:?Usage: prepare-php-repro-container.sh CONFIG_ID PRIVATE_STATE}"
state="${2:?Usage: prepare-php-repro-container.sh CONFIG_ID PRIVATE_STATE}"
[[ "$image" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ -d "$state" && ! -L "$state" && -O "$state" && "$(realpath "$state")" = "$state" ]]
[[ -f "$state/artifacts/box.phar" && ! -L "$state/artifacts/box.phar" ]]
mkdir -p "$state/profile"
[[ ! -L "$state/profile" && "$(realpath "$state/profile")" = "$state/profile" ]]
for command in php composer box; do
ln -sf "$root/scripts/ci/run-php-phar-container.sh" "$state/profile/$command"
ln -sf "$root/scripts/ci/run-php-repro-container.sh" "$state/bin/$command"
done
export CLI_PHP_CI_ROOT="$root" CLI_PHP_CI_IMAGE="$image" CLI_PHP_CI_STATE="$state" CLI_PHP_CI_REPRO=1
"$state/bin/php" "$root/scripts/ci/probe-php-phar-runtime.php" 8.4
CLI_PHP_CI_TEST_IMAGE="$image" python3 -I -S -B "$root/scripts/ci/test-php-container-tls.py" -v
CLI_PHP_CI_TEST_IMAGE="$image" python3 -I -S -B "$root/scripts/ci/test_php_repro_container.py" -v
47 changes: 47 additions & 0 deletions scripts/ci/prepare-php-repro-tree.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Only the host prepares archived controls. PHP receives a read-only tree.
set -euo pipefail
umask 077
source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
state="${CLI_PHP_CI_STATE:?PHP CI state is required}"
tree="${1:?Usage: prepare-php-repro-tree.sh ARCHIVED_TREE}"
[[ "${CLI_PHP_CI_REPRO:-}" = 1 && "${CLI_PHP_CI_ROOT:-}" = "$source_root" ]]
[[ -d "$state" && ! -L "$state" && -O "$state" && "$(realpath "$state")" = "$state" ]]
[[ -d "$tree" && ! -L "$tree" && -O "$tree" && "$(realpath "$tree")" = "$tree" ]]
case "$tree" in "${RUNNER_TEMP:?RUNNER_TEMP is required}"/dw-cli-repro.*/source.[12]) ;; *) exit 1 ;; esac
case "$tree/" in "$state/"*|"$source_root/"*) echo 'Reproducibility trees must stay outside runtime writable data and source.' >&2; exit 1 ;; *) ;; esac
for directory in "$tree/vendor" "$tree/build"; do
[[ ! -L "$directory" ]]
mkdir -p "$directory"
[[ "$(realpath "$directory")" = "$directory" && -O "$directory" ]]
done
generated="$tree/src/GeneratedBuildInfo.php"
[[ ! -L "$generated" ]]
if [[ ! -e "$generated" ]]; then (set -o noclobber; : > "$generated"); fi
[[ -f "$generated" && -O "$generated" && -n "$(find "$generated" -prune -type f -links 1)" ]]
# Input mtimes alone do not pin the archive metadata under the container
# runtime. Box supports one explicit timestamp for every PHAR entry.
python3 -I -S -B - "$tree/box.json.dist" "$tree/box.json" "${SOURCE_DATE_EPOCH:?SOURCE_DATE_EPOCH is required}" <<'PYCONFIG'
import json,os,re,sys
configuration=json.load(open(sys.argv[1]))
assert re.fullmatch(r'[0-9]{1,12}',sys.argv[3],flags=re.ASCII)
configuration['timestamp']='@'+sys.argv[3]
fd=os.open(sys.argv[2],os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o400)
with os.fdopen(fd,'w') as output:
json.dump(configuration,output,indent=4)
output.write('\n')
PYCONFIG
# The existing launcher queries the Git directory. This empty metadata is not
# a source revision and is excluded from the PHAR; build identity stays explicit.
[[ ! -e "$tree/.git" && ! -L "$tree/.git" ]]
git init --quiet "$tree"
metadata="$state/repro-tree"
if [[ -e "$metadata" || -L "$metadata" ]]; then
[[ -f "$metadata" && ! -L "$metadata" && -O "$metadata" \
&& -n "$(find "$metadata" -prune -type f -links 1 -perm 0400)" ]]
fi
held="$(mktemp "$state/.repro-tree.XXXXXX")"
trap 'rm -f -- "$held"' EXIT
printf '%s\n' "$tree" > "$held"
chmod 0400 "$held"
mv -f -- "$held" "$metadata"
36 changes: 36 additions & 0 deletions scripts/ci/run-php-repro-container.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
command="${0##*/}"
case "$command" in php|composer|box) ;; *) echo 'Invoke this reproducibility launcher as php, composer or box.' >&2; exit 2 ;; esac
source_root="${CLI_PHP_CI_ROOT:?PHP CI source root is required}"
state="${CLI_PHP_CI_STATE:?PHP CI state is required}"
[[ "${CLI_PHP_CI_REPRO:-}" = 1 ]]
root="$source_root"
metadata="$state/repro-tree"
if [[ -e "$metadata" || -L "$metadata" ]]; then
[[ -f "$metadata" && ! -L "$metadata" && -O "$metadata" \
&& -n "$(find "$metadata" -prune -type f -links 1 -perm 0400)" ]] || exit 1
mapfile -t fields < "$metadata"
[[ "${#fields[@]}" = 1 ]]
tree="${fields[0]}"
case "$tree" in "${RUNNER_TEMP:?RUNNER_TEMP is required}"/dw-cli-repro.*/source.[12]) ;; *) exit 1 ;; esac
case "$PWD/" in "$tree/"*)
[[ -d "$tree" && ! -L "$tree" && -O "$tree" && "$(realpath "$tree")" = "$tree" ]]
case "$tree/" in "$state/"*|"$source_root/"*) exit 1 ;; *) ;; esac
root="$tree" ;;
*) ;;
esac
fi
# The helper's archive cache is writable build data. Execute only the pinned
# read-only Box artifact, including when the original build script invokes PHP.
arguments=("$@")
for argument in "$@"; do
if [[ "$argument" = "$root/build/.tools/box" ]]; then
[[ "$command" = php && "$#" -ge 3 && "$1" = -d && "$2" = phar.readonly=0 && "$3" = "$argument" ]] || exit 1
arguments=(-d phar.readonly=0 "$root/scripts/ci/exec-box.php" "$state/artifacts/box.phar" "${@:4}")
break
fi
done
classic="$state/profile/$command"
[[ -L "$classic" && "$(realpath "$classic")" = "$source_root/scripts/ci/run-php-phar-container.sh" ]]
CLI_PHP_CI_ROOT="$root" exec "$classic" "${arguments[@]}"
35 changes: 35 additions & 0 deletions scripts/ci/setup-php-repro-container.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
metadata="$(mktemp -d "${RUNNER_TEMP:?RUNNER_TEMP is required}/cli-repro-setup.XXXXXX")"
trap 'rm -rf -- "$metadata"' EXIT
: > "$metadata/environment"
: > "$metadata/path"
# Reuse the accepted build/ancestry source without changing the admitted test
# profile. Its private outputs are parsed as data, never sourced as shell code.
GITHUB_ENV="$metadata/environment" GITHUB_PATH="$metadata/path" \
bash "$root/scripts/ci/setup-php-phar-container.sh"
bindings="$(python3 -I -S -B - "$metadata/environment" "$metadata/path" "$root" <<'PY'
from pathlib import Path
import re,sys
lines = Path(sys.argv[1]).read_text().splitlines()
assert len(lines) == 6
values = dict(line.split('=', 1) for line in lines)
assert set(values) == {'CLI_PHP_CI_IMAGE','CLI_PHP_CI_ROOT','CLI_PHP_CI_STATE','COMPOSER_HOME','COMPOSER_CACHE_DIR','TMPDIR'}
assert values['CLI_PHP_CI_ROOT'] == sys.argv[3]
assert re.fullmatch(r'sha256:[0-9a-f]{64}', values['CLI_PHP_CI_IMAGE'])
state = values['CLI_PHP_CI_STATE']
assert values['COMPOSER_HOME'] == state+'/composer-home'
assert values['COMPOSER_CACHE_DIR'] == state+'/composer-home/cache'
assert values['TMPDIR'] == state+'/tmp'
assert Path(sys.argv[2]).read_text() == state+'/bin\n'
print(values['CLI_PHP_CI_IMAGE'])
print(state)
PY
)"
mapfile -t fields <<< "$bindings"
bash "$root/scripts/ci/prepare-php-repro-container.sh" "${fields[0]}" "${fields[1]}"
cat "$metadata/environment" >> "${GITHUB_ENV:?GITHUB_ENV is required}"
echo "CLI_PHP_CI_REPRO=1" >> "${GITHUB_ENV:?GITHUB_ENV is required}"
cat "$metadata/path" >> "${GITHUB_PATH:?GITHUB_PATH is required}"
Loading
Loading