Skip to content

Commit 70428c8

Browse files
authored
fix multiarch docker binary build (#1461)
1 parent 6839da2 commit 70428c8

File tree

7 files changed

+144
-47
lines changed

7 files changed

+144
-47
lines changed

.github/workflows/publish_docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
env:
2727
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
2828
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
29-
DOCKER_REPO: "avaplatform/subnet-evm"
29+
IMAGE_NAME: "avaplatform/subnet-evm"
3030
VM_ID: ${{ inputs.vm_id }}
3131
PUBLISH: 1
3232
PLATFORMS: "linux/amd64,linux/arm64"

.github/workflows/tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ jobs:
192192
runs-on: ubuntu-latest
193193
steps:
194194
- uses: actions/checkout@v4
195+
- name: Install qemu (required for cross-platform builds)
196+
run: |
197+
sudo apt update
198+
sudo apt -y install qemu-system qemu-user-static
195199
- name: Check image build
196200
shell: bash
197201
run: bash -x scripts/tests.build_docker_image.sh

Dockerfile

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
# syntax=docker/dockerfile:experimental
2-
31
# ============= Setting up base Stage ================
42
# AVALANCHEGO_NODE_IMAGE needs to identify an existing node image and should include the tag
5-
ARG AVALANCHEGO_NODE_IMAGE
3+
# This value is not intended to be used but silences a warning
4+
ARG AVALANCHEGO_NODE_IMAGE="invalid-image"
65

76
# ============= Compilation Stage ================
87
FROM --platform=$BUILDPLATFORM golang:1.23.6-bullseye AS builder
@@ -12,25 +11,48 @@ WORKDIR /build
1211
# Copy avalanche dependencies first (intermediate docker image caching)
1312
# Copy avalanchego directory if present (for manual CI case, which uses local dependency)
1413
COPY go.mod go.sum avalanchego* ./
15-
1614
# Download avalanche dependencies using go mod
17-
RUN go mod download && go mod tidy -compat=1.23
15+
RUN go mod download && go mod tidy
1816

1917
# Copy the code into the container
2018
COPY . .
2119

2220
# Ensure pre-existing builds are not available for inclusion in the final image
2321
RUN [ -d ./build ] && rm -rf ./build/* || true
2422

23+
24+
ARG TARGETPLATFORM
25+
ARG BUILDPLATFORM
26+
27+
# Configure a cross-compiler if the target platform differs from the build platform.
28+
#
29+
# build_env.sh is used to capture the environmental changes required by the build step since RUN
30+
# environment state is not otherwise persistent.
31+
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ] && [ "$BUILDPLATFORM" != "linux/arm64" ]; then \
32+
apt-get update && apt-get install -y gcc-aarch64-linux-gnu && \
33+
echo "export CC=aarch64-linux-gnu-gcc" > ./build_env.sh \
34+
; elif [ "$TARGETPLATFORM" = "linux/amd64" ] && [ "$BUILDPLATFORM" != "linux/amd64" ]; then \
35+
apt-get update && apt-get install -y gcc-x86-64-linux-gnu && \
36+
echo "export CC=x86_64-linux-gnu-gcc" > ./build_env.sh \
37+
; else \
38+
echo "export CC=gcc" > ./build_env.sh \
39+
; fi
40+
2541
# Pass in SUBNET_EVM_COMMIT as an arg to allow the build script to set this externally
2642
ARG SUBNET_EVM_COMMIT
2743
ARG CURRENT_BRANCH
2844

29-
RUN export SUBNET_EVM_COMMIT=$SUBNET_EVM_COMMIT && export CURRENT_BRANCH=$CURRENT_BRANCH && ./scripts/build.sh build/subnet-evm
45+
RUN . ./build_env.sh && \
46+
echo "{CC=$CC, TARGETPLATFORM=$TARGETPLATFORM, BUILDPLATFORM=$BUILDPLATFORM}" && \
47+
export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
48+
export CURRENT_BRANCH=$CURRENT_BRANCH && \
49+
export SUBNET_EVM_COMMIT=$SUBNET_EVM_COMMIT && \
50+
./scripts/build.sh build/subnet-evm
3051

3152
# ============= Cleanup Stage ================
32-
FROM $AVALANCHEGO_NODE_IMAGE AS builtImage
53+
FROM $AVALANCHEGO_NODE_IMAGE AS execution
3354

3455
# Copy the evm binary into the correct location in the container
3556
ARG VM_ID=srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy
36-
COPY --from=builder /build/build/subnet-evm /avalanchego/build/plugins/$VM_ID
57+
ENV AVAGO_PLUGIN_DIR="/avalanchego/build/plugins"
58+
COPY --from=builder /build/build/subnet-evm $AVAGO_PLUGIN_DIR/$VM_ID

plugin/runner/runner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func Run(versionStr string) {
2222
os.Exit(1)
2323
}
2424
if printVersion && versionStr != "" {
25-
fmt.Print(versionStr)
25+
fmt.Println(versionStr)
2626
os.Exit(0)
2727
}
2828
if err := ulimit.Set(ulimit.DefaultFDLimit, logging.NoLog{}); err != nil {

scripts/build_docker_image.sh

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,33 @@ BUILD_IMAGE_ID=${BUILD_IMAGE_ID:-"${CURRENT_BRANCH}"}
3636
#
3737
# Reference: https://docs.docker.com/build/buildkit/
3838
DOCKER_CMD="docker buildx build"
39-
39+
ispush=0
4040
if [[ -n "${PUBLISH}" ]]; then
41-
DOCKER_CMD="${DOCKER_CMD} --push"
42-
43-
echo "Pushing $DOCKERHUB_REPO:$BUILD_IMAGE_ID"
44-
41+
echo "Pushing $IMAGE_NAME:$BUILD_IMAGE_ID"
42+
ispush=1
4543
# A populated DOCKER_USERNAME env var triggers login
4644
if [[ -n "${DOCKER_USERNAME:-}" ]]; then
4745
echo "$DOCKER_PASS" | docker login --username "$DOCKER_USERNAME" --password-stdin
4846
fi
4947
fi
5048

51-
# Build a multi-arch image if requested
49+
# Build a specified platform image if requested
5250
if [[ -n "${PLATFORMS}" ]]; then
5351
DOCKER_CMD="${DOCKER_CMD} --platform=${PLATFORMS}"
52+
if [[ "$PLATFORMS" == *,* ]]; then ## Multi-arch
53+
ispush=1
54+
fi
55+
fi
56+
57+
if [[ $ispush -eq 1 ]]; then
58+
DOCKER_CMD="${DOCKER_CMD} --push"
59+
else
60+
## Single arch
61+
#
62+
# Building a single-arch image with buildx and having the resulting image show up
63+
# in the local store of docker images (ala 'docker build') requires explicitly
64+
# loading it from the buildx store with '--load'.
65+
DOCKER_CMD="${DOCKER_CMD} --load"
5466
fi
5567

5668
VM_ID=${VM_ID:-"${DEFAULT_VM_ID}"}
@@ -61,15 +73,28 @@ fi
6173
# Default to the release image. Will need to be overridden when testing against unreleased versions.
6274
AVALANCHEGO_NODE_IMAGE="${AVALANCHEGO_NODE_IMAGE:-${AVALANCHEGO_IMAGE_NAME}:${AVALANCHE_VERSION}}"
6375

64-
echo "Building Docker Image: $DOCKERHUB_REPO:$BUILD_IMAGE_ID based of AvalancheGo@$AVALANCHE_VERSION"
65-
${DOCKER_CMD} -t "$DOCKERHUB_REPO:$BUILD_IMAGE_ID" -t "$DOCKERHUB_REPO:${DOCKERHUB_TAG}" \
76+
# Build the avalanchego image if it cannot be pulled. This will usually be due to
77+
# AVALANCHE_VERSION being not yet merged since the image is published post-merge.
78+
if ! docker pull "${AVALANCHEGO_NODE_IMAGE}"; then
79+
# Use a image name without a repository (i.e. without 'avaplatform/' prefix ) to build a
80+
# local image that will not be pushed.
81+
export AVALANCHEGO_IMAGE_NAME="avalanchego"
82+
echo "Building ${AVALANCHEGO_IMAGE_NAME}:${AVALANCHE_VERSION} locally"
83+
84+
source "${SUBNET_EVM_PATH}"/scripts/lib_avalanchego_clone.sh
85+
clone_avalanchego "${AVALANCHE_VERSION}"
86+
SKIP_BUILD_RACE=1 DOCKER_IMAGE="${AVALANCHEGO_IMAGE_NAME}" "${AVALANCHEGO_CLONE_PATH}"/scripts/build_image.sh
87+
fi
88+
89+
echo "Building Docker Image: $IMAGE_NAME:$BUILD_IMAGE_ID based of AvalancheGo@$AVALANCHE_VERSION"
90+
${DOCKER_CMD} -t "$IMAGE_NAME:$BUILD_IMAGE_ID" -t "$IMAGE_NAME:${DOCKERHUB_TAG}" \
6691
"$SUBNET_EVM_PATH" -f "$SUBNET_EVM_PATH/Dockerfile" \
6792
--build-arg AVALANCHEGO_NODE_IMAGE="$AVALANCHEGO_NODE_IMAGE" \
6893
--build-arg SUBNET_EVM_COMMIT="$SUBNET_EVM_COMMIT" \
6994
--build-arg CURRENT_BRANCH="$CURRENT_BRANCH" \
7095
--build-arg VM_ID="$VM_ID"
7196

7297
if [[ -n "${PUBLISH}" && $CURRENT_BRANCH == "master" ]]; then
73-
echo "Tagging current image as $DOCKERHUB_REPO:latest"
74-
docker buildx imagetools create -t "$DOCKERHUB_REPO:latest" "$DOCKERHUB_REPO:$BUILD_IMAGE_ID"
98+
echo "Tagging current image as $IMAGE_NAME:latest"
99+
docker buildx imagetools create -t "$IMAGE_NAME:latest" "$IMAGE_NAME:$BUILD_IMAGE_ID"
75100
fi

scripts/constants.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ set -euo pipefail
88
# Set the PATHS
99
GOPATH="$(go env GOPATH)"
1010
DEFAULT_PLUGIN_DIR="${HOME}/.avalanchego/plugins"
11+
DEFAULT_VM_NAME="subnet-evm"
1112
DEFAULT_VM_ID="srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy"
1213

1314
# Avalabs docker hub
1415
# avaplatform/avalanchego - defaults to local as to avoid unintentional pushes
15-
# You should probably set it - export DOCKER_REPO='avaplatform/subnet-evm'
16-
DOCKERHUB_REPO=${DOCKER_REPO:-"subnet-evm"}
16+
# You should probably set it - export IMAGE_NAME='avaplatform/subnet-evm'
17+
IMAGE_NAME=${IMAGE_NAME:-"subnet-evm"}
1718

1819
# Shared between ./scripts/build_docker_image.sh and ./scripts/tests.build_docker_image.sh
1920
AVALANCHEGO_IMAGE_NAME="${AVALANCHEGO_IMAGE_NAME:-avaplatform/avalanchego}"
@@ -51,3 +52,6 @@ fi
5152
# We use "export" here instead of just setting a bash variable because we need
5253
# to pass this flag to all child processes spawned by the shell.
5354
export CGO_CFLAGS="-O2 -D__BLST_PORTABLE__"
55+
56+
# CGO_ENABLED is required for multi-arch builds.
57+
export CGO_ENABLED=1

scripts/tests.build_docker_image.sh

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,76 @@ set -euo pipefail
55
# Sanity check the image build by attempting to build and run the image without error.
66

77
# Directory above this script
8-
SUBNET_EVM_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd )
8+
SUBNET_EVM_PATH=$(
9+
cd "$(dirname "${BASH_SOURCE[0]}")"
10+
cd .. && pwd
11+
)
912
# Load the constants
1013
source "$SUBNET_EVM_PATH"/scripts/constants.sh
1114

1215
# Load the versions
1316
source "$SUBNET_EVM_PATH"/scripts/versions.sh
1417

15-
# Use the default node image
16-
AVALANCHEGO_NODE_IMAGE="${AVALANCHEGO_IMAGE_NAME}:${AVALANCHE_VERSION}"
17-
18-
# Build the avalanchego image if it cannot be pulled. This will usually be due to
19-
# AVALANCHE_VERSION being not yet merged since the image is published post-merge.
20-
if ! docker pull "${AVALANCHEGO_NODE_IMAGE}"; then
21-
# Use a image name without a repository (i.e. without 'avaplatform/' prefix ) to build a
22-
# local image that will not be pushed.
23-
export AVALANCHEGO_IMAGE_NAME="avalanchego"
24-
echo "Building ${AVALANCHEGO_IMAGE_NAME}:${AVALANCHE_VERSION} locally"
25-
26-
source "${SUBNET_EVM_PATH}"/scripts/lib_avalanchego_clone.sh
27-
clone_avalanchego "${AVALANCHE_VERSION}"
28-
SKIP_BUILD_RACE=1 DOCKER_IMAGE="${AVALANCHEGO_IMAGE_NAME}" "${AVALANCHEGO_CLONE_PATH}"/scripts/build_image.sh
29-
fi
30-
31-
# Build a local image
32-
bash -x "${SUBNET_EVM_PATH}"/scripts/build_docker_image.sh
33-
34-
# Check that the image can be run and contains the plugin
35-
echo "Checking version of the plugin provided by the image"
36-
docker run -t --rm "${DOCKERHUB_REPO}:${DOCKERHUB_TAG}" /avalanchego/build/plugins/"${DEFAULT_VM_ID}" --version
37-
echo "" # --version output doesn't include a newline
38-
echo "Successfully checked image build"
18+
build_and_test() {
19+
local imagename="${1}"
20+
local vm_id="${2}"
21+
local multiarch_image="${3}"
22+
23+
if [[ "${multiarch_image}" == true ]]; then
24+
local arches="linux/amd64,linux/arm64"
25+
else
26+
# Test only the host platform for single arch builds
27+
local host_arch
28+
host_arch="$(go env GOARCH)"
29+
local arches="linux/$host_arch"
30+
fi
31+
32+
local imgtag="testtag"
33+
34+
PLATFORMS="${arches}" \
35+
BUILD_IMAGE_ID="${imgtag}" \
36+
VM_ID=$"${vm_id}" \
37+
IMAGE_NAME="${imagename}" \
38+
./scripts/build_docker_image.sh
39+
40+
echo "listing images"
41+
docker images
42+
43+
# Check all of the images expected to have been built
44+
local target_images=(
45+
"$imagename:$imgtag"
46+
"$imagename:$DOCKERHUB_TAG"
47+
)
48+
IFS=',' read -r -a archarray <<<"$arches"
49+
for arch in "${archarray[@]}"; do
50+
for target_image in "${target_images[@]}"; do
51+
echo "checking sanity of image $target_image for $arch by running '${VM_ID} version'"
52+
docker run -t --rm --platform "$arch" "$target_image" /avalanchego/build/plugins/"${VM_ID}" --version
53+
done
54+
done
55+
}
56+
57+
VM_ID="${VM_ID:-${DEFAULT_VM_ID}}"
58+
59+
echo "checking build of single-arch image"
60+
build_and_test "subnet-evm" "${VM_ID}" false
61+
62+
echo "starting local docker registry to allow verification of multi-arch image builds"
63+
REGISTRY_CONTAINER_ID="$(docker run --rm -d -P registry:2)"
64+
REGISTRY_PORT="$(docker port "$REGISTRY_CONTAINER_ID" 5000/tcp | grep -v "::" | awk -F: '{print $NF}')"
65+
66+
echo "starting docker builder that supports multiplatform builds"
67+
# - '--driver-opt network=host' enables the builder to use the local registry
68+
docker buildx create --use --name ci-builder --driver-opt network=host
69+
70+
# Ensure registry and builder cleanup on teardown
71+
function cleanup {
72+
echo "stopping local docker registry"
73+
docker stop "${REGISTRY_CONTAINER_ID}"
74+
echo "removing multiplatform builder"
75+
docker buildx rm ci-builder
76+
}
77+
trap cleanup EXIT
78+
79+
echo "checking build of multi-arch images"
80+
build_and_test "localhost:${REGISTRY_PORT}/subnet-evm" "${VM_ID}" true

0 commit comments

Comments
 (0)