From ebbc0ff76ed3b6a205bec139a7e1405502235c12 Mon Sep 17 00:00:00 2001 From: Gilles Filippini <pini@debian.org> Date: Fri, 7 Apr 2023 16:41:51 +0200 Subject: [PATCH] ci: enable multiplatform builds with docker buildx * Build from the Dockerfile and copy the artifacts from the resulting image * Use cross-compilation at build stage when building for a foreign architecture * Use 'docker buildx build ...' in CI to enable multiplatform builds If buildx is not installed use: DOCKER_BUILDKIT=1 docker build ... * Gilab runner must expose tag 'multiplatform' which means it has native amd64 arch + emulated arm64 support: https://github.com/tonistiigi/binfmt/tree/deploy/v7.0.0-28#installing-emulators $ docker run --privileged --rm tonistiigi/binfmt --install arm64 --- .dockerignore | 1 + .gitlab-ci.yml | 268 +++++++++++++++++++--------------------------- docker/Dockerfile | 87 ++++++++++----- 3 files changed, 168 insertions(+), 188 deletions(-) diff --git a/.dockerignore b/.dockerignore index c602282b3..1c3094162 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,4 @@ docker/Dockerfile docker-compose.yml arm-build/ **/target/ +build/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6352c8a28..d85ac2e87 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,9 @@ +# Runner tags: +# - dind: Docker in Docker +# - multiplatform: support amd64 and arm64 architectures +# https://github.com/tonistiigi/binfmt/tree/deploy/v7.0.0-28#installing-emulators +# $ docker run --privileged --rm tonistiigi/binfmt --install arm64 + stages: - schedule - labels @@ -58,95 +64,59 @@ fmt_and_clippy: - cargo clippy -- -V - cargo clippy --all --tests -- -D warnings -build_debug: - extends: .env - rules: - - if: $CI_COMMIT_TAG - when: never - - if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "master"' - changes: - - Cargo.lock - - when: never +.docker_build: + image: docker:20.10 + script: + - mkdir -p build + # A builder name and node name must be specified so that the command doesn't fail when the builder exists already + - docker buildx create --name gitlab-runner-buildx --node gitlab-runner-buildx0 --use + - docker buildx ls + - echo docker buildx build --tag "$IMAGE_NAME:$IMAGE_TAG" -f docker/Dockerfile $DOCKER_BUILD_OPTIONS . + - docker buildx build --tag "$IMAGE_NAME:$IMAGE_TAG" -f docker/Dockerfile $DOCKER_BUILD_OPTIONS . + tags: + - dind + +.docker_build_save: + extends: .docker_build stage: build script: - - cargo clean -p duniter - - cargo build --locked - - mkdir build - - mv target/debug/duniter build/duniter + - !reference [.docker_build, script] + - docker run -d --rm --name "$CONTAINER" --entrypoint "" --user "$(id -u)" "$IMAGE_NAME:$IMAGE_TAG" sleep infinity + - docker cp "$CONTAINER:/usr/local/bin/duniter" build/ + - docker stop "$CONTAINER" artifacts: paths: - build/ expire_in: 3 day - cache: - - key: - files: - - Cargo.lock - paths: - - target/debug - policy: push -build_debug_with_cache: - extends: .env +build_debug: + extends: .docker_build_save rules: - - changes: - - Cargo.lock - when: never - if: $CI_COMMIT_TAG when: never - if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "master"' - when: never - stage: build - script: - - cargo clean -p duniter - - cargo build --locked - - mkdir build - - mv target/debug/duniter build/duniter - artifacts: - paths: - - build/ - expire_in: 3 day - cache: - - key: - files: - - Cargo.lock - paths: - - target/debug - policy: pull + variables: + IMAGE_NAME: "duniter/duniter-v2s" + IMAGE_TAG: "debug-sha-$CI_COMMIT_SHORT_SHA" + DOCKER_BUILD_OPTIONS: "--load --build-arg debug=1" + CONTAINER: "duniter-v2s_$IMAGE_TAG" build_release: - extends: .env + extends: .docker_build_save rules: - if: "$CI_COMMIT_TAG && $CI_COMMIT_TAG =~ /^v*/" - - when: never - stage: build - script: - - cargo build --locked --release - - mkdir build - - mv target/release/duniter build/duniter - artifacts: - paths: - - build/ - expire_in: 3 day - -build_release_manual: - extends: .env - rules: - - if: $CI_COMMIT_TAG - when: never + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "master"' - when: manual - stage: build - allow_failure: true - script: - - cargo build --locked --release - - mkdir build - - mv target/release/duniter build/duniter - artifacts: - paths: - - build/ - expire_in: 3 day + variables: + IMAGE_NAME: "duniter/duniter-v2s" + IMAGE_TAG: "sha-$CI_COMMIT_SHORT_SHA" + DOCKER_BUILD_OPTIONS: "--load --platform linux/amd64" + CONTAINER: "duniter-v2s_$IMAGE_TAG" -tests_debug: - extends: .env +test_debug: + stage: tests + extends: .docker_build rules: - if: $CI_COMMIT_REF_NAME =~ /^wip*$/ when: manual @@ -154,128 +124,106 @@ tests_debug: when: never - if: '$CI_MERGE_REQUEST_ID || $CI_COMMIT_BRANCH == "master"' - when: manual - stage: tests variables: - DUNITER_BINARY_PATH: "../build/duniter" - DUNITER_END2END_TESTS_SPAWN_NODE_TIMEOUT: "20" - script: - - cargo test --workspace --exclude duniter-end2end-tests --exclude duniter-live-tests - - cargo cucumber -i account_creation* - - cargo cucumber -i certification* - - cargo cucumber -i identity_creation* - - cargo cucumber -i monetary_mass* - - cargo cucumber -i oneshot_account* - - cargo cucumber -i transfer_all* - after_script: - - cd target/debug/deps/ - - rm cucumber_tests-*.d - - mv cucumber_tests* ../../../build/duniter-cucumber - artifacts: - paths: - - build/ - expire_in: 3 day + IMAGE_NAME: "duniter/duniter-v2s-test" + IMAGE_TAG: "debug-sha-$CI_COMMIT_SHORT_SHA" + DOCKER_BUILD_OPTIONS: "--target build --build-arg debug=1 --build-arg cucumber=1" -tests_release: - extends: .env +test_release: + stage: tests + extends: .docker_build rules: - if: "$CI_COMMIT_TAG && $CI_COMMIT_TAG =~ /^v*/" - when: never - stage: tests variables: - DUNITER_BINARY_PATH: "../build/duniter" - DUNITER_END2END_TESTS_SPAWN_NODE_TIMEOUT: "20" - script: - - cargo test --workspace --exclude duniter-end2end-tests --exclude duniter-live-tests - - cargo cucumber -i account_creation* - - cargo cucumber -i certification* - - cargo cucumber -i identity_creation* - - cargo cucumber -i monetary_mass* - - cargo cucumber -i oneshot_account* - - cargo cucumber -i transfer_all* - after_script: - - cd target/debug/deps/ - - rm cucumber_tests-*.d - - mv cucumber_tests* ../../../build/duniter-cucumber - artifacts: - paths: - - build/ - expire_in: 3 day - dependencies: - - build_release + IMAGE_NAME: "duniter/duniter-v2s-test" + IMAGE_TAG: "sha-$CI_COMMIT_SHORT_SHA" + DOCKER_BUILD_OPTIONS: "--target build --build-arg cucumber=1" -.docker-build-app-image: +deploy_docker_debug_sha: stage: deploy - image: docker:18.06 - tags: - - docker - services: - - docker:dind - before_script: - - docker info - script: - - docker pull $CI_REGISTRY_IMAGE:$IMAGE_TAG || true - - docker build --cache-from $CI_REGISTRY_IMAGE:$IMAGE_TAG --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" -f $DOCKERFILE_PATH . - - docker login -u "duniterteam" -p "$DUNITERTEAM_PASSWD" - - docker tag "$CI_REGISTRY_IMAGE:$IMAGE_TAG" "duniter/duniter-v2s:$IMAGE_TAG" - - docker push "duniter/duniter-v2s:$IMAGE_TAG" - -deploy_docker_test_image: - extends: .docker-build-app-image + extends: .docker_build rules: - - if: $CI_COMMIT_REF_NAME =~ /^wip*$/ - when: manual - - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH == "master"' + - if: $CI_COMMIT_TAG when: never - - when: manual - allow_failure: true + - if: $CI_COMMIT_BRANCH == "master" + script: + - docker login -u "duniterteam" -p "$DUNITERTEAM_PASSWD" + - !reference [.docker_build, script] variables: - DOCKERFILE_PATH: "docker/Dockerfile" - IMAGE_TAG: "test-image-$CI_COMMIT_SHORT_SHA" + IMAGE_NAME: "duniter/duniter-v2s" + IMAGE_TAG: "debug-sha-$CI_COMMIT_SHORT_SHA" + DOCKER_BUILD_OPTIONS: "--push --build-arg debug=1" -deploy_docker_debug_sha: - extends: .docker-build-app-image +deploy_docker_release_sha: + stage: deploy + extends: .docker_build rules: - if: $CI_COMMIT_TAG when: never - - if: $CI_COMMIT_BRANCH == "master" - variables: - DOCKERFILE_PATH: "docker/Dockerfile" - IMAGE_TAG: "debug-sha-$CI_COMMIT_SHORT_SHA" - after_script: + - when: manual + script: - docker login -u "duniterteam" -p "$DUNITERTEAM_PASSWD" - - docker tag "duniter/duniter-v2s:$IMAGE_TAG" "duniter/duniter-v2s:debug-latest" - - docker push "duniter/duniter-v2s:debug-latest" + - !reference [.docker_build, script] + variables: + IMAGE_NAME: "duniter/duniter-v2s" + IMAGE_TAG: "sha-$CI_COMMIT_SHORT_SHA" + DOCKER_BUILD_OPTIONS: "--push --platform linux/amd64" -deploy_docker_release_sha: - extends: .docker-build-app-image +deploy_docker_release_sha_multiplatform: + stage: deploy + needs: ["deploy_docker_release_sha"] + extends: .docker_build rules: - if: $CI_COMMIT_TAG when: never - when: manual - allow_failure: true + script: + - docker login -u "duniterteam" -p "$DUNITERTEAM_PASSWD" + - !reference [.docker_build, script] variables: - DOCKERFILE_PATH: "docker/Dockerfile" + IMAGE_NAME: "duniter/duniter-v2s" IMAGE_TAG: "sha-$CI_COMMIT_SHORT_SHA" - dependencies: - - build_release_manual + DOCKER_BUILD_OPTIONS: "--push --platform linux/amd64,linux/arm64" + tags: + - dind + - multiplatform deploy_docker_release_tag: - extends: .docker-build-app-image + stage: deploy + extends: .docker_build rules: - if: "$CI_COMMIT_TAG && $CI_COMMIT_TAG =~ /^v*/" - when: never + script: + - docker login -u "duniterteam" -p "$DUNITERTEAM_PASSWD" + - !reference [.docker_build, script] variables: - DOCKERFILE_PATH: "docker/Dockerfile" + IMAGE_NAME: "duniter/duniter-v2s" IMAGE_TAG: "$CI_COMMIT_TAG" - after_script: - - docker login -u "duniterteam" -p "$DUNITERTEAM_PASSWD" - - docker tag "duniter/duniter-v2s:$IMAGE_TAG" "duniter/duniter-v2s:latest" - - docker push "duniter/duniter-v2s:latest" - dependencies: - - build_release + DOCKER_BUILD_OPTIONS: "--push --platform linux/amd64 --tag $IMAGE_NAME:latest" + +deploy_docker_release_tag_multiplatform: + stage: deploy + needs: ["deploy_docker_release_tag"] + extends: .docker_build + rules: + - if: "$CI_COMMIT_TAG && $CI_COMMIT_TAG =~ /^v*/" + - when: never + script: + - docker login -u "$REPO_DOCKER_USER" -p "$REPO_DOCKER_PASS" + - !reference [.docker_build, script] + variables: + IMAGE_NAME: "pinidh/duniter-v2s" + IMAGE_TAG: "$CI_COMMIT_TAG" + DOCKER_BUILD_OPTIONS: "--push --platform linux/amd64,linux/arm64 --tag $IMAGE_NAME:latest" + tags: + - dind + - multiplatform readme_docker_release_tag: stage: deploy_readme + needs: ["deploy_docker_release_tag"] rules: - if: "$CI_COMMIT_TAG && $CI_COMMIT_TAG =~ /^v*/" - when: never diff --git a/docker/Dockerfile b/docker/Dockerfile index bb2f489c8..3683f585c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,45 +1,77 @@ +# Build with `docker buildx build ...` +# If buildx is not installed use `DOCKER_BUILDKIT=1 docker build ...` + # ------------------------------------------------------------------------------ # Build Stage # ------------------------------------------------------------------------------ -# Building for Debian buster because we need the binary to be compatible -# with the image paritytech/ci-linux:production (currently based on -# debian:buster-slim) used by the gitlab CI -FROM rust:1-buster as build +# When building for a foreign arch, use cross-compilation +# https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/ +FROM --platform=$BUILDPLATFORM rust:1-bullseye as build +ARG BUILDPLATFORM +ARG TARGETPLATFORM + +# We need the target arch triplet in both Debian and rust flavor +RUN echo "DEBIAN_ARCH_TRIPLET='$(dpkg-architecture -A${TARGETPLATFORM#linux/} -qDEB_TARGET_MULTIARCH)'" >>/root/dynenv +RUN . /root/dynenv && \ + echo "RUST_ARCH_TRIPLET='$(echo "$DEBIAN_ARCH_TRIPLET" | sed -E 's/-linux-/-unknown&/')'" >>/root/dynenv + WORKDIR /root # Copy source tree COPY . . -RUN test -x build/duniter || \ - ( \ - apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y clang cmake protobuf-compiler \ - ) +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y clang cmake protobuf-compiler # build duniter -ARG threads=1 -RUN test -x build/duniter || \ - ( \ - CARGO_PROFILE_RELEASE_LTO="true" \ - cargo build --release -j $threads && \ - mkdir -p build && \ - mv target/release/duniter build/ \ - ) - -# Create fake duniter-cucumber if is not exist -# The goal is to avoid error later, this binary is optional -RUN test -x build/duniter-cucumber || \ - ( \ - mkdir -p build && \ - touch build/duniter-cucumber \ - ) +ARG debug=0 +RUN if [ "$debug" = 0 ]; then \ + echo "CARGO_PROFILE_RELEASE_LTO=true; export CARGO_PROFILE_RELEASE_LTO" >>/root/dynenv && \ + echo "CARGO_OPTIONS=--release" >>/root/dynenv && \ + echo "TARGET_FOLDER=release" >>/root/dynenv; \ + else \ + echo "TARGET_FOLDER=debug" >>/root/dynenv; \ + fi + +# Configure cross-build environment if need be +RUN set -x && \ + if [ "$TARGETPLATFORM" != "$BUILDPLATFORM" ]; then \ + . /root/dynenv && \ + apt install -y gcc-$DEBIAN_ARCH_TRIPLET binutils-$DEBIAN_ARCH_TRIPLET && \ + rustup target add "$RUST_ARCH_TRIPLET" && \ + : https://github.com/rust-lang/cargo/issues/4133 && \ + echo "RUSTFLAGS='-C linker=$DEBIAN_ARCH_TRIPLET-gcc'; export RUSTFLAGS" >>/root/dynenv; \ + fi + +# Build +RUN set -x && \ + cat /root/dynenv && \ + . /root/dynenv && \ + cargo build --locked $CARGO_OPTIONS --target "$RUST_ARCH_TRIPLET" && \ + mkdir -p build && \ + mv target/$RUST_ARCH_TRIPLET/$TARGET_FOLDER/duniter build/ + +# Run tests if requested, expted when cross-building +ARG cucumber=0 +RUN if [ "$cucumber" != 0 ] && [ "$TARGETPLATFORM" = "$BUILDPLATFORM" ]; then \ + cargo test --workspace --exclude duniter-end2end-tests --exclude duniter-live-tests && \ + cargo cucumber -i account_creation* && \ + cargo cucumber -i certification* && \ + cargo cucumber -i identity_creation* && \ + cargo cucumber -i monetary_mass* && \ + cargo cucumber -i oneshot_account* && \ + cargo cucumber -i transfer_all* && \ + cd target/debug/deps/ && \ + rm cucumber_tests-*.d && \ + mv cucumber_tests* ../../../build/duniter-cucumber; \ + fi # ------------------------------------------------------------------------------ # Final Stage # ------------------------------------------------------------------------------ -FROM debian:buster-slim +FROM debian:bullseye-slim LABEL maintainer="Gilles Filippini <gilles.filippini@pini.fr>" LABEL version="0.0.0" @@ -55,6 +87,5 @@ ENTRYPOINT ["docker-entrypoint"] USER duniter # Intall -COPY --from=build /root/build/duniter /usr/local/bin/duniter -COPY --from=build /root/build/duniter-cucumber /usr/local/bin/duniter-cucumber +COPY --from=build /root/build /usr/local/bin/ COPY docker/docker-entrypoint /usr/local/bin/ -- GitLab