diff --git a/.github/workflows/runner-cache.yml b/.github/workflows/runner-cache.yml new file mode 100644 index 0000000..05e25ed --- /dev/null +++ b/.github/workflows/runner-cache.yml @@ -0,0 +1,58 @@ +name: Runner cache validation +on: + push: + workflow_dispatch: +permissions: + contents: read +jobs: + diagnostics: + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + runner: + - blacksmith-2vcpu-ubuntu-2204 + - blacksmith-2vcpu-ubuntu-2204-arm + - blacksmith-2vcpu-ubuntu-2404 + - blacksmith-2vcpu-ubuntu-2404-arm + - depot-ubuntu-22.04 + - depot-ubuntu-22.04-arm + - depot-ubuntu-24.04 + - depot-ubuntu-24.04-arm + php: [5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Inspect runner and image + run: python3 scripts/inspect-runner.py + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: shivammathur/setup-php + ref: 421f97feafdad867301f2429af811a147575170f + path: action + persist-credentials: false + - name: Apply candidate and check runner detection + run: | + cp overrides/*.sh action/src/scripts/ + bash scripts/check-detection.sh + - name: Set up PHP from builds cache + uses: ./action + with: + php-version: ${{ matrix.php }} + extensions: curl, dom, gd, intl, mbstring, mysqli, pdo_mysql, pdo_sqlite, xml, zip + coverage: none + tools: composer + env: + use_builds_cache: 'true' + update: 'true' + fail_fast: 'true' + - name: Validate cached PHP and package health + run: bash scripts/check-php.sh ${{ matrix.php }} + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: diagnostics-${{ matrix.runner }} + path: reports/ + if-no-files-found: error diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index b73d562..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Runner workflow -on: - push: -jobs: - runner: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [blacksmith-2vcpu-ubuntu-2404, blacksmith-2vcpu-ubuntu-2404-arm, ubuntu-24.04, ubuntu-24.04-arm] - php-version: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4 - - 8.0 - - 8.1 - - 8.2 - - 8.3 - - 8.4 - - 8.5 - - 8.6 - - 8.7 - - steps: - - name: Setup PHP - uses: shivammathur/setup-php@develop - with: - php-version: ${{ matrix.php-version }} - env: - use_builds_cache: true - verbose: true - - name: Testing PHP version - env: - EXPECTED_PHP_VERSION: ${{ matrix.php-version }} - run: | - php -r '$expected = getenv("EXPECTED_PHP_VERSION"); if(strpos(phpversion(), $expected) === false) {throw new Exception("Wrong PHP version Installed");}' - php -v - php -m - - diff --git a/overrides/darwin.sh b/overrides/darwin.sh new file mode 100644 index 0000000..c2aca17 --- /dev/null +++ b/overrides/darwin.sh @@ -0,0 +1,350 @@ +# Handle dependency extensions +handle_dependency_extensions() { + local formula=$1 + local extension=$2 + formula_file="${tap_dir:?}/$ext_tap/Formula/$extension@${version:?}.rb" + [ -e "$formula_file" ] || formula_file="$tap_dir/$ext_tap/Formula/$formula@$version.rb" + if [ -e "$formula_file" ]; then + IFS=" " read -r -a dependency_extensions <<< "$(grep -Eo "shivammathur.*@" "$formula_file" | xargs -I {} -n 1 basename '{}' | cut -d '@' -f 1 | tr '\n' ' ')" + for dependency_extension in "${dependency_extensions[@]}"; do + sudo sed -Ei '' "/=(.*\/)?\"?$dependency_extension(.so)?$/d" "${ini_file:?}" + done + fi + suffix="$(get_php_formula_suffix)" + if [[ -n "$suffix" ]]; then + brew_opts=(-sf) + patch_abstract_file >/dev/null 2>&1 + for dependency_extension in "${dependency_extensions[@]}"; do + safe_brew install --skip-link "${brew_opts[@]}" "$ext_tap/$dependency_extension@$version" >/dev/null 2>&1 && + brew link --overwrite --force "$dependency_extension@$version" >/dev/null 2>&1 && + copy_brew_extensions "$dependency_extension" || return $? + done + fi +} + +# Helper function to disable an extension. +disable_extension_helper() { + local extension=$1 + local disable_dependents=${2:-false} + get_extension_map + if [ "$disable_dependents" = "true" ]; then + disable_extension_dependents "$extension" + fi + sudo sed -Ei '' "/=(.*\/)?\"?$extension(.so)?$/d" "${ini_file:?}" + sudo rm -rf "$scan_dir"/*"$extension"* /tmp/php"$version"_extensions + mkdir -p /tmp/extdisabled/"$version" + echo '' | sudo tee /tmp/extdisabled/"$version"/"$extension" >/dev/null 2>&1 +} + +# Function to get extension name from brew formula. +get_extension_from_formula() { + local formula=$1 + local extension + extension=$(grep -E "^$formula=" "$src"/configs/brew_extensions | cut -d '=' -f 2) + [[ -z "$extension" ]] && extension="$(echo "$formula" | sed -E "s/pecl_|php|[0-9]//g")" + echo "$extension" +} + +# Function to get renamed formula. +get_renamed_formula() { + local formula=$1 + formula_renames_json="$tap_dir/$ext_tap/formula_renames.json" + if [ -e "$formula_renames_json" ] && grep -q "$formula@$version\":" "$formula_renames_json"; then + grep "$formula@$version\":" "$formula_renames_json" | cut -d ':' -f 2 | tr -d ' ",' | cut -d '@' -f 1 + else + echo "$formula" + fi +} + +# Function to copy extension binaries to the extension directory. +copy_brew_extensions() { + local formula=$1 + formula_file="$tap_dir/$ext_tap/Formula/$formula@$version.rb" + deps="$(grep -Eo 'depends_on "shivammathur[^"]+' "$formula_file" | cut -d '/' -f 3 | tr '\n' ' ')" + IFS=' ' read -r -a deps <<< "$formula@$version $deps" + for dependency in "${deps[@]}"; do + extension_file="${brew_prefix:?}/opt/$dependency/$(get_extension_from_formula "${dependency%@*}").so" + [ -e "$extension_file" ] && sudo cp "$extension_file" "$ext_dir" + done + if [ -d "$brew_prefix"/Cellar/"$formula"@"$version" ]; then + sudo find -- "$brew_prefix"/Cellar/"$formula"@"$version" -name "*.dylib" -exec cp {} "$ext_dir" \; + fi +} + +# Install an extension and its variant-specific dependencies with the current Brew state. +install_brew_extension() { + local formula=$1 + local extension=$2 + handle_dependency_extensions "$formula" "$extension" && + safe_brew install --skip-link "${brew_opts[@]}" "$ext_tap/$formula@$version" && + brew link --overwrite --force "$formula@$version" && + copy_brew_extensions "$formula" +} + +# Function to install a php extension from shivammathur/extensions tap. +add_brew_extension() { + formula=$1 + prefix=$2 + expected_version=${3:-} + extension="$(get_extension_from_formula "$formula")" + enable_extension "$extension" "$prefix" + if check_extension "$extension" && { [ -z "$expected_version" ] || check_extension_version "$extension" "$expected_version"; }; then + add_log "${tick:?}" "$extension" "Enabled" + else + add_brew_tap "$php_tap" + add_brew_tap "$ext_tap" + formula="$(get_renamed_formula "$formula")" + ( + patch_brew + # Keep the watchdog, but refresh before retrying a failed first attempt. + SETUP_PHP_BREW_RETRY_ATTEMPTS=1 install_brew_extension "$formula" "$extension" || { + update_dependencies && install_brew_extension "$formula" "$extension" + } + ) >/dev/null 2>&1 || { + if [ -n "$expected_version" ]; then + pecl_install "$extension-$expected_version" || pecl_install "$extension" + else + pecl_install "$extension" + fi + } >/dev/null 2>&1 + add_extension_log "$extension" "Installed and enabled" + fi +} + +# Function to patch the abstract file in the extensions tap. +patch_abstract_file() { + abstract_path="$tap_dir"/"$ext_tap"/Abstract/abstract-php-extension.rb + if [[ -e "$abstract_path" && ! -e /tmp/abstract_patch ]]; then + echo '' | sudo tee /tmp/abstract_patch >/dev/null 2>&1 + sudo sed -i '' -e "s|php@#{\(.*\)}|php@#{\1}$suffix|g" -e "s|php_version /|\"#{php_version}$suffix\" /|g" "$abstract_path" + fi +} + +# Helper function to add an extension. +add_extension_helper() { + local extension=$1 + prefix=$2 + if [[ "$version" =~ ${old_versions:?} ]] && [ "$extension" = "imagick" ]; then + run_script "php5-darwin" "${version/./}" "$extension" >/dev/null 2>&1 + else + pecl_install "$extension" >/dev/null 2>&1 && + if [[ "$version" =~ ${old_versions:?} ]]; then echo "$prefix=$ext_dir/$extension.so" >>"$ini_file"; fi + fi + add_extension_log "$extension" "Installed and enabled" +} + +# Function to handle request to add phpize and php-config. +add_devtools() { + tool=$1 + add_log "${tick:?}" "$tool" "Added $tool $semver" +} + +# Function to handle request to add PECL. +add_pecl() { + enable_extension xml extension >/dev/null 2>&1 + configure_pecl >/dev/null 2>&1 + pear_version=$(get_tool_version "pecl" "version") + add_log "${tick:?}" "PECL" "Found PECL $pear_version" +} + +# Link opcache extension to extensions directory. +link_opcache() { + opcache_ini="$brew_prefix"/etc/php/"$version"/conf.d/ext-opcache.ini + if [ -e "$opcache_ini" ]; then + opcache_ext=$(grep -Eo "zend_extension.*opcache.*\.so" "$opcache_ini" | cut -d '"' -f 2) + sudo ln -sf "$opcache_ext" "$ext_dir" + fi +} + +# Patch brew to overwrite packages. +patch_brew() { + formula_installer="${brew_repo:?}"/Library/Homebrew/formula_installer.rb + code=" keg.link\(verbose: verbose\?" + sudo sed -Ei '' "s/$code.*/$code, overwrite: true\)/" "$formula_installer" + # shellcheck disable=SC2064 + trap "sudo sed -Ei '' 's/$code.*/$code, overwrite: overwrite?\)/' $formula_installer" exit +} + +# Function to update dependencies. +update_dependencies() { + if ! [ -e /tmp/update_dependencies ]; then + for repo in "$brew_repo" "${core_repo:?}"; do + if [ -e "$repo" ]; then + git_retry -C "$repo" fetch origin main && git -C "$repo" reset --hard origin/main || return $? + fi + done + echo '' | sudo tee /tmp/update_dependencies >/dev/null 2>&1 + fi + patch_brew +} + +# Function to get PHP version if it is already installed using Homebrew. +get_brewed_php() { + cellar="$brew_prefix"/Cellar + php_cellar="$cellar"/php + if [ -d "$cellar" ] && ! [[ "$(find "$cellar" -maxdepth 1 -name "php@$version*" | wc -l 2>/dev/null)" -eq 0 ]]; then + php_semver + elif [ -d "$php_cellar" ] && ! [[ "$(find "$php_cellar" -maxdepth 1 -name "$version*" | wc -l 2>/dev/null)" -eq 0 ]]; then + php_semver + else + echo 'false'; + fi +} + +# Function to setup PHP from the cached builds. +setup_cached_versions() { + latest="releases/download/php-$version" run_script \ + "php-darwin" "$version" "${debug:?}" "${ts:?}" "" "${INPUT_EXTENSIONS:-}" >/dev/null 2>&1 +} + +# Function to setup PHP 5.6 and newer using Homebrew. +add_php() { + local exit_code + action=$1 + existing_version=$2 + suffix="$(get_php_formula_suffix)" + php_keg="php@$version$suffix" + php_formula="shivammathur/php/$php_keg" + if [[ "$existing_version" = "false" || -n "$suffix" || "$action" = "upgrade" ]]; then + if [ "$(uname -m)" = "x86_64" ] || { [ "${runner:?}" != "self-hosted" ] && [ "${use_builds_cache:-true}" != "false" ]; }; then + setup_cached_versions && return 0 + [ "$(uname -m)" != "x86_64" ] || return 1 + fi + update_dependencies + add_brew_tap "$php_tap" + fi + if [[ "$existing_version" != "false" && -z "$suffix" ]]; then + if [ "$action" = "upgrade" ]; then + safe_brew install --only-dependencies "$php_formula" || return $? + safe_brew upgrade -f --overwrite "$php_formula" || return $? + else + brew unlink "$php_keg" + fi + else + safe_brew install --only-dependencies "$php_formula" || return $? + safe_brew install --skip-link -f --overwrite "$php_formula" 2>/dev/null || { + exit_code=$? + # A timeout has exhausted its retries; do not start another build via upgrade. + [ "$exit_code" -ne 124 ] || return "$exit_code" + safe_brew upgrade -f --overwrite "$php_formula" || return $? + } + fi + brew link --force --overwrite "$php_keg" || (sudo chown -R "$(id -un)":"$(id -gn)" "$brew_prefix" && brew link --force --overwrite "$php_keg") +} + +# Function to get formula suffix +get_php_formula_suffix() { + local suffix + [ "${debug:?}" = "debug" ] && suffix="-debug" + [ "${ts:?}" = "zts" ] && suffix="$suffix-zts" + echo "$suffix" +} + +# Function to get extra version. +php_extra_version() { + php_formula_file="$tap_dir"/"$php_tap"/Formula/php@"$version".rb + if [ -e "$php_formula_file" ] && ! grep -q "deprecate!" "$php_formula_file" && grep -Eq "archive/[0-9a-zA-Z]+" "$php_formula_file"; then + echo " ($(grep -Eo "archive/[0-9a-zA-Z]+" "$php_formula_file" | cut -d'/' -f 2))" + fi +} + +# Function to set php.ini +add_php_config() { + if ! [ -e "$ini_dir"/php.ini-development ]; then + sudo cp "$ini_dir"/php.ini "$ini_dir"/php.ini-development + fi + if [[ "$ini" = "production" || "$ini" = "development" ]]; then + sudo cp "$ini_dir"/php.ini-"$ini" "$ini_dir"/php.ini + elif [ "$ini" = "none" ]; then + echo '' | sudo tee "${ini_file[@]}" >/dev/null 2>&1 + fi +} + +# Function to get scan directory. +get_scan_dir() { + if [[ "$version" =~ ${old_versions:?} ]]; then + php --ini | grep additional | sed -e "s|.*: s*||" + else + echo "$ini_dir"/conf.d + fi +} + +# Function to handle self-hosted runner setup. +self_hosted_helper() { + sudo mkdir -p /opt/hostedtoolcache >/dev/null 2>&1 || true +} + +# Function to Setup PHP. +setup_php() { + step_log "Setup PHP" + php_config="$(command -v php-config 2>/dev/null)" + update=true + check_pre_installed + existing_version=$([ "$(uname -m)" != "x86_64" ] && get_brewed_php || echo false) + status="Found" + if [[ "$version" =~ ${old_versions:?} ]]; then + run_script "php5-darwin" "${version/./}" >/dev/null 2>&1 + status="Installed" + elif [[ "${existing_version:0:3}" != "$version" || -n "$(get_php_formula_suffix)" ]]; then + add_php "install" "$existing_version" >/dev/null 2>&1 || { + add_log "${cross:?}" "PHP" "Could not install PHP $version" + exit 1 + } + status="Installed" + elif [[ "${existing_version:0:3}" = "$version" && "${update:?}" = "true" ]]; then + brew_php_version="$(brew info --json "php@$version" 2>/dev/null | jq -r '.[].versions.stable')" + if [ "$brew_php_version" != "$existing_version" ]; then + add_php "upgrade" "$existing_version" >/dev/null 2>&1 || { + add_log "${cross:?}" "PHP" "Could not upgrade PHP $version" + exit 1 + } + status="Upgraded" + fi + fi + php_config="$(command -v php-config)" + ext_dir="$(sed -n "s/.*extension_dir=['\"]\(.*\)['\"].*/\1/p" "$php_config")" + ini_dir="$(php_ini_path)" + scan_dir="$(get_scan_dir)" + ini_file="$ini_dir"/php.ini + sudo mkdir -m 777 -p "$ext_dir" "$HOME/.composer" + sudo chmod 777 "$ini_file" "${tool_path_dir:?}" + semver="$(php_semver)" + extra_version="$(php_extra_version)" + configure_php + link_opcache + set_output "php-version" "$semver" + if [ "${semver%.*}" != "$version" ]; then + add_log "${cross:?}" "PHP" "Could not setup PHP $version" + exit 1 + fi + + sudo cp "$src"/configs/pm/*.json "$RUNNER_TOOL_CACHE/" + add_log "$tick" "PHP" "$status PHP $semver$extra_version" +} + +# Variables +version=${1:-'8.5'} +ini=${2:-'production'} +src=${0%/*}/.. +php_formula=shivammathur/php/php@"$version" +scripts="$src"/scripts +ext_tap=shivammathur/homebrew-extensions +php_tap=shivammathur/homebrew-php +export HOMEBREW_CHANGE_ARCH_TO_ARM=1 +export HOMEBREW_NO_AUTO_UPDATE=1 +export HOMEBREW_NO_ENV_HINTS=1 +export HOMEBREW_NO_INSTALL_CLEANUP=1 +export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 +export HOMEBREW_NO_INSTALL_FROM_API=1 + +# shellcheck source=. +. "${scripts:?}"/unix.sh +. "${scripts:?}"/tools/brew.sh +. "${scripts:?}"/tools/retry.sh +. "${scripts:?}"/tools/add_tools.sh +. "${scripts:?}"/extensions/source.sh +. "${scripts:?}"/extensions/add_extensions.sh +configure_brew +read_env +self_hosted_setup +setup_php diff --git a/overrides/linux.sh b/overrides/linux.sh new file mode 100644 index 0000000..c6b6e2b --- /dev/null +++ b/overrides/linux.sh @@ -0,0 +1,353 @@ +# Function to add sudo +add_sudo() { + if ! command -v sudo >/dev/null; then + check_package sudo || apt-get update + apt-get install -y sudo || (apt-get update && apt-get install -y sudo) + fi +} + +# Function to link apt-fast to apt-get +link_apt_fast() { + if ! command -v apt-fast >/dev/null; then + sudo ln -sf /usr/bin/apt-get /usr/bin/apt-fast + trap "sudo rm -f /usr/bin/apt-fast 2>/dev/null" exit + fi +} + +# Function to setup environment for self-hosted runners. +self_hosted_helper() { + if ! command -v apt-fast >/dev/null; then + sudo ln -sf /usr/bin/apt-get /usr/bin/apt-fast + trap "sudo rm -f /usr/bin/apt-fast 2>/dev/null" exit + fi + sudo mkdir -p /opt/hostedtoolcache >/dev/null 2>&1 || true + install_packages apt-transport-https ca-certificates curl file make jq unzip autoconf automake gcc g++ gnupg +} + +# Function to fix broken packages. +fix_broken_packages() { + sudo apt --fix-broken install >/dev/null 2>&1 +} + +# Function to install a package +install_packages() { + packages=("$@") + if ! [ -e /etc/dpkg/dpkg.cfg.d/force-confnew ]; then + echo "force-confnew" | sudo tee /etc/dpkg/dpkg.cfg.d/force-confnew >/dev/null 2>&1 + trap "sudo rm -f /etc/dpkg/dpkg.cfg.d/force-confnew 2>/dev/null" exit + fi + $apt_install "${packages[@]}" >/dev/null 2>&1 || (update_lists && fix_broken_packages && $apt_install "${packages[@]}" >/dev/null 2>&1) +} + +# Function to disable an extension. +disable_extension_helper() { + local extension=$1 + local disable_dependents=${2:-false} + get_extension_map + if [ "$disable_dependents" = "true" ]; then + disable_extension_dependents "$extension" + fi + sudo sed -Ei "/=(.*\/)?\"?$extension(.so)?\"?$/d" "${ini_file[@]}" "$pecl_file" + sudo find "$ini_dir"/.. -name "*-$extension.ini" -not -path "*phar.ini" -not -path "*pecl.ini" -not -path "*mods-available*" -delete >/dev/null 2>&1 || true + sudo rm -f /tmp/php"$version"_extensions + mkdir -p /tmp/extdisabled/"$version" + echo '' | sudo tee /tmp/extdisabled/"$version"/"$extension" >/dev/null 2>&1 +} + +# Function to add PDO extension. +add_pdo_extension() { + pdo_ext="pdo_$1" + if check_extension "$pdo_ext"; then + add_log "${tick:?}" "$pdo_ext" "Enabled" + else + ext=$1 + ext_name=$1 + if shared_extension pdo; then + disable_extension_helper pdo + echo "extension=pdo.so" | sudo tee "${ini_file[@]/php.ini/conf.d/10-pdo.ini}" >/dev/null 2>&1 + fi + if [ "$ext" = "mysql" ]; then + enable_extension "mysqlnd" "extension" + ext_name='mysqli' + elif [ "$ext" = "dblib" ]; then + ext_name="sybase" + elif [ "$ext" = "firebird" ]; then + install_packages libfbclient2 >/dev/null 2>&1 + enable_extension "pdo_firebird" "extension" + ext_name="interbase" + elif [ "$ext" = "sqlite" ]; then + ext="sqlite3" + ext_name="sqlite3" + fi + add_extension "$ext_name" "extension" >/dev/null 2>&1 + add_extension "$pdo_ext" "extension" >/dev/null 2>&1 + add_extension_log "$pdo_ext" "Enabled" + fi +} + +# Function to check if a package exists +check_package() { + apt-cache policy "$1" 2>/dev/null | grep -q 'Candidate' +} + +# Helper function to add an extension. +add_extension_helper() { + local extension=$1 + packages=(php"$version"-"$extension") + add_ppa ondrej/php >/dev/null 2>&1 || update_ppa ondrej/php + [ "${debug:?}" = "debug" ] && check_package php"$version"-"$extension"-dbgsym && packages+=(php"$version"-"$extension"-dbgsym) + (check_package "${packages[0]}" && install_packages "${packages[@]}") || pecl_install "$extension" + add_extension_log "$extension" "Installed and enabled" + sudo chmod 777 "${ini_file[@]}" +} + +# Function to setup phpize and php-config. +add_devtools() { + tool=$1 + if ! command -v "$tool$version" >/dev/null; then + install_packages "php$version-dev" + fi + switch_version "phpize" "php-config" + add_extension xml extension >/dev/null 2>&1 + add_log "${tick:?}" "$tool" "Added $tool $semver" +} + +# Function to setup PHP from the shivammathur/php-builder builds. +setup_php_builder() { + run_script "php-builder" "${runner:?}" "$version" "${debug:?}" "${ts:?}" +} + +# Function to setup PHP 5.3, PHP 5.4 and PHP 5.5. +setup_old_versions() { + run_script "php5-ubuntu" "$version" +} + +# Function to setup PHP from the cached builds. +setup_cached_versions() { + run_script "php-ubuntu" "$version" "${debug:?}" "${ts:?}" +} + +# Function to get the link path for an alternative. +alternative_link() { + case "$1" in + php-cgi-bin) echo "/usr/lib/cgi-bin/php" ;; + php-fpm) echo "/usr/sbin/php-fpm" ;; + php-fpm.sock) echo "/run/php/php-fpm.sock" ;; + *) echo "/usr/bin/$1" ;; + esac +} + +# Function to get the target path for an alternative. +alternative_target() { + case "$1" in + php-cgi-bin) echo "/usr/lib/cgi-bin/php$version" ;; + php-fpm) echo "/usr/sbin/php-fpm$version" ;; + php-fpm.sock) echo "/run/php/php$version-fpm.sock" ;; + *) echo "/usr/bin/$1$version" ;; + esac +} + +# Function to register an alternative if the versioned binary exists but is missing. +register_alternative() { + local tool=$1 + local link target priority state_file + target="$(alternative_target "$tool")" + [ -e "$target" ] || return 0 + state_file="/var/lib/dpkg/alternatives/$tool" + if sudo test -r "$state_file" && sudo grep -Fxq "$target" "$state_file"; then + return 0 + fi + link="$(alternative_link "$tool")" + priority="${version//./}" + sudo update-alternatives --install "$link" "$tool" "$target" "$priority" >/dev/null 2>&1 +} + +# Function to register and switch an alternative. +set_alternative() { + local tool=$1 + local target + target="$(alternative_target "$tool")" + [ -e "$target" ] || return 0 + register_alternative "$tool" || return 1 + sudo update-alternatives --set "$tool" "$target" >/dev/null 2>&1 +} + +# Function to add PECL. +add_pecl() { + add_devtools phpize >/dev/null 2>&1 + if ! command -v pecl >/dev/null; then + install_packages php-pear + fi + configure_pecl >/dev/null 2>&1 + pear_version=$(get_tool_version "pecl" "version") + add_log "${tick:?}" "PECL" "Added PECL $pear_version" +} + +# Function to switch versions of PHP binaries. +switch_version() { + tools=("$@") + to_wait=() + if ! (( ${#tools[@]} )); then + tools+=(php-cgi-bin php-fpm php-fpm.sock pear pecl php phar phar.phar php-cgi php-config phpize phpdbg) + fi + for tool in "${tools[@]}"; do + set_alternative "$tool" & + to_wait+=($!) + done + wait "${to_wait[@]}" +} + +# Function to get packages to install +get_php_packages() { + sed "s/[^ ]*/php$version-&/g" "$src"/configs/php_packages | tr '\n' ' ' + if [ "${debug:?}" = "debug" ]; then + sed "s/[^ ]*/php$version-&-dbgsym/g" "$src"/configs/php_debug_packages | tr '\n' ' ' + fi +} + +# Function to install packaged PHP +add_packaged_php() { + add_ppa ondrej/php >/dev/null 2>&1 || update_ppa ondrej/php + IFS=' ' read -r -a packages <<<"$(get_php_packages)" + install_packages "${packages[@]}" +} + +# Function to update PHP. +update_php() { + initial_version="$(php_semver)$(php_extra_version)" + add_php + updated_version="$(php_semver)$(php_extra_version)" + if [ "$updated_version" != "$initial_version" ]; then + status="Updated to" + else + status="Found" + fi +} + +# Function to install PHP. +add_php() { + if [ "${runner:?}" = "self-hosted" ] || [ "${use_builds_cache:-true}" = "false" ]; then + if [[ "$version" =~ ${php_builder_versions:?} || "$ts" = "zts" ]]; then + setup_php_builder + else + add_packaged_php + switch_version >/dev/null 2>&1 + add_pecl + fi + elif [[ "$version" =~ ${old_versions:?} ]]; then + setup_old_versions + else + setup_cached_versions + fi + status="Installed" +} + +# Function to ini file for pear and link it to each SAPI. +link_pecl_file() { + echo '' | sudo tee "$pecl_file" >/dev/null 2>&1 + for file in "${ini_file[@]}"; do + sapi_scan_dir="$(realpath -m "$(dirname "$file")")/conf.d" + if [ "$sapi_scan_dir" != "$scan_dir" ] && ! [ -h "$sapi_scan_dir" ]; then + sudo mkdir -p "$sapi_scan_dir" + sudo ln -sf "$pecl_file" "$sapi_scan_dir/99-pecl.ini" + fi + done +} + +# Function to get extra version. +php_extra_version() { + if [ -e /etc/php/"$version"/COMMIT ]; then + echo " ($(cat "/etc/php/$version/COMMIT"))" + fi +} + +# Function to set php.ini +add_php_config() { + php_lib_dir=/usr/lib/php/"$version" + current_ini="$php_lib_dir"/php.ini-current + current=$(cat "$current_ini" 2>/dev/null) + if [ "$current" = "$ini" ]; then + return; + fi + if [[ "$ini" = "production" && "x$current" != "xproduction" ]]; then + echo "${ini_file[@]}" | xargs -n 1 -P 6 sudo cp "$php_lib_dir"/php.ini-production + if [ -e "$php_lib_dir"/php.ini-production.cli ]; then + sudo cp "$php_lib_dir"/php.ini-production.cli "$ini_dir"/php.ini + fi + elif [ "$ini" = "development" ]; then + echo "${ini_file[@]}" | xargs -n 1 -P 6 sudo cp "$php_lib_dir"/php.ini-development + elif [ "$ini" = "none" ]; then + echo '' | sudo tee "${ini_file[@]}" >/dev/null 2>&1 + fi + echo "$ini" | sudo tee "$current_ini" >/dev/null 2>&1 +} + +# Function to Setup PHP +setup_php() { + step_log "Setup PHP" + sudo mkdir -m 777 -p /var/run /run/php + php_config="$(command -v php-config)" + check_pre_installed + if [[ -z "$php_config" ]] || [ "$(php_semver | cut -c 1-3)" != "$version" ]; then + if [ ! -e "/usr/bin/php$version" ] || [ ! -e "/usr/bin/php-config$version" ]; then + add_php >/dev/null 2>&1 + else + if ! [[ "$version" =~ ${old_versions:?} ]]; then + switch_version >/dev/null 2>&1 + fi + if [ "${update:?}" = "true" ]; then + update_php >/dev/null 2>&1 + else + status="Switched to" + fi + fi + php_config="$(command -v php-config)" + else + if [ "$update" = "true" ]; then + update_php >/dev/null 2>&1 + else + status="Found" + fi + fi + if ! command -v php"$version" >/dev/null; then + add_log "${cross:?}" "PHP" "Could not setup PHP $version" + exit 1 + fi + ext_dir="/usr/$(grep -Po "extension_dir=..[^/]*/\K[^'\"]*" "$php_config")" + ini_dir="$(php_ini_path)" + scan_dir="$ini_dir"/conf.d + pecl_file="$scan_dir"/99-pecl.ini + semver="$(php_semver)" + extra_version="$(php_extra_version)" + export ext_dir + mapfile -t ini_file < <(sudo find "$ini_dir/.." -name "php.ini" -exec readlink -m {} +) + link_pecl_file + configure_php + set_output "php-version" "$semver" + sudo rm -rf /usr/local/bin/phpunit >/dev/null 2>&1 + sudo chmod 777 "${ini_file[@]}" "$pecl_file" "${tool_path_dir:?}" + sudo cp "$src"/configs/pm/*.json "$RUNNER_TOOL_CACHE/" + add_log "${tick:?}" "PHP" "$status PHP $semver$extra_version" +} + +# Variables +version=${1:-'8.5'} +ini=${2:-'production'} +src=${0%/*}/.. +debconf_fix="DEBIAN_FRONTEND=noninteractive" +apt_install="sudo $debconf_fix apt-fast install -y --no-install-recommends" +scripts="$src"/scripts + +add_sudo >/dev/null 2>&1 +link_apt_fast >/dev/null 2>&1 + +. /etc/os-release +# shellcheck source=. +. "${scripts:?}"/unix.sh +. "${scripts:?}"/tools/ppa.sh +. "${scripts:?}"/tools/add_tools.sh +. "${scripts:?}"/extensions/source.sh +. "${scripts:?}"/extensions/add_extensions.sh +read_env +self_hosted_setup +setup_php diff --git a/overrides/unix.sh b/overrides/unix.sh new file mode 100644 index 0000000..b76afae --- /dev/null +++ b/overrides/unix.sh @@ -0,0 +1,336 @@ +# Variables +export tick="✓" +export cross="✗" +export curl_opts=(-sL) +export old_versions="5.[3-5]" +export jit_versions="8.[0-9]" +export php_builder_versions="8.[3-9]" +export nightly_versions="8.[6-9]" +export xdebug3_versions="7.[2-4]|8.[0-9]" +export latest="releases/latest/download" +export github="https://github.com/shivammathur" +export jsdeliver="https://cdn.jsdelivr.net/gh/shivammathur" +export setup_php="https://setup-php.com" + +if [ -n "${GITHUB_ACTIONS}" ]; then + export GROUP='::group::' + export END_GROUP='::endgroup::' +else + export GROUP='' + export END_GROUP='' +fi + +# Function to log start of a operation. +step_log() { + local message=$1 + printf "\n\033[90;1m==> \033[0m\033[37;1m%s\033[0m\n" "$message" +} + +# Function to log result of a operation. +add_log() { + local mark=$1 + local subject=$2 + local message=$3 + if [ "$mark" = "$tick" ]; then + printf "\033[32;1m%s \033[0m\033[34;1m%s \033[0m\033[90;1m%s\033[0m\n" "$mark" "$subject" "$message" + elif [ "$mark" = "!" ]; then + printf "\033[33;1m%s \033[0m\033[34;1m%s \033[0m\033[90;1m%s\033[0m\n" "$mark" "$subject" "$message" + else + printf "\033[31;1m%s \033[0m\033[34;1m%s \033[0m\033[90;1m%s\033[0m\n" "$mark" "$subject" "$message" + [ "$fail_fast" = "true" ] && exit 1 + fi +} + +# Function to set output on GitHub Actions. +set_output() { + name=$1 + value=$2 + if [ "${GITHUB_ACTIONS}" = "true" ]; then + echo "${name}=${value}" | tee -a "$GITHUB_OUTPUT" >/dev/null 2>&1 + fi +} + +# Function to run sensitive code without tracing. Expand tokens inside the function, not its arguments. +without_trace() { + local setup_php_trace_flags=$- + set +x + "$@" + local setup_php_trace_status=$? + [[ "$setup_php_trace_flags" == *x* ]] && set -x + return "$setup_php_trace_status" +} + +# Function to check third-party runners eligible for the builds cache. +check_builds_cache_runner() { + local pid=$PPID executable + while [[ "$pid" =~ ^[0-9]+$ && "$pid" -gt 1 ]]; do + executable="$(readlink -f "/proc/$pid/exe")" || return 1 + if [[ "$executable" = */bin/Runner.Worker ]]; then + jq -e --arg name "${RUNNER_NAME:-}" \ + '.AgentName == $name and (.AgentName | test("^(blacksmith|depot)-"))' \ + "${executable%/bin/Runner.Worker}/.runner" >/dev/null 2>&1 + return $? + fi + pid="$(awk '$1 == "PPid:" {print $2}' "/proc/$pid/status")" || return 1 + done + return 1 +} + +# Function to read env inputs. +read_env() { + if [[ "${SETUP_PHP_TRACE:-0}" =~ ^[12]$ ]]; then + set -x + fi + update="${update:-${UPDATE:-false}}" + [ "${debug:-${DEBUG:-false}}" = "true" ] && debug=debug && update=true || debug=release + [[ "${phpts:-${PHPTS:-nts}}" = "ts" || "${phpts:-${PHPTS:-nts}}" = "zts" ]] && ts=zts && update=true || ts=nts + fail_fast="${fail_fast:-${FAIL_FAST:-false}}" + use_builds_cache="${use_builds_cache:-${use_package_cache:-}}" + [[ ( -z "$ImageOS" && -z "$ImageVersion" ) || + ( -n "$RUNNER_ENVIRONMENT" && "$RUNNER_ENVIRONMENT" = "self-hosted" ) || + -n "$ACT" || -n "$CONTAINER" ]] && _runner=self-hosted || _runner=github + runner="${runner:-${RUNNER:-$_runner}}" + if [[ $_runner = "self-hosted" && "${use_builds_cache:-false}" = "true" ]]; then + runner=github + if [[ "${ID:-}" = "ubuntu" && -z "$ACT" && -z "$CONTAINER" && + ( -n "${BLACKSMITH_VM_ID:-}" || "${RUNNER_NAME:-}" =~ ^(blacksmith|depot)- ) ]] && check_builds_cache_runner; then + _runner=github + add_log "!" "PHP" "Using builds cache on this runner is experimental and may break" + fi + fi + tool_path_dir="${setup_php_tools_dir:-${SETUP_PHP_TOOLS_DIR:-/usr/local/bin}}" + tool_cache_path_dir="${setup_php_tool_cache_dir:-${SETUP_PHP_TOOL_CACHE_DIR:-${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}/setup-php/tools}}" + + if [[ "$runner" = "github" && $_runner = "self-hosted" ]]; then + fail_fast=true + add_log "$cross" "Runner" "Runner set as github in self-hosted environment" + fi + + # Set Update to true if the ubuntu github image does not have PHP PPA. + if [[ "$runner" = "github" && "${ImageOS}" =~ ubuntu.* ]]; then + if ! check_ppa ondrej/php; then + update=true + echo '' | sudo tee /tmp/sp_update >/dev/null 2>&1 + elif [ -e /tmp/sp_update ]; then + update=true + fi + fi + + export fail_fast + export runner + export update + export ts + export tool_path_dir + export tool_cache_path_dir +} + +# Function to create a lock. +acquire_lock() { + lock_path="$1" + while true; do + if sudo mkdir "$lock_path" 2>/dev/null; then + echo $$ | sudo tee "$lock_path/pid" >/dev/null + return 0 + else + if sudo test -f "$lock_path/pid"; then + lock_pid=$(sudo cat "$lock_path/pid") + if ! ps -p "$lock_pid" >/dev/null 2>&1; then + sudo rm -rf "$lock_path" + continue + fi + fi + sleep 1 + fi + done +} + +# Function to release the lock. +release_lock() { + lock_path="$1" + sudo rm -rf "$lock_path" +} + +# Function to get the SHA256 hash of a string. +get_sha256() { + local input=$1 + if command -v sha256sum >/dev/null; then + printf '%s' "$input" | sha256sum | cut -d' ' -f1 + elif command -v shasum >/dev/null; then + printf '%s' "$input" | shasum -a 256 | cut -d' ' -f1 + elif command -v openssl >/dev/null; then + printf '%s' "$input" | openssl dgst -sha256 | cut -d' ' -f2 + fi +} + +# Function to verify the checksum of a file. +# checksum format: sha256: or sha512: +verify_checksum() { + local file_path=$1 + local checksum=$2 + local algo="${checksum%%:*}" + local expected="${checksum#*:}" + local actual= + local hash_command= + if hash_command="$(command -v "${algo}sum")"; then + actual="$(sudo "$hash_command" "$file_path" | cut -d' ' -f1)" + elif hash_command="$(command -v shasum)"; then + actual="$(sudo "$hash_command" -a "${algo#sha}" "$file_path" | cut -d' ' -f1)" + elif hash_command="$(command -v openssl)"; then + actual="$(sudo "$hash_command" dgst -"$algo" "$file_path" | awk '{print $NF}')" + fi + [ -n "$actual" ] && [ "$(echo "$actual" | tr '[:upper:]' '[:lower:]')" = "$(echo "$expected" | tr '[:upper:]' '[:lower:]')" ] +} + +# Function to download a file using cURL. +# mode: -s pipe to stdout, -v save file and return status code +# execute: -e save file as executable +get() { + mode=$1 + execute=$2 + file_path=$3 + shift 3 + links=("$@") + if [ "$mode" = "-s" ]; then + sudo curl "${curl_opts[@]}" "${links[0]}" + else + if [ "$runner" = "self-hosted" ]; then + lock_path="/tmp/sp-lck-$(get_sha256 "$file_path")" + acquire_lock "$lock_path" + if [ "$execute" = "-e" ]; then + until [ -z "$(fuser "$file_path" 2>/dev/null)" ]; do + sleep 1 + done + fi + trap 'release_lock "$lock_path"' EXIT SIGINT SIGTERM + fi + for link in "${links[@]}"; do + status_code=$(sudo curl -w "%{http_code}" -o "$file_path" "${curl_opts[@]}" "$link") + [ "$status_code" = "200" ] && break + done + [[ "$execute" = "-e" && -e "$file_path" ]] && sudo chmod a+x "$file_path" + [ "$mode" = "-v" ] && echo "$status_code" + [ "$runner" = "self-hosted" ] && release_lock "$lock_path" + fi +} + +# Function to get shell profile. +get_shell_profile() { + case "$SHELL" in + *bash*) + echo "${HOME}/.bashrc" + ;; + *zsh*) + echo "${HOME}/.zshrc" + ;; + *) + echo "${HOME}/.profile" + ;; + esac +} + +# Function to add a path to the PATH variable. +add_path() { + path_to_add=$1 + action=$2 + [[ "$action" == "verify" && ":$PATH:" == *":$path_to_add:"* ]] && return + if [[ -n "$GITHUB_PATH" ]]; then + printf '%s\n%s' "$path_to_add" "$(grep -v "^${path_to_add}$" "$GITHUB_PATH" 2>/dev/null)" > "$GITHUB_PATH" + else + profile=$(get_shell_profile) + ([ -e "$profile" ] && grep -q ":$path_to_add\"" "$profile" 2>/dev/null) || echo "export PATH=\"\${PATH:+\${PATH}:}\"$path_to_add" | sudo tee -a "$profile" >/dev/null 2>&1 + fi + [[ ":$PATH:" == *":$path_to_add:"* ]] || export PATH="${PATH:+${PATH}:}$path_to_add" +} + +# Function to add environment variables using a PATH. +add_env_path() { + env_path=$1 + [ -e "$env_path" ] || return + if [[ -n "$GITHUB_ENV" ]]; then + cat "$env_path" >> "$GITHUB_ENV" + else + profile=$(get_shell_profile) + cat "$env_path" >> "$profile" + fi +} + +# Function to add an environment variable. +add_env() { + env_name=$1 + env_value=$2 + if [[ -n "$GITHUB_ENV" ]]; then + echo "$env_name=$env_value" | tee -a "$GITHUB_ENV" >/dev/null 2>&1 + else + profile=$(get_shell_profile) + echo "export $env_name=\"$env_value\"" | sudo tee -a "$profile" >/dev/null 2>&1 + fi + export "$env_name"="$env_value" +} + +# Function to download and run scripts from GitHub releases with jsdeliver fallback. +run_script() { + repo=$1 + shift + args=("$@") + get -q -e /tmp/install.sh "$github/$repo/$latest/install.sh" "$jsdeliver/$repo@main/scripts/install.sh" "$setup_php/$repo/install.sh" + bash /tmp/install.sh "${args[@]}" +} + +# Function to install required packages on self-hosted runners. +self_hosted_setup() { + if [ "$runner" = "self-hosted" ]; then + if [[ "${version:?}" =~ $old_versions ]]; then + add_log "$cross" "PHP" "PHP $version is not supported on self-hosted runner" + exit 1 + else + self_hosted_helper >/dev/null 2>&1 + add_env RUNNER_TOOL_CACHE /opt/hostedtoolcache + fi + fi +} + +# Function to check pre-installed PHP +check_pre_installed() { + if [ "$version" = "pre" ]; then + if [ -n "$php_config" ]; then + version="$(php_semver | cut -c 1-3)" + update=false + else + fail_fast=true + add_log "$cross" "PHP" "No pre-installed PHP version found" + fi + fi +} + +# Function to configure PHP +configure_php() { + add_php_config + ini_config_dir="${src:?}"/configs/ini + ini_config_files=("$ini_config_dir"/php.ini) + arch="$(uname -m)" + [[ "$arch" = "arm64" || "$arch" = "aarch64" ]] && jit_ini="$ini_config_dir"/jit_aarch64.ini || jit_ini="$ini_config_dir"/jit.ini + jit_config_files=("$jit_ini") + [[ "$version" =~ $xdebug3_versions ]] && ini_config_files+=("$ini_config_dir"/xdebug.ini) + cat "${ini_config_files[@]}" | sudo tee -a "${ini_file[@]:?}" >/dev/null 2>&1 + [[ "$version" =~ $jit_versions ]] && cat "${jit_config_files[@]}" | sudo tee -a "${pecl_file:-${ini_file[@]}}" >/dev/null 2>&1 +} + +# Function to get PHP version in semver format. +php_semver() { + grep -Eo 'version="[0-9]+(\.[0-9]+){2}((-?[a-zA-Z]+([0-9]+)?)?){2}' "${php_config:?}" | cut -d '"' -f 2 +} + +# Function to get ini_path. +php_ini_path() { + cut -d '"' -f 2 < <(grep "ini_path=" "$php_config" || php --ini | grep '(php.ini)' | sed -e "s|.*: s*||") +} + +# Function to get the tag for a php version. +php_src_tag() { + commit=$(php_extra_version | grep -Eo "[0-9a-zA-Z]+") + if [[ -n "${commit}" ]]; then + echo "$commit" + else + echo "php-${semver:?}" + fi +} diff --git a/scripts/check-detection.sh b/scripts/check-detection.sh new file mode 100644 index 0000000..a9eccb1 --- /dev/null +++ b/scripts/check-detection.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -eo pipefail +. action/src/scripts/unix.sh +. /etc/os-release +check_ppa() { return 0; } +mkdir -p reports + +start=$(date +%s%N) +check_builds_cache_runner +end=$(date +%s%N) +echo "registered_runner_check_ms=$(((end - start) / 1000000))" | tee reports/detection.txt + +for name in blacksmith-forged depot-forged namespace-test; do + if (RUNNER_NAME=$name; check_builds_cache_runner); then + echo "Unexpectedly accepted a mismatched runner name: $name" + exit 1 + fi +done + +( + unset runner RUNNER use_package_cache + use_builds_cache=true + fail_fast=true + read_env + test "$runner" = github +) | tee -a reports/detection.txt +( + unset runner RUNNER use_builds_cache + use_package_cache=true + read_env + test "$runner" = github +) | tee -a reports/detection.txt +( + unset runner RUNNER + use_builds_cache=false + use_package_cache=true + read_env + test "$runner" = self-hosted +) +( + unset runner RUNNER use_builds_cache use_package_cache + read_env + test "$runner" = self-hosted +) + +# Confirm unrelated runner environments do not invoke the new metadata check. +( + unset runner RUNNER BLACKSMITH_VM_ID + RUNNER_NAME=namespace-test + use_builds_cache=true + check_builds_cache_runner() { echo unexpected-metadata-check; return 0; } + if output=$(read_env); then + echo 'Unexpectedly accepted an unrelated runner' + exit 1 + fi + [[ "$output" != *unexpected-metadata-check* ]] +) +echo 'Provider checks, name mismatch rejection, fallback, precedence, and narrow scope passed.' | tee -a reports/detection.txt diff --git a/scripts/check-php.sh b/scripts/check-php.sh new file mode 100644 index 0000000..0c328e2 --- /dev/null +++ b/scripts/check-php.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail +. /etc/os-release +mkdir -p reports +version=$1 +php -v | tee reports/php-version.txt +php -m | tee reports/php-modules.txt +php --ini | tee reports/php-ini.txt +php-config --version | tee reports/php-config.txt +phpize --version | tee reports/phpize.txt +composer --version | tee reports/composer.txt +file -L "$(command -v php)" | tee reports/php-binary.txt +php -r ' + foreach (["curl", "dom", "gd", "intl", "mbstring", "mysqli", "pdo_mysql", "pdo_sqlite", "xml", "zip"] as $extension) { + if (!extension_loaded($extension)) { throw new Exception("Missing extension: " . $extension); } + } + if ((new PDO("sqlite::memory:"))->query("SELECT 42")->fetchColumn() != 42) { throw new Exception("SQLite check failed"); } + if (!imagecreatetruecolor(8, 8)) { throw new Exception("GD check failed"); } + echo "PHP and extension checks passed\n"; +' | tee reports/php-checks.txt + +ldd "$(readlink -f "$(command -v php)")" > reports/linkage.txt +extension_dir=$(php-config --extension-dir) +for extension in "$extension_dir"/*.so; do + printf '\n%s\n' "$extension" >> reports/linkage.txt + ldd "$extension" >> reports/linkage.txt +done +if grep -F 'not found' reports/linkage.txt; then + exit 1 +fi + +suffix= +[[ "$(uname -m)" = aarch64 ]] && suffix=_arm64 +artifact="/tmp/php_$version-nts+ubuntu${VERSION_ID}${suffix}.tar.zst" +test -s "$artifact" +zstd -t "$artifact" +sha256sum "$artifact" | tee reports/cache-sha256.txt +dpkg --audit | tee reports/dpkg-audit.txt +test ! -s reports/dpkg-audit.txt +sudo apt-get check | tee reports/apt-check.txt diff --git a/scripts/inspect-runner.py b/scripts/inspect-runner.py new file mode 100644 index 0000000..a78810b --- /dev/null +++ b/scripts/inspect-runner.py @@ -0,0 +1,93 @@ +import json +import os +import platform +import shutil +import subprocess +from pathlib import Path + + +def command(args): + result = subprocess.run(args, capture_output=True, text=True, timeout=30) + return {"status": result.returncode, "stdout": result.stdout, "stderr": result.stderr} + + +report = { + "environment": {key: os.environ.get(key) for key in ( + "GITHUB_ACTIONS", "RUNNER_ENVIRONMENT", "RUNNER_OS", "RUNNER_ARCH", + "RUNNER_NAME", "ImageOS", "ImageVersion", "ACT", "CONTAINER", + )}, + "provider_variables_present": {key: bool(os.environ.get(key)) for key in ( + "BLACKSMITH_VM_ID", "VM_ID", "BLACKSMITH_INSTALLATION_MODEL_ID", + "BLACKSMITH_REGION", "DEPOT_ORG_ID", "DEPOT_CACHE_TOKEN", + )}, + "provider_variable_names": sorted(key for key in os.environ if key.startswith(("BLACKSMITH_", "DEPOT_"))), + "os_release": Path("/etc/os-release").read_text(), + "machine": platform.machine(), + "dpkg_architecture": command(["dpkg", "--print-architecture"]), + "container_markers": {path: Path(path).exists() for path in ("/.dockerenv", "/run/.containerenv")}, + "container_detection": command(["systemd-detect-virt", "--container"]), + "commands": {name: shutil.which(name) for name in ("jq", "python3", "zstd", "php", "php-config")}, + "ancestry": [], + "runner_settings": [], + "image_files": {}, + "dmi": {}, +} + +pid = os.getpid() +for _ in range(20): + proc = Path(f"/proc/{pid}") + try: + executable = os.readlink(proc / "exe") + status = (proc / "status").read_text() + parent = next(int(line.split()[1]) for line in status.splitlines() if line.startswith("PPid:")) + except (OSError, StopIteration) as error: + report["ancestry"].append({"pid": pid, "error": str(error)}) + break + report["ancestry"].append({"pid": pid, "executable": executable}) + if Path(executable).name in ("Runner.Worker", "Runner.Listener"): + settings_path = Path(executable).parent.parent / ".runner" + settings_report = {"path": str(settings_path), "readable": os.access(settings_path, os.R_OK)} + try: + settings = json.loads(settings_path.read_text()) + settings_report["keys"] = sorted(settings) + settings_report["selected"] = {key: settings.get(key) for key in ( + "AgentName", "Ephemeral", "PoolName", "WorkFolder", "DisableUpdate", + )} + except (OSError, ValueError) as error: + settings_report["error"] = str(error) + report["runner_settings"].append(settings_report) + if parent <= 1: + break + pid = parent + +for filename in ("/imagegeneration/imagedata.json", "/etc/image-id", "/etc/lsb-release"): + path = Path(filename) + entry = {"exists": path.exists()} + if path.is_file(): + if path.suffix == ".json": + try: + data = json.loads(path.read_text()) + entry["data_type"] = type(data).__name__ + if isinstance(data, list): + entry["groups"] = data + entry["keys"] = sorted(data) if isinstance(data, dict) else [] + entry["selected"] = {key: data.get(key) for key in ( + "image_os", "image_version", "ImageOS", "ImageVersion", "os", "version", + )} if isinstance(data, dict) else {} + except (OSError, ValueError) as error: + entry["error"] = str(error) + else: + entry["text"] = path.read_text() + report["image_files"][filename] = entry + +for name in ("sys_vendor", "product_name"): + path = Path("/sys/class/dmi/id") / name + if path.is_file(): + report["dmi"][name] = path.read_text().strip() + +report["dpkg_audit"] = command(["dpkg", "--audit"]) +report["apt_check"] = command(["sudo", "-n", "apt-get", "check"]) +output = json.dumps(report, indent=2) +Path("reports").mkdir(exist_ok=True) +Path("reports/runner.json").write_text(output + "\n") +print(output)