diff --git a/.github/workflows/bootstrap_testing.yml b/.github/workflows/bootstrap_testing.yml index 76aa29e83ff..cafdabc29bb 100644 --- a/.github/workflows/bootstrap_testing.yml +++ b/.github/workflows/bootstrap_testing.yml @@ -83,32 +83,34 @@ jobs: - name: Install base dependencies run: | # Attempt to run the package installation up to 10 times to mitigate transient network issues + ok='' for n in $(seq 1 10); do { echo "Attempt #$n of 10 to install base dependencies:" case "${{ matrix.distribution }}" in *ubuntu*|*debian*) - apt-get update - apt-get install -y sudo git passwd + apt-get update && + apt-get install -y sudo git passwd ;; *fedora*|*rockylinux*|*almalinux*) dnf install -y sudo git passwd findutils # findutils=xargs ;; *suse*) - zypper --non-interactive refresh - zypper --non-interactive install sudo git shadow findutils tar # findutils=xargs + zypper --non-interactive refresh && + zypper --non-interactive install sudo git shadow findutils tar # findutils=xargs ;; *gentoo*) - emaint sync - emerge --noreplace --ask=n sudo dev-vcs/git shadow findutils # findutils=xargs + { emaint sync -a || emerge --sync --quiet || emerge-webrsync -q ; } && + emerge --noreplace --ask=n sudo dev-vcs/git shadow findutils # findutils=xargs ;; *archlinux*|*cachyos*|*manjaro*) - pacman -Syu --noconfirm - pacman -S --noconfirm sudo git + pacman -Syu --noconfirm && + pacman -S --noconfirm sudo git ;; esac - } && break || sleep 10 + } && { ok=1; break ; } || sleep 10 done + [ -n "$ok" ] || exit 1 # Fix PAM configuration for sudo in containers # Fix /etc/shadow permissions - common issue in container environments @@ -196,10 +198,21 @@ jobs: - macos-15 # Apple Silicon ARM64 - macos-15-intel # Intel x64 - macos-26 # Apple Silicon ARM64 + - macos-26-intel # Intel x64 runs-on: ${{ matrix.os }} steps: + - name: Install base dependencies + run: | + # Attempt to run the brew update up to 10 times to mitigate transient issues + for n in $(seq 1 10); do + brew update && exit 0 + brew update-reset || true + sleep 10 + done + exit 1 + - name: Checkout repository uses: actions/checkout@v7 with: diff --git a/util/env-bootstrap.sh b/util/env-bootstrap.sh index 47238a78dff..076a67571d8 100755 --- a/util/env-bootstrap.sh +++ b/util/env-bootstrap.sh @@ -43,6 +43,49 @@ # corrupt captured output and break pattern matching throughout this script. unset GREP_OPTIONS GREP_COLOR GREP_COLORS + # Force the C locale so `tr`/`grep`/`sed`/`sort` behave the same on every + # system, and clear other variables which alter tool behavior. + export LC_ALL=C + unset CDPATH POSIXLY_CORRECT TAR_OPTIONS + + # Drop out of any inherited Python venv; `deactivate` doesn't exist in this + # process, so strip its $PATH entries and marker variables by hand. + if [ -n "${VIRTUAL_ENV:-}" ]; then + clean_path='' + old_ifs="${IFS}" + IFS=':' + for path_entry in $PATH; do + case "$path_entry" in + "$VIRTUAL_ENV"/bin | "$VIRTUAL_ENV"/Scripts) ;; + *) clean_path="${clean_path:+${clean_path}:}${path_entry}" ;; + esac + done + IFS="${old_ifs}" + PATH="${clean_path}" + export PATH + unset VIRTUAL_ENV VIRTUAL_ENV_PROMPT clean_path old_ifs path_entry + fi + + # Wipe all PYTHON* variables (keeping PYTHON_TARGET_VERSION, which this + # script uses) so the user's Python settings can't leak into the + # interpreters and builds managed by `uv`. + saved_python_target_version="${PYTHON_TARGET_VERSION:-}" + for env_var_name in $(env | LC_ALL=C sed -n 's/^\(PYTHON[A-Za-z0-9_]*\)=.*/\1/p'); do + unset "$env_var_name" 2>/dev/null || true + done + [ -z "$saved_python_target_version" ] || export PYTHON_TARGET_VERSION="$saved_python_target_version" + unset saved_python_target_version env_var_name + export PYTHONNOUSERSITE=1 + + # Clear conda/pip/uv overrides which would affect dependency resolution or + # builds; proxy, TLS, and index/mirror variables stay for corporate networks. + unset CONDA_PREFIX CONDA_DEFAULT_ENV + unset PIP_REQUIRE_VIRTUALENV PIP_TARGET PIP_PREFIX PIP_USER + unset SETUPTOOLS_USE_DISTUTILS + unset UV_NO_BUILD_ISOLATION UV_OFFLINE UV_NO_INDEX \ + UV_CONSTRAINT UV_BUILD_CONSTRAINT UV_OVERRIDE \ + UV_SYSTEM_PYTHON UV_NO_MANAGED_PYTHON + BOOTSTRAP_TMPDIR="$(mktemp -d /tmp/qmk-bootstrap-failure.XXXXXX)" trap 'rm -rf "$BOOTSTRAP_TMPDIR" >/dev/null 2>&1 || true' EXIT FAILURE_FILE="${BOOTSTRAP_TMPDIR}/fail" @@ -175,6 +218,15 @@ __EOT__ fi } + check_release_tag() { + # An empty tag means the GitHub API call failed, usually from rate limiting. + if [ -z "$2" ]; then + echo "Could not determine the latest $1 release." >&2 + echo "If GitHub API rate limits are the cause, set GITHUB_TOKEN to raise them." >&2 + exit 1 + fi + } + fn_os() { local os_name=$(echo ${1:-} | tr 'A-Z' 'a-z') if [ -z "$os_name" ]; then @@ -285,6 +337,10 @@ __EOT__ print_package_manager_deps_and_delay() { get_package_manager_deps | tr ' ' '\n' | sort | xargs -I'{}' echo " - {}" >&2 exit_if_execution_failed + if [ -n "${1:-}" ]; then + echo >&2 + echo "$1" >&2 + fi preinstall_delay || exit 1 } @@ -294,7 +350,11 @@ __EOT__ macos) if [ -n "$(command -v brew 2>/dev/null || true)" ]; then echo "It will also install the following system packages using 'brew':" >&2 - print_package_manager_deps_and_delay + local intel_note="" + if [ "$(fn_arch)" = "X64" ]; then + intel_note="NOTE: Homebrew no longer provides pre-built packages for Intel Macs, so some of the above may be built from source. This can take a long time." + fi + print_package_manager_deps_and_delay "$intel_note" brew update @@ -308,12 +368,14 @@ __EOT__ fi done - if [ -n "${existing:-}" ]; then - brew upgrade $existing - fi - if [ -n "${new:-}" ]; then - brew install $new - fi + # Homebrew no longer builds Intel macOS bottles (tier 3); when a + # bottle is missing, retry the formula as a source build. + for dep in ${existing:-}; do + brew upgrade "$dep" || brew upgrade --build-from-source "$dep" + done + for dep in ${new:-}; do + brew install "$dep" || brew install --build-from-source "$dep" + done else echo "Please install 'brew' to continue. See https://brew.sh/ for more information." >&2 exit 1 @@ -437,6 +499,7 @@ __EOT__ install_toolchains() { # Get the latest toolchain release from https://github.com/qmk/qmk_toolchains local latest_toolchains_release=$(github_api_call repos/qmk/qmk_toolchains/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$') + check_release_tag qmk_toolchains "$latest_toolchains_release" # Download the specific release asset with a matching keyword local toolchain_url=$(github_api_call repos/qmk/qmk_toolchains/releases/tags/$latest_toolchains_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep -E "qmk_toolchains-.*$(fn_os)$(fn_arch)") if [ -z "$toolchain_url" ]; then @@ -464,6 +527,7 @@ __EOT__ # Get the latest flashing tools release from https://github.com/qmk/qmk_flashutils local latest_flashutils_release=$(github_api_call repos/qmk/qmk_flashutils/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$') + check_release_tag qmk_flashutils "$latest_flashutils_release" # Download the specific release asset with a matching keyword local flashutils_url=$(github_api_call repos/qmk/qmk_flashutils/releases/tags/$latest_flashutils_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep -E "qmk_flashutils-.*$osarchvariant") if [ -z "$flashutils_url" ]; then @@ -486,10 +550,7 @@ __EOT__ install_linux_udev_rules() { # Get the latest qmk_udev release local latest_udev_release=$(github_api_call repos/qmk/qmk_udev/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$') - if [ -z "$latest_udev_release" ]; then - echo "Could not determine latest qmk_udev release." >&2 - exit 1 - fi + check_release_tag qmk_udev "$latest_udev_release" echo "Using qmk_udev release: $latest_udev_release" >&2 # Download the udev rules file @@ -541,6 +602,7 @@ __EOT__ install_windows_drivers() { # Get the latest driver installer release from https://github.com/qmk/qmk_driver_installer local latest_driver_installer_release=$(github_api_call repos/qmk/qmk_driver_installer/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$') + check_release_tag qmk_driver_installer "$latest_driver_installer_release" # Download the specific release asset local driver_installer_url=$(github_api_call repos/qmk/qmk_driver_installer/releases/tags/$latest_driver_installer_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep '\.exe') if [ -z "$driver_installer_url" ]; then @@ -600,7 +662,14 @@ __EOT__ setup_paths # Work out where we want to install the distribution and tools now that `uv` is installed - export QMK_DISTRIB_DIR="$(posix_ish_path "${QMK_DISTRIB_DIR:-$(printf 'import platformdirs\nprint(platformdirs.user_data_dir("qmk"))' | uv_command run --quiet --python $PYTHON_TARGET_VERSION --with platformdirs -)}")" + export QMK_DISTRIB_DIR="$(posix_ish_path "${QMK_DISTRIB_DIR:-$(printf 'import platformdirs\nprint(platformdirs.user_data_dir("qmk"))' | uv_command run --quiet --no-project --python $PYTHON_TARGET_VERSION --with platformdirs -)}")" + + # `export` masks any failure of the `uv` invocation above, so bail out here + # rather than continue with an empty directory. + if [ -z "$QMK_DISTRIB_DIR" ]; then + echo "Could not determine the QMK distribution directory." >&2 + exit 1 + fi # Clear out the distrib directory if necessary if [ -z "${SKIP_CLEAN:-}" ] || [ -z "${SKIP_QMK_TOOLCHAINS:-}" -a -z "${SKIP_QMK_FLASHUTILS:-}" ]; then