diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..7014b618e047416c60e54abf2a7f853efce90261 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,155 @@ +variables: + KUBERNETES_CPU_REQUEST: 32 + KUBERNETES_MEMORY_REQUEST: 16Gi + FORCE_PREP_TEST_ASSETS: + value: "false" + options: + - "true" + - "false" + description: "If true, forces test assets to be prepped even if cached from a previous run." + FORCE_PREP_DOCKER_ASSETS: + value: "false" + options: + - "true" + - "false" + description: "If true, forces docker assets to be prepped even if cached from a previous run." + +stages: + - prep + - build + - test + - deploy + +prep-docker-assets: + stage: prep + image: shrinkwraptool/base-slim:latest + tags: + - amd64 + cache: + key: docker-assets + policy: pull-push + paths: + - docker/assets/ + script: + - | + if [ -f ./docker/assets/.cache_exists ] && [ "$FORCE_PREP_DOCKER_ASSETS" == "false" ]; then + echo "Cache already exists and prep not forced: skipping." + else + ./docker/build.sh --version none --arch x86_64 + ./docker/build.sh --version none --arch aarch64 + echo > ./docker/assets/.cache_exists + fi + +build-docker-image: + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + parallel: + matrix: + - TAG: + - amd64 + - arm64 + tags: + - ${TAG} + cache: + key: docker-assets + policy: pull + paths: + - docker/assets/ + script: + - mkdir -p /kaniko/.docker + - echo "{\"auths\":{\"${CI_REGISTRY_IMAGE}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json + - ./docker/build.sh --driver kaniko --registry ${CI_REGISTRY_IMAGE} --version ${CI_PIPELINE_IID} + +build-docker-manifest: + stage: build + image: shrinkwraptool/base-slim:latest + tags: + - amd64 + needs: + - build-docker-image + script: + - mkdir bin + - export PATH=${PWD}/bin:${PATH} + - curl -LsS https://github.com/estesp/manifest-tool/releases/download/v2.0.6/binaries-manifest-tool-2.0.6.tar.gz | tar -xz -C bin + - ./docker/publish.sh --driver manifest-tool --user ${CI_REGISTRY_USER} --password ${CI_REGISTRY_PASSWORD} --registry ${CI_REGISTRY_IMAGE} --version ${CI_PIPELINE_IID} + +build-documentation: + stage: build + image: shrinkwraptool/base-slim:latest + tags: + - amd64 + script: + - pip3 install requests + - | + if [ ${CI_COMMIT_BRANCH} == ${CI_DEFAULT_BRANCH} ]; then + ./documentation/rtdbuild.py --domain https://readthedocs.com --token ${READTHEDOCS_TOKEN} --project ${READTHEDOCS_PROJECT} --branch latest --state active + else + ./documentation/rtdbuild.py --domain https://readthedocs.com --token ${READTHEDOCS_TOKEN} --project ${READTHEDOCS_PROJECT} --branch main --state inactive + ./documentation/rtdbuild.py --domain https://readthedocs.com --token ${READTHEDOCS_TOKEN} --project ${READTHEDOCS_PROJECT} --branch ${CI_COMMIT_BRANCH} --state inactive + fi + +prep-test-assets: + stage: prep + image: shrinkwraptool/base-slim:latest + tags: + - amd64 + cache: + key: test-assets + policy: pull-push + paths: + - test/assets/ + script: + - | + if [ -f ./test/assets/.cache_exists ] && [ "$FORCE_PREP_TEST_ASSETS" == "false" ]; then + echo "Cache already exists and prep not forced: skipping." + else + ./test/genassets.sh + echo > ./test/assets/.cache_exists + fi + +test-self: + stage: test + image: registry.gitlab.arm.com/tooling/shrinkwrap/base-full:${CI_PIPELINE_IID} + parallel: + matrix: + - TAG: + - amd64 + - arm64 + tags: + - ${TAG} + cache: + key: test-assets + policy: pull + paths: + - test/assets/ + script: + - export PATH=$PWD/shrinkwrap:$PATH + - export SHRINKWRAP_BUILD=$PWD/shrinkwrap_workspace + - export SHRINKWRAP_PACKAGE=$PWD/shrinkwrap_workspace/package + - ./test/test.py --runtime null --smoke-test --fvpjobs 4 --junit selftest-${TAG}.xml + artifacts: + name: selftest-${TAG} + paths: + - selftest-${TAG}.xml + reports: + junit: selftest-${TAG}.xml + +deploy-docker-image: + stage: deploy + image: shrinkwraptool/base-slim:latest + tags: + - amd64 + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + script: + - mkdir bin + - export PATH=${PWD}/bin:${PATH} + - curl -LsS https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 > bin/regctl + - chmod 755 bin/regctl + - regctl registry login -u ${DOCKERHUB_USER} -p ${DOCKERHUB_PASSWORD} docker.io + - regctl image copy registry.gitlab.arm.com/tooling/shrinkwrap/base-slim-nofvp:${CI_PIPELINE_IID} docker.io/shrinkwraptool/base-slim-nofvp:latest + - regctl image copy registry.gitlab.arm.com/tooling/shrinkwrap/base-slim:${CI_PIPELINE_IID} docker.io/shrinkwraptool/base-slim:latest + - regctl image copy registry.gitlab.arm.com/tooling/shrinkwrap/base-full-nofvp:${CI_PIPELINE_IID} docker.io/shrinkwraptool/base-full-nofvp:latest + - regctl image copy registry.gitlab.arm.com/tooling/shrinkwrap/base-full:${CI_PIPELINE_IID} docker.io/shrinkwraptool/base-full:latest diff --git a/docker/Dockerfile.full b/docker/Dockerfile.full index 8b45c1e81a429795b731554d6a75f1ce86003558..e007691f2d934dda1762cf50ab857ceaa15f0a11 100644 --- a/docker/Dockerfile.full +++ b/docker/Dockerfile.full @@ -20,7 +20,7 @@ RUN apt-get install --assume-yes --no-install-recommends --option=debug::pkgProb # even omit it by providing the special name "none". ARG TCH_PKG_NAME_AARCH32 ARG TCH_PATH_AARCH32 -COPY ${TCH_PKG_NAME_AARCH32} /tools/. +COPY assets/${TCH_PKG_NAME_AARCH32} /tools/. RUN cd /tools \ && if [ "${TCH_PKG_NAME_AARCH32}" != "none" ]; then \ tar xf ${TCH_PKG_NAME_AARCH32}; \ @@ -36,7 +36,7 @@ RUN apt-get install --assume-yes --no-install-recommends --option=debug::pkgProb ARG TCH_LLVM_PKG_NAME ARG TCH_LLVM_PATH -COPY ${TCH_LLVM_PKG_NAME} /tools/. +COPY assets/${TCH_LLVM_PKG_NAME} /tools/. RUN cd /tools \ && if [ "${TCH_LLVM_PKG_NAME}" != "none" ]; then \ tar xf ${TCH_LLVM_PKG_NAME}; \ diff --git a/docker/Dockerfile.fvp b/docker/Dockerfile.fvp index aa2d0be743a0bb1e79574dbdb2a0957c00de5edd..9b2026de62e69012a60ba8373649a48ffcbe6c73 100644 --- a/docker/Dockerfile.fvp +++ b/docker/Dockerfile.fvp @@ -10,7 +10,7 @@ FROM ${BASE} ARG FVP_PKG_NAME ARG FVP_MODEL_DIR ARG FVP_PLUGIN_DIR -COPY ${FVP_PKG_NAME} /tools/. +COPY assets/${FVP_PKG_NAME} /tools/. RUN cd /tools \ && if [ "${FVP_PKG_NAME}" != "none" ]; then \ tar xf ${FVP_PKG_NAME}; \ diff --git a/docker/Dockerfile.slim b/docker/Dockerfile.slim index c6bf3f686c971dba6e6678dc66f21459625f86d4..bf3beb977d5ee7c6f88f8f63a761569969a3f50c 100644 --- a/docker/Dockerfile.slim +++ b/docker/Dockerfile.slim @@ -36,7 +36,8 @@ RUN apt-get install --assume-yes --no-install-recommends --option=debug::pkgProb wget RUN pip3 install \ termcolor \ - tuxmake + tuxmake \ + pyyaml # Install packages requried by TF-A. # From https://trustedfirmware-a.readthedocs.io/en/latest/getting_started/prerequisites.html. @@ -175,7 +176,7 @@ RUN mkdir /tools # even omit it by providing the special name "none". ARG TCH_PKG_NAME_AARCH64 ARG TCH_PATH_AARCH64 -COPY ${TCH_PKG_NAME_AARCH64} /tools/. +COPY assets/${TCH_PKG_NAME_AARCH64} /tools/. RUN cd /tools \ && if [ "${TCH_PKG_NAME_AARCH64}" != "none" ]; then \ tar xf ${TCH_PKG_NAME_AARCH64}; \ diff --git a/docker/build.sh b/docker/build.sh index 528f7721f0c47cf96dc5f79d1d8091848ea41eb5..a68a145927ed976c31f8eb0ca787eb9fb6818d7d 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -1,36 +1,104 @@ -#!/bin/bash +#!/bin/sh # Copyright (c) 2022, Arm Limited. # SPDX-License-Identifier: MIT set -e -function usage() +usage() { - cat << EOF +cat << EOF Builds and optionally publishes shrinkwrap docker images for the architecture of the host system. (x86_64 and aarch64 are currently supported). Usage: -$(basename $0) +$(basename $0) [--driver docker|kaniko] [--registry ] [--arch ] --version Where: + --driver determines how the image will be built: + docker: (default) and will use "docker build" and "docker push". + This is usually what you want when running locally. + kaniko: Uses kaniko, which does not require access to the docker daemon. + Useful on CI systems where running inside a container. Must be + running inside the kaniko container. + + --arch optionally provides a target arch label that overrides the default + `uname -m`. Must be either "aarch64" or "x86_64". Unless `--version none`, + must match the arch of the machine used to run the script. For + `--version none` can be set to any supported arch to download that arch's + package cache. + + is the registry to publish to (defaults to docker.io/shrinkwraptool). is something like "latest" or "v1.0.0". -If is "local", the resulting image is NOT pushed to the remote repository. +The user is responsible for having already configured the driver connection to +the registry. + +If is "local" and --driver is "docker", the resulting image is NOT pushed +to the remote repository but instead kept locally by docker. + +If is "none", the package cache is synced but no image is built. EOF } +DRIVER="docker" +REGISTRY="docker.io/shrinkwraptool" +VERSION= +ARCH=$(uname -m) + # Parse command line. -if [ "$#" -ne 1 ]; then - usage - exit 1 +while [ $# -gt 0 ]; do + case $1 in + --version) + VERSION="$2" + shift # past argument + shift # past value + ;; + --registry) + REGISTRY="$2" + shift # past argument + shift # past value + ;; + --driver) + DRIVER="$2" + shift # past argument + shift # past value + ;; + --arch) + ARCH="$2" + shift # past argument + shift # past value + ;; + -h|--help) + usage + exit 0 + ;; + -*|--*) + echo "error: unexpected named argument! ($1)" + usage + exit 1 + ;; + *) + echo "error: unexpected positional argument! ($1)" + usage + exit 1 + ;; + esac +done + +if [ -z ${VERSION} ]; then + echo "error: no version provided!" + usage + exit 1 +fi + +if [ "${DRIVER}" != "docker" ] && [ "${DRIVER}" != "kaniko" ]; then + echo "error: invalid driver provided! (${DRIVER})" + usage + exit 1 fi -VERSION="${1}" -ARCH=$(uname -p) -REGISTRY=shrinkwraptool # Configure the arch-specific variables which are passed to the Dockerfile. -if [ "${ARCH}" == "x86_64" ]; then +if [ "${ARCH}" = "x86_64" ]; then TCH_PKG_URL_AARCH64=https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel TCH_PKG_NAME_AARCH64=arm-gnu-toolchain-13.2.rel1-x86_64-aarch64-none-elf.tar.xz TCH_PATH_AARCH64=arm-gnu-toolchain-13.2.Rel1-x86_64-aarch64-none-elf/bin @@ -44,8 +112,8 @@ if [ "${ARCH}" == "x86_64" ]; then FVP_PKG_NAME=FVP_Base_RevC-2xAEMvA_11.24_11_Linux64.tgz FVP_MODEL_DIR=Base_RevC_AEMvA_pkg/models/Linux64_GCC-9.3 FVP_PLUGIN_DIR=Base_RevC_AEMvA_pkg/plugins/Linux64_GCC-9.3 -# ARCH is "aarch64" on Ubuntu, or "arm" on Mac OS -elif [ "${ARCH}" == "aarch64" ] || [ "${ARCH}" == "arm" ]; then +# ARCH is "aarch64" on Ubuntu, or "arm64" on Mac OS +elif [ "${ARCH}" = "aarch64" ] || [ "${ARCH}" = "arm64" ]; then TCH_PKG_URL_AARCH64=https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel TCH_PKG_NAME_AARCH64=arm-gnu-toolchain-13.2.rel1-aarch64-aarch64-none-elf.tar.xz TCH_PATH_AARCH64=arm-gnu-toolchain-13.2.Rel1-aarch64-aarch64-none-elf/bin @@ -61,57 +129,123 @@ elif [ "${ARCH}" == "aarch64" ] || [ "${ARCH}" == "arm" ]; then FVP_PLUGIN_DIR=Base_RevC_AEMvA_pkg/plugins/Linux64_armv8l_GCC-9.3 else echo "Host architecture ${ARCH} not supported" + usage exit 1 fi -echo "Building for version ${VERSION} for ${ARCH}..." - -# Build the image. -wget -q -O ${TCH_PKG_NAME_AARCH64} ${TCH_PKG_URL_AARCH64}/${TCH_PKG_NAME_AARCH64} -wget -q -O ${TCH_LLVM_PKG_NAME} ${TCH_LLVM_PKG_URL}/${TCH_LLVM_PKG_NAME} -wget -q -O ${TCH_PKG_NAME_AARCH32} ${TCH_PKG_URL_AARCH32}/${TCH_PKG_NAME_AARCH32} -wget -q -O ${FVP_PKG_NAME} ${FVP_PKG_URL}/${FVP_PKG_NAME} -docker build \ - --build-arg=BASE=docker.io/library/debian:bookworm-slim \ - --build-arg=TCH_PKG_NAME_AARCH64=${TCH_PKG_NAME_AARCH64} \ - --build-arg=TCH_PATH_AARCH64=${TCH_PATH_AARCH64} \ - --file=Dockerfile.slim \ - --tag=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ - . -docker build \ - --build-arg=BASE=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ - --build-arg=FVP_PKG_NAME=${FVP_PKG_NAME} \ - --build-arg=FVP_MODEL_DIR=${FVP_MODEL_DIR} \ - --build-arg=FVP_PLUGIN_DIR=${FVP_PLUGIN_DIR} \ - --file=Dockerfile.fvp \ - --tag=${REGISTRY}/base-slim:${VERSION}-${ARCH} \ - . -docker build \ - --build-arg=BASE=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ - --build-arg=TCH_PKG_NAME_AARCH32=${TCH_PKG_NAME_AARCH32} \ - --build-arg=TCH_PATH_AARCH32=${TCH_PATH_AARCH32} \ - --build-arg=TCH_LLVM_PKG_NAME=${TCH_LLVM_PKG_NAME} \ - --build-arg=TCH_LLVM_PATH=${TCH_LLVM_PATH} \ - --file=Dockerfile.full \ - --tag=${REGISTRY}/base-full-nofvp:${VERSION}-${ARCH} \ - . -docker build \ - --build-arg=BASE=${REGISTRY}/base-full-nofvp:${VERSION}-${ARCH} \ - --build-arg=FVP_PKG_NAME=${FVP_PKG_NAME} \ - --build-arg=FVP_MODEL_DIR=${FVP_MODEL_DIR} \ - --build-arg=FVP_PLUGIN_DIR=${FVP_PLUGIN_DIR} \ - --file=Dockerfile.fvp \ - --tag=${REGISTRY}/base-full:${VERSION}-${ARCH} \ - . -rm -rf ${TCH_PKG_NAME_AARCH64} > /dev/null 2>&1 || true -rm -rf ${TCH_LLVM_PKG_NAME} > /dev/null 2>&1 || true -rm -rf ${TCH_PKG_NAME_AARCH32} > /dev/null 2>&1 || true -rm -rf ${FVP_PKG_NAME} > /dev/null 2>&1 || true - -# If not a local version, publish the image. -if [ "${VERSION}" != "local" ]; then - docker push ${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} - docker push ${REGISTRY}/base-slim:${VERSION}-${ARCH} - docker push ${REGISTRY}/base-full-nofvp:${VERSION}-${ARCH} - docker push ${REGISTRY}/base-full:${VERSION}-${ARCH} +echo "Building image for ${ARCH} with driver=${DRIVER},registry=${REGISTRY},version=${VERSION}..." + +wget_or_cache() +{ + FILE=$1 + URL=$2 + + if [ ! -f ${FILE} ]; then + wget -q -O ${FILE} ${URL} + fi +} + +# Do everything relative to this script's directory. +ROOT=$( dirname $( readlink -f "$0" ) ) +cd ${ROOT} + +# Grab the pre-built packages. +mkdir -p assets +wget_or_cache assets/${TCH_PKG_NAME_AARCH64} ${TCH_PKG_URL_AARCH64}/${TCH_PKG_NAME_AARCH64} +wget_or_cache assets/${TCH_LLVM_PKG_NAME} ${TCH_LLVM_PKG_URL}/${TCH_LLVM_PKG_NAME} +wget_or_cache assets/${TCH_PKG_NAME_AARCH32} ${TCH_PKG_URL_AARCH32}/${TCH_PKG_NAME_AARCH32} +wget_or_cache assets/${FVP_PKG_NAME} ${FVP_PKG_URL}/${FVP_PKG_NAME} + +# Short circuit building the images if requested. +if [ "${VERSION}" = "none" ]; then + exit +fi + +# Build the images. +if [ "${DRIVER}" = "docker" ]; then + docker build \ + --build-arg=BASE=registry.gitlab.arm.com/tooling/shrinkwrap/bookworm-slim \ + --build-arg=TCH_PKG_NAME_AARCH64=${TCH_PKG_NAME_AARCH64} \ + --build-arg=TCH_PATH_AARCH64=${TCH_PATH_AARCH64} \ + --file=Dockerfile.slim \ + --tag=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ + . + docker build \ + --build-arg=BASE=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ + --build-arg=FVP_PKG_NAME=${FVP_PKG_NAME} \ + --build-arg=FVP_MODEL_DIR=${FVP_MODEL_DIR} \ + --build-arg=FVP_PLUGIN_DIR=${FVP_PLUGIN_DIR} \ + --file=Dockerfile.fvp \ + --tag=${REGISTRY}/base-slim:${VERSION}-${ARCH} \ + . + docker build \ + --build-arg=BASE=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ + --build-arg=TCH_PKG_NAME_AARCH32=${TCH_PKG_NAME_AARCH32} \ + --build-arg=TCH_PATH_AARCH32=${TCH_PATH_AARCH32} \ + --build-arg=TCH_LLVM_PKG_NAME=${TCH_LLVM_PKG_NAME} \ + --build-arg=TCH_LLVM_PATH=${TCH_LLVM_PATH} \ + --file=Dockerfile.full \ + --tag=${REGISTRY}/base-full-nofvp:${VERSION}-${ARCH} \ + . + docker build \ + --build-arg=BASE=${REGISTRY}/base-full-nofvp:${VERSION}-${ARCH} \ + --build-arg=FVP_PKG_NAME=${FVP_PKG_NAME} \ + --build-arg=FVP_MODEL_DIR=${FVP_MODEL_DIR} \ + --build-arg=FVP_PLUGIN_DIR=${FVP_PLUGIN_DIR} \ + --file=Dockerfile.fvp \ + --tag=${REGISTRY}/base-full:${VERSION}-${ARCH} \ + . + + # If not a local version, publish the image. + if [ "${VERSION}" != "local" ]; then + docker push ${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} + docker push ${REGISTRY}/base-slim:${VERSION}-${ARCH} + docker push ${REGISTRY}/base-full-nofvp:${VERSION}-${ARCH} + docker push ${REGISTRY}/base-full:${VERSION}-${ARCH} + fi +elif [ "${DRIVER}" = "kaniko" ]; then + # Build the images. + /kaniko/executor \ + --cleanup \ + --cache=true --cache-copy-layers --cache-run-layers \ + --build-arg=BASE=registry.gitlab.arm.com/tooling/shrinkwrap/bookworm-slim \ + --build-arg=TCH_PKG_NAME_AARCH64=${TCH_PKG_NAME_AARCH64} \ + --build-arg=TCH_PATH_AARCH64=${TCH_PATH_AARCH64} \ + --dockerfile=Dockerfile.slim \ + --destination=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ + --context=. + /kaniko/executor \ + --cleanup \ + --cache=true --cache-copy-layers --cache-run-layers \ + --build-arg=BASE=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ + --build-arg=FVP_PKG_NAME=${FVP_PKG_NAME} \ + --build-arg=FVP_MODEL_DIR=${FVP_MODEL_DIR} \ + --build-arg=FVP_PLUGIN_DIR=${FVP_PLUGIN_DIR} \ + --dockerfile=Dockerfile.fvp \ + --destination=${REGISTRY}/base-slim:${VERSION}-${ARCH} \ + --context=. + /kaniko/executor \ + --cleanup \ + --cache=true --cache-copy-layers --cache-run-layers \ + --build-arg=BASE=${REGISTRY}/base-slim-nofvp:${VERSION}-${ARCH} \ + --build-arg=TCH_PKG_NAME_AARCH32=${TCH_PKG_NAME_AARCH32} \ + --build-arg=TCH_PATH_AARCH32=${TCH_PATH_AARCH32} \ + --build-arg=TCH_LLVM_PKG_NAME=${TCH_LLVM_PKG_NAME} \ + --build-arg=TCH_LLVM_PATH=${TCH_LLVM_PATH} \ + --dockerfile=Dockerfile.full \ + --destination=${REGISTRY}/base-full-nofvp:${VERSION}-${ARCH} \ + --context=. + /kaniko/executor \ + --cleanup \ + --cache=true --cache-copy-layers --cache-run-layers \ + --build-arg=BASE=${REGISTRY}/base-full-nofvp:${VERSION}-${ARCH} \ + --build-arg=FVP_PKG_NAME=${FVP_PKG_NAME} \ + --build-arg=FVP_MODEL_DIR=${FVP_MODEL_DIR} \ + --build-arg=FVP_PLUGIN_DIR=${FVP_PLUGIN_DIR} \ + --dockerfile=Dockerfile.fvp \ + --destination=${REGISTRY}/base-full:${VERSION}-${ARCH} \ + --context=. +else + echo "Driver ${DRIVER} not supported" + exit 1 fi diff --git a/docker/publish.sh b/docker/publish.sh index 48f3f6830a2bbbf803f280e08927c1adbca879ab..e89e0c4d7167b32234498f90ad5e314db2ac6da0 100755 --- a/docker/publish.sh +++ b/docker/publish.sh @@ -1,59 +1,173 @@ -#!/bin/bash +#!/bin/sh # Copyright (c) 2022, Arm Limited. # SPDX-License-Identifier: MIT -set -ex +set -e -function usage() +usage() { - cat << EOF +cat << EOF Creates a manifest list for a multiarch docker image and publishes it so that users can pull an image using a generic name and the variant for their machine architecture is automatically selected. Usage: -$(basename $0) +$(basename $0) [--driver docker|manifest-tool] [--user --password ] [--registry ] --version Where: + --driver determines how the image will be published: + docker: (default) and will use "docker manifest". This is usually + what you want when running locally. + manifest-tool: Uses manifest-tool, which does not require access to the + docker daemon. Useful on CI systems where running inside a + container. + + is required by manifest-tool to access the registry + is required by manifest-tool to access the registry + is the registry to publish to (defaults to docker.io/shrinkwraptool). is something like "latest" or "v1.0.0". An image for each of the supported architectures must have already been built and published with . + +The user is responsible for having already configured the driver connection to +the registry. EOF } +DRIVER="docker" +USER= +PASSWORD= +REGISTRY="docker.io/shrinkwraptool" +VERSION= + # Parse command line. -if [ "$#" -ne 1 ]; then - usage - exit 1 +while [ $# -gt 0 ]; do + case $1 in + --version) + VERSION="$2" + shift # past argument + shift # past value + ;; + --registry) + REGISTRY="$2" + shift # past argument + shift # past value + ;; + --driver) + DRIVER="$2" + shift # past argument + shift # past value + ;; + --user) + USER="$2" + shift # past argument + shift # past value + ;; + --password) + PASSWORD="$2" + shift # past argument + shift # past value + ;; + -h|--help) + usage + exit 0 + ;; + -*|--*) + echo "error: unexpected named argument! ($1)" + usage + exit 1 + ;; + *) + echo "error: unexpected positional argument! ($1)" + usage + exit 1 + ;; + esac +done + +if [ -z ${VERSION} ]; then + echo "error: no version provided!" + usage + exit 1 +fi + +if [ "${DRIVER}" != "docker" ] && [ "${DRIVER}" != "manifest-tool" ]; then + echo "error: invalid driver provided! (${DRIVER})" + usage + exit 1 +fi + +if [ "${DRIVER}" = "manifest-tool" ]; then + if [ -z ${USER} ] || [ -z ${PASSWORD} ]; then + echo "error: --user and --password are required when using manifest-tool!" + usage + exit 1 + fi +fi + +echo "Publishing multiarch image with driver=${DRIVER},registry=${REGISTRY},version=${VERSION}..." + +# Do everything relative to this script's directory. +ROOT=$( dirname $( readlink -f "$0" ) ) +cd ${ROOT} + +if [ "${DRIVER}" = "docker" ]; then + # base-slim-nofvp + docker manifest create ${REGISTRY}/base-slim-nofvp:${VERSION} \ + ${REGISTRY}/base-slim-nofvp:${VERSION}-aarch64 \ + ${REGISTRY}/base-slim-nofvp:${VERSION}-x86_64 + docker manifest push ${REGISTRY}/base-slim-nofvp:${VERSION} + docker manifest rm ${REGISTRY}/base-slim-nofvp:${VERSION} + + # base-slim + docker manifest create ${REGISTRY}/base-slim:${VERSION} \ + ${REGISTRY}/base-slim:${VERSION}-aarch64 \ + ${REGISTRY}/base-slim:${VERSION}-x86_64 + docker manifest push ${REGISTRY}/base-slim:${VERSION} + docker manifest rm ${REGISTRY}/base-slim:${VERSION} + + # base-full-nofvp + docker manifest create ${REGISTRY}/base-full-nofvp:${VERSION} \ + ${REGISTRY}/base-full-nofvp:${VERSION}-aarch64 \ + ${REGISTRY}/base-full-nofvp:${VERSION}-x86_64 + docker manifest push ${REGISTRY}/base-full-nofvp:${VERSION} + docker manifest rm ${REGISTRY}/base-full-nofvp:${VERSION} + + # base-full + docker manifest create ${REGISTRY}/base-full:${VERSION} \ + ${REGISTRY}/base-full:${VERSION}-aarch64 \ + ${REGISTRY}/base-full:${VERSION}-x86_64 + docker manifest push ${REGISTRY}/base-full:${VERSION} + docker manifest rm ${REGISTRY}/base-full:${VERSION} +elif [ "${DRIVER}" = "manifest-tool" ]; then + manifest_publish() + { + NAME=$1 + MANIFEST=$(mktemp) + + cat << EOF > ${MANIFEST} +image: ${REGISTRY}/${NAME}:${VERSION} +manifests: + - image: ${REGISTRY}/${NAME}:${VERSION}-aarch64 + platform: + architecture: arm64 + os: linux + - image: ${REGISTRY}/${NAME}:${VERSION}-x86_64 + platform: + architecture: amd64 + os: linux +EOF + + manifest-tool-linux-amd64 --username ${USER} --password ${PASSWORD} push from-spec ${MANIFEST} + rm -rf ${MANIFEST} + } + + manifest_publish "base-slim-nofvp" + manifest_publish "base-slim" + manifest_publish "base-full-nofvp" + manifest_publish "base-full" +else + echo "Driver ${DRIVER} not supported" + exit 1 fi -VERSION="$1" -REGISTRY=shrinkwraptool - -# base-slim-nofvp -docker manifest create ${REGISTRY}/base-slim-nofvp:${VERSION} \ - ${REGISTRY}/base-slim-nofvp:${VERSION}-aarch64 \ - ${REGISTRY}/base-slim-nofvp:${VERSION}-x86_64 -docker manifest push ${REGISTRY}/base-slim-nofvp:${VERSION} -docker manifest rm ${REGISTRY}/base-slim-nofvp:${VERSION} - -# base-slim -docker manifest create ${REGISTRY}/base-slim:${VERSION} \ - ${REGISTRY}/base-slim:${VERSION}-aarch64 \ - ${REGISTRY}/base-slim:${VERSION}-x86_64 -docker manifest push ${REGISTRY}/base-slim:${VERSION} -docker manifest rm ${REGISTRY}/base-slim:${VERSION} - -# base-full-nofvp -docker manifest create ${REGISTRY}/base-full-nofvp:${VERSION} \ - ${REGISTRY}/base-full-nofvp:${VERSION}-aarch64 \ - ${REGISTRY}/base-full-nofvp:${VERSION}-x86_64 -docker manifest push ${REGISTRY}/base-full-nofvp:${VERSION} -docker manifest rm ${REGISTRY}/base-full-nofvp:${VERSION} - -# base-full -docker manifest create ${REGISTRY}/base-full:${VERSION} \ - ${REGISTRY}/base-full:${VERSION}-aarch64 \ - ${REGISTRY}/base-full:${VERSION}-x86_64 -docker manifest push ${REGISTRY}/base-full:${VERSION} -docker manifest rm ${REGISTRY}/base-full:${VERSION} diff --git a/documentation/rtdbuild.py b/documentation/rtdbuild.py new file mode 100755 index 0000000000000000000000000000000000000000..debd63ab49bf7c732a99008d9516bae8984c92ab --- /dev/null +++ b/documentation/rtdbuild.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +# (C) COPYRIGHT 2022 ARM Limited or its affiliates. +# ALL RIGHTS RESERVED + + +import requests +import time +import argparse + + +parser = argparse.ArgumentParser(description='Build documentation using readthedocs.') +parser.add_argument('--domain', default='https://readthedocs.com', + help='readthedocs url (default=https://readthedocs.com)') +parser.add_argument('--token', required=True, + help='readthedocs authentication token') +parser.add_argument('--project', required=True, + help='readthedocs project slug') +parser.add_argument('--branch', required=True, help='git branch to build') +parser.add_argument('--timeout', default=240, type=int, + help='timeout in seconds (default=240)') +parser.add_argument('--state', default='inactive', choices=['inactive', 'active', 'hidden'], + help='target documentation state') + + +args = parser.parse_args() + + +def git_branch_to_rtd_version(branch): + return branch.replace('/', '-') + + +def build_readthedocs(domain, token, project, version='latest', timeout=240): + """ + Requests that the readthedocs server builds the specified version of the specified project, + and waits for the build to complete. + + The following error codes may be returned: + 0: Docs built successfully + 1: Docs failed to build + 2: General error (domain incorrect, project/version unknown, timeout, etc) + """ + try: + headers = {'Authorization': f'token {token}'} + + print(f'INFO: setting state for {project}/{version} to {args.state}') + + update_version_url = f'{domain}/api/v3/projects/{project}/versions/{version}/' + state = { + "active": args.state in ['active', 'hidden'], + "hidden": args.state == 'hidden', + "privacy_level": "public", + } + rsp = requests.patch(update_version_url, headers=headers, json=state) + if not rsp.ok: + print(f'ERROR: project/version unknown ({project}, {version})') + return 2 + + print(f'INFO: building docs for {project}/{version}') + + start_build_url = f'{domain}/api/v3/projects/{project}/versions/{version}/builds/' + rsp = requests.post(start_build_url, headers=headers) + if not rsp.ok: + print(f'ERROR: project/version unknown ({project}, {version})') + return 2 + + build_id = rsp.json()["build"]["id"] + query_build_url = f'{domain}/api/v3/projects/{project}/builds/{build_id}/' + + start = time.perf_counter() + success = None + while success is None and time.perf_counter() - start < timeout: + rsp = requests.get(query_build_url, headers=headers).json() + success = rsp['success'] + if success is None: + time.sleep(5) + + if success is True: + print(f'INFO: docs built successfully for {project}, {version}') + return 0 + elif success is False: + print(f'INFO: docs failed to build for {project}, {version}') + return 1 + else: + raise TimeoutError('timeout') + + except Exception as err: + print(f'ERROR: server unresponsive: {err}') + return 2 + + +exit(build_readthedocs(args.domain, + args.token, + args.project, + git_branch_to_rtd_version(args.branch), + args.timeout)) diff --git a/test/assets/buildroot.config b/test/buildroot.config similarity index 100% rename from test/assets/buildroot.config rename to test/buildroot.config diff --git a/test/assets/genassets.sh b/test/genassets.sh similarity index 81% rename from test/assets/genassets.sh rename to test/genassets.sh index 35373f0e5d14993b564764740537ed6da988c38d..f9e4827c661aa4fd89a8854544388bb9f4f0d9a3 100755 --- a/test/assets/genassets.sh +++ b/test/genassets.sh @@ -11,12 +11,15 @@ # Exit on error and echo commands. set -ex -ASSETS_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -BUILD_DIR=${ASSETS_DIR}/build +SOURCE_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ASSETS_DIR=${SOURCE_DIR}/assets +BUILD_DIR=${SOURCE_DIR}/build export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- -# Delete any previous build directory and start from scratch. +# Delete any previous assets and build directories and start from scratch. +rm -rf ${ASSETS_DIR} &> /dev/null +mkdir -p ${ASSETS_DIR} rm -rf ${BUILD_DIR} &> /dev/null mkdir -p ${BUILD_DIR} pushd ${BUILD_DIR} @@ -39,7 +42,7 @@ chmod +x buildroot_overlay/etc/init.d/S10poweroff git clone https://github.com/buildroot/buildroot.git cd buildroot git checkout 2022.05.3 -cp ${ASSETS_DIR}/buildroot.config .config +cp ${SOURCE_DIR}/buildroot.config .config ./utils/config --set-val BR2_ROOTFS_OVERLAY "\"${BUILD_DIR}/buildroot_overlay\"" make olddefconfig make BR2_JLEVEL=`nproc` @@ -56,7 +59,7 @@ cp arch/arm64/boot/Image ${ASSETS_DIR}/. cd - # Build a bootwrapper axf. -git clone git://git.kernel.org/pub/scm/linux/kernel/git/mark/boot-wrapper-aarch64.git +git clone https://git.kernel.org/pub/scm/linux/kernel/git/mark/boot-wrapper-aarch64.git cd boot-wrapper-aarch64 autoreconf -i ./configure \ diff --git a/test/test.py b/test/test.py index 54ef6a0f066e3e69556f7e56a22c564340f24507..78051d572a8029d909e771e43d8c1b002f919f48 100755 --- a/test/test.py +++ b/test/test.py @@ -5,15 +5,19 @@ import argparse import json +import multiprocessing as mp import os import re import subprocess +import sys import tempfile +from xml.sax.saxutils import escape, quoteattr import yaml RUNTIME = None IMAGE = None +FVPJOBS = None ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets') @@ -27,45 +31,43 @@ CONFIGS = [ { 'config': 'ns-preload.yaml', 'btvars': {}, - 'rtvars': {'KERNEL': KERNEL, 'ROOTFS': ROOTFS}, - 'arch': {'start': 'v8.0', 'end': ARCH_LATEST}, - }, - { - 'config': 'ns-edk2.yaml', - 'btvars': {}, - 'rtvars': {'KERNEL': KERNEL, 'ROOTFS': ROOTFS}, + 'rtvars': { + 'default': {'KERNEL': KERNEL, 'ROOTFS': ROOTFS}, + }, 'arch': {'start': 'v8.0', 'end': ARCH_LATEST}, }, { 'config': 'ns-edk2.yaml', 'btvars': {}, 'rtvars': { - 'KERNEL': KERNEL, - 'ROOTFS': ROOTFS, - 'CMDLINE': '\"console=ttyAMA0 earlycon=pl011,0x1c090000 root=/dev/vda ip=dhcp acpi=force\"' + 'dt': {'KERNEL': KERNEL, 'ROOTFS': ROOTFS}, + 'acpi': { + 'KERNEL': KERNEL, + 'ROOTFS': ROOTFS, + 'CMDLINE': '\"console=ttyAMA0 earlycon=pl011,0x1c090000 root=/dev/vda ip=dhcp acpi=force\"' + }, }, 'arch': {'start': 'v8.0', 'end': ARCH_LATEST}, }, - { - 'config': 'ffa-tftf.yaml', - 'btvars': {}, - 'rtvars': {'KERNEL': KERNEL, 'ROOTFS': ROOTFS}, - 'arch': {'start': 'v8.5', 'end': 'v8.7'}, # BL2 freezes from v8.8. Haven't traced root cause yet. - }, { 'config': 'ffa-tftf.yaml', 'btvars': {}, 'rtvars': { - 'KERNEL': KERNEL, - 'ROOTFS': ROOTFS, - 'CMDLINE': '\"console=ttyAMA0 earlycon=pl011,0x1c090000 root=/dev/vda ip=dhcp acpi=force\"' + 'dt': {'KERNEL': KERNEL, 'ROOTFS': ROOTFS}, + 'acpi': { + 'KERNEL': KERNEL, + 'ROOTFS': ROOTFS, + 'CMDLINE': '\"console=ttyAMA0 earlycon=pl011,0x1c090000 root=/dev/vda ip=dhcp acpi=force\"' + }, }, 'arch': {'start': 'v8.5', 'end': 'v8.7'}, # BL2 freezes from v8.8. Haven't traced root cause yet. }, { 'config': 'bootwrapper.yaml', 'btvars': {}, - 'rtvars': {'BOOTWRAPPER': BOOTWRAPPER, 'ROOTFS': ROOTFS}, + 'rtvars': { + 'default': {'BOOTWRAPPER': BOOTWRAPPER, 'ROOTFS': ROOTFS}, + }, 'arch': {'start': 'v8.0', 'end': ARCH_LATEST}, }, ] @@ -109,72 +111,94 @@ def arch_in_range(arch, start, end): return start <= arch and arch <= end -def print_result(r): - def report(status, type, config, overlay): - desc = f'{status.upper()}: {type}: {config},{overlay}' - count = (1, 0) if status == 'pass' else (0, 1) - return count[0], count[1], desc - - if r['type'] == 'build': - configs = r['configs'] - elif r['type'] == 'run': - configs = [r['config']] +def test_name(r): + def add_part(parts, result, name): + if name in result and result[name]: + if not (name == 'tag' and result[name] == 'default'): + parts.append(result[name]) + + parts = [] + add_part(parts, r, 'type') + add_part(parts, r, 'config') + add_part(parts, r, 'overlay') + add_part(parts, r, 'tag') + return ':'.join(parts) + + +def print_testcase(f, case): + if case['status'] == 'pass': + element = 'system-out' + prop = '' + if case['status'] == 'fail': + element = 'failure' + prop = ' type="failure"' + if case['status'] == 'error': + element = 'error' + prop = ' type="error"' + if case['status'] == 'skip': + element = 'skipped' + prop = ' type="skipped"' + + print(f' ', file=f) + if case['error'] == None: + print(f' <{element}{prop}/>', file=f) else: - assert(False) + print(f' <{element}{prop}>', file=f) + print(escape(case['error']), file=f) + print(f' ', file=f) + print(' ', file=f) + + +def print_testsuite(f, suitename, cases): + print(f' ', file=f) + for case in cases: + print_testcase(f, case) + print(' ', file=f) - nr_pass = 0 - nr_fail = 0 - for c in configs: - p, f, desc = report(r['status'], - r['type'], - c, - r['overlay']) - nr_pass += p - nr_fail += f - print(desc) - return nr_pass, nr_fail +def print_junit_results(f, suitename, cases): + print('', file=f) + print('', file=f) + print_testsuite(f, suitename, cases) + print('', file=f) -def print_results(): - print('TEST REPORT JSON') - print(json.dumps(results, indent=4)) +def print_results(junit=None): + if junit: + with open(junit, 'w') as f: + print_junit_results(f, 'selftest', results) nr_pass = 0 - nr_fail = 0 print('TEST REPORT SUMMARY') for r in results: - p, f = print_result(r) - nr_pass += p - nr_fail += f + print(f'{r["status"].upper()}: {test_name(r)}') + if r['status'] == 'pass': + nr_pass += 1 - print(f'pass: {nr_pass}, fail: {nr_fail}') + print(f'pass: {nr_pass}, fail: {len(results) - nr_pass}') + return nr_pass == len(results) -class WrongExit(Exception): - pass - -def run(cmd, timeout=None, expect=0): +def run(cmd, timeout=None, expect=0, capture=False): print(f'+ {cmd}') - ret = subprocess.run(cmd, timeout=timeout, shell=True) + ret = subprocess.run(cmd, timeout=timeout, shell=True, + universal_newlines=True, + stdout=subprocess.PIPE if capture else None, + stderr=subprocess.STDOUT if capture else None) if ret.returncode != expect: - raise WrongExit(ret) + raise subprocess.CalledProcessError(ret.returncode, ret.args, + output=ret.stdout, stderr=ret.stderr) + return ret.stdout def build_configs(configs, overlay=None, btvarss=None): - result = { - 'type': 'build', - 'status': 'fail', - 'error': None, - 'configs': configs, - 'overlay': overlay, - 'btvarss': btvarss, - } + + status = 'pass' + error = None rt = f'-R {RUNTIME} -I {IMAGE}' - overlay = f'-o {overlay}' if overlay else '' - cleanargs = f'{" ".join(configs)} {overlay}' + cleanargs = f'{" ".join(configs)} {f"-o {overlay}" if overlay else ""}' if btvarss is None: btvarss = [{}] * len(configs) @@ -195,62 +219,92 @@ def build_configs(configs, overlay=None, btvarss=None): version=(1, 2)) with open(tmpfilename, 'r') as tmpfile: print(tmpfile.read()) - buildargs = f'{tmpfilename} {overlay}' + buildargs = f'{tmpfilename} {f"-o {overlay}" if overlay else ""}' try: run(f'shrinkwrap {rt} clean {cleanargs}', None) run(f'shrinkwrap {rt} buildall {buildargs}', None) - result['status'] = 'pass' except Exception as e: - result['error'] = str(e) + status = 'fail' + error = str(e) + + global results + results += [{ + 'type': 'build', + 'status': status, + 'error': error, + 'config': config, + 'overlay': overlay, + 'btvars': btvars, + } for config, btvars in zip(configs, btvarss)] + + +def run_config(config, overlay, rtvars, tag, capture): - results.append(result) + def make_rtcmds(rtvars): + return ' '.join([f'-r {k}={v}' for k, v in rtvars.items()]) + runargs = make_rtcmds(rtvars) -def run_config(config, overlay=None, runargs=None, runtime=600): result = { 'type': 'run', 'status': 'fail', 'error': None, 'config': config, 'overlay': overlay, - 'runargs': runargs, - 'runtime': runtime, + 'rtvars': rtvars, + 'tag': tag, } rt = f'-R {RUNTIME} -I {IMAGE}' overlay = f'-o {overlay}' if overlay else '' - runargs = runargs if runargs else '' args = f'{config} {overlay} {runargs}' try: - run(f'shrinkwrap {rt} run {args}', runtime) + stdout = run(f'shrinkwrap {rt} run {args}', + timeout=600, capture=capture) result['status'] = 'pass' + except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e: + stdout = e.stdout + result['error'] = str(e) except Exception as e: + stdout = None result['error'] = str(e) - results.append(result) + return result, stdout + +def run_configs(configs, overlay=None, rtvarss=None): -def make_rtcmds(rtvars): - return ' '.join([f'-r {k}={v}' for k, v in rtvars.items()]) + if rtvarss is None: + rtvarss = [{'default': {}}] * len(configs) + params = [] + for config, _rtvars in zip(configs, rtvarss): + for tag, rtvars in _rtvars.items(): + params.append((config, overlay, rtvars, tag, FVPJOBS > 1)) -def do_main(smoke_test): - if smoke_test: + with mp.Pool(processes=FVPJOBS) as pool: + for result, stdout in pool.starmap(run_config, params): + results.append(result) + if stdout: + sys.stdout.write(stdout) + + +def do_main(args): + if args.smoke_test: arches = set([c['arch']['end'] for c in CONFIGS]) else: arches = list(arch_range('v8.0', ARCH_LATEST)) for arch in arches: - configs = [c['config'] for c in CONFIGS if arch_in_range(arch, c['arch']['end'] if smoke_test else c['arch']['start'], c['arch']['end'])] - btvarss = [c['btvars'] for c in CONFIGS if arch_in_range(arch, c['arch']['end'] if smoke_test else c['arch']['start'], c['arch']['end'])] - rtvarss = [c['rtvars'] for c in CONFIGS if arch_in_range(arch, c['arch']['end'] if smoke_test else c['arch']['start'], c['arch']['end'])] + configs = [c['config'] for c in CONFIGS if arch_in_range(arch, c['arch']['end'] if args.smoke_test else c['arch']['start'], c['arch']['end'])] + btvarss = [c['btvars'] for c in CONFIGS if arch_in_range(arch, c['arch']['end'] if args.smoke_test else c['arch']['start'], c['arch']['end'])] + rtvarss = [c['rtvars'] for c in CONFIGS if arch_in_range(arch, c['arch']['end'] if args.smoke_test else c['arch']['start'], c['arch']['end'])] if len(configs) == 0: continue build_configs(configs, f'arch/{arch}.yaml', btvarss=btvarss) - for config, rtvars in zip(configs, rtvarss): - run_config(config, f'arch/{arch}.yaml', make_rtcmds(rtvars)) + run_configs(configs, f'arch/{arch}.yaml', rtvarss=rtvarss) # Special-case configs that don't support arch overrides. build_configs(['cca-3world.yaml', 'cca-4world.yaml'], @@ -258,10 +312,14 @@ def do_main(smoke_test): {'GUEST_ROOTFS': ROOTFS}, {'GUEST_ROOTFS': ROOTFS} ]) - run_config('cca-3world.yaml', None, make_rtcmds({'KERNEL': KERNEL, 'ROOTFS': ROOTFS})) - run_config('cca-4world.yaml', None, make_rtcmds({'KERNEL': KERNEL, 'ROOTFS': ROOTFS})) + run_configs(['cca-3world.yaml', 'cca-4world.yaml'], None, + rtvarss=[ + {'default': {'KERNEL': KERNEL, 'ROOTFS': ROOTFS}}, + {'default': {'KERNEL': KERNEL, 'ROOTFS': ROOTFS}}, + ]) - print_results() + success = print_results(args.junit) + exit(not success) def main(): @@ -286,6 +344,14 @@ def main(): help="""If using a container runtime, specifies the name of the image to use. Defaults to the official shrinkwrap image.""") + parser.add_argument('-j', '--junit', + metavar='file', required=False, default=None, + help="""Optionally output results in junit format to specified file.""") + + parser.add_argument('-f', '--fvpjobs', + metavar='count', required=False, default=1, type=int, + help="""Maximum number of FVPs to run in parallel.""") + parser.add_argument('-s', '--smoke-test', required=False, default=False, action='store_true', help="""If specified, run a smaller selection of tests.""") @@ -294,10 +360,12 @@ def main(): global RUNTIME global IMAGE + global FVPJOBS RUNTIME = args.runtime IMAGE = args.image + FVPJOBS = args.fvpjobs - do_main(args.smoke_test) + do_main(args) if __name__ == "__main__":