Skip to content

Commit 41a7499

Browse files
authored
Merge pull request #6987 from snyk/chore/CLI-1633-improve-windows-build-time-e2e
chore: Reduce Windows CI build time
2 parents 9b60075 + b01c2ce commit 41a7499

8 files changed

Lines changed: 366 additions & 83 deletions

File tree

.circleci/config.yml

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,53 @@ commands:
270270
$bashLine = 'export PATH="' + $bashPath + ':$PATH"'
271271
[System.IO.File]::AppendAllText($bashEnvScript, $bashLine + "`n", (New-Object System.Text.UTF8Encoding $false))
272272
273+
# Minimal Windows toolchain for COMPILING the CLI: Node, Python, Make only.
274+
# Excludes .NET/Maven/Gradle (needed only by acceptance tests), which keeps
275+
# this cache small and fast to restore compared to the full tools cache.
276+
install-deps-windows-native-build-min:
277+
steps:
278+
- run:
279+
name: Ensure tools cache directory
280+
shell: powershell
281+
command: |
282+
$cacheDir = '<< pipeline.parameters.windows_cache_dir >>'
283+
if (-not (Test-Path $cacheDir)) {
284+
New-Item -ItemType Directory -Path $cacheDir | Out-Null
285+
}
286+
- restore_cache:
287+
name: Restoring Windows build tools cache
288+
keys:
289+
- windows-build-tools-cache-v2-{{ arch }}-{{ checksum ".nvmrc" }}
290+
- run:
291+
name: Install Node.js (native)
292+
shell: powershell
293+
command: |
294+
.\scripts\windows\install-node.ps1
295+
- run:
296+
name: Install Python 3.12.8 (native)
297+
shell: powershell
298+
command: |
299+
.\scripts\windows\install-python3.ps1
300+
- run:
301+
name: Install GNU Make 4.4.1 (native)
302+
shell: powershell
303+
command: |
304+
.\scripts\windows\install-make.ps1
305+
- run:
306+
name: Ensure Python and uv
307+
shell: powershell
308+
command: |
309+
if (Test-Path '<< pipeline.parameters.windows_cache_dir >>/<< pipeline.parameters.windows_env_script >>') { . '<< pipeline.parameters.windows_cache_dir >>/<< pipeline.parameters.windows_env_script >>' }
310+
.\scripts\windows\ensure-python-uv.ps1
311+
- save_cache:
312+
name: Saving Windows build tools cache
313+
key: windows-build-tools-cache-v2-{{ arch }}-{{ checksum ".nvmrc" }}
314+
paths:
315+
- << pipeline.parameters.windows_cache_dir >>
316+
- C:\ProgramData\nvm
317+
- C:\tools\make
318+
- C:\Program Files\Python312
319+
273320
install-deps-windows-native-build:
274321
steps:
275322
- run:
@@ -283,7 +330,7 @@ commands:
283330
- restore_cache:
284331
name: Restoring Windows tools cache
285332
keys:
286-
- windows-tools-cache-v5-{{ arch }}-{{ checksum ".nvmrc" }}
333+
- windows-tools-cache-v7-{{ arch }}-{{ checksum ".nvmrc" }}
287334
- run:
288335
name: Install Node.js (native)
289336
shell: powershell
@@ -322,10 +369,14 @@ commands:
322369
.\scripts\windows\ensure-python-uv.ps1
323370
- save_cache:
324371
name: Saving Windows tools cache
325-
key: windows-tools-cache-v5-{{ arch }}-{{ checksum ".nvmrc" }}
372+
key: windows-tools-cache-v7-{{ arch }}-{{ checksum ".nvmrc" }}
326373
paths:
327374
- << pipeline.parameters.windows_cache_dir >>
328375
- C:\ProgramData\nvm
376+
- C:\tools\maven
377+
- C:\tools\make
378+
- C:\Program Files\dotnet
379+
- C:\Program Files\Python312
329380

330381
install-deps-windows-native-full-signing:
331382
steps:
@@ -386,6 +437,19 @@ commands:
386437
name: No dependencies to install
387438
command: echo all done!
388439

