diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 571a9555..4f706373 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -16,6 +16,18 @@ jobs: steps: - name: checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.7.1 + - name: Cache tool binaries + id: cache-tools + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: tools-cache + key: perfspect-tools-binaries-${{ hashFiles('tools/Makefile', 'tools/**/*.Dockerfile', 'tools/**/*.patch') }} + - name: Set cache hit flag + if: steps.cache-tools.outputs.cache-hit == 'true' + run: | + echo "TOOLS_CACHE_HIT=true" >> $GITHUB_ENV - name: build perfspect run: | builder/build.sh diff --git a/.gitignore b/.gitignore index f685c188..3fef3bae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /debug_out /tools/bin /tools/bin-aarch64 +/tools-cache /dist /internal/script/resources/x86_64 /internal/script/resources/aarch64 diff --git a/Makefile b/Makefile index 9e87ec5b..55c09a79 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ check_static: .PHONY: check_license check_license: @echo "Confirming source files have license headers..." - @for f in `find . -type f ! -path './perfspect_202*' ! -path './tools/bin/*' ! -path './tools/bin-aarch64/*' ! -path './internal/script/resources/*' ! -path './scripts/.venv/*' ! -path './test/output/*' ! -path './debug_out/*' ! -path './tools/perf-archive/*' ! -path './tools/avx-turbo/*' \( -name "*.go" -o -name "*.s" -o -name "*.html" -o -name "Makefile" -o -name "*.sh" -o -name "*.Dockerfile" -o -name "*.py" \)`; do \ + @for f in `find . -type f ! -path './perfspect_202*' ! -path './tools/bin/*' ! -path './tools/bin-aarch64/*' ! -path './tools-cache/*' ! -path './internal/script/resources/*' ! -path './scripts/.venv/*' ! -path './test/output/*' ! -path './debug_out/*' ! -path './tools/perf-archive/*' ! -path './tools/avx-turbo/*' \( -name "*.go" -o -name "*.s" -o -name "*.html" -o -name "Makefile" -o -name "*.sh" -o -name "*.Dockerfile" -o -name "*.py" \)`; do \ if ! grep -E 'SPDX-License-Identifier: BSD-3-Clause' "$$f" >/dev/null; then echo "Error: license not found: $$f"; fail=1; fi; \ done; if [ -n "$$fail" ]; then exit 1; fi @@ -162,9 +162,14 @@ sweep: rm -f perfspect.log .PHONY: clean -clean: sweep +clean: sweep clean-tools-cache @echo "Cleaning up..." rm -f perfspect rm -f perfspect-aarch64 sudo rm -rf dist rm -rf internal/script/resources + +.PHONY: clean-tools-cache +clean-tools-cache: + @echo "Removing cached tool binaries..." + rm -rf tools-cache diff --git a/builder/build.Dockerfile b/builder/build.Dockerfile index da46ad0e..a92421fa 100644 --- a/builder/build.Dockerfile +++ b/builder/build.Dockerfile @@ -7,22 +7,33 @@ # build perfspect: # $ docker run --rm -v "$PWD":/localrepo -w /localrepo perfspect-builder:v1 make dist -ARG REGISTRY= -ARG PREFIX= -ARG TAG= +ARG REGISTRY +ARG PREFIX +ARG TAG=v1 +ARG TOOLS_IMAGE=${REGISTRY}${PREFIX}perfspect-tools:${TAG} # STAGE 1 - image contains pre-built tools components, rebuild the image to rebuild the tools components -FROM ${REGISTRY}${PREFIX}perfspect-tools:${TAG} AS tools +FROM ${TOOLS_IMAGE} AS tools # STAGE 2 - image contains perfspect's Go components build environment FROM golang:1.25.5@sha256:20b91eda7a9627c127c0225b0d4e8ec927b476fa4130c6760928b849d769c149 +# install system dependencies +RUN apt-get update && apt-get install -y jq +# allow git to operate in the mounted repository regardless of the user +RUN git config --global --add safe.directory /localrepo +# pre-install Go tools used by make check +RUN go install honnef.co/go/tools/cmd/staticcheck@latest && \ + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \ + go install golang.org/x/vuln/cmd/govulncheck@latest && \ + go install github.com/securego/gosec/v2/cmd/gosec@latest # copy the tools binaries and source from the previous stage RUN mkdir /prebuilt RUN mkdir /prebuilt/tools COPY --from=tools /bin/ /prebuilt/tools COPY --from=tools /oss_source.tgz /prebuilt/ COPY --from=tools /oss_source.tgz.md5 /prebuilt/ -# allow git to operate in the mounted repository regardless of the user -RUN git config --global --add safe.directory /localrepo -# install jq as it is used in the Makefile to create the manifest -RUN apt-get update -RUN apt-get install -y jq +# pre-download Go module dependencies (changes when go.mod/go.sum change) +WORKDIR /tmp/deps +COPY go.mod go.sum ./ +RUN go mod download + +WORKDIR / \ No newline at end of file diff --git a/builder/build.sh b/builder/build.sh index 216b749b..0154dd23 100755 --- a/builder/build.sh +++ b/builder/build.sh @@ -4,15 +4,125 @@ # run this script from repo's root directory -set -ex +set -e TAG=v1 -# build tools image -docker build -f tools/build.Dockerfile --tag perfspect-tools:$TAG ./tools +CACHE_DIR="tools-cache" +# A few files that must be present in the cache for it to be valid +REQUIRED_CACHE_FILES=( + "$CACHE_DIR/bin/x86_64/perf" + "$CACHE_DIR/bin/aarch64/perf" + "$CACHE_DIR/bin/x86_64/fio" + "$CACHE_DIR/bin/aarch64/fio" + "$CACHE_DIR/oss_source.tgz" + "$CACHE_DIR/oss_source.tgz.md5" +) + +cache_ready() { + local missing=0 + for f in "${REQUIRED_CACHE_FILES[@]}"; do + if [ ! -s "$f" ]; then + echo "Cached tools missing required file: $f" + missing=1 + fi + done + if [ "$missing" -eq 1 ]; then + return 1 + fi + return 0 +} + +invalidate_cache() { + if [ -d "$CACHE_DIR" ]; then + echo "Removing incomplete cached tools at $CACHE_DIR" + rm -rf "$CACHE_DIR" + fi +} + +# Determine if we're in GitHub Actions +if [ -n "$GITHUB_ACTIONS" ]; then + # Use buildx with GitHub Actions cache + CACHE_FROM="--cache-from type=gha,scope=perfspect-tools" + CACHE_TO="--cache-to type=gha,mode=max,scope=perfspect-tools" + BUILD_CMD="docker buildx build --load" +else + # Local build without cache export + CACHE_FROM="" + CACHE_TO="" + BUILD_CMD="docker build" +fi + +# Check if we can use cached binaries +USE_CACHE="" +if [ "$TOOLS_CACHE_HIT" = "true" ]; then + # GitHub Actions cache hit + USE_CACHE="true" +elif [ -z "$SKIP_TOOLS_CACHE" ] && [ -d "$CACHE_DIR/bin" ]; then + # Local cache exists and not disabled + echo "Found local tools cache in $CACHE_DIR/" + echo "To force rebuild, run: SKIP_TOOLS_CACHE=1 builder/build.sh" + if cache_ready; then + USE_CACHE="true" + else + echo "Local tools cache is incomplete; it will be discarded and rebuilt." + invalidate_cache + fi +fi + +# build tools image (or use cached binaries) +if [ "$USE_CACHE" = "true" ]; then + echo "Using cached tool binaries, creating minimal tools image" + # Create a minimal Dockerfile that packages the cached binaries + cat > /tmp/cached-tools.Dockerfile << EOF +FROM scratch AS output +COPY $CACHE_DIR/bin /bin +COPY $CACHE_DIR/oss_source.tgz /oss_source.tgz +COPY $CACHE_DIR/oss_source.tgz.md5 /oss_source.tgz.md5 +EOF + docker build -f /tmp/cached-tools.Dockerfile -t perfspect-tools:$TAG . + rm /tmp/cached-tools.Dockerfile +else + echo "Building tools from source" + $BUILD_CMD -f tools/build.Dockerfile \ + $CACHE_FROM $CACHE_TO \ + --tag perfspect-tools:$TAG ./tools + + # Extract binaries for caching (both GitHub Actions and local) + if [ -z "$SKIP_TOOLS_CACHE" ]; then + echo "Extracting tool binaries to tools-cache/ for future builds" + rm -rf "$CACHE_DIR" + mkdir -p "$CACHE_DIR" + # Use a helper container to extract files from the scratch-based image + # Create a temporary Dockerfile that copies from the tools image + cat > /tmp/extract-tools.Dockerfile << EOF +FROM perfspect-tools:$TAG AS source +FROM busybox:latest +COPY --from=source /bin /output/bin +COPY --from=source /oss_source.tgz /output/ +COPY --from=source /oss_source.tgz.md5 /output/ +CMD ["true"] +EOF + # Build the extractor image + docker build -q -f /tmp/extract-tools.Dockerfile -t perfspect-tools-extractor:$TAG . > /dev/null + # Create container and extract files + CONTAINER_ID=$(docker create perfspect-tools-extractor:$TAG) + docker cp $CONTAINER_ID:/output/. "$CACHE_DIR"/ + docker rm $CONTAINER_ID > /dev/null + # Cleanup + docker rmi perfspect-tools-extractor:$TAG > /dev/null + rm /tmp/extract-tools.Dockerfile + echo "Tools cached locally. Next build will be faster!" + fi +fi # build the perfspect builder image -docker build -f builder/build.Dockerfile --build-arg TAG=$TAG --tag perfspect-builder:$TAG . +# Note: Use regular docker build (not buildx) because it needs access to the +# locally built perfspect-tools:$TAG image. The Go module and tool caching +# is handled via Docker's built-in layer caching. +docker build -f builder/build.Dockerfile \ + --build-arg TAG=$TAG \ + --tag perfspect-builder:$TAG . # build perfspect using the builder image docker container run \ @@ -20,4 +130,4 @@ docker container run \ -w /localrepo \ --rm \ perfspect-builder:$TAG \ - make dist \ No newline at end of file + make dist