diff --git a/.github/packaging/Dockerfile.deb b/.github/packaging/Dockerfile.deb new file mode 100644 index 000000000000..abd80b1b7bc2 --- /dev/null +++ b/.github/packaging/Dockerfile.deb @@ -0,0 +1,59 @@ +# Build container for DEB packaging of avalanchego and subnet-evm. +# +# Based on Ubuntu 22.04 (jammy) so the produced binary's glibc floor +# matches the oldest supported target release. Source tree is bind-mounted +# at runtime, not COPY'd. +# +# Usage (via build-builder-image.sh with DOCKERFILE=Dockerfile.deb): +# DOCKERFILE=Dockerfile.deb .github/packaging/scripts/build-builder-image.sh +# docker run --rm -v .:/build -v ./build/deb:/output avalanchego-deb-builder ... + +FROM ubuntu:22.04 + +ARG GO_VERSION=INVALID +ARG GO_CHECKSUM=INVALID +ARG TARGETARCH + +ENV DEBIAN_FRONTEND=noninteractive + +# Install build dependencies +# - gcc: required for cgo (CGO_ENABLED=1 in scripts/constants.sh) +# - gettext: envsubst for nfpm config template expansion +# - gnupg: GPG key import for nfpm-native DEB signing +# - git: version detection in build scripts +# - curl: downloading Go and nfpm +RUN apt-get update && apt-get install -y \ + gcc \ + gettext \ + gnupg \ + git \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install Go (with SHA256 verification) +RUN curl -fsSL -o /tmp/go.tar.gz \ + "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" \ + && echo "${GO_CHECKSUM} /tmp/go.tar.gz" | sha256sum -c - \ + && tar -C /usr/local -xzf /tmp/go.tar.gz \ + && rm /tmp/go.tar.gz +ENV PATH="/usr/local/go/bin:${PATH}" + +# Install nfpm (with SHA256 verification via checksums.txt) +ARG NFPM_VERSION=2.41.1 +# nfpm releases use x86_64 and arm64 (not aarch64) +RUN case "${TARGETARCH}" in \ + amd64) NFPM_ARCH="x86_64" ;; \ + arm64) NFPM_ARCH="arm64" ;; \ + *) echo "Unsupported arch: ${TARGETARCH}" && exit 1 ;; \ + esac && \ + NFPM_TARBALL="nfpm_${NFPM_VERSION}_Linux_${NFPM_ARCH}.tar.gz" && \ + curl -fsSL -o /tmp/nfpm.tar.gz \ + "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/${NFPM_TARBALL}" && \ + curl -fsSL -o /tmp/checksums.txt \ + "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/checksums.txt" && \ + EXPECTED=$(grep " ${NFPM_TARBALL}$" /tmp/checksums.txt | awk '{print $1}') && \ + echo "${EXPECTED} /tmp/nfpm.tar.gz" | sha256sum -c - && \ + tar -C /usr/local/bin -xzf /tmp/nfpm.tar.gz nfpm && \ + rm /tmp/nfpm.tar.gz /tmp/checksums.txt + +WORKDIR /build diff --git a/.github/packaging/Taskfile.yml b/.github/packaging/Taskfile.yml index 43c332221039..7b1752c4d4df 100644 --- a/.github/packaging/Taskfile.yml +++ b/.github/packaging/Taskfile.yml @@ -1,6 +1,8 @@ -# RPM packaging tasks for avalanchego and subnet-evm. +# Packaging tasks for avalanchego and subnet-evm (RPM and DEB). # -# Builds RPMs inside a Rocky Linux 9 container (glibc 2.34) with GPG signing. +# RPMs are built inside a Rocky Linux 9 container (glibc 2.34). +# DEBs are built inside an Ubuntu 22.04 container (glibc 2.35). +# Both formats are signed inline by nfpm. # PACKAGING_TAG defaults to v0.0.0 for local testing; set for release builds. version: '3' @@ -14,7 +16,7 @@ vars: PACKAGING_GO_VERSION: sh: go list -m -f '{{ "{{.GoVersion}}" }}' | head -1 # Map uname -m to RPM arch names (arm64 -> aarch64). - PACKAGING_HOST_ARCH: + PACKAGING_RPM_HOST_ARCH: sh: | arch=$(uname -m) case "${arch}" in @@ -28,8 +30,19 @@ vars: # Default tag for local testing; overridden by CI for release builds. PACKAGING_TAG: sh: echo "${PACKAGING_TAG:-v0.0.0}" - PACKAGING_DOCKER_IMAGE: avalanchego-rpm-builder - PACKAGING_OUTPUT_DIR: '{{.REPO_ROOT}}/build/rpm' + # Map uname -m to DEB arch names (x86_64 -> amd64). + PACKAGING_DEB_HOST_ARCH: + sh: | + arch=$(uname -m) + case "${arch}" in + x86_64) echo "amd64" ;; + aarch64|arm64) echo "arm64" ;; + *) echo "${arch}" ;; + esac + PACKAGING_RPM_DOCKER_IMAGE: avalanchego-rpm-builder + PACKAGING_DEB_DOCKER_IMAGE: avalanchego-deb-builder + PACKAGING_RPM_OUTPUT_DIR: '{{.REPO_ROOT}}/build/rpm' + PACKAGING_DEB_OUTPUT_DIR: '{{.REPO_ROOT}}/build/deb' tasks: default: @@ -43,70 +56,76 @@ tasks: - task: build-avalanchego-rpm - task: build-subnet-evm-rpm - build-builder-docker-image: + build-rpm-builder-docker-image: desc: Builds the RPM builder Docker image internal: true env: GO_VERSION: '{{.PACKAGING_GO_VERSION}}' - DOCKER_IMAGE: '{{.PACKAGING_DOCKER_IMAGE}}' + DOCKER_IMAGE: '{{.PACKAGING_RPM_DOCKER_IMAGE}}' CONTEXT_DIR: '{{.REPO_ROOT}}/.github/packaging' DOCKERFILE: Dockerfile.rpm cmds: - cmd: '{{.REPO_ROOT}}/.github/packaging/scripts/build-builder-image.sh' - build-avalanchego-rpm: - desc: Builds RPM for avalanchego + build-package: + desc: Builds a package in the supplied builder Docker image + internal: true vars: - PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .PACKAGING_HOST_ARCH}}' - RPM_TAG: '{{.PACKAGING_TAG}}' + PASSPHRASE_ENV: 'NFPM_{{.NFPM_PACKAGER | upper}}_PASSPHRASE' env: GPG_KEY_PASSPHRASE: '{{.GPG_KEY_PASSPHRASE}}' - deps: [build-builder-docker-image] + VERSION: '{{trimPrefix "v" .TAG}}' + TAG: '{{.TAG}}' cmds: - - cmd: mkdir -p {{.PACKAGING_OUTPUT_DIR}} + - cmd: mkdir -p {{.OUTPUT_DIR}} - cmd: >- docker run --rm -v {{.REPO_ROOT}}:/build - -v {{.PACKAGING_OUTPUT_DIR}}:/output + -v {{.OUTPUT_DIR}}:/output {{if .GPG_KEY_FILE}}-v {{.GPG_KEY_FILE}}:{{.GPG_KEY_FILE}}:ro{{end}} - -e PKG_FORMAT=RPM - -e PACKAGE=avalanchego - -e VERSION={{trimPrefix "v" .RPM_TAG}} - -e TAG={{.RPM_TAG}} + -e NFPM_PACKAGER={{.NFPM_PACKAGER}} + -e PACKAGE={{.PACKAGE}} + -e VERSION + -e TAG -e PACKAGE_ARCH={{.PACKAGE_ARCH}} -e OUTPUT_DIR=/output -e AVALANCHEGO_COMMIT={{.PACKAGING_GIT_COMMIT}} {{if .GPG_KEY_FILE}}-e GPG_KEY_FILE={{.GPG_KEY_FILE}}{{end}} {{if .GPG_KEY_PASSPHRASE}}-e GPG_KEY_PASSPHRASE{{end}} - {{.PACKAGING_DOCKER_IMAGE}} + {{.DOCKER_IMAGE}} .github/packaging/scripts/build-package.sh + build-avalanchego-rpm: + desc: Builds RPM for avalanchego + vars: + PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .RPM_ARCH | default .PACKAGING_RPM_HOST_ARCH}}' + deps: [build-rpm-builder-docker-image] + cmds: + - task: build-package + vars: + PACKAGE: avalanchego + NFPM_PACKAGER: rpm + TAG: '{{.PACKAGING_TAG}}' + PACKAGE_ARCH: '{{.PACKAGE_ARCH}}' + OUTPUT_DIR: '{{.PACKAGING_RPM_OUTPUT_DIR}}' + DOCKER_IMAGE: '{{.PACKAGING_RPM_DOCKER_IMAGE}}' + GPG_KEY_FILE: '{{.GPG_KEY_FILE | default ""}}' + build-subnet-evm-rpm: desc: Builds RPM for subnet-evm vars: - PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .PACKAGING_HOST_ARCH}}' - RPM_TAG: '{{.PACKAGING_TAG}}' - env: - GPG_KEY_PASSPHRASE: '{{.GPG_KEY_PASSPHRASE}}' - deps: [build-builder-docker-image] + PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .RPM_ARCH | default .PACKAGING_RPM_HOST_ARCH}}' + deps: [build-rpm-builder-docker-image] cmds: - - cmd: mkdir -p {{.PACKAGING_OUTPUT_DIR}} - - cmd: >- - docker run --rm - -v {{.REPO_ROOT}}:/build - -v {{.PACKAGING_OUTPUT_DIR}}:/output - {{if .GPG_KEY_FILE}}-v {{.GPG_KEY_FILE}}:{{.GPG_KEY_FILE}}:ro{{end}} - -e PKG_FORMAT=RPM - -e PACKAGE=subnet-evm - -e VERSION={{trimPrefix "v" .RPM_TAG}} - -e TAG={{.RPM_TAG}} - -e PACKAGE_ARCH={{.PACKAGE_ARCH}} - -e OUTPUT_DIR=/output - -e AVALANCHEGO_COMMIT={{.PACKAGING_GIT_COMMIT}} - {{if .GPG_KEY_FILE}}-e GPG_KEY_FILE={{.GPG_KEY_FILE}}{{end}} - {{if .GPG_KEY_PASSPHRASE}}-e GPG_KEY_PASSPHRASE{{end}} - {{.PACKAGING_DOCKER_IMAGE}} - .github/packaging/scripts/build-package.sh + - task: build-package + vars: + PACKAGE: subnet-evm + NFPM_PACKAGER: rpm + TAG: '{{.PACKAGING_TAG}}' + PACKAGE_ARCH: '{{.PACKAGE_ARCH}}' + OUTPUT_DIR: '{{.PACKAGING_RPM_OUTPUT_DIR}}' + DOCKER_IMAGE: '{{.PACKAGING_RPM_DOCKER_IMAGE}}' + GPG_KEY_FILE: '{{.GPG_KEY_FILE | default ""}}' test-build-rpms: desc: Builds and validates RPMs end-to-end @@ -119,6 +138,71 @@ tasks: env: TAG: '{{.PACKAGING_TAG}}' GIT_COMMIT: '{{.PACKAGING_GIT_COMMIT}}' - PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .PACKAGING_HOST_ARCH}}' + PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .PACKAGING_RPM_HOST_ARCH}}' cmds: - cmd: '{{.REPO_ROOT}}/.github/packaging/scripts/validate-rpm.sh' + + # ── DEB packaging tasks ────────────────────────────────────────── + + build-debs: + desc: Builds DEBs for both avalanchego and subnet-evm + cmds: + - task: build-avalanchego-deb + - task: build-subnet-evm-deb + + build-deb-builder-docker-image: + desc: Builds the DEB builder Docker image + internal: true + env: + GO_VERSION: '{{.PACKAGING_GO_VERSION}}' + DOCKER_IMAGE: '{{.PACKAGING_DEB_DOCKER_IMAGE}}' + CONTEXT_DIR: '{{.REPO_ROOT}}/.github/packaging' + DOCKERFILE: Dockerfile.deb + cmds: + - cmd: '{{.REPO_ROOT}}/.github/packaging/scripts/build-builder-image.sh' + + build-avalanchego-deb: + desc: Builds DEB for avalanchego + vars: + PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .DEB_ARCH | default .PACKAGING_DEB_HOST_ARCH}}' + deps: [build-deb-builder-docker-image] + cmds: + - task: build-package + vars: + PACKAGE: avalanchego + NFPM_PACKAGER: deb + TAG: '{{.PACKAGING_TAG}}' + PACKAGE_ARCH: '{{.PACKAGE_ARCH}}' + OUTPUT_DIR: '{{.PACKAGING_DEB_OUTPUT_DIR}}' + DOCKER_IMAGE: '{{.PACKAGING_DEB_DOCKER_IMAGE}}' + GPG_KEY_FILE: '{{.GPG_KEY_FILE | default ""}}' + + build-subnet-evm-deb: + desc: Builds DEB for subnet-evm + vars: + PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .DEB_ARCH | default .PACKAGING_DEB_HOST_ARCH}}' + deps: [build-deb-builder-docker-image] + cmds: + - task: build-package + vars: + PACKAGE: subnet-evm + NFPM_PACKAGER: deb + TAG: '{{.PACKAGING_TAG}}' + PACKAGE_ARCH: '{{.PACKAGE_ARCH}}' + OUTPUT_DIR: '{{.PACKAGING_DEB_OUTPUT_DIR}}' + DOCKER_IMAGE: '{{.PACKAGING_DEB_DOCKER_IMAGE}}' + GPG_KEY_FILE: '{{.GPG_KEY_FILE | default ""}}' + + test-build-debs: + desc: Builds and validates DEBs end-to-end + cmds: + - task: build-debs + - task: validate-debs + + validate-debs: + desc: Validates built DEBs by installing and smoke testing in fresh containers + env: + TAG: '{{.PACKAGING_TAG}}' + GIT_COMMIT: '{{.PACKAGING_GIT_COMMIT}}' + cmds: + - cmd: '{{.REPO_ROOT}}/.github/packaging/scripts/validate-deb.sh' diff --git a/.github/packaging/nfpm/avalanchego-deb.yml b/.github/packaging/nfpm/avalanchego-deb.yml new file mode 100644 index 000000000000..0a638890df64 --- /dev/null +++ b/.github/packaging/nfpm/avalanchego-deb.yml @@ -0,0 +1,19 @@ +name: avalanchego +arch: "${PACKAGE_ARCH}" +version: "${VERSION}" +maintainer: "Ava Labs " +description: "AvalancheGo node — the official Avalanche protocol implementation" +homepage: "https://github.com/ava-labs/avalanchego" +license: "BSD-3-Clause" +depends: + - "libc6 (>= 2.34)" +contents: + - src: "${BINARY_PATH}" + dst: /usr/local/bin/avalanchego + file_info: + mode: 0755 +changelog: "${NFPM_CHANGELOG}" +deb: + compression: gzip + signature: + key_file: "${NFPM_SIGNING_KEY}" diff --git a/.github/packaging/nfpm/subnet-evm-deb.yml b/.github/packaging/nfpm/subnet-evm-deb.yml new file mode 100644 index 000000000000..085e1354ad5f --- /dev/null +++ b/.github/packaging/nfpm/subnet-evm-deb.yml @@ -0,0 +1,20 @@ +name: subnet-evm +arch: "${PACKAGE_ARCH}" +version: "${VERSION}" +maintainer: "Ava Labs " +description: "Subnet-EVM plugin for AvalancheGo" +homepage: "https://github.com/ava-labs/avalanchego" +license: "BSD-3-Clause" +depends: + - "libc6 (>= 2.34)" +contents: + - src: "${BINARY_PATH}" + # SUBNET_EVM_VM_ID is sourced from graft/subnet-evm/scripts/default-vm-data.sh + dst: /usr/local/lib/avalanchego/plugins/${SUBNET_EVM_VM_ID} + file_info: + mode: 0755 +changelog: "${NFPM_CHANGELOG}" +deb: + compression: gzip + signature: + key_file: "${NFPM_SIGNING_KEY}" diff --git a/.github/packaging/scripts/build-package.sh b/.github/packaging/scripts/build-package.sh index 37c0f6123b01..103dacf91f51 100755 --- a/.github/packaging/scripts/build-package.sh +++ b/.github/packaging/scripts/build-package.sh @@ -1,20 +1,40 @@ #!/usr/bin/env bash -# Build and sign a Linux package inside the container. +# Build and sign a Linux package with nfpm inside the container. +# +# Required env vars: +# NFPM_PACKAGER - nfpm packager: "rpm" or "deb" +# PACKAGE - "avalanchego" or "subnet-evm" +# VERSION - Semantic version without "v" prefix (e.g., "1.14.1") +# TAG - Git tag (e.g., "v1.14.1") +# PACKAGE_ARCH - Architecture (x86_64/aarch64 for RPM, amd64/arm64 for DEB) +# OUTPUT_DIR - Directory for the output package (bind-mounted from host) +# +# Optional env vars: +# GPG_KEY_FILE - Path to GPG private key +# GPG_KEY_PASSPHRASE - Passphrase for the GPG key (re-exported internally +# to nfpm's NFPM__PASSPHRASE) +# AVALANCHEGO_COMMIT - Git commit hash (auto-detected if not set) set -euo pipefail -: "${PACKAGE:?PACKAGE must be set (avalanchego or subnet-evm)}" -: "${VERSION:?VERSION must be set (semver without v prefix, e.g. 1.14.1)}" -: "${TAG:?TAG must be set (git tag, e.g. v1.14.1)}" -: "${PACKAGE_ARCH:?PACKAGE_ARCH must be set (x86_64 or aarch64)}" -: "${OUTPUT_DIR:?OUTPUT_DIR must be set (bind-mounted output dir)}" +: "${VERSION:?VERSION must be set}" +: "${TAG:?TAG must be set}" +: "${PACKAGE_ARCH:?PACKAGE_ARCH must be set}" +: "${OUTPUT_DIR:?OUTPUT_DIR must be set}" +: "${NFPM_PACKAGER:?NFPM_PACKAGER must be set (rpm or deb)}" -: "${PKG_FORMAT:?PKG_FORMAT must be set (RPM or DEB)}" -pkg_format_lower="${PKG_FORMAT,,}" +NFPM_PACKAGER="${NFPM_PACKAGER,,}" +pkg_format_upper="${NFPM_PACKAGER^^}" REPO_ROOT="/build" PACKAGING_DIR="${REPO_ROOT}/.github/packaging" +NFPM_CONFIG_TEMPLATE="${PACKAGING_DIR}/nfpm/${PACKAGE}-${NFPM_PACKAGER}.yml" +NFPM_CONFIG_RESOLVED="${REPO_ROOT}/build/${PACKAGE}-${NFPM_PACKAGER}-resolved.yml" +if [[ ! -f "${NFPM_CONFIG_TEMPLATE}" ]]; then + echo "Unknown nfpm packager or package: ${NFPM_PACKAGER} / ${PACKAGE}" >&2 + exit 1 +fi # shellcheck disable=SC1091 source "${PACKAGING_DIR}/scripts/lib-build-common.sh" @@ -22,8 +42,11 @@ source "${PACKAGING_DIR}/scripts/lib-build-common.sh" # Well-known paths referenced by nfpm configs export NFPM_CHANGELOG="${REPO_ROOT}/build/nfpm-changelog.yml" export NFPM_SIGNING_KEY="${REPO_ROOT}/build/gpg/signing-key.asc" +export NFPM_RPM_PASSPHRASE="${NFPM_RPM_PASSPHRASE:-}" +export NFPM_DEB_PASSPHRASE="${NFPM_DEB_PASSPHRASE:-}" +GPG_KEY_FILE="${GPG_KEY_FILE:-}" -echo "=== Building ${PACKAGE} ${PKG_FORMAT} for ${PACKAGE_ARCH} (tag: ${TAG}) ===" +echo "=== Building ${PACKAGE} ${pkg_format_upper} for ${PACKAGE_ARCH} (tag: ${TAG}) ===" init_build_env build_binary "${PACKAGE}" @@ -31,13 +54,12 @@ generate_changelog "${VERSION}" # ── GPG signing ─────────────────────────────────────────────────── -GPG_KEY_FILE="${GPG_KEY_FILE:-}" -GPG_PUBLIC_KEY="${OUTPUT_DIR}/GPG-KEY-avalanchego" +GPG_PUBLIC_KEY="${OUTPUT_DIR}/${pkg_format_upper}-GPG-KEY-avalanchego" # nfpm reads the signing passphrase from a packager-specific env var # (NFPM_RPM_PASSPHRASE, NFPM_DEB_PASSPHRASE, ...); mirror our format- # agnostic GPG_KEY_PASSPHRASE into the name nfpm expects. -nfpm_passphrase_var="NFPM_${PKG_FORMAT}_PASSPHRASE" +nfpm_passphrase_var="NFPM_${pkg_format_upper}_PASSPHRASE" export "${nfpm_passphrase_var}=${GPG_KEY_PASSPHRASE:-}" # Ephemeral keys use a known throwaway passphrase so local and CI builds @@ -46,19 +68,19 @@ if [[ -z "${GPG_KEY_FILE}" ]]; then use_ephemeral_gpg_passphrase "${nfpm_passphrase_var}" fi -setup_gpg "${GPG_KEY_FILE}" "${GPG_PUBLIC_KEY}" "${PKG_FORMAT}" +setup_gpg "${GPG_KEY_FILE}" "${GPG_PUBLIC_KEY}" "${pkg_format_upper}" # ── Package with nfpm ───────────────────────────────────────────── export VERSION PACKAGE_ARCH BINARY_PATH -PKG_FILENAME="${PACKAGE}-${TAG}-${PACKAGE_ARCH}.${pkg_format_lower}" +PKG_FILENAME="${PACKAGE}-${TAG}-${PACKAGE_ARCH}.${NFPM_PACKAGER}" PKG_PATH="${OUTPUT_DIR}/${PKG_FILENAME}" run_nfpm_package \ - "${PACKAGING_DIR}/nfpm/${PACKAGE}-${pkg_format_lower}.yml" \ - "${REPO_ROOT}/build/${PACKAGE}-${pkg_format_lower}-resolved.yml" \ - "${pkg_format_lower}" \ + "${NFPM_CONFIG_TEMPLATE}" \ + "${NFPM_CONFIG_RESOLVED}" \ + "${NFPM_PACKAGER}" \ "${PKG_PATH}" -echo "${PKG_FORMAT} built: ${PKG_PATH}" +echo "${pkg_format_upper} built: ${PKG_PATH}" diff --git a/.github/packaging/scripts/lib-build-common.sh b/.github/packaging/scripts/lib-build-common.sh index 0242e7925afd..af75772bbcda 100644 --- a/.github/packaging/scripts/lib-build-common.sh +++ b/.github/packaging/scripts/lib-build-common.sh @@ -71,11 +71,15 @@ build_binary() { # Resolve the subnet-evm VM ID from the canonical constants file. # Sets SUBNET_EVM_VM_ID (global) as a side effect. +# +# Older subnet-evm constants.sh revisions (e.g., v1.14.1) print +# "Using branch: ..." to stdout when sourced; discard that while +# capturing so SUBNET_EVM_VM_ID is exactly DEFAULT_VM_ID. resolve_subnet_evm_vm_id() { SUBNET_EVM_VM_ID="$( { # shellcheck disable=SC1091 - source "${REPO_ROOT}/graft/subnet-evm/scripts/constants.sh" + source "${REPO_ROOT}/graft/subnet-evm/scripts/constants.sh" >/dev/null # shellcheck disable=SC2154 : "${DEFAULT_VM_ID:?DEFAULT_VM_ID must be set by constants.sh}" } >&2 diff --git a/.github/packaging/scripts/lib-validate-common.sh b/.github/packaging/scripts/lib-validate-common.sh new file mode 100755 index 000000000000..43e529ef3c19 --- /dev/null +++ b/.github/packaging/scripts/lib-validate-common.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Shared functions for package validation scripts. +# Sourced by validate-deb.sh. + +# Detect host architecture for the given package format. +# Sets PACKAGE_ARCH (global) if not already set by the caller. +# +# Args: format ("RPM" or "DEB") +detect_host_arch() { + local format="${1:?format required}" + if [[ -n "${PACKAGE_ARCH:-}" ]]; then + return # already set by caller + fi + local arch + arch=$(uname -m) + case "${format}" in + RPM) + case "${arch}" in + x86_64) PACKAGE_ARCH="x86_64" ;; + arm64) PACKAGE_ARCH="aarch64" ;; + *) PACKAGE_ARCH="${arch}" ;; + esac + ;; + DEB) + case "${arch}" in + x86_64) PACKAGE_ARCH="amd64" ;; + aarch64|arm64) PACKAGE_ARCH="arm64" ;; + *) PACKAGE_ARCH="${arch}" ;; + esac + ;; + esac +} diff --git a/.github/packaging/scripts/validate-deb.sh b/.github/packaging/scripts/validate-deb.sh new file mode 100755 index 000000000000..51388b83179e --- /dev/null +++ b/.github/packaging/scripts/validate-deb.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Post-build validation of DEB packages. +# +# Validates locally-built DEBs by running fresh Ubuntu containers for both +# jammy (22.04) and noble (24.04): verify the nFPM-native _gpgorigin +# signature, install the package, and run the smoke test. +# +# Required env vars: +# TAG - Git tag (e.g., "v1.14.1") +# GIT_COMMIT - Full git commit hash used to build the binaries +# +# Optional env vars: +# PACKAGE_ARCH - DEB architecture ("amd64" or "arm64"), defaults to host + +set -euo pipefail + +: "${TAG:?TAG must be set}" +: "${GIT_COMMIT:?GIT_COMMIT must be set}" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +DEB_DIR="${REPO_ROOT}/build/deb" +SCRIPTS_DIR="${REPO_ROOT}/.github/packaging/scripts" + +# shellcheck disable=SC1091 +source "${SCRIPTS_DIR}/lib-build-common.sh" +# shellcheck disable=SC1091 +source "${SCRIPTS_DIR}/lib-validate-common.sh" + +detect_host_arch DEB +resolve_subnet_evm_vm_id + +assert_files_exist "${DEB_DIR}" \ + "avalanchego-${TAG}-${PACKAGE_ARCH}.deb" \ + "subnet-evm-${TAG}-${PACKAGE_ARCH}.deb" + +# ── Verify + install + smoke test (both jammy and noble) ───────── +# nfpm stores a detached GPG signature in the `_gpgorigin` ar member, +# covering debian-binary + control.tar.* + data.tar.* concatenated in +# ar-member order. Verifying with `gpg --verify` keeps the same flow +# on every supported Ubuntu release. + +for UBUNTU_IMAGE in ubuntu:22.04 ubuntu:24.04; do + echo "=== Verify, install and smoke test in fresh ${UBUNTU_IMAGE} container ===" + docker run --rm \ + -v "${DEB_DIR}:/debs:ro" \ + -v "${SCRIPTS_DIR}/smoke-test.sh:/smoke-test.sh:ro" \ + -e "TAG=${TAG}" \ + -e "PACKAGE_ARCH=${PACKAGE_ARCH}" \ + -e "GIT_COMMIT=${GIT_COMMIT}" \ + -e "SUBNET_EVM_VM_ID=${SUBNET_EVM_VM_ID}" \ + "${UBUNTU_IMAGE}" \ + bash -euxc ' + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y binutils gnupg + + verify_deb_signature() { + local deb="$1" + local workdir + workdir=$(mktemp -d) + ( cd "${workdir}" && ar x "${deb}" ) + cat "${workdir}/debian-binary" \ + "${workdir}"/control.tar.* \ + "${workdir}"/data.tar.* > "${workdir}/combined" + gpg --verify "${workdir}/_gpgorigin" "${workdir}/combined" + rm -rf "${workdir}" + } + + if [[ -f /debs/DEB-GPG-KEY-avalanchego ]]; then + gpg --batch --import /debs/DEB-GPG-KEY-avalanchego + verify_deb_signature "/debs/avalanchego-${TAG}-${PACKAGE_ARCH}.deb" + verify_deb_signature "/debs/subnet-evm-${TAG}-${PACKAGE_ARCH}.deb" + else + echo "Skipping GPG verification (unsigned build)" + fi + + dpkg -i "/debs/avalanchego-${TAG}-${PACKAGE_ARCH}.deb" + dpkg -i "/debs/subnet-evm-${TAG}-${PACKAGE_ARCH}.deb" + + bash /smoke-test.sh \ + /usr/local/bin/avalanchego \ + "/usr/local/lib/avalanchego/plugins/${SUBNET_EVM_VM_ID}" \ + "${GIT_COMMIT}" + ' +done + +echo "=== DEB validation complete (jammy + noble) ===" diff --git a/.github/packaging/scripts/validate-rpm.sh b/.github/packaging/scripts/validate-rpm.sh index 8b5a1dd30384..d00e3a01a6f6 100755 --- a/.github/packaging/scripts/validate-rpm.sh +++ b/.github/packaging/scripts/validate-rpm.sh @@ -31,11 +31,11 @@ docker run --rm \ rockylinux:9 \ bash -euxc ' # Import GPG key and verify signatures (always produced by the build). - if [[ ! -f /rpms/GPG-KEY-avalanchego ]]; then - echo "ERROR: GPG-KEY-avalanchego not found; build did not export a key" >&2 + if [[ ! -f /rpms/RPM-GPG-KEY-avalanchego ]]; then + echo "ERROR: RPM-GPG-KEY-avalanchego not found; build did not export a key" >&2 exit 1 fi - rpm --import /rpms/GPG-KEY-avalanchego + rpm --import /rpms/RPM-GPG-KEY-avalanchego rpm -K "/rpms/avalanchego-'"${TAG}"'-'"${PACKAGE_ARCH}"'.rpm" rpm -K "/rpms/subnet-evm-'"${TAG}"'-'"${PACKAGE_ARCH}"'.rpm" diff --git a/.github/packaging/scripts/workflow-setup-packaging.sh b/.github/packaging/scripts/workflow-setup-packaging.sh index c7becef4efa3..a5a3e0c2b9a7 100755 --- a/.github/packaging/scripts/workflow-setup-packaging.sh +++ b/.github/packaging/scripts/workflow-setup-packaging.sh @@ -41,6 +41,17 @@ echo "Resolved tag: ${TAG}" >&2 GPG_PRIVATE_KEY="${GPG_PRIVATE_KEY:-}" RELEASE="${RELEASE:-}" +# Release events (tag push, workflow_dispatch) require a signing key. An +# empty key would silently fall back to ephemeral generation in +# build-package.sh and publish unsigned-by-real-key packages. +if [[ -z "${GPG_PRIVATE_KEY}" ]]; then + if [[ -n "${TAG_INPUT}" ]] || [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then + echo "Error: GPG signing key required for release builds." >&2 + echo "Set the RPM_GPG_PRIVATE_KEY repository secret." >&2 + exit 1 + fi +fi + if [[ -n "${GPG_PRIVATE_KEY}" ]]; then GPG_KEY_FILE="$(mktemp)" chmod 600 "${GPG_KEY_FILE}" diff --git a/.github/workflows/build-deb-pkg.sh b/.github/workflows/build-deb-pkg.sh deleted file mode 100755 index e01fe6017dcd..000000000000 --- a/.github/workflows/build-deb-pkg.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -DEBIAN_BASE_DIR=$PKG_ROOT/debian -AVALANCHE_BUILD_BIN_DIR=$DEBIAN_BASE_DIR/usr/local/bin -TEMPLATE=.github/workflows/debian/template -DEBIAN_CONF=$DEBIAN_BASE_DIR/DEBIAN - -mkdir -p "$DEBIAN_BASE_DIR" -mkdir -p "$DEBIAN_CONF" -mkdir -p "$AVALANCHE_BUILD_BIN_DIR" - -# Assume binaries are at default locations -OK=$(cp ./build/avalanchego "$AVALANCHE_BUILD_BIN_DIR") -if [[ $OK -ne 0 ]]; then - exit "$OK"; -fi - -OK=$(cp $TEMPLATE/control "$DEBIAN_CONF"/control) -if [[ $OK -ne 0 ]]; then - exit "$OK"; -fi - -echo "Build debian package..." -cd "$PKG_ROOT" -echo "Tag: $TAG" -VER=$TAG -if [[ $TAG =~ ^v ]]; then - VER=$(echo "$TAG" | tr -d 'v') -fi -NEW_VERSION_STRING="Version: $VER" -NEW_ARCH_STRING="Architecture: $ARCH" -sed -i "s/Version.*/$NEW_VERSION_STRING/g" debian/DEBIAN/control -sed -i "s/Architecture.*/$NEW_ARCH_STRING/g" debian/DEBIAN/control -dpkg-deb --build debian "avalanchego-$TAG-$ARCH.deb" -aws s3 cp "avalanchego-$TAG-$ARCH.deb" "s3://${BUCKET}/linux/debs/ubuntu/$RELEASE/$ARCH/" diff --git a/.github/workflows/build-deb-release.yml b/.github/workflows/build-deb-release.yml new file mode 100644 index 000000000000..bc1b5954ef41 --- /dev/null +++ b/.github/workflows/build-deb-release.yml @@ -0,0 +1,140 @@ +name: build-deb-packages + +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag to build DEBs for' + required: true + push: + tags: + - "v*" + pull_request: + paths: + - ".github/packaging/**" + - ".github/workflows/build-deb-release.yml" + +jobs: + build-deb: + strategy: + matrix: + include: + - arch: amd64 + runner: ubuntu-22.04 + deb_arch: amd64 + - arch: arm64 + runner: ubuntu-22.04-arm + deb_arch: arm64 + runs-on: ${{ matrix.runner }} + permissions: + contents: read + outputs: + tag: ${{ steps.setup.outputs.tag }} + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + # When building an older tag via workflow_dispatch, the tag's tree + # may not contain the packaging scripts. Overlay them from the + # workflow branch so that packaging:test-build-debs is available. + - name: Overlay packaging scripts from workflow branch + if: github.event.inputs.tag + uses: actions/checkout@v4 + with: + path: .packaging-overlay + sparse-checkout: .github/packaging + + - name: Apply packaging overlay + if: github.event.inputs.tag + run: | + rm -rf .github/packaging + mv .packaging-overlay/.github/packaging .github/packaging + rm -rf .packaging-overlay + shell: bash + + - uses: ./.github/actions/setup-go-for-project + + - name: Set up packaging environment + id: setup + run: ./.github/packaging/scripts/workflow-setup-packaging.sh + shell: bash + env: + TAG_INPUT: ${{ github.event.inputs.tag }} + GPG_PRIVATE_KEY: ${{ github.event_name != 'pull_request' && secrets.RPM_GPG_PRIVATE_KEY || '' }} + + - name: Build and validate DEBs + run: ./scripts/run_task.sh --taskfile .github/packaging/Taskfile.yml test-build-debs + env: + PACKAGING_TAG: ${{ steps.setup.outputs.tag }} + GPG_KEY_FILE: ${{ steps.setup.outputs.gpg-key-file }} + GPG_KEY_PASSPHRASE: ${{ github.event_name != 'pull_request' && secrets.RPM_GPG_PASSPHRASE || '' }} + + - name: Upload DEBs as artifacts + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: debs-${{ matrix.deb_arch }} + path: | + build/deb/avalanchego-${{ steps.setup.outputs.tag }}-${{ matrix.deb_arch }}.deb + build/deb/subnet-evm-${{ steps.setup.outputs.tag }}-${{ matrix.deb_arch }}.deb + build/deb/DEB-GPG-KEY-avalanchego + + - name: Cleanup + if: always() + run: | + if [[ -n "${{ steps.setup.outputs.gpg-key-file }}" ]]; then + rm -f "${{ steps.setup.outputs.gpg-key-file }}" + fi + sudo rm -rf build/deb build/gpg + shell: bash + + # Release-only S3 upload. Runs in its own job so id-token: write is + # never granted to a job that checks out and executes PR-controlled + # packaging scripts. + upload-debs-s3: + needs: build-deb + if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || github.event_name == 'workflow_dispatch' + strategy: + matrix: + deb_arch: [amd64, arm64] + runs-on: ubuntu-22.04 + permissions: + id-token: write + contents: read + + steps: + - name: Download DEB artifacts + uses: actions/download-artifact@v4 + with: + name: debs-${{ matrix.deb_arch }} + path: build/deb + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_DEPLOY_SA_ROLE_ARN }} + role-session-name: githubrolesession + aws-region: us-east-1 + + - name: Install aws cli + run: | + if ! command -v aws &>/dev/null; then + sudo snap install aws-cli --classic + fi + + - name: Upload DEBs to S3 + env: + TAG: ${{ needs.build-deb.outputs.tag }} + DEB_ARCH: ${{ matrix.deb_arch }} + BUCKET: ${{ secrets.BUCKET }} + run: | + for release in jammy noble; do + aws s3 cp "build/deb/avalanchego-${TAG}-${DEB_ARCH}.deb" \ + "s3://${BUCKET}/linux/debs/ubuntu/${release}/${DEB_ARCH}/" + aws s3 cp "build/deb/subnet-evm-${TAG}-${DEB_ARCH}.deb" \ + "s3://${BUCKET}/linux/debs/ubuntu/${release}/${DEB_ARCH}/" + aws s3 cp "build/deb/DEB-GPG-KEY-avalanchego" \ + "s3://${BUCKET}/linux/debs/ubuntu/${release}/" + done diff --git a/.github/workflows/build-rpm-release.yml b/.github/workflows/build-rpm-release.yml index ee52a6be11a5..c153091f270d 100644 --- a/.github/workflows/build-rpm-release.yml +++ b/.github/workflows/build-rpm-release.yml @@ -55,7 +55,7 @@ jobs: if: github.event.inputs.tag run: | rm -rf .github/packaging - cp -r .packaging-overlay/.github/packaging .github/packaging + mv .packaging-overlay/.github/packaging .github/packaging rm -rf .packaging-overlay shell: bash @@ -85,7 +85,7 @@ jobs: path: | build/rpm/avalanchego-${{ steps.setup.outputs.tag }}-${{ matrix.rpm_arch }}.rpm build/rpm/subnet-evm-${{ steps.setup.outputs.tag }}-${{ matrix.rpm_arch }}.rpm - build/rpm/GPG-KEY-avalanchego + build/rpm/RPM-GPG-KEY-avalanchego - name: Cleanup if: always() diff --git a/.github/workflows/build-ubuntu-amd64-release.yml b/.github/workflows/build-ubuntu-amd64-release.yml deleted file mode 100644 index b515b924ea8c..000000000000 --- a/.github/workflows/build-ubuntu-amd64-release.yml +++ /dev/null @@ -1,131 +0,0 @@ -name: build-amd64-debian-packages - -on: - workflow_dispatch: - inputs: - tag: - description: 'Tag to include in artifact name' - required: true - push: - tags: - - "*" - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - build-jammy-amd64-package: - runs-on: ubuntu-22.04 - permissions: - id-token: write - contents: read - - steps: - - uses: actions/checkout@v5 - - uses: ./.github/actions/setup-go-for-project - - run: go version - - - name: Build the avalanchego binaries - run: ./scripts/run_task.sh build - - - name: Install aws cli - run: sudo snap install aws-cli --classic - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ secrets.AWS_DEPLOY_SA_ROLE_ARN }} - role-session-name: githubrolesession - aws-region: us-east-1 - - - name: Try to get tag from git - if: "${{ github.event.inputs.tag == '' }}" - id: get_tag_from_git - run: | - echo "TAG=${GITHUB_REF/refs\/tags\//}" >> "$GITHUB_ENV" - shell: bash - - - name: Try to get tag from workflow dispatch - if: "${{ github.event.inputs.tag != '' }}" - id: get_tag_from_workflow - run: | - echo "TAG=${{ github.event.inputs.tag }}" >> "$GITHUB_ENV" - shell: bash - - - name: Create debian package - run: ./.github/workflows/build-deb-pkg.sh - env: - PKG_ROOT: /tmp/avalanchego - TAG: ${{ env.TAG }} - BUCKET: ${{ secrets.BUCKET }} - ARCH: "amd64" - RELEASE: "jammy" - - - name: Save as Github artifact - uses: actions/upload-artifact@v6 - with: - name: jammy - path: /tmp/avalanchego/avalanchego-${{ env.TAG }}-amd64.deb - - - name: Cleanup - run: | - rm -rf ./build - rm -rf /tmp/avalanchego - - build-noble-amd64-package: - runs-on: ubuntu-24.04 - permissions: - id-token: write - contents: read - - steps: - - uses: actions/checkout@v5 - - uses: ./.github/actions/setup-go-for-project - - run: go version - - - name: Build the avalanchego binaries - run: ./scripts/run_task.sh build - - - name: Install aws cli - run: sudo snap install aws-cli --classic - - - name: Try to get tag from git - if: "${{ github.event.inputs.tag == '' }}" - id: get_tag_from_git - run: | - echo "TAG=${GITHUB_REF/refs\/tags\//}" >> "$GITHUB_ENV" - shell: bash - - - name: Try to get tag from workflow dispatch - if: "${{ github.event.inputs.tag != '' }}" - id: get_tag_from_workflow - run: | - echo "TAG=${{ github.event.inputs.tag }}" >> "$GITHUB_ENV" - shell: bash - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ secrets.AWS_DEPLOY_SA_ROLE_ARN }} - role-session-name: githubrolesession - aws-region: us-east-1 - - - name: Create debian package - run: ./.github/workflows/build-deb-pkg.sh - env: - PKG_ROOT: /tmp/avalanchego - TAG: ${{ env.TAG }} - BUCKET: ${{ secrets.BUCKET }} - ARCH: "amd64" - RELEASE: "noble" - - - name: Save as Github artifact - uses: actions/upload-artifact@v6 - with: - name: noble - path: /tmp/avalanchego/avalanchego-${{ env.TAG }}-amd64.deb - - - name: Cleanup - run: | - rm -rf ./build - rm -rf /tmp/avalanchego diff --git a/.github/workflows/build-ubuntu-arm64-release.yml b/.github/workflows/build-ubuntu-arm64-release.yml deleted file mode 100644 index 5c322f1ed4e1..000000000000 --- a/.github/workflows/build-ubuntu-arm64-release.yml +++ /dev/null @@ -1,131 +0,0 @@ -name: build-arm64-debian-packages - -on: - workflow_dispatch: - inputs: - tag: - description: 'Tag to include in artifact name' - required: true - push: - tags: - - "*" - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - build-jammy-arm64-package: - runs-on: ubuntu-22.04-arm - permissions: - id-token: write - contents: read - - steps: - - uses: actions/checkout@v5 - - uses: ./.github/actions/setup-go-for-project - - run: go version - - - name: Build the avalanchego binaries - run: ./scripts/run_task.sh build - - - name: Install aws cli - run: sudo snap install aws-cli --classic - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ secrets.AWS_DEPLOY_SA_ROLE_ARN }} - role-session-name: githubrolesession - aws-region: us-east-1 - - - name: Try to get tag from git - if: "${{ github.event.inputs.tag == '' }}" - id: get_tag_from_git - run: | - echo "TAG=${GITHUB_REF/refs\/tags\//}" >> "$GITHUB_ENV" - shell: bash - - - name: Try to get tag from workflow dispatch - if: "${{ github.event.inputs.tag != '' }}" - id: get_tag_from_workflow - run: | - echo "TAG=${{ github.event.inputs.tag }}" >> "$GITHUB_ENV" - shell: bash - - - name: Create debian package - run: ./.github/workflows/build-deb-pkg.sh - env: - PKG_ROOT: /tmp/avalanchego - TAG: ${{ env.TAG }} - BUCKET: ${{ secrets.BUCKET }} - ARCH: "arm64" - RELEASE: "jammy" - - - name: Save as Github artifact - uses: actions/upload-artifact@v6 - with: - name: jammy - path: /tmp/avalanchego/avalanchego-${{ env.TAG }}-arm64.deb - - - name: Cleanup - run: | - rm -rf ./build - rm -rf /tmp/avalanchego - - build-noble-arm64-package: - runs-on: ubuntu-24.04-arm - permissions: - id-token: write - contents: read - - steps: - - uses: actions/checkout@v5 - - uses: ./.github/actions/setup-go-for-project - - run: go version - - - name: Build the avalanchego binaries - run: ./scripts/run_task.sh build - - - name: Install aws cli - run: sudo snap install aws-cli --classic - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ secrets.AWS_DEPLOY_SA_ROLE_ARN }} - role-session-name: githubrolesession - aws-region: us-east-1 - - - name: Try to get tag from git - if: "${{ github.event.inputs.tag == '' }}" - id: get_tag_from_git - run: | - echo "TAG=${GITHUB_REF/refs\/tags\//}" >> "$GITHUB_ENV" - shell: bash - - - name: Try to get tag from workflow dispatch - if: "${{ github.event.inputs.tag != '' }}" - id: get_tag_from_workflow - run: | - echo "TAG=${{ github.event.inputs.tag }}" >> "$GITHUB_ENV" - shell: bash - - - name: Create debian package - run: ./.github/workflows/build-deb-pkg.sh - env: - PKG_ROOT: /tmp/avalanchego - TAG: ${{ env.TAG }} - BUCKET: ${{ secrets.BUCKET }} - ARCH: "arm64" - RELEASE: "noble" - - - name: Save as Github artifact - uses: actions/upload-artifact@v6 - with: - name: noble - path: /tmp/avalanchego/avalanchego-${{ env.TAG }}-arm64.deb - - - name: Cleanup - run: | - rm -rf ./build - rm -rf /tmp/avalanchego diff --git a/.github/workflows/debian/template/control b/.github/workflows/debian/template/control deleted file mode 100644 index 6ad8bcaf7691..000000000000 --- a/.github/workflows/debian/template/control +++ /dev/null @@ -1,8 +0,0 @@ -Package: avalanchego -Version: 0.1.0 -Section: misc -Priority: optional -Architecture: arm64 -Depends: -Maintainer: Stephen Buttolph -Description: The Avalanche platform binaries diff --git a/docs/design/linux-packaging.md b/docs/design/linux-packaging.md new file mode 100644 index 000000000000..4a00cd59bcb5 --- /dev/null +++ b/docs/design/linux-packaging.md @@ -0,0 +1,330 @@ +# Linux Packaging for AvalancheGo + +## Overview + +Ship signed Linux packages for `avalanchego` and `subnet-evm`: + +- RPM packages for RHEL 9.x customers +- DEB packages for Ubuntu customers + +Packages are built with `nfpm` inside per-format containers, signed with +GPG, and published as GitHub Actions artifacts on non-PR runs. DEB +release builds also publish Ubuntu S3 assets. + +## Decisions + +- **Package formats:** RPM and DEB are built through the same Taskfile + and shared packaging scripts, with format-specific `nfpm` + configuration files. +- **Target distros:** RPM builds target RHEL 9.x compatibility with a + Rocky Linux 9 container (`rockylinux:9`, glibc 2.34). DEB builds use an + Ubuntu 22.04 container (`ubuntu:22.04`). +- **Target architectures:** RPM uses `x86_64` and `aarch64`; DEB uses + `amd64` and `arm64`. +- **Binary linking:** Dynamic linking against glibc (not static musl). + See [Binary Linking](#binary-linking) for analysis. + +## Packaging + +### Build tooling + +Packages are built with [nfpm](https://nfpm.goreleaser.com/) inside +format-specific containers. The build is orchestrated by +`.github/packaging/Taskfile.yml`, included from the root Taskfile as +`packaging:`, and uses shared scripts under `.github/packaging/scripts/`. + +### RPM packages + +Two RPM packages: + +| Package | Install path | +|---------|-------------| +| avalanchego | `/var/opt/avalanchego/bin/avalanchego` | +| subnet-evm | `/var/opt/avalanchego/plugins/` | + +Both declare `glibc >= 2.34` as a dependency. The subnet-evm plugin +path uses the VM ID from `graft/subnet-evm/scripts/default-vm-data.sh` +(with a fallback to grepping `constants.sh` on tags that predate the +dedicated data file). + +### DEB packages + +Two DEB packages: + +| Package | Install path | +|---------|-------------| +| avalanchego | `/usr/local/bin/avalanchego` | +| subnet-evm | `/usr/local/lib/avalanchego/plugins/` | + +Both declare `libc6 (>= 2.34)` as a dependency. The subnet-evm plugin +path uses the VM ID from `graft/subnet-evm/scripts/default-vm-data.sh` +(with a fallback to grepping `constants.sh` on tags that predate the +dedicated data file). + +### GPG signing + +RPMs and DEBs are always signed. In CI release builds, a real GPG key is +provided by GitHub Actions secrets. For local builds and PR validation, +an ephemeral GPG key (RSA 4096, known throwaway passphrase, 1-day +expiry) is generated to exercise the same passphrase-protected signing +path without requiring a real key. +Release events (tag push and `workflow_dispatch`) fail fast in +`workflow-setup-packaging.sh` if no signing-key secret is configured, so +a misconfigured release cannot silently fall back to the ephemeral key. + +RPMs are signed inline by `nfpm` via the `rpm.signature.key_file` +configuration in `.github/packaging/nfpm/*-rpm.yml`. + +DEBs are signed inline by `nfpm` via the `deb.signature.key_file` +configuration in `.github/packaging/nfpm/*-deb.yml`. The signature is +written as a detached GPG signature in the `_gpgorigin` ar member, +covering the concatenation of `debian-binary`, the control archive, and +the data archive in ar-member order. Validation runs `gpg --verify` +against that concatenation; no post-build or distro-specific signing +tool is involved. + +### Version smoke test + +The `--version` output format is +`avalanchego/1.14.1 [database=v1.4.5, rpcchainvm=44, commit=abcd1234, ...]`. +The version comes from compiled constants (`version/constants.go`), +not the git tag, so it may not match RC tags. The smoke test verifies: +- `avalanchego --version` output starts with `avalanchego/` (binary runs) +- `avalanchego --version` output contains the git commit hash captured + during the build (correct source was compiled) +- The subnet-evm plugin is installed at the VM ID path, is executable, + and its `--version` output contains the same git commit hash + +### CI workflow + +`.github/workflows/build-rpm-release.yml` and +`.github/workflows/build-deb-release.yml` trigger on tag push, +`workflow_dispatch`, and `pull_request` for packaging paths. On PRs, the +full builds run as smoke tests with ephemeral GPG keys and synthetic tags. + +Both workflows invoke the build via +`scripts/run_task.sh --taskfile .github/packaging/Taskfile.yml `. +Locally, the same tasks are reachable as `task packaging:` because +the root Taskfile includes the packaging Taskfile under that namespace. + +RPM workflow: + +1. Run `test-build-rpms` (builds both packages, then validates in a fresh + `rockylinux:9` container: signature verification, installation, smoke + test) +2. Upload RPMs and GPG public key as GitHub artifacts on non-PR runs + +DEB workflow: + +1. Run `test-build-debs` (builds both packages, then validates in fresh + `ubuntu:22.04` and `ubuntu:24.04` containers: signature verification, + installation, smoke test) +2. Upload DEBs and the exported GPG public key as artifacts on non-PR runs +3. Upload release artifacts to S3 on tag push and `workflow_dispatch`: + `.deb` files go under `linux/debs/ubuntu/{jammy,noble}/{arch}/`; the + GPG public key goes one level above, at + `linux/debs/ubuntu/{jammy,noble}/`, since one signing key serves all + architectures of a given release. + +### Architecture mapping + +RPM mapping: + +| `uname -m` | Docker `TARGETARCH` | RPM arch | +|-------------|-------------------|----------| +| `x86_64` | `amd64` | `x86_64` | +| `arm64` / `aarch64` | `arm64` | `aarch64` | + +DEB mapping: + +| `uname -m` | Docker `TARGETARCH` | DEB arch | +|-------------|-------------------|----------| +| `x86_64` | `amd64` | `amd64` | +| `arm64` / `aarch64` | `arm64` | `arm64` | + +The Taskfile maps `uname -m` to package-format arch names. Dockerfiles +use Docker's `TARGETARCH` (Go-style) for the Go download URL. + +## Binary Linking + +### Problem Statement + +We need to ship Linux packages for RHEL/Rocky and Ubuntu, while general +CI runs mostly on Ubuntu. Different distros ship different glibc +versions, so a dynamically-linked Ubuntu binary may not run on older +target distros. A binary linked on Ubuntu 24.04 (glibc 2.39) would not +run on RHEL 9 (glibc 2.34), and may not run on Ubuntu 22.04 (glibc +2.35). + +### Proposed Solution + +Build dynamically-linked binaries against the target glibc version using +container builds as a stopgap, with Bazel hermetic toolchains as the +target solution. Minimize investment in pre-Bazel linking workarounds — +the container approach is acceptable for shipping Linux packages now. Do +not adopt static musl compilation without a performance test suite that +would catch regressions. + +### Why dynamic glibc + +- glibc's memory allocator is dramatically faster than musl's under + multi-threaded workloads. Synthetic benchmarks show 2x-700x slowdowns + for musl malloc under contention ([nickb.dev][1]). More broadly, + musl multi-threaded performance can degrade significantly — a 30x + slowdown was observed in multi-threaded benchmarks for a distributed + query engine ([Andy Grove][2]). + With firewood using jemalloc and Go managing its own heap, the + practical impact on this binary is unquantified but likely much + smaller. No profiling has been done. Performance is critical for a + blockchain node and this risk is not acceptable without data. +- glibc has full locale, NSS, and DNS support. No thread stack size + surprises (default 8MB, set by ulimit -s vs musl's 128KB). +- Go's race detector works with glibc but not musl. +- glibc has strong forward compatibility — binaries linked against an + older glibc run on newer versions (though subtle behavioral changes + between versions are possible; see residual risk under Bazel section). + +### Portability via container builds (stopgap) + +Build inside a container matching the target glibc version: `rockylinux:9` +for RPMs and `ubuntu:22.04` for DEBs. The packaging Taskfile runs these +builder containers with Docker. This has a testing gap: CI commonly runs +on Ubuntu 24.04 (glibc 2.39), so packages built in an older container can +have a different glibc floor than binaries built directly on the runner. +To truly "test what we ship," CI would need to also run tests inside the +same older-glibc container, potentially doubling CI overhead. For now, +this testing gap is probably acceptable. Package smoke tests validate +that the binary loads and runs correctly on the target platform. Full e2e +testing on the target glibc is likely not worth the effort until Bazel +enables testing against the same glibc version used for the release +build. + +### Bazel hermetic toolchain (target solution) + +Bazelification is in progress but not immediately available. + +Bazel 8 with `hermetic_cc_toolchain` would solve the "test what you +ship" problem without doubling CI or requiring containers. The +toolchain would be decoupled from the host — `bazel test` and `bazel +build` would use the same toolchain targeting the same glibc version +(e.g., glibc 2.34), regardless of the CI runner's host glibc. The test +binary and the release binary would be linked against the same glibc — +no duplication, no divergence. The toolchain bundles glibc versions +2.17 through 2.38. + +This would cover both Rust and Go. `rust_static_library` would produce +a `.a` with `CcInfo`, directly consumable by `go_library` via `cdeps`. +Both Rust compilation and Go's CGO link would use the same sysroot, so +a single `--platforms` flag would ensure that the Rust FFI and Go +binary target the same glibc version. The build graph would be: + +``` +rust_static_library (firewood_ffi, targeting x86_64-unknown-linux-gnu) + → provides CcInfo +go_library (firewood bridge, cgo=True, cdeps=[firewood_ffi]) + → go_binary (avalanchego) + → linked by hermetic_cc_toolchain targeting gnu.2.34 +``` + +Known rough edges that the firewood bazel work would need to explore: +- TLS errors reported with zig toolchain + Rust async runtimes + ([zig#12833][3]) +- `rules_rust` musl target selection had bugs requiring explicit + platform constraints ([rules_rust#2726][4]) +- Building `tikv-jemalloc-sys` under Bazel requires a + `rules_foreign_cc` workaround since it uses autotools + +There is a residual risk: a binary built and tested against an older +glibc but run against a newer one in production. glibc's forward +compatibility is strong in practice, but newer versions can change +behavior in subtle ways (bug fixes that code inadvertently depended +on, performance characteristics of allocator or threading primitives, +new default security hardening). This risk is low but non-zero, and is +inherent to any strategy that doesn't test on the exact production +glibc. + +### Alternatives Considered + +#### Static linking with musl + +Static musl binaries have no runtime dependency on host glibc, which +makes them portable across any Linux distro regardless of glibc version. +This means a single binary per architecture — no need to build and test +against multiple glibc versions, no container matrix in CI, and no risk +of glibc version mismatch at deploy time. This simplicity is why many +projects (including Alpine-based Docker images) default to musl. + +However, the risks are significant for this project: + +- **Allocator performance:** See "Why dynamic glibc" above for details. + Firewood's jemalloc mitigates Rust-side allocations, but any C-level + malloc calls in CGO-invoked code still hit musl's allocator. +- **Thread stack size:** musl defaults to 128KB (vs glibc 8MB, set by + ulimit -s). This has caused segfaults in other CGO projects (Grafana + with SQLite, [grafana#79773][5]). Not observed with firewood, but + untested under deep call stacks. +- **Firewood FFI:** `firewood-go-ethhash/ffi` ships pre-compiled `.a` + for `*-linux-gnu` only. Would need to build and publish + musl-compatible `.a` files. +- **Race detector:** Go's race detector does not work with musl + ([Honnef][6]). Must maintain a separate glibc build for race-enabled + CI. +- **Testing gap:** We lack a performance test suite that would catch + musl-induced regressions. Switching to static musl without adequate + perf testing risks shipping slower binaries unknowingly. + +#### Static linking with glibc + +Would combine glibc's performance with static portability. The classic +objections (NSS/iconv/dlopen, [RedHat][7]) don't actually apply to this stack: +Go's `netgo`+`osusergo` build tags bypass NSS entirely, neither Go nor Rust +uses iconv, and firewood's file I/O (mmap, pwrite, fsync) uses only thin +syscall wrappers that work identically in static and dynamic builds. The +CockroachDB thread-local-storage crash ([Schottdorf][8]) was triggered by NSS +module loading, which `netgo`+`osusergo` eliminates. + +However, this approach is still not recommended: (1) you still must +target a specific glibc version, so the portability benefit over dynamic +linking evaporates — you need container builds or a hermetic toolchain +either way; (2) no one in the Go ecosystem appears to ship Go+CGO +statically linked against glibc in production, making this untested +territory; (3) the Bazel hermetic toolchain with dynamic linking is +strictly better, avoiding all static glibc edge cases while providing +the same reproducibility. + +### Industry Practice: How Go Projects Ship Binaries + +A common misconception is that "distroless" container images imply +static linking. In fact, Google's distroless project provides three +image variants serving different linking models: + +| Image | Contents | Use case | +|-------|----------|----------| +| `distroless/static` | CA certs, tzdata only | Statically linked binaries (no libc) | +| `distroless/base-nossl` | Above + **glibc** | Dynamically linked binaries | +| `distroless/base` | Above + **glibc + libssl** | Dynamically linked binaries needing TLS | + +The vast majority of Go projects using `distroless/static` build with +`CGO_ENABLED=0` — producing pure Go binaries with no libc dependency +at all. This is not static glibc linking; it is no libc linking. + +Projects that require CGO (for C libraries like RocksDB, SQLite, GEOS, +or in our case firewood FFI) fall into one of three camps. No +well-known Go project statically links against glibc in production: + +| Strategy | Projects | Container base | +|----------|----------|---------------| +| Pure Go (`CGO_ENABLED=0`) | etcd, Kubernetes, Prometheus | `distroless/static` or `scratch` | +| CGO + dynamic glibc | CockroachDB ([cockroach#3392][9]) | Red Hat UBI minimal | +| CGO + static musl | Recommended for CGO projects needing static binaries ([DoltHub blog][10]) | `distroless/static` | + +[1]: https://nickb.dev/blog/default-musl-allocator-considered-harmful-to-performance/ +[2]: https://andygrove.io/2020/05/why-musl-extremely-slow/ +[3]: https://github.com/ziglang/zig/issues/12833 +[4]: https://github.com/bazelbuild/rules_rust/issues/2726 +[5]: https://github.com/grafana/grafana/issues/79773 +[6]: https://honnef.co/articles/statically-compiled-go-programs-always-even-with-cgo-using-musl/ +[7]: https://developers.redhat.com/articles/2023/08/31/how-we-ensure-statically-linked-applications-stay-way +[8]: https://tschottdorf.github.io/golang-static-linking-bug +[9]: https://github.com/cockroachdb/cockroach/issues/3392 +[10]: https://www.dolthub.com/blog/2024-05-01-cgo-tradeoffs/