440+
setup-go-build-cache:
441+
description: >-
442+
Pin GOMODCACHE and GOCACHE to workspace paths so CircleCI cache
443+
restore/save works on Linux, macOS, and Windows executors.
444+
steps:
445+
- run:
446+
name: Configure Go module and build cache paths
447+
shell: bash
448+
command: |
449+
mkdir -p .go-cache/mod .go-cache/build
450+
echo "export GOMODCACHE=\"$(pwd)/.go-cache/mod\"" >> "$BASH_ENV"
451+
echo "export GOCACHE=\"$(pwd)/.go-cache/build\"" >> "$BASH_ENV"
452+
389453
setup-go-private-modules:
390454
description: >-
391455
Configure git and Go to fetch private snyk modules using the
@@ -829,12 +893,11 @@ workflows:
829893
go_arch: amd64
830894
go_download_base_url: << pipeline.parameters.fips_go_download_base_url >>
831895
go_version: << pipeline.parameters.fips_go_version >>
832-
make_target: build clean-golang build-fips BUILD_MODE=private
833-
install_deps_extension: windows-native-full-signing
896+
make_target: build build-fips verify-fips-artifacts BUILD_MODE=private
897+
install_deps_extension: windows-native-build-min
834898
install_path: 'C:\'
835899
executor: win-server2022-amd64
836900
context:
837-
- snyk-windows-signing
838901
- iac-cli
839902
- go-private-modules
840903
requires:
@@ -1729,17 +1792,24 @@ jobs:
17291792
go_version: << parameters.go_version >>
17301793
extraction_path: << parameters.install_path >>
17311794
- setup-go-private-modules
1795+
- setup-go-build-cache
17321796
- restore_cache:
1733-
key: go-build-{{ arch }}-{{ checksum "cliv2/go.sum" }}
1797+
name: Restoring Go module and build cache
1798+
keys:
1799+
- go-build-v2-{{ arch }}-{{ checksum "cliv2/go.sum" }}
1800+
- go-build-v2-{{ arch }}-
17341801
- make-binary:
17351802
static: << parameters.static_binary >>
17361803
go_arch: << parameters.go_arch >>
17371804
go_target_os: << parameters.go_target_os >>
17381805
make_target: << parameters.make_target >>
17391806
c_compiler: << parameters.c_compiler >>
17401807
- save_cache:
1741-
key: go-build-{{ arch }}-{{ checksum "cliv2/go.sum" }}
1742-
paths: [/home/circleci/go/pkg/mod]
1808+
name: Saving Go module and build cache
1809+
key: go-build-v2-{{ arch }}-{{ checksum "cliv2/go.sum" }}
1810+
paths:
1811+
- .go-cache/mod
1812+
- .go-cache/build
17431813
- store_artifacts:
17441814
path: binary-releases
17451815
- persist_to_workspace:
@@ -1856,7 +1926,11 @@ jobs:
18561926
- install-deps-<< parameters.install_deps_extension >>
18571927
- run:
18581928
name: Signing << parameters.go_os >> artifact
1859-
command: make << parameters.make_target >> GOOS=<< parameters.go_os >> GOARCH=<< parameters.go_arch >>
1929+
shell: bash
1930+
command: |
1931+
# Load Windows $Env:Path values of tools necessary for signing (like make).
1932+
if [ -f "/c/tools-cache/<< pipeline.parameters.windows_bash_env_script >>" ]; then source "/c/tools-cache/<< pipeline.parameters.windows_bash_env_script >>"; fi
1933+
make << parameters.make_target >> GOOS=<< parameters.go_os >> GOARCH=<< parameters.go_arch >>
18601934
- store_artifacts:
18611935
path: ./binary-releases
18621936
- persist_to_workspace:

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,14 @@ build-fips: pre-build $(BINARY_OUTPUT_FOLDER)/fips/version
254254
@cd $(EXTENSIBLE_CLI_DIR); $(MAKE) fips build-full install bindir=$(WORKING_DIR)/$(BINARY_OUTPUT_FOLDER)/fips USE_LEGACY_EXECUTABLE_NAME=1 BUILD_MODE=$(BUILD_MODE)
255255
@$(MAKE) clean-package-files
256256

257+
# Confirms the FIPS binary is genuinely distinct from the normal binary. This is
258+
# the safety net that lets us build normal + FIPS in one job without clean-golang:
259+
# if the FIPS build were ever skipped, the two binaries would be identical and
260+
# this target fails. Requires both `build` and `build-fips` to have run first.
261+
.PHONY: verify-fips-artifacts
262+
verify-fips-artifacts:
263+
@bash ./scripts/verify-fips-build.sh $(BINARY_OUTPUT_FOLDER) $(BINARY_OUTPUT_FOLDER)/fips
264+
257265
.PHONY: build-experimental
258266
build-experimental: pre-build $(BINARY_OUTPUT_FOLDER)/experimental/version
259267
@cd $(EXTENSIBLE_CLI_DIR); $(MAKE) experimental build-full install bindir=$(WORKING_DIR)/$(BINARY_OUTPUT_FOLDER)/experimental USE_LEGACY_EXECUTABLE_NAME=1 BUILD_MODE=$(BUILD_MODE)

scripts/verify-fips-build.sh

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env bash
2+
# Verifies that the FIPS build actually produced a FIPS binary that is distinct
3+
# from the normal build.
4+
#
5+
# Why this exists: the normal and FIPS builds compile to the same intermediate
6+
# output path and are only distinguished by the crypto backend. If a regression
7+
# (e.g. removing `clean-golang` combined with a stale build output) caused the
8+
# FIPS build to be skipped, the FIPS binary would just be a copy of the normal
9+
# one. This script fails loudly in that case.
10+
#
11+
# Two independent gates, both must pass:
12+
# 1. The FIPS binary's embedded Go build info must show a FIPS crypto backend
13+
# (GOEXPERIMENT=systemcrypto/cngcrypto/opensslcrypto/boringcrypto).
14+
# 2. The FIPS binary must be byte-different from the normal binary.
15+
#
16+
# The script never exits 0 unless it could actually perform these checks.
17+
#
18+
# Usage: verify-fips-build.sh <normal-dir> <fips-dir>
19+
set -euo pipefail
20+
21+
normal_dir="${1:?normal binary directory required}"
22+
fips_dir="${2:?fips binary directory required}"
23+
24+
# Best-effort: load the Windows env (PATH for go/sha tools) when running on the
25+
# Windows executor. The caller (e.g. the make-binary step) normally already
26+
# sources this and exports PATH, which this child process inherits; this is a
27+
# fallback for standalone runs. Override the location with SNYK_WIN_ENV_SCRIPT.
28+
win_env_script="${SNYK_WIN_ENV_SCRIPT:-/c/tools-cache/snyk-env.sh}"
29+
if [ -f "$win_env_script" ]; then
30+
# shellcheck disable=SC1090
31+
source "$win_env_script"
32+
fi
33+
34+
# The verification depends on the Go toolchain to read embedded build info.
35+
if ! command -v go >/dev/null 2>&1; then
36+
echo "[verify-fips] ERROR: 'go' not found on PATH; cannot inspect build info. Failing."
37+
exit 1
38+
fi
39+
40+
find_binary() {
41+
local dir="$1"
42+
local f
43+
shopt -s nullglob
44+
for f in "$dir"/snyk-*; do
45+
case "$f" in
46+
*.sha256 | *.sha256.asc | *.asc) continue ;;
47+
esac
48+
[ -f "$f" ] || continue
49+
echo "$f"
50+
return 0
51+
done
52+
return 1
53+
}
54+
55+
hash_file() {
56+
if command -v sha256sum >/dev/null 2>&1; then
57+
sha256sum "$1" | awk '{print $1}'
58+
elif command -v shasum >/dev/null 2>&1; then
59+
shasum -a 256 "$1" | awk '{print $1}'
60+
else
61+
echo "NOHASH"
62+
fi
63+
}
64+
65+
log_build_settings() {
66+
local label="$1"
67+
local bin="$2"
68+
echo "[verify-fips] Build settings ($label): $bin"
69+
go version -m "$bin" 2>/dev/null | grep -E '^[[:space:]]*build[[:space:]]' || echo " (no build info available)"
70+
}
71+
72+
# True if the binary's Go build info records a FIPS crypto backend. Microsoft's
73+
# Go toolchain sets this via GOEXPERIMENT; the concrete value is systemcrypto,
74+
# or the platform backend it resolves to (cngcrypto on Windows, opensslcrypto on
75+
# Linux). boringcrypto is included for completeness.
76+
binary_has_fips_backend() {
77+
go version -m "$1" 2>/dev/null \
78+
| grep -E '^[[:space:]]*build[[:space:]]+GOEXPERIMENT=' \
79+
| grep -qiE 'systemcrypto|cngcrypto|opensslcrypto|boringcrypto'
80+
}
81+
82+
normal_bin="$(find_binary "$normal_dir" || true)"
83+
fips_bin="$(find_binary "$fips_dir" || true)"
84+
85+
if [ -z "$normal_bin" ] || [ -z "$fips_bin" ]; then
86+
echo "[verify-fips] ERROR: could not locate binaries (normal='$normal_bin', fips='$fips_bin')"
87+
exit 1
88+
fi
89+
90+
echo "=================================================================="
91+
echo "[verify-fips] Normal binary: $normal_bin"
92+
echo "[verify-fips] FIPS binary: $fips_bin"
93+
echo "------------------------------------------------------------------"
94+
log_build_settings "normal" "$normal_bin"
95+
log_build_settings "fips" "$fips_bin"
96+
echo "------------------------------------------------------------------"
97+
98+
failed=0
99+
100+
# Gate 1: the FIPS binary must actually be built with a FIPS crypto backend.
101+
if binary_has_fips_backend "$fips_bin"; then
102+
echo "[verify-fips] Gate 1 PASSED: FIPS binary reports a FIPS crypto backend."
103+
else
104+
echo "[verify-fips] Gate 1 FAILED: FIPS binary does NOT report a FIPS crypto backend (GOEXPERIMENT)."
105+
echo "[verify-fips] The build under $fips_dir is not FIPS-enabled."
106+
failed=1
107+
fi
108+
109+
# Sanity: the normal binary should NOT report a FIPS backend. Surface loudly.
110+
if binary_has_fips_backend "$normal_bin"; then
111+
echo "[verify-fips] WARNING: the normal binary unexpectedly reports a FIPS crypto backend."
112+
fi
113+
114+
# Gate 2: the FIPS and normal binaries must differ. Identical bytes mean the
115+
# FIPS build did not take effect (e.g. it was skipped). If no hashing tool is
116+
# available we cannot perform this gate, so we fail rather than pass silently.
117+
normal_hash="$(hash_file "$normal_bin")"
118+
fips_hash="$(hash_file "$fips_bin")"
119+
echo "[verify-fips] normal sha256: $normal_hash"
120+
echo "[verify-fips] fips sha256: $fips_hash"
121+
122+
if [ "$normal_hash" = "NOHASH" ] || [ "$fips_hash" = "NOHASH" ]; then
123+
echo "[verify-fips] Gate 2 FAILED: no sha256 tool available; cannot verify the binaries differ."
124+
failed=1
125+
elif [ "$normal_hash" = "$fips_hash" ]; then
126+
echo "[verify-fips] Gate 2 FAILED: FIPS and normal binaries are IDENTICAL (FIPS build skipped?)."
127+
failed=1
128+
else
129+
echo "[verify-fips] Gate 2 PASSED: FIPS binary is distinct from the normal binary."
130+
fi
131+
132+
echo "=================================================================="
133+
if [ "$failed" -ne 0 ]; then
134+
echo "[verify-fips] FAILED: FIPS verification did not pass."
135+
exit 1
136+
fi
137+
echo "[verify-fips] PASSED: FIPS binary is genuine and distinct from the normal binary."

scripts/windows/ensure-python-uv.ps1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ Param()
33
$ErrorActionPreference = 'Stop'
44

55
try {
6+
if (Get-Command uv -ErrorAction SilentlyContinue) {
7+
Write-Host "[uv-cache] HIT: uv already installed; skipping pip install."
8+
return
9+
}
10+
611
$python = Get-Command python -ErrorAction SilentlyContinue
712
if (-not $python) {
813
$python = Get-Command python3 -ErrorAction SilentlyContinue

scripts/windows/install-dotnet-sdk.ps1

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,53 @@ if (Test-Path $envScript) {
1414

1515
try {
1616
$dotnetVersion = '8.0.100'
17-
$installerPath = Join-Path $cacheDir "dotnet-sdk-$dotnetVersion-win-x64.exe"
18-
$expectedSha256 = 'd77a87a78264fcfb1703a7064795ccb10938cdfaea64a03cb0f36b1cda379f82'
17+
$dotnetExe = 'C:\Program Files\dotnet\dotnet.exe'
18+
$dotnetPath = $null
1919

20-
if (-not (Test-Path $cacheDir)) {
21-
New-Item -ItemType Directory -Path $cacheDir | Out-Null
20+
if (Test-Path $dotnetExe) {
21+
# `dotnet --version` reports the SDK selected for the current dir (usually the
22+
# newest installed), so it can't confirm a specific version. `--list-sdks`
23+
# enumerates every installed SDK, which is what we need to detect 8.0.100.
24+
$installedSdks = & $dotnetExe --list-sdks 2>&1
25+
# `--list-sdks` prints "<version> [<path>]"; require the exact version to be
26+
# followed by whitespace or end-of-line so e.g. "8.0.100-preview" is not
27+
# treated as "8.0.100", while a bare version line still matches.
28+
if ($installedSdks | Where-Object { $_ -match ('^' + [regex]::Escape($dotnetVersion) + '(\s|$)') }) {
29+
Write-Host "[dotnet-cache] HIT: .NET SDK $dotnetVersion already installed; skipping installer."
30+
$dotnetPath = Split-Path $dotnetExe -Parent
31+
}
2232
}
2333

24-
if (-not (Test-Path $installerPath)) {
25-
Write-Host "Downloading .NET SDK $dotnetVersion installer..."
26-
$url = "https://builds.dotnet.microsoft.com/dotnet/Sdk/$dotnetVersion/dotnet-sdk-$dotnetVersion-win-x64.exe"
27-
curl.exe -L $url -o $installerPath
28-
}
34+
if (-not $dotnetPath) {
35+
$installerPath = Join-Path $cacheDir "dotnet-sdk-$dotnetVersion-win-x64.exe"
36+
$expectedSha256 = 'd77a87a78264fcfb1703a7064795ccb10938cdfaea64a03cb0f36b1cda379f82'
2937

30-
Write-Host 'Verifying .NET SDK installer checksum...'
31-
$hash = Get-FileHash -Path $installerPath -Algorithm SHA256
32-
if ($hash.Hash.ToLower() -ne $expectedSha256.ToLower()) {
33-
throw "Checksum verification failed for $installerPath. Expected $expectedSha256 but got $($hash.Hash.ToLower())."
34-
}
38+
if (-not (Test-Path $cacheDir)) {
39+
New-Item -ItemType Directory -Path $cacheDir | Out-Null
40+
}
3541

36-
Write-Host "Installing .NET SDK $dotnetVersion..."
37-
& $installerPath /install /quiet /norestart /log "$cacheDir\dotnet-sdk-install.log"
42+
if (-not (Test-Path $installerPath)) {
43+
Write-Host "Downloading .NET SDK $dotnetVersion installer..."
44+
$url = "https://builds.dotnet.microsoft.com/dotnet/Sdk/$dotnetVersion/dotnet-sdk-$dotnetVersion-win-x64.exe"
45+
curl.exe -L $url -o $installerPath
46+
}
3847

39-
# Locate installed dotnet.exe using the known default installation path (%ProgramFiles%\dotnet)
40-
$dotnetExe = "C:\Program Files\dotnet\dotnet.exe"
41-
if (-not (Test-Path $dotnetExe)) {
42-
throw ".NET SDK $dotnetVersion did not install correctly; expected $dotnetExe to exist."
43-
}
48+
Write-Host 'Verifying .NET SDK installer checksum...'
49+
$hash = Get-FileHash -Path $installerPath -Algorithm SHA256
50+
if ($hash.Hash.ToLower() -ne $expectedSha256.ToLower()) {
51+
throw "Checksum verification failed for $installerPath. Expected $expectedSha256 but got $($hash.Hash.ToLower())."
52+
}
53+
54+
Write-Host "Installing .NET SDK $dotnetVersion..."
55+
& $installerPath /install /quiet /norestart /log "$cacheDir\dotnet-sdk-install.log"
4456

45-
$dotnetPath = Split-Path $dotnetExe -Parent
57+
# Locate installed dotnet.exe using the known default installation path (%ProgramFiles%\dotnet)
58+
if (-not (Test-Path $dotnetExe)) {
59+
throw ".NET SDK $dotnetVersion did not install correctly; expected $dotnetExe to exist."
60+
}
61+
62+
$dotnetPath = Split-Path $dotnetExe -Parent
63+
}
4664

4765
Write-Host "Adding $dotnetPath to PATH for current session..."
4866
$Env:Path = "$dotnetPath;" + $Env:Path

0 commit comments

Comments
 (0)