diff --git a/.github/renovate.json b/.github/renovate.json index d6c5d81..53dc607 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -28,17 +28,17 @@ }, { "customType": "regex", - "description": "Track the latest stable LLVM major version from official releases", + "description": "Track LLVM releases (major and patch) from the official GitHub releases; the full version drives the CI cache of the LLVM apt packages", "managerFilePatterns": [ "/(^|/)Dockerfile$/" ], "matchStrings": [ - "# renovate: datasource=github-releases depName=llvm/llvm-project\\s*\\nARG LLVM_VERSION=(?\\d+)" + "# renovate: datasource=github-releases depName=llvm/llvm-project\\s*\\nARG LLVM_VERSION=(?\\d+\\.\\d+\\.\\d+)" ], "depNameTemplate": "llvm/llvm-project", "datasourceTemplate": "github-releases", - "extractVersionTemplate": "^llvmorg-(?\\d+)\\.\\d+\\.\\d+$", - "versioningTemplate": "semver-coerced" + "extractVersionTemplate": "^llvmorg-(?\\d+\\.\\d+\\.\\d+)$", + "versioningTemplate": "semver" } ] } diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2573bca..92d7afb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,6 +32,8 @@ jobs: artifact_name: ${{ needs.Prepare.outputs.artifact_name }} artifact_run_id: ${{ needs.Prepare.outputs.artifact_run_id }} archive_name: ${{ needs.Prepare.outputs.archive_name }} + llvm_cache_key_amd64: ${{ needs.Prepare.outputs.llvm_cache_key_amd64 }} + llvm_cache_key_arm64: ${{ needs.Prepare.outputs.llvm_cache_key_arm64 }} Linux_Emulated: needs: Prepare uses: ./.github/workflows/ci_emulated_linux.yaml @@ -39,6 +41,7 @@ jobs: artifact_name: ${{ needs.Prepare.outputs.artifact_name }} artifact_run_id: ${{ needs.Prepare.outputs.artifact_run_id }} archive_name: ${{ needs.Prepare.outputs.archive_name }} + llvm_cache_key_s390x: ${{ needs.Prepare.outputs.llvm_cache_key_s390x }} macOS: needs: Prepare uses: ./.github/workflows/ci_macos.yaml @@ -60,3 +63,5 @@ jobs: artifact_name: ${{ needs.Prepare.outputs.artifact_name }} artifact_run_id: ${{ needs.Prepare.outputs.artifact_run_id }} archive_name: ${{ needs.Prepare.outputs.archive_name }} + llvm_cache_key_amd64: ${{ needs.Prepare.outputs.llvm_cache_key_amd64 }} + llvm_cache_key_arm64: ${{ needs.Prepare.outputs.llvm_cache_key_arm64 }} diff --git a/.github/workflows/ci_coverage.yaml b/.github/workflows/ci_coverage.yaml index cb471cc..5f393d8 100644 --- a/.github/workflows/ci_coverage.yaml +++ b/.github/workflows/ci_coverage.yaml @@ -16,6 +16,14 @@ on: archive_name: required: true type: string + llvm_cache_key_amd64: + description: actions/cache key of the pre-fetched LLVM apt packages for amd64 (from ci_prepare.yaml) + required: true + type: string + llvm_cache_key_arm64: + description: actions/cache key of the pre-fetched LLVM apt packages for arm64 (from ci_prepare.yaml) + required: true + type: string jobs: init: runs-on: ubuntu-24.04 @@ -44,9 +52,20 @@ jobs: else echo "needs-init=false" >> "${GITHUB_OUTPUT}" fi + - name: Restore LLVM apt packages from cache + if: steps.check.outputs.needs-init == 'true' + uses: actions/cache/restore@v6 + with: + path: llvm-debs/trixie-amd64 + key: ${{ inputs.llvm_cache_key_amd64 }} + fail-on-cache-miss: true - name: Initialize extension if: steps.check.outputs.needs-init == 'true' run: | + # pskel init only needs the bundled PHP, so skip the Valgrind source build (and its download). + docker compose build \ + --build-arg SKIP_VALGRIND=1 \ + shell docker compose run -v "$(pwd)/ext:/ext" --rm shell pskel init skeleton - name: Upload initialized extension if: steps.check.outputs.needs-init == 'true' @@ -84,6 +103,12 @@ jobs: with: name: initialized-ext-coverage path: ext/ + - name: Restore LLVM apt packages from cache + uses: actions/cache/restore@v6 + with: + path: llvm-debs/trixie-${{ matrix.platform == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }} + key: ${{ matrix.platform == 'ubuntu-24.04-arm' && inputs.llvm_cache_key_arm64 || inputs.llvm_cache_key_amd64 }} + fail-on-cache-miss: true - name: Build container and run coverage run: | rm -f "ext/.gitkeep" @@ -135,6 +160,12 @@ jobs: with: name: initialized-ext-coverage path: ext/ + - name: Restore LLVM apt packages from cache + uses: actions/cache/restore@v6 + with: + path: llvm-debs/trixie-amd64 + key: ${{ inputs.llvm_cache_key_amd64 }} + fail-on-cache-miss: true - name: Build container run: | docker compose build \ diff --git a/.github/workflows/ci_emulated_linux.yaml b/.github/workflows/ci_emulated_linux.yaml index 7d59058..237ec5d 100644 --- a/.github/workflows/ci_emulated_linux.yaml +++ b/.github/workflows/ci_emulated_linux.yaml @@ -11,6 +11,11 @@ on: archive_name: required: true type: string + llvm_cache_key_s390x: + description: actions/cache key of the pre-fetched LLVM apt packages for s390x (from ci_prepare.yaml); empty when unavailable + required: false + default: '' + type: string jobs: Linux: runs-on: ubuntu-24.04 @@ -74,6 +79,13 @@ jobs: echo "exists=false" >> "${GITHUB_OUTPUT}" echo "Image does not exist, will build: ${{ steps.image-tag.outputs.tag }}" fi + - name: Restore LLVM apt packages from cache + if: steps.check-image.outputs.exists == 'false' && matrix.distro != 'alpine' && steps.cut-arch.outputs.arch == 's390x' && inputs.llvm_cache_key_s390x != '' + uses: actions/cache/restore@v6 + with: + path: llvm-debs/${{ matrix.distro }}-${{ steps.cut-arch.outputs.arch }} + key: ${{ inputs.llvm_cache_key_s390x }} + fail-on-cache-miss: true - name: Build container if: steps.check-image.outputs.exists == 'false' run: | diff --git a/.github/workflows/ci_native_linux.yaml b/.github/workflows/ci_native_linux.yaml index f668555..7b0a3eb 100644 --- a/.github/workflows/ci_native_linux.yaml +++ b/.github/workflows/ci_native_linux.yaml @@ -12,6 +12,14 @@ on: archive_name: required: true type: string + llvm_cache_key_amd64: + description: actions/cache key of the pre-fetched LLVM apt packages for amd64 (from ci_prepare.yaml) + required: true + type: string + llvm_cache_key_arm64: + description: actions/cache key of the pre-fetched LLVM apt packages for arm64 (from ci_prepare.yaml) + required: true + type: string jobs: Linux: env: @@ -36,6 +44,45 @@ jobs: ARCHIVE_PATH="${RUNNER_TEMP}/source-archive/${ARCHIVE_NAME}" tar -xzf "${ARCHIVE_PATH}" --strip-components=1 -C "${GITHUB_WORKSPACE}" rm -f "${ARCHIVE_PATH}" + - name: Detect toolchain cache keys + if: matrix.distro != 'alpine' + id: toolchain + env: + LLVM_CACHE_KEY_AMD64: ${{ inputs.llvm_cache_key_amd64 }} + LLVM_CACHE_KEY_ARM64: ${{ inputs.llvm_cache_key_arm64 }} + run: | + VALGRIND_VERSION="$(sed -n 's/^ARG VALGRIND_VERSION=//p' "Dockerfile" | head -n 1)" + if test -z "${VALGRIND_VERSION}"; then + echo "Error: VALGRIND_VERSION not found in Dockerfile." >&2 + exit 1 + fi + ARCH="$(dpkg --print-architecture)" + case "${ARCH}" in + amd64) LLVM_CACHE_KEY="${LLVM_CACHE_KEY_AMD64}";; + arm64) LLVM_CACHE_KEY="${LLVM_CACHE_KEY_ARM64}";; + *) echo "Error: no LLVM cache key for architecture ${ARCH}." >&2; exit 1;; + esac + if test -z "${LLVM_CACHE_KEY}"; then + echo "Error: empty LLVM cache key for architecture ${ARCH}." >&2 + exit 1 + fi + echo "valgrind_version=${VALGRIND_VERSION}" >> "${GITHUB_OUTPUT}" + echo "arch=${ARCH}" >> "${GITHUB_OUTPUT}" + echo "llvm_cache_key=${LLVM_CACHE_KEY}" >> "${GITHUB_OUTPUT}" + - name: Restore Valgrind tarball from cache + if: matrix.distro != 'alpine' + uses: actions/cache/restore@v6 + with: + path: valgrind-${{ steps.toolchain.outputs.valgrind_version }}.tar.bz2 + key: valgrind-tarball-${{ steps.toolchain.outputs.valgrind_version }} + fail-on-cache-miss: true + - name: Restore LLVM apt packages from cache + if: matrix.distro != 'alpine' + uses: actions/cache/restore@v6 + with: + path: llvm-debs/${{ matrix.distro }}-${{ steps.toolchain.outputs.arch }} + key: ${{ steps.toolchain.outputs.llvm_cache_key }} + fail-on-cache-miss: true - name: Pull PHP container image id: pull-image run: | diff --git a/.github/workflows/ci_prepare.yaml b/.github/workflows/ci_prepare.yaml index 469a019..5356120 100644 --- a/.github/workflows/ci_prepare.yaml +++ b/.github/workflows/ci_prepare.yaml @@ -12,13 +12,32 @@ on: archive_name: description: Name of the tar.gz workspace archive value: ${{ jobs.prepare.outputs.archive_name }} + llvm_cache_key_amd64: + description: actions/cache key of the pre-fetched LLVM apt packages for amd64 + value: ${{ jobs.prepare.outputs.llvm_cache_key_amd64 }} + llvm_cache_key_arm64: + description: actions/cache key of the pre-fetched LLVM apt packages for arm64 + value: ${{ jobs.prepare.outputs.llvm_cache_key_arm64 }} + llvm_cache_key_s390x: + description: actions/cache key of the pre-fetched LLVM apt packages for s390x (empty when they could not be fetched) + value: ${{ jobs.prepare.outputs.llvm_cache_key_s390x }} jobs: prepare: runs-on: ubuntu-24.04 + env: + LLVM_APT_CODENAME: trixie + # Architectures whose LLVM apt packages are pre-fetched once here and shared with every Linux container build. + LLVM_ARCHITECTURES: amd64 arm64 s390x + # Subset of LLVM_ARCHITECTURES that is best-effort: when apt.llvm.org cannot provide their packages, only the + # affected (emulated) build jobs fall back to installing from apt.llvm.org instead of the whole CI run failing. + LLVM_OPTIONAL_ARCHITECTURES: s390x outputs: artifact_name: ${{ steps.metadata.outputs.artifact_name }} artifact_run_id: ${{ steps.metadata.outputs.artifact_run_id }} archive_name: ${{ steps.metadata.outputs.archive_name }} + llvm_cache_key_amd64: ${{ steps.llvm-keys.outputs.cache_key_amd64 }} + llvm_cache_key_arm64: ${{ steps.llvm-keys.outputs.cache_key_arm64 }} + llvm_cache_key_s390x: ${{ steps.llvm-keys.outputs.cache_key_s390x }} steps: - name: Checkout repository uses: actions/checkout@v7 @@ -50,6 +69,158 @@ jobs: -o "${VALGRIND_TARBALL}" \ "https://sourceware.org/pub/valgrind/valgrind-${VALGRIND_VERSION}.tar.bz2" fi + - name: Detect LLVM version and packages + id: llvm + run: | + LLVM_VERSION="$(sed -n 's/^ARG LLVM_VERSION=//p' "Dockerfile" | head -n 1)" + LLVM_PACKAGES="$(sed -n 's/^ARG LLVM_PACKAGES="\(.*\)"$/\1/p' "Dockerfile" | head -n 1)" + if test -z "${LLVM_VERSION}" || test -z "${LLVM_PACKAGES}"; then + echo "Error: LLVM_VERSION or LLVM_PACKAGES not found in Dockerfile." >&2 + exit 1 + fi + if ! printf '%s\n' "${LLVM_VERSION}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "Error: LLVM_VERSION must be a full release version (major.minor.patch), got '${LLVM_VERSION}'." >&2 + exit 1 + fi + LLVM_MAJOR="${LLVM_VERSION%%.*}" + PACKAGES_HASH="$(printf '%s' "${LLVM_PACKAGES}" | sed "s/@/${LLVM_MAJOR}/g" | sha256sum | cut -c1-12)" + echo "version=${LLVM_VERSION}" >> "${GITHUB_OUTPUT}" + echo "major=${LLVM_MAJOR}" >> "${GITHUB_OUTPUT}" + echo "packages=${LLVM_PACKAGES}" >> "${GITHUB_OUTPUT}" + # The full release version (bumped by Renovate from the LLVM GitHub releases) is part of the key, so a new + # LLVM release or a change of the package list are the only events that invalidate the cache and make this + # job download from apt.llvm.org again. + for ARCH in ${LLVM_ARCHITECTURES}; do + CACHE_KEY="llvm-debs-${LLVM_APT_CODENAME}-${ARCH}-${LLVM_VERSION}-${PACKAGES_HASH}" + echo "${ARCH}: ${CACHE_KEY}" + echo "cache_key_${ARCH}=${CACHE_KEY}" >> "${GITHUB_OUTPUT}" + done + - name: Cache LLVM apt packages (amd64) + uses: actions/cache@v6 + with: + path: llvm-debs/${{ env.LLVM_APT_CODENAME }}-amd64 + key: ${{ steps.llvm.outputs.cache_key_amd64 }} + - name: Cache LLVM apt packages (arm64) + uses: actions/cache@v6 + with: + path: llvm-debs/${{ env.LLVM_APT_CODENAME }}-arm64 + key: ${{ steps.llvm.outputs.cache_key_arm64 }} + - name: Cache LLVM apt packages (s390x) + uses: actions/cache@v6 + with: + path: llvm-debs/${{ env.LLVM_APT_CODENAME }}-s390x + key: ${{ steps.llvm.outputs.cache_key_s390x }} + - name: Download LLVM apt packages if not cached + env: + LLVM_VERSION: ${{ steps.llvm.outputs.version }} + LLVM_MAJOR: ${{ steps.llvm.outputs.major }} + LLVM_PACKAGES: ${{ steps.llvm.outputs.packages }} + run: | + LLVM_FETCH_ARCHITECTURES="" + DOCKER_ARGS=() + for ARCH in ${LLVM_ARCHITECTURES}; do + LLVM_DEBS_DIR="llvm-debs/${LLVM_APT_CODENAME}-${ARCH}" + if test -f "${LLVM_DEBS_DIR}/Packages" && test -f "${LLVM_DEBS_DIR}/Release"; then + echo "LLVM apt packages for ${ARCH} restored from cache." + continue + fi + mkdir -p "${LLVM_DEBS_DIR}" + LLVM_FETCH_ARCHITECTURES="${LLVM_FETCH_ARCHITECTURES} ${ARCH}" + DOCKER_ARGS+=(-v "${PWD}/${LLVM_DEBS_DIR}:/out/${ARCH}") + done + if test -z "${LLVM_FETCH_ARCHITECTURES}"; then + echo "All LLVM apt packages restored from cache, apt.llvm.org is not contacted." + exit 0 + fi + LLVM_APT_KEYRING="${RUNNER_TEMP}/llvm-snapshot.gpg" + curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors "https://apt.llvm.org/llvm-snapshot.gpg.key" \ + | gpg --dearmor --yes -o "${LLVM_APT_KEYRING}" + cat > "${RUNNER_TEMP}/fetch-llvm-debs.sh" <<'EOF' + export DEBIAN_FRONTEND="noninteractive" + LLVM_APT_KEYRING="/usr/share/keyrings/llvm-snapshot.gpg" + LLVM_APT_PACKAGES="$(printf '%s' "${LLVM_PACKAGES}" | sed "s/@/${LLVM_MAJOR}/g")" + apt-get update + apt-get install -y --no-install-recommends "ca-certificates" "apt-utils" + for ARCH in ${LLVM_FETCH_ARCHITECTURES}; do + dpkg --add-architecture "${ARCH}" + done + echo "deb [signed-by=${LLVM_APT_KEYRING}] https://apt.llvm.org/${LLVM_APT_CODENAME}/ llvm-toolchain-${LLVM_APT_CODENAME}-${LLVM_MAJOR} main" \ + > "/etc/apt/sources.list.d/llvm.list" + # A missing index of an optional architecture must not abort the whole fetch: the download below fails only + # for that architecture. + apt-get -o Acquire::Retries=5 update \ + || echo "::warning::apt-get update reported errors, continuing with the package indexes that could be fetched." + for ARCH in ${LLVM_FETCH_ARCHITECTURES}; do + echo "[Pskel] Downloading LLVM ${LLVM_MAJOR} apt packages for ${ARCH}." >&2 + rm -f /var/cache/apt/archives/*.deb + if ! apt-get -o Acquire::Retries=5 install -y --no-install-recommends --download-only \ + $(for PACKAGE in ${LLVM_APT_PACKAGES}; do printf '%s:%s ' "${PACKAGE}" "${ARCH}"; done); then + case " ${LLVM_OPTIONAL_ARCHITECTURES} " in + *" ${ARCH} "*) + echo "::warning::Could not download the LLVM ${LLVM_MAJOR} apt packages for ${ARCH} from apt.llvm.org, container builds for ${ARCH} will install directly from apt.llvm.org." + continue + ;; + *) + exit 1 + ;; + esac + fi + find /var/cache/apt/archives -maxdepth 1 -name '*.deb' \ + \( -name "*_${ARCH}.deb" -o -name '*_all.deb' \) -exec cp -t "/out/${ARCH}/" {} + + cp "${LLVM_APT_KEYRING}" "/out/${ARCH}/" + cd "/out/${ARCH}" + apt-ftparchive packages . > "Packages" + apt-ftparchive release . > "Release" + APT_VERSION="$(sed -n "/^Package: clang-${LLVM_MAJOR}\$/,/^\$/s/^Version: //p" "Packages" | head -n 1)" + echo "[Pskel] Fetched clang-${LLVM_MAJOR} ${APT_VERSION} for ${ARCH} (LLVM_VERSION=${LLVM_VERSION})." >&2 + # apt.llvm.org publishes snapshot builds of the release branch, versioned like "1:23.1.1~++..." while + # the branch heads towards 23.1.1, so a build that predates the release named by LLVM_VERSION is detectable. + if dpkg --compare-versions "${APT_VERSION#*:}" lt "${LLVM_VERSION}"; then + echo "::warning::apt.llvm.org does not provide LLVM ${LLVM_VERSION} for ${ARCH} yet (found ${APT_VERSION}). This older build stays cached until LLVM_VERSION changes or the llvm-debs cache entries are deleted." + fi + ls -la + du -sh . + cd - >/dev/null + done + EOF + docker run --rm -i \ + -e LLVM_VERSION -e LLVM_MAJOR -e LLVM_PACKAGES -e LLVM_APT_CODENAME -e LLVM_OPTIONAL_ARCHITECTURES \ + -e "LLVM_FETCH_ARCHITECTURES=${LLVM_FETCH_ARCHITECTURES}" \ + -v "${LLVM_APT_KEYRING}:/usr/share/keyrings/llvm-snapshot.gpg:ro" \ + "${DOCKER_ARGS[@]}" \ + "debian:${LLVM_APT_CODENAME}-slim" sh -eu -s < "${RUNNER_TEMP}/fetch-llvm-debs.sh" + - name: Resolve LLVM cache keys + id: llvm-keys + env: + CACHE_KEY_AMD64: ${{ steps.llvm.outputs.cache_key_amd64 }} + CACHE_KEY_ARM64: ${{ steps.llvm.outputs.cache_key_arm64 }} + CACHE_KEY_S390X: ${{ steps.llvm.outputs.cache_key_s390x }} + run: | + resolve() { + ARCH="$1" + CACHE_KEY="$2" + LLVM_DEBS_DIR="llvm-debs/${LLVM_APT_CODENAME}-${ARCH}" + if test -f "${LLVM_DEBS_DIR}/Packages" && test -f "${LLVM_DEBS_DIR}/Release"; then + echo "${ARCH}: ${CACHE_KEY}" + echo "cache_key_${ARCH}=${CACHE_KEY}" >> "${GITHUB_OUTPUT}" + return 0 + fi + case " ${LLVM_OPTIONAL_ARCHITECTURES} " in + *" ${ARCH} "*) + # Leave no directory behind, so that actions/cache does not store an empty entry under this key. + rm -rf "${LLVM_DEBS_DIR}" + echo "${ARCH}: no pre-fetched packages available (optional architecture)." + echo "cache_key_${ARCH}=" >> "${GITHUB_OUTPUT}" + ;; + *) + echo "Error: LLVM apt packages for ${ARCH} are missing." >&2 + exit 1 + ;; + esac + } + resolve amd64 "${CACHE_KEY_AMD64}" + resolve arm64 "${CACHE_KEY_ARM64}" + resolve s390x "${CACHE_KEY_S390X}" - name: Record artifact metadata id: metadata run: | @@ -61,10 +232,14 @@ jobs: id: archive env: ARCHIVE_NAME: ${{ steps.metadata.outputs.archive_name }} + VALGRIND_VERSION: ${{ steps.valgrind.outputs.version }} run: | ARCHIVE_PATH="${RUNNER_TEMP}/${ARCHIVE_NAME}" REPO_DIR="$(basename "${GITHUB_WORKSPACE}")" - tar -czf "${ARCHIVE_PATH}" -C "$(dirname "${GITHUB_WORKSPACE}")" "${REPO_DIR}" + tar -czf "${ARCHIVE_PATH}" \ + --exclude="${REPO_DIR}/valgrind-${VALGRIND_VERSION}.tar.bz2" \ + --exclude="${REPO_DIR}/llvm-debs" \ + -C "$(dirname "${GITHUB_WORKSPACE}")" "${REPO_DIR}" echo "archive-path=${ARCHIVE_PATH}" >> "${GITHUB_OUTPUT}" - name: Upload prepared workspace uses: actions/upload-artifact@v7 diff --git a/.gitignore b/.gitignore index 36c9cb3..bcedd22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /ext/*.dep /ext/*~ /valgrind-*.tar.bz2 +/llvm-debs *.DS_Store lcov.info diff --git a/Dockerfile b/Dockerfile index 6f254a0..39e242e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,19 +2,27 @@ ARG PLATFORM=${BUILDPLATFORM:-linux/amd64} ARG IMAGE=php ARG TAG=8.5-cli-trixie ARG SKIP_VALGRIND=0 +# Full LLVM release version (major.minor.patch), kept up to date by Renovate from the official LLVM GitHub releases. +# Only the major part selects the apt.llvm.org suite and the package names. The full version exists so that the +# CI cache of the LLVM apt packages is invalidated (and apt.llvm.org contacted again) exactly when a new LLVM +# release is published, and never otherwise. # renovate: datasource=github-releases depName=llvm/llvm-project -ARG LLVM_VERSION=23 +ARG LLVM_VERSION=23.1.0 +# LLVM apt packages installed into the base image ("@" is replaced with the LLVM major version). +ARG LLVM_PACKAGES="clang-@ libclang-rt-@-dev lld-@ libc++-@-dev libc++abi-@-dev llvm-@ llvm-@-dev llvm-@-runtime" FROM --platform=${PLATFORM} ${IMAGE}:${TAG} AS base ARG LLVM_VERSION +ARG LLVM_PACKAGES ENV USE_ZEND_ALLOC=0 ENV USE_TRACKED_ALLOC=1 ENV ZEND_DONT_UNLOAD_MODULES=1 ENV LC_ALL="C" -RUN docker-php-source extract \ +RUN --mount=type=bind,source=.,target=/build_context \ + docker-php-source extract \ && if test -f "/etc/debian_version"; then \ apt-get update && \ DEBIAN_FRONTEND="noninteractive" apt-get install -y "bison" "re2c" "zlib1g-dev" "libsqlite3-dev" "libxml2-dev" \ @@ -24,20 +32,32 @@ RUN docker-php-source extract \ "unzip" && \ LLVM_APT_CODENAME="$(. "/etc/os-release" && printf '%s' "${VERSION_CODENAME}")" && \ test -n "${LLVM_APT_CODENAME}" && \ + LLVM_MAJOR="${LLVM_VERSION%%.*}" && \ + LLVM_APT_KEYRING="/usr/share/keyrings/llvm-snapshot.gpg" && \ + LLVM_APT_SOURCE="deb [signed-by=${LLVM_APT_KEYRING}] https://apt.llvm.org/${LLVM_APT_CODENAME}/ llvm-toolchain-${LLVM_APT_CODENAME}-${LLVM_MAJOR} main" && \ + LLVM_DEBS_DIR="/build_context/llvm-debs/${LLVM_APT_CODENAME}-$(dpkg --print-architecture)" && \ mkdir -p "/usr/share/keyrings" && \ - curl -fsSL "https://apt.llvm.org/llvm-snapshot.gpg.key" | gpg --dearmor --yes -o "/usr/share/keyrings/llvm-snapshot.gpg" && \ - echo "deb [signed-by=/usr/share/keyrings/llvm-snapshot.gpg] https://apt.llvm.org/${LLVM_APT_CODENAME}/ llvm-toolchain-${LLVM_APT_CODENAME}-${LLVM_VERSION} main" > "/etc/apt/sources.list.d/llvm.list" && \ + if test -f "${LLVM_DEBS_DIR}/llvm-snapshot.gpg"; then \ + cp "${LLVM_DEBS_DIR}/llvm-snapshot.gpg" "${LLVM_APT_KEYRING}"; \ + else \ + curl -fsSL "https://apt.llvm.org/llvm-snapshot.gpg.key" | gpg --dearmor --yes -o "${LLVM_APT_KEYRING}"; \ + fi && \ + if test -f "${LLVM_DEBS_DIR}/Packages" && test -f "${LLVM_DEBS_DIR}/Release"; then \ + echo "[Pskel] Installing LLVM ${LLVM_MAJOR} (release ${LLVM_VERSION}) from pre-fetched apt packages in ${LLVM_DEBS_DIR}." >&2 && \ + echo "deb [trusted=yes] file:${LLVM_DEBS_DIR} ./" > "/etc/apt/sources.list.d/llvm.list"; \ + else \ + echo "[Pskel] Installing LLVM ${LLVM_MAJOR} (release ${LLVM_VERSION}) from apt.llvm.org." >&2 && \ + echo "${LLVM_APT_SOURCE}" > "/etc/apt/sources.list.d/llvm.list"; \ + fi && \ apt-get update && \ - apt-get install --no-install-recommends -y \ - "clang-${LLVM_VERSION}" \ - "libclang-rt-${LLVM_VERSION}-dev" "lld-${LLVM_VERSION}" \ - "libc++-${LLVM_VERSION}-dev" "libc++abi-${LLVM_VERSION}-dev" \ - "llvm-${LLVM_VERSION}" "llvm-${LLVM_VERSION}-dev" "llvm-${LLVM_VERSION}-runtime" && \ - update-alternatives --install "/usr/bin/clang" clang "/usr/bin/clang-${LLVM_VERSION}" 100 && \ - update-alternatives --install "/usr/bin/clang++" clang++ "/usr/bin/clang++-${LLVM_VERSION}" 100 && \ - update-alternatives --install "/usr/bin/ld.lld" ld.lld "/usr/bin/ld.lld-${LLVM_VERSION}" 100 && \ - update-alternatives --install "/usr/bin/llvm-symbolizer" llvm-symbolizer "/usr/bin/llvm-symbolizer-${LLVM_VERSION}" 100 && \ - update-alternatives --install "/usr/bin/llvm-config" llvm-config "/usr/bin/llvm-config-${LLVM_VERSION}" 100; \ + apt-get install --no-install-recommends -y $(printf '%s' "${LLVM_PACKAGES}" | sed "s/@/${LLVM_MAJOR}/g") && \ + dpkg-query -W -f='[Pskel] Installed ${Package} ${Version}\n' "clang-${LLVM_MAJOR}" >&2 && \ + echo "${LLVM_APT_SOURCE}" > "/etc/apt/sources.list.d/llvm.list" && \ + update-alternatives --install "/usr/bin/clang" clang "/usr/bin/clang-${LLVM_MAJOR}" 100 && \ + update-alternatives --install "/usr/bin/clang++" clang++ "/usr/bin/clang++-${LLVM_MAJOR}" 100 && \ + update-alternatives --install "/usr/bin/ld.lld" ld.lld "/usr/bin/ld.lld-${LLVM_MAJOR}" 100 && \ + update-alternatives --install "/usr/bin/llvm-symbolizer" llvm-symbolizer "/usr/bin/llvm-symbolizer-${LLVM_MAJOR}" 100 && \ + update-alternatives --install "/usr/bin/llvm-config" llvm-config "/usr/bin/llvm-config-${LLVM_MAJOR}" 100; \ else \ apk add --no-cache "bison" "zlib-dev" "sqlite-dev" "libxml2-dev" "linux-headers" \ "autoconf" "pkgconfig" "make" "gcc" "g++" "musl-dbg" \ @@ -148,6 +168,7 @@ FROM --platform=${PLATFORM} base AS devcontainer ARG LLVM_VERSION RUN if test -f "/etc/debian_version"; then \ + LLVM_MAJOR="${LLVM_VERSION%%.*}" && \ mkdir -p -m 755 "/etc/apt/keyrings" && \ curl -fsSL "https://cli.github.com/packages/githubcli-archive-keyring.gpg" -o "/etc/apt/keyrings/githubcli-archive-keyring.gpg" && \ chmod go+r "/etc/apt/keyrings/githubcli-archive-keyring.gpg" && \ @@ -155,14 +176,11 @@ RUN if test -f "/etc/debian_version"; then \ apt-get update && \ apt-get install -y --no-install-recommends \ "gh" "vim" "gdb" \ - "clang-tools-${LLVM_VERSION}" "clang-format-${LLVM_VERSION}" "clang-tidy-${LLVM_VERSION}" "lldb-${LLVM_VERSION}" && \ - update-alternatives --install "/usr/bin/clang-tidy" clang-tidy "/usr/bin/clang-tidy-${LLVM_VERSION}" 100 && \ - update-alternatives --install "/usr/bin/lldb" lldb "/usr/bin/lldb-${LLVM_VERSION}" 100 && \ - update-alternatives --install "/usr/bin/clang-format" clang-format "/usr/bin/clang-format-${LLVM_VERSION}" 100 && \ + "clang-tools-${LLVM_MAJOR}" "clang-format-${LLVM_MAJOR}" "clang-tidy-${LLVM_MAJOR}" "lldb-${LLVM_MAJOR}" && \ + update-alternatives --install "/usr/bin/clang-tidy" clang-tidy "/usr/bin/clang-tidy-${LLVM_MAJOR}" 100 && \ + update-alternatives --install "/usr/bin/lldb" lldb "/usr/bin/lldb-${LLVM_MAJOR}" 100 && \ + update-alternatives --install "/usr/bin/clang-format" clang-format "/usr/bin/clang-format-${LLVM_MAJOR}" 100 && \ rm -rf "/var/lib/apt/lists/"*; \ else \ apk add --no-cache "github-cli"; \ - fi && \ - curl -fsSL "https://claude.ai/install.sh" | bash - -ENV PATH="/root/.local/bin:${PATH}" + fi diff --git a/README.md b/README.md index a22fc16..8919653 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,9 @@ $ gdb --args -dextension=./modules/your_extension_name.so example.p ### Q: How is Valgrind installed? A: Valgrind is downloaded from [sourceware.org](https://sourceware.org/pub/valgrind/) and built from source when the container image is built. The version is pinned by the `VALGRIND_VERSION` build argument in the `Dockerfile`. If you place a pre-fetched `valgrind-.tar.bz2` in the repository root (excluded from Git via `.gitignore`), it is used instead of downloading. Passing the `SKIP_VALGRIND=1` build argument skips building Valgrind entirely. Valgrind is only supported on Debian-based images: building a non-Debian (e.g. Alpine) image fails unless `SKIP_VALGRIND=1` is passed explicitly. +### Q: How is LLVM installed? +A: On Debian-based images, the LLVM toolchain is installed from [apt.llvm.org](https://apt.llvm.org/) when the container image is built. The `LLVM_VERSION` build argument in the `Dockerfile` holds the full upstream release version (e.g. `23.1.0`) and is kept up to date by Renovate from the official [LLVM GitHub releases](https://github.com/llvm/llvm-project/releases); only its major part selects the apt.llvm.org suite and the package names, and the package list is defined by the `LLVM_PACKAGES` build argument. If a directory `llvm-debs/-/` (e.g. `llvm-debs/trixie-amd64/`) containing the `.deb` files together with `Packages` and `Release` indexes exists in the repository root (excluded from Git via `.gitignore`), it is used as a local apt repository instead of apt.llvm.org. In CI, the Prepare workflow pre-fetches these packages once per architecture (amd64, arm64 and s390x) and shares them with the Linux build, coverage and emulated jobs through the GitHub Actions cache. The cache key contains `LLVM_VERSION`, so apt.llvm.org is only contacted by the Prepare job when Renovate bumps `LLVM_VERSION` (or `LLVM_PACKAGES` changes); otherwise no CI job accesses apt.llvm.org at all. The only exception is the s390x set, which is fetched on a best-effort basis: if apt.llvm.org cannot provide it, only the emulated s390x jobs fall back to installing directly from apt.llvm.org. Note that apt.llvm.org serves snapshot builds of the LLVM release branch rather than the exact tagged release, so the cached packages are whichever build was published at the time of the fetch (the Prepare job warns when that build is older than `LLVM_VERSION`). To re-fetch them without a version bump, delete the `llvm-debs-*` entries from the Actions cache (e.g. with `gh cache delete`). + ### Q: Can I use editors other than Visual Studio Code? A: While not recommended, you can use any editor that supports [Development Containers](https://containers.dev). diff --git a/README_ja.md b/README_ja.md index 4bcce70..ef8b23a 100644 --- a/README_ja.md +++ b/README_ja.md @@ -139,6 +139,9 @@ $ gdb --args -dextension=./modules/your_extension_name.so example.p ### Q: Valgrind はどのようにインストールされますか? A: コンテナイメージのビルド時に [sourceware.org](https://sourceware.org/pub/valgrind/) からソースをダウンロードしてビルドされます。バージョンは `Dockerfile` のビルド引数 `VALGRIND_VERSION` で固定されています。事前に取得した `valgrind-.tar.bz2` をリポジトリのトップレベルに配置すると (`.gitignore` により Git 管理外)、ダウンロードの代わりにそれが使用されます。また、ビルド引数 `SKIP_VALGRIND=1` を指定することで Valgrind のビルド自体をスキップできます。Valgrind は Debian ベースのイメージでのみサポートされるため、非 Debian (Alpine など) のイメージをビルドする場合は `SKIP_VALGRIND=1` を明示的に指定しない限りビルドは失敗します。 +### Q: LLVM はどのようにインストールされますか? +A: Debian ベースのイメージでは、コンテナイメージのビルド時に [apt.llvm.org](https://apt.llvm.org/) から LLVM ツールチェインをインストールします。`Dockerfile` のビルド引数 `LLVM_VERSION` には上流のリリースバージョン全体 (例: `23.1.0`) が入っており、Renovate が公式の [LLVM GitHub リリース](https://github.com/llvm/llvm-project/releases) を追跡して更新します。apt.llvm.org のスイートとパッケージ名の選択にはそのメジャー部分のみが使われ、パッケージ一覧はビルド引数 `LLVM_PACKAGES` で定義されています。リポジトリのトップレベルに `.deb` ファイルと `Packages` / `Release` インデックスを含む `llvm-debs/-/` ディレクトリ (例: `llvm-debs/trixie-amd64/`) を配置すると (`.gitignore` により Git 管理外)、apt.llvm.org の代わりにそれがローカル apt リポジトリとして使用されます。CI では Prepare ワークフローがこのパッケージ群をアーキテクチャ (amd64, arm64, s390x) ごとに 1 回だけ事前取得し、GitHub Actions cache 経由で Linux のビルド / カバレッジ / エミュレーション job に共有します。cache キーには `LLVM_VERSION` が含まれるため、apt.llvm.org へアクセスするのは Renovate が `LLVM_VERSION` を更新したとき (または `LLVM_PACKAGES` を変更したとき) の Prepare job だけで、それ以外の CI job は apt.llvm.org へ一切アクセスしません。唯一の例外は s390x で、こちらはベストエフォートで取得するため、apt.llvm.org から取得できなかった場合はエミュレーション (s390x) job だけが従来通り apt.llvm.org から直接インストールします。なお apt.llvm.org はタグ付けされたリリースそのものではなく LLVM リリースブランチのスナップショットビルドを配布しているため、cache に入るのは取得時点で公開されていたビルドになります (`LLVM_VERSION` より古いビルドだった場合は Prepare job が warning を出します)。バージョン更新なしに再取得したい場合は、Actions cache から `llvm-debs-*` エントリを削除してください (例: `gh cache delete`)。 + ### Q: Visual Studio Code 以外のエディタは使用できますか? A: 推奨はしませんが、 [Development Containers](https://containers.dev) 対応のエディタであれば使用可能です。