diff --git a/.aqua/aqua-checksums.json b/.aqua/aqua-checksums.json index 8acf0ef2..ecb23c96 100644 --- a/.aqua/aqua-checksums.json +++ b/.aqua/aqua-checksums.json @@ -1,23 +1,23 @@ { "checksums": [ { - "id": "github_release/github.com/casey/just/1.58.0/just-1.58.0-aarch64-apple-darwin.tar.gz", - "checksum": "50AE3E996C974A0BF32EA7D10F495070DF33F1B43E0616B2769E3D4821ED8F48", + "id": "github_release/github.com/casey/just/1.59.0/just-1.59.0-aarch64-apple-darwin.tar.gz", + "checksum": "6574CEA774ED4CAE12E24B6AA11857DE33E80DAB4667CF628FD5840DF96A2A9D", "algorithm": "sha256" }, { - "id": "github_release/github.com/casey/just/1.58.0/just-1.58.0-aarch64-unknown-linux-musl.tar.gz", - "checksum": "748237128C4C40CBDABC65E841D05CEBA13CC23A91EABA395495894C1D9764DF", + "id": "github_release/github.com/casey/just/1.59.0/just-1.59.0-aarch64-unknown-linux-musl.tar.gz", + "checksum": "23871B1702BFD550FB5F901778FD4290ECF2EE034A45A889FBE96D89B188086D", "algorithm": "sha256" }, { - "id": "github_release/github.com/casey/just/1.58.0/just-1.58.0-x86_64-pc-windows-msvc.zip", - "checksum": "759F16FB7AA17C5C8B9594B6D4A8C1A6630DFD042CF2B3FF84841454D3D188DC", + "id": "github_release/github.com/casey/just/1.59.0/just-1.59.0-x86_64-pc-windows-msvc.zip", + "checksum": "9C5463E2D0974E31BEE71346AA5CBF19167DE1585A4ABC1BAE7B3B04604EA9C0", "algorithm": "sha256" }, { - "id": "github_release/github.com/casey/just/1.58.0/just-1.58.0-x86_64-unknown-linux-musl.tar.gz", - "checksum": "4A5CC2F53E6F0F8C59092A6CC38291EB729D46A7DD95D3AE582008881B84931D", + "id": "github_release/github.com/casey/just/1.59.0/just-1.59.0-x86_64-unknown-linux-musl.tar.gz", + "checksum": "409044617B6F06517C1035E55CA38C8C856FF8B650E41BC2AA1EF82D5903A23E", "algorithm": "sha256" }, { @@ -46,28 +46,28 @@ "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.12.1/limen_0.12.1_darwin_arm64.tar.gz", - "checksum": "2E32B06524608968CD0A0D7CB4165A7F31FD5CD2072EB9F46FE3E5D3020D5E3A", + "id": "github_release/github.com/farcloser/limen/v0.13.0/limen_0.13.0_darwin_arm64.tar.gz", + "checksum": "02ACAA3C7D7D5D09D253621C94657F3A314F91BD79B14CA24702A3CD0C127324", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.12.1/limen_0.12.1_linux_amd64.tar.gz", - "checksum": "B7B2011C0ED40F7E804CDDF441329FBA17A186F3F8F26BD4F3F4C6EE4678337F", + "id": "github_release/github.com/farcloser/limen/v0.13.0/limen_0.13.0_linux_amd64.tar.gz", + "checksum": "23FB86F58C599488CBD11E1887E739A7AA9ED7CE8DAD4495FD001B7CBFFEFF81", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.12.1/limen_0.12.1_linux_arm64.tar.gz", - "checksum": "43FA62F928B20086A8FA8E8736E144295B6272DF793F8880EC505CF6B428FD47", + "id": "github_release/github.com/farcloser/limen/v0.13.0/limen_0.13.0_linux_arm64.tar.gz", + "checksum": "1DBD0AF2FE6DE16FDBF9482FD1CCDCFB40551ECA0CD4A3B218030BBAEA249847", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.12.1/limen_0.12.1_windows_amd64.tar.gz", - "checksum": "2D3CB0E188470978669517FE52831420687C8902C690081AAB608AB8BECFDF9A", + "id": "github_release/github.com/farcloser/limen/v0.13.0/limen_0.13.0_windows_amd64.tar.gz", + "checksum": "B84995C3308DF6D4709AB821F884E0AB897F9A4E67EF19AB6BDA9FBD03208AA3", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.12.1/limen_0.12.1_windows_arm64.tar.gz", - "checksum": "52ADAD8DF300CAA2FE2B18A12C1DD05CCEC24488F5D6761C81BA14999613F43E", + "id": "github_release/github.com/farcloser/limen/v0.13.0/limen_0.13.0_windows_arm64.tar.gz", + "checksum": "724F1D5EC4B7B22F8C1A490D99D3962A928FD94CA5406008B7AF1079B4064B26", "algorithm": "sha256" }, { diff --git a/.aqua/aqua.yaml b/.aqua/aqua.yaml index bb01c75c..dd2deb8c 100644 --- a/.aqua/aqua.yaml +++ b/.aqua/aqua.yaml @@ -18,7 +18,7 @@ registries: packages: # --- farcloser tools (local registry; standard once registered upstream) --- - - name: farcloser/limen@v0.12.1 # renovate: depName=farcloser/limen + - name: farcloser/limen@v0.13.0 # renovate: depName=farcloser/limen registry: local # --- toolchain + binary-release tools (standard registry, aqua-verified) --- - name: mikefarah/yq@v4.54.1 diff --git a/.github/actions/cache/action.yaml b/.github/actions/cache/action.yaml index 368551c7..b3b0ceb0 100644 --- a/.github/actions/cache/action.yaml +++ b/.github/actions/cache/action.yaml @@ -1,6 +1,6 @@ name: Job caches description: > - The caches a canonical job keeps: aqua's package store and, with go, the Go build and module caches, plus the linter's with linter-cache. Called three times per job, each mode once: attach (before setup-aqua), restore (right after setup-aqua, before the job links or runs a tool) and save (after the job's last command). On Windows they live in one disk image (windows-cache-image): attach restores and attaches it, since aqua's root is inside it, restore does nothing, and save detaches and saves it. On Linux and macOS attach does nothing, restore restores the store and the Go caches as files, and save saves them. Every save is on main only, and only on a miss: a cache a pull request run saves is scoped to that pull request and restorable by nothing else, and a key is never overwritten (the cache budget, caches.yaml). The Go caches and the image accumulate, so their keys carry the UTC day and main refreshes them at most once a day; the store is fully determined by its pins. What save needs, attach and restore leave in the job's environment, so save cannot use another key or path list, and a mode called twice in one job fails. + The caches a canonical job keeps: aqua's package store and, with go, the Go build and module caches, plus the linter's with linter-cache. Called three times per job, each mode once: attach (before setup-aqua), restore (right after setup-aqua, before the job links or runs a tool) and save (after the job's last command). On Windows they live in one disk image (windows-cache-image): attach restores and attaches it, since aqua's root is inside it, restore does nothing, and save detaches and saves it. On Linux and macOS attach does nothing, restore restores the store and the Go caches as files, and save saves them. Every save is on the default branch only, read from the event (a scheduled run is on it by definition), never assumed by name, and only on a miss: a cache a pull request run saves is scoped to that pull request and restorable by nothing else, and a key is never overwritten (the cache budget, caches.yaml). The Go caches and the image accumulate, so their keys carry the UTC day and the default branch refreshes them at most once a day; the store is fully determined by its pins. What save needs, attach and restore leave in the job's environment, so save cannot use another key or path list, and a mode called twice in one job fails. inputs: mode: description: attach, restore or save. @@ -10,7 +10,7 @@ inputs: required: false default: 'true' linter-cache: - description: true to keep the linter's .build/cache with the Go caches (restore). + description: true to keep the linter's $LIMEN_BUILD/cache with the Go caches (restore). required: false default: 'false' aqua-fallback: @@ -82,7 +82,7 @@ runs: echo "mod=$(aqua exec -- go env GOMODCACHE)" echo "version=$(aqua exec -- go env GOVERSION)" echo "day=$(date -u +%Y%m%d)" - if [ "$LINTER" = true ]; then echo "lint=.build/cache"; else echo "lint="; fi + if [ "$LINTER" = true ]; then echo "lint=${LIMEN_BUILD}/cache"; else echo "lint="; fi } >> "$GITHUB_OUTPUT" # An empty line in path is dropped, so without linter-cache the list is @@ -125,20 +125,20 @@ runs: echo "LIMEN_CACHE_GO_PATHS_END" } >> "$GITHUB_ENV" - # --- save: main only, on a miss --- - - if: inputs.mode == 'save' && runner.os != 'Windows' && github.ref == 'refs/heads/main' && env.LIMEN_CACHE_AQUA_KEY != '' && env.LIMEN_CACHE_AQUA_HIT != 'true' + # --- save: the default branch only, on a miss --- + - if: inputs.mode == 'save' && runner.os != 'Windows' && (github.event_name == 'schedule' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) && env.LIMEN_CACHE_AQUA_KEY != '' && env.LIMEN_CACHE_AQUA_HIT != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.AQUA_ROOT_DIR }}/pkgs key: ${{ env.LIMEN_CACHE_AQUA_KEY }} - - if: inputs.mode == 'save' && runner.os != 'Windows' && github.ref == 'refs/heads/main' && env.LIMEN_CACHE_GO_KEY != '' && env.LIMEN_CACHE_GO_HIT != 'true' + - if: inputs.mode == 'save' && runner.os != 'Windows' && (github.event_name == 'schedule' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) && env.LIMEN_CACHE_GO_KEY != '' && env.LIMEN_CACHE_GO_HIT != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.LIMEN_CACHE_GO_PATHS }} key: ${{ env.LIMEN_CACHE_GO_KEY }} - - if: inputs.mode == 'save' && runner.os == 'Windows' && github.ref == 'refs/heads/main' && env.LIMEN_CACHE_IMAGE_KEY != '' && env.LIMEN_CACHE_IMAGE_HIT != 'true' + - if: inputs.mode == 'save' && runner.os == 'Windows' && (github.event_name == 'schedule' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) && env.LIMEN_CACHE_IMAGE_KEY != '' && env.LIMEN_CACHE_IMAGE_HIT != 'true' uses: ./.github/actions/windows-cache-image with: mode: save diff --git a/.github/actions/setup-aqua/action.yaml b/.github/actions/setup-aqua/action.yaml index e0e7cfa2..20dbed99 100644 --- a/.github/actions/setup-aqua/action.yaml +++ b/.github/actions/setup-aqua/action.yaml @@ -32,6 +32,17 @@ runs: fi export AQUA_ROOT_DIR echo "AQUA_ROOT_DIR=${AQUA_ROOT_DIR}" >>"$GITHUB_ENV" + # The checkout's output directory, the path main.just's LIMEN_BUILD + # computes, for the steps after this one (cache paths, a project's own + # steps); a recipe computes it again itself. Kept when set: on windows + # the cache image is attached first and exports it. Equal to main.just's + # value only with the checkout at the workspace root (actions/checkout + # without `path:`), as every canonical workflow checks out. + LIMEN_BUILD="${LIMEN_BUILD:-$GITHUB_WORKSPACE/.build}" + if command -v cygpath >/dev/null 2>&1; then + LIMEN_BUILD="$(cygpath -m "$LIMEN_BUILD")" + fi + echo "LIMEN_BUILD=${LIMEN_BUILD}" >>"$GITHUB_ENV" tmp="$(mktemp -d)" # Retries for transient runner failures. Windows curl (schannel) refuses # a handshake when the CA's revocation server is unreachable (exit 35, diff --git a/.github/actions/windows-cache-image/action.yaml b/.github/actions/windows-cache-image/action.yaml index 7e038a9c..ff72809c 100644 --- a/.github/actions/windows-cache-image/action.yaml +++ b/.github/actions/windows-cache-image/action.yaml @@ -81,9 +81,13 @@ runs: } catch { Write-Host "Defender exclusion skipped: $_" } - # The linter cache lives under the workspace (main.just pins it there). - New-Item -ItemType Directory -Force -Path C:\vcache\golangci, "$env:GITHUB_WORKSPACE\.build" | Out-Null - New-Item -ItemType Junction -Path "$env:GITHUB_WORKSPACE\.build\cache" -Target C:\vcache\golangci | Out-Null + # The linter cache lives under the checkout's output directory + # (main.just's LIMEN_BUILD, the same path). This step runs before + # setup-aqua, so it is the one that exports it for the steps after. + $build = ($env:GITHUB_WORKSPACE -replace '\\', '/') + '/.build' + New-Item -ItemType Directory -Force -Path C:\vcache\golangci, $build | Out-Null + New-Item -ItemType Junction -Path "$build/cache" -Target C:\vcache\golangci | Out-Null + "LIMEN_BUILD=$build" >> $env:GITHUB_ENV "GOCACHE=C:\vcache\go-build" >> $env:GITHUB_ENV "GOMODCACHE=C:\vcache\mod" >> $env:GITHUB_ENV "AQUA_ROOT_DIR=C:/vcache/aqua" >> $env:GITHUB_ENV @@ -93,7 +97,7 @@ runs: shell: pwsh run: | $ErrorActionPreference = 'Stop' - Remove-Item "$env:GITHUB_WORKSPACE\.build\cache" -Force + Remove-Item "$env:LIMEN_BUILD/cache" -Force Dismount-DiskImage -ImagePath C:\vhd\cache.vhdx | Out-Null - if: inputs.mode == 'save' diff --git a/.github/workflows/caches.yaml b/.github/workflows/caches.yaml index f2b46e7d..3021e58d 100644 --- a/.github/workflows/caches.yaml +++ b/.github/workflows/caches.yaml @@ -3,14 +3,14 @@ # The cache budget. GitHub keeps 10 GB of Actions caches per repository and # evicts the least recently used beyond it, the current entries included, so # a repository over budget runs cold at random. The canonical lanes save on -# main only (a cache a pull request run saves is scoped to that pull request -# and restorable by nothing else), and every pin or go.sum change starts a new -# generation of every key while the old one lingers until eviction (and the -# caches that accumulate, Go's and the fuzz corpus, start one a day). This -# keeps one generation per key on main: after each ci run on main, and once a -# day, every entry under a canonical prefix that is not the newest of its -# family is deleted. GitHub's own token, `gh` from the runner image; no -# checkout, no third-party actions. +# the default branch only (a cache a pull request run saves is scoped to that +# pull request and restorable by nothing else), and every pin or go.sum change +# starts a new generation of every key while the old one lingers until +# eviction (and the caches that accumulate, Go's and the fuzz corpus, start +# one a day). This keeps one generation per key on the default branch: after +# each ci run there, and once a day, every entry under a canonical prefix that +# is not the newest of its family is deleted. GitHub's own token, `gh` from +# the runner image; no checkout, no third-party actions. name: caches on: @@ -43,13 +43,15 @@ jobs: # Listing and deleting caches is an Actions write. actions: write steps: - - name: One generation per key on main + - name: One generation per key on the default branch env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | - # Main's entries, newest first. Listed before the loop so a refused - # token or a failed call fails the step: fed through a process + # The default branch's entries, newest first, the branch read from + # the repository, never assumed by name (a scheduled run has no + # event naming it). Listed before the loop so a refused token or a + # failed call fails the step: fed through a process # substitution, the loop would read nothing and pass, and the prune # would silently not happen. A family is a key without its UTC day # and content hash (and, for the Go caches, without the Go version, @@ -60,7 +62,8 @@ jobs: # other's older generation. The first of each family is kept, the # rest deleted. Only the canonical prefixes; whatever else is in the # store is not ours to judge. - entries=$(gh cache list --ref refs/heads/main --limit 500 --sort created_at --order desc \ + default_branch=$(gh api "repos/$GH_REPO" --jq .default_branch) + entries=$(gh cache list --ref "refs/heads/$default_branch" --limit 500 --sort created_at --order desc \ --json id,key,version,sizeInBytes --jq '.[] | "\(.id)\t\(.key)\t\(.version)\t\(.sizeInBytes)"') kept= while IFS=$'\t' read -r id key version size; do diff --git a/.github/workflows/limen-verify.yaml b/.github/workflows/limen-verify.yaml index 2447dfc1..1f7eea88 100644 --- a/.github/workflows/limen-verify.yaml +++ b/.github/workflows/limen-verify.yaml @@ -106,9 +106,10 @@ jobs: # The cached corpus is what makes fuzzing cumulative across runs. Keyed # on the fuzz sources so a changed target restarts its corpus, and on - # the UTC day so that main saves what a day of runs found (a key is - # never overwritten); restore-keys keep the rest. Saved on main only - # (caches.yaml). + # the UTC day so that the default branch saves what a day of runs found + # (a key is never overwritten); restore-keys keep the rest. Saved on the + # default branch only (caches.yaml), read from the event, never assumed + # by name. - id: fuzzdir run: | echo "dir=$(go env GOCACHE)/fuzz" >> "$GITHUB_OUTPUT" @@ -125,7 +126,7 @@ jobs: - name: Fuzz run: just do test go fuzz - - if: github.ref == 'refs/heads/main' && steps.corpus.outputs.cache-hit != 'true' + - if: (github.event_name == 'schedule' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) && steps.corpus.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.fuzzdir.outputs.dir }} @@ -172,8 +173,9 @@ jobs: - name: Install every pinned tool run: aqua install - # Saved on main only, on a miss (caches.yaml); the verify leg that - # shares the key may have saved it first, and a key is never overwritten. + # Saved on the default branch only, on a miss (caches.yaml); the verify + # leg that shares the key may have saved it first, and a key is never + # overwritten. - uses: ./.github/actions/cache with: mode: save diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index bade96f0..42e4b4a8 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -69,8 +69,8 @@ jobs: - name: Scan run: just security - # Saved on main only, on a miss (the cache budget: caches.yaml). The - # scheduled run is on main too. + # Saved on the default branch only, on a miss (the cache budget: + # caches.yaml); the scheduled run is on it by definition. - uses: ./.github/actions/cache with: mode: save diff --git a/.limen/aqua.yaml b/.limen/aqua.yaml index b6a9bc64..4cc96df7 100644 --- a/.limen/aqua.yaml +++ b/.limen/aqua.yaml @@ -24,7 +24,7 @@ packages: # Not here: golang/go. The Go toolchain is each repository's own pin, in its # .aqua/aqua.yaml above the import (seeded once by `limen fix`, Renovate's # afterwards): a standard-library fix must not wait for a limen release. - - name: casey/just@1.58.0 + - name: casey/just@1.59.0 - name: koalaman/shellcheck@v0.11.0 - name: forkcloser/yamlfmt@v0.1.0 registry: local diff --git a/.limen/just/build-go.just b/.limen/just/build-go.just index 212729c0..00c38da2 100644 --- a/.limen/just/build-go.just +++ b/.limen/just/build-go.just @@ -64,18 +64,18 @@ release: (_banner "build go" "release") ldflags="${ldflags} ${BUILD_GO_LDFLAGS:-}" extra=() [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" - mkdir -p .build + mkdir -p "$LIMEN_BUILD" for dir in cmd/*/; do name=$(basename "${dir}") ls "${dir}"*.go >/dev/null 2>&1 || continue - echo "→ .build/${name}" + echo "→ $LIMEN_BUILD/${name}" # A command that is a module of its own (cmd//go.mod) builds # from inside it: from the root, go refuses a package outside the main # module. Every variant below does the same. src=. pkg="./${dir%/}" [ ! -f "${dir}go.mod" ] || { src="${dir%/}"; pkg=.; } go -C "${src}" build -trimpath -buildmode=pie -tags='{{ go_tags }}' \ - -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "${PWD}/.build/${name}" "${pkg}" + -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "$LIMEN_BUILD/${name}" "${pkg}" done # Debug: optimizations and inlining off, symbols kept, real paths kept — no @@ -95,15 +95,15 @@ debug: (_banner "build go" "debug") ldflags="${ldflags} ${BUILD_GO_LDFLAGS:-}" extra=() [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" - mkdir -p .build + mkdir -p "$LIMEN_BUILD" for dir in cmd/*/; do name=$(basename "${dir}") ls "${dir}"*.go >/dev/null 2>&1 || continue - echo "→ .build/${name}-debug" + echo "→ $LIMEN_BUILD/${name}-debug" src=. pkg="./${dir%/}" [ ! -f "${dir}go.mod" ] || { src="${dir%/}"; pkg=.; } go -C "${src}" build -buildmode=pie -gcflags='all=-N -l' -tags='{{ go_tags }}' \ - -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "${PWD}/.build/${name}-debug" "${pkg}" + -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "$LIMEN_BUILD/${name}-debug" "${pkg}" done # Race: a diagnostic build with the race detector, which requires cgo (forced @@ -117,16 +117,16 @@ race: (_banner "build go" "race") ldflags="-linkmode=external -X main.version=${version} ${BUILD_GO_LDFLAGS:-}" extra=() [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" - mkdir -p .build + mkdir -p "$LIMEN_BUILD" for dir in cmd/*/; do name=$(basename "${dir}") ls "${dir}"*.go >/dev/null 2>&1 || continue - echo "→ .build/${name}-race" + echo "→ $LIMEN_BUILD/${name}-race" src=. pkg="./${dir%/}" [ ! -f "${dir}go.mod" ] || { src="${dir%/}"; pkg=.; } go -C "${src}" build -race -tags='{{ go_tags }}' \ -ldflags "${ldflags}" \ - ${extra[@]+"${extra[@]}"} -o "${PWD}/.build/${name}-race" "${pkg}" + ${extra[@]+"${extra[@]}"} -o "$LIMEN_BUILD/${name}-race" "${pkg}" done # Static: fully static external link. Preconditions enforced, not commented: @@ -152,14 +152,14 @@ static: (_banner "build go" "static") ldflags="-linkmode=external -extldflags=-static -s -w -X main.version=${version} ${BUILD_GO_LDFLAGS:-}" extra=() [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" - mkdir -p .build + mkdir -p "$LIMEN_BUILD" for dir in cmd/*/; do name=$(basename "${dir}") ls "${dir}"*.go >/dev/null 2>&1 || continue - echo "→ .build/${name}-static" + echo "→ $LIMEN_BUILD/${name}-static" src=. pkg="./${dir%/}" [ ! -f "${dir}go.mod" ] || { src="${dir%/}"; pkg=.; } go -C "${src}" build -trimpath -tags='{{ go_tags }}' \ -ldflags "${ldflags}" \ - ${extra[@]+"${extra[@]}"} -o "${PWD}/.build/${name}-static" "${pkg}" + ${extra[@]+"${extra[@]}"} -o "$LIMEN_BUILD/${name}-static" "${pkg}" done diff --git a/.limen/just/fix-go.just b/.limen/just/fix-go.just index 062677ca..c628d9a4 100644 --- a/.limen/just/fix-go.just +++ b/.limen/just/fix-go.just @@ -39,21 +39,21 @@ go_fixers := "wsl_v5,godot,tagalign,nolintlint" # runs and no other, since `go vet` honours neither the rendered lint # configuration nor `//nolint` and a fix must not fail on a finding lint # never reports (book/linting-go.md). -code: (_banner "fix go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform ".build/tools/golangci-lint run --fix --issues-exit-code 0 --enable-only " + go_fixers + " -c .build/golangci.yml") +code: (_banner "fix go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform '"$LIMEN_BUILD/tools/golangci-lint" run --fix --issues-exit-code 0 --enable-only ' + go_fixers + ' -c "$LIMEN_BUILD/golangci.yml"') #!/usr/bin/env bash set -euo pipefail max=5 pass=0 # Check first: an already-formatted tree costs one --diff and no rewrite. - while ! .build/tools/golangci-lint fmt --diff -c .build/golangci.yml >/dev/null 2>&1; do + while ! "$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$LIMEN_BUILD/golangci.yml" >/dev/null 2>&1; do pass=$((pass + 1)) if [ "${pass}" -gt "${max}" ]; then echo "formatters did not converge after ${max} passes — they are fighting each other." >&2 echo "The residual diff below reverses on the next pass; fix the formatter config." >&2 - .build/tools/golangci-lint fmt --diff -c .build/golangci.yml >&2 || true + "$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$LIMEN_BUILD/golangci.yml" >&2 || true exit 1 fi - .build/tools/golangci-lint fmt -c .build/golangci.yml + "$LIMEN_BUILD/tools/golangci-lint" fmt -c "$LIMEN_BUILD/golangci.yml" done if ! go vet -asmdecl -buildtag ./...; then echo "the fixers left a tree that does not compile: repair what they broke above, then run again." >&2 diff --git a/.limen/just/lib.just b/.limen/just/lib.just index 7b95640b..e85cdd0a 100644 --- a/.limen/just/lib.just +++ b/.limen/just/lib.just @@ -64,7 +64,7 @@ _per-platform +cmd: # -o is resolved relative to -C. CGO_ENABLED=0 throughout the tool builds: # the tools are pure Go, and with cgo on by default wherever a C compiler # exists, `net` alone would call one to build them. -go_tool_build := "mkdir -p .build/tools && GOOS='' GOARCH='' CGO_ENABLED=0 go -C .limen/tools build -o '../../.build/tools/'" +go_tool_build := 'mkdir -p "$LIMEN_BUILD/tools" && GOOS="" GOARCH="" CGO_ENABLED=0 go -C .limen/tools build -o "$LIMEN_BUILD/tools/"' # Every Go-built tool the recipes run — the Go-source analyzers and the rest # alike — is a `tool` directive in a module under .limen/tools/, built natively ONCE @@ -82,7 +82,7 @@ _go-tool name pkg: set -euo pipefail pattern='^(tool[[:space:]]+)?[[:space:]]*{{ pkg }}([[:space:]]|$)' if grep -qE "$pattern" '.limen/tools/{{ name }}/go.mod' 2>/dev/null; then - mkdir -p .build/tools && GOOS='' GOARCH='' CGO_ENABLED=0 go -C '.limen/tools/{{ name }}' build -o '../../../.build/tools/' '{{ pkg }}' + mkdir -p "$LIMEN_BUILD/tools" && GOOS='' GOARCH='' CGO_ENABLED=0 go -C '.limen/tools/{{ name }}' build -o "$LIMEN_BUILD/tools/" '{{ pkg }}' elif grep -qE "$pattern" .limen/tools/go.mod 2>/dev/null; then {{ go_tool_build }} '{{ pkg }}' else @@ -96,8 +96,8 @@ _go-tool name pkg: _vulndb: #!/usr/bin/env bash set -euo pipefail - mkdir -p .build - ${LIMEN_BIN:-limen} vulndb .build/vulndb > .build/vulndb.url + mkdir -p "$LIMEN_BUILD" + ${LIMEN_BIN:-limen} vulndb "$LIMEN_BUILD/vulndb" > "$LIMEN_BUILD/vulndb.url" # The golangci-lint configuration the lint and fix recipes run: the baseline # limen-lint-go embeds with the project's .lint-go.yaml applied, rendered on @@ -111,5 +111,5 @@ _vulndb: # LIMEN_LINT_GO_BIN is for the limen repository itself, which builds it from # its own tree (same idea as LIMEN_BIN in lint.just). _golangci-config: - ${LIMEN_LINT_GO_BIN:-limen-lint-go} check .build/tools/golangci-lint - ${LIMEN_LINT_GO_BIN:-limen-lint-go} render -o .build/golangci.yml + ${LIMEN_LINT_GO_BIN:-limen-lint-go} check "$LIMEN_BUILD/tools/golangci-lint" + ${LIMEN_LINT_GO_BIN:-limen-lint-go} render -o "$LIMEN_BUILD/golangci.yml" diff --git a/.limen/just/lint-go.just b/.limen/just/lint-go.just index 21f18b18..9d2c8439 100644 --- a/.limen/just/lint-go.just +++ b/.limen/just/lint-go.just @@ -24,8 +24,8 @@ default: code vet mod licenses nilaway # The formatter is build-graph-independent and runs once. The last step # enforces the suppression doctrine (book/linting-go.md, "silencing a # finding"): a finding is silenced by its rule, never by its linter. -code: (_banner "lint go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform ".build/tools/golangci-lint run -c .build/golangci.yml") _suppression-check - .build/tools/golangci-lint fmt --diff -c .build/golangci.yml +code: (_banner "lint go" "go") (_go-tool "golangci-lint" "github.com/golangci/golangci-lint/v2/cmd/golangci-lint") _golangci-config (_per-platform '"$LIMEN_BUILD/tools/golangci-lint" run -c "$LIMEN_BUILD/golangci.yml"') _suppression-check + "$LIMEN_BUILD/tools/golangci-lint" fmt --diff -c "$LIMEN_BUILD/golangci.yml" # Polices the shape the book requires (linting-go.md, "silencing a # finding"): nolintlint checks a directive's form, not which linter it names, @@ -136,13 +136,13 @@ nilaway: (_banner "lint go" "nilaway") (_go-tool "nilaway" "go.uber.org/nilaway/ echo "CGO_ENABLED=1: cross-platform runs are impossible (foreign-platform package" >&2 echo "loading needs that platform's C toolchain) — native platform only." >&2 # shellcheck disable=SC2086 # the flags are a list by contract - .build/tools/nilaway $flags ./... || fail=1 + "$LIMEN_BUILD/tools/nilaway" $flags ./... || fail=1 else platforms=(darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64) for platform in "${platforms[@]}"; do echo "→ GOOS=${platform%/*} GOARCH=${platform#*/}" # shellcheck disable=SC2086 # the flags are a list by contract - CGO_ENABLED=0 GOOS="${platform%/*}" GOARCH="${platform#*/}" .build/tools/nilaway $flags ./... || fail=1 + CGO_ENABLED=0 GOOS="${platform%/*}" GOARCH="${platform#*/}" "$LIMEN_BUILD/tools/nilaway" $flags ./... || fail=1 done fi if [ "$fail" = 1 ] && [ "$mode" != blocking ]; then @@ -167,11 +167,11 @@ nilaway: (_banner "lint go" "nilaway") (_go-tool "nilaway" "go.uber.org/nilaway/ # graph, the repository's own LICENSE is the `license` rule's, and go-licenses # classifies a license by its text, so the Closed-source notice limen writes # failed every package of such a module. -licenses: (_banner "lint go" "licenses") (_go-tool "go-licenses" "github.com/google/go-licenses/v2") (_per-platform 'GOROOT="$(go env GOROOT)" .build/tools/go-licenses check --include_tests --ignore "$(go list -m)" $(${LIMEN_LINT_GO_BIN:-limen-lint-go} flags licenses) ./...') +licenses: (_banner "lint go" "licenses") (_go-tool "go-licenses" "github.com/google/go-licenses/v2") (_per-platform 'GOROOT="$(go env GOROOT)" "$LIMEN_BUILD/tools/go-licenses" check --include_tests --ignore "$(go list -m)" $(${LIMEN_LINT_GO_BIN:-limen-lint-go} flags licenses) ./...') # Unreachable functions, by whole-program call-graph analysis (deeper than the # per-package `unused` linter in golangci). -test keeps library-only repos # analyzable (test binaries serve as roots) and stops dead code from hiding # behind test-only callers. Informational: native only, not in the default set. deadcode: (_banner "lint go" "deadcode") (_go-tool "deadcode" "golang.org/x/tools/cmd/deadcode") - .build/tools/deadcode -test ./... + "$LIMEN_BUILD/tools/deadcode" -test ./... diff --git a/.limen/just/lint.just b/.limen/just/lint.just index 7cb5a945..c35bb41f 100644 --- a/.limen/just/lint.just +++ b/.limen/just/lint.just @@ -77,7 +77,7 @@ dockerfile: (_banner "lint" "dockerfile") (_go-tool "godolint" "github.com/forkc done < "$list" # godolint runs shellcheck on RUN lines when it finds one on PATH — the # pinned one, here. - [ ${#files[@]} -eq 0 ] || .build/tools/godolint "${files[@]}" + [ ${#files[@]} -eq 0 ] || "$LIMEN_BUILD/tools/godolint" "${files[@]}" yaml: (_banner "lint" "yaml") yamlfmt -conf .limen/.yamlfmt -lint @@ -142,13 +142,21 @@ shell: (_banner "lint" "shell") # name is only a label, so module separators are flattened. The directory # reaches awk through ENVIRON, never -v: -v processes escapes, and a # windows temp path's backslashes are eaten (\l becomes l). + # Each body ends with one line naming the root's exports as assigned + # (`: "${NAME:=}"`): just puts them in every recipe's environment, but a + # body read alone references LIMEN_BUILD unassigned (SC2154 under + # check-unassigned-uppercase). Appended, so the reported lines stay the + # recipe's own, and behind an SC2317 directive, since after a body's + # last `exit` the line is unreachable. printf '%s' "$dump" | jq -r ' - def recipes_of(prefix): + ((.assignments // {}) | to_entries | map(select(.value.export) | "\"${" + .key + ":=}\"") + | if length == 0 then [] else ["# shellcheck disable=SC2317", ": " + join(" ")] end) as $exports + | def recipes_of(prefix): ((.recipes // {}) | to_entries[] | (.value.body | map(map(if type == "string" then . else "INTERP" end) | join(""))) as $lines | select(($lines[0] // "") | test("^#!.*\\b(bash|dash|ksh|sh)\\b")) - | prefix + .key, ($lines | join("\n")), ([30] | implode)), + | prefix + .key, ($lines + $exports | join("\n")), ([30] | implode)), ((.modules // {}) | to_entries[] | .key as $mod | .value | recipes_of(prefix + $mod + "::")); recipes_of("")' | tr -d '\r' | LINT_SHELL_DIR="$tmpdir" awk ' @@ -215,9 +223,9 @@ commits range="": (_banner "lint" "commits") (_go-tool "git-validation" "github. fi export GIT_CONFIG_COUNT=$n if [ -n "$range" ]; then - .build/tools/git-validation -run DCO,short-subject,dangling-whitespace -range "$range" + "$LIMEN_BUILD/tools/git-validation" --run DCO,short-subject,dangling-whitespace --range "$range" else - .build/tools/git-validation -run DCO,short-subject,dangling-whitespace + "$LIMEN_BUILD/tools/git-validation" --run DCO,short-subject,dangling-whitespace fi # .lint-signers lists everyone who authors here, email and key, in git's # allowed-signers format: every commit in the range must be signed by a diff --git a/.limen/just/main.just b/.limen/just/main.just index 0ea04e67..916a4ef9 100644 --- a/.limen/just/main.just +++ b/.limen/just/main.just @@ -94,10 +94,20 @@ export CC := if xcode_bin == '' { env('CC', '') } else { xcode_bin / 'clang' } export CXX := if xcode_bin == '' { env('CXX', '') } else { xcode_bin / 'clang++' } export SDKROOT := if xcode_sdk == '' { env('SDKROOT', '') } else { xcode_sdk } +# The checkout's output directory, gitignored: what a recipe writes and keeps +# between runs goes under it, in a subdirectory named for what it holds. The +# only spelling of the name: the shared recipes and the project's own use +# {{ LIMEN_BUILD }} or $LIMEN_BUILD, a script a recipe runs reads it from the +# environment, and CI's setup-aqua and windows-cache-image actions export the +# same path for the steps after them. Absolute, so a script that changes +# directory still finds it; forward slashes on windows, which git-bash's own +# tools and the native ones both read. +export LIMEN_BUILD := replace(justfile_directory(), '\', '/') / '.build' + # Hermetic, per-project linter cache (same doctrine as PATH and GOROOT: no -# shared mutable state across repos). Lives under .build/ — gitignored, and -# local runs behave like CI instead of diverging on cache state. -export GOLANGCI_LINT_CACHE := justfile_directory() / '.build/cache/golangci-lint' +# shared mutable state across repos). Lives under LIMEN_BUILD, so local runs +# behave like CI instead of diverging on cache state. +export GOLANGCI_LINT_CACHE := LIMEN_BUILD / 'cache/golangci-lint' # just's default recipe is the first one defined, so a project's first own # recipe replaces this one. diff --git a/.limen/just/perf-go.just b/.limen/just/perf-go.just index 29ba6369..b2d67268 100644 --- a/.limen/just/perf-go.just +++ b/.limen/just/perf-go.just @@ -109,8 +109,8 @@ profile: (_banner "perf go" "profile") (_go-tool "dot" "github.com/forkcloser/do #!/usr/bin/env bash set -euo pipefail export CGO_ENABLED="{{ go_cgo }}" - prof=.build/profiles - docs=.build/profiles-docs + prof="$LIMEN_BUILD/profiles" + docs="$LIMEN_BUILD/profiles-docs" mkdir -p "$prof" "$docs" # Rendered from pprof's -dot text rather than pprof's own -png, which # shells out to a dot it expects on PATH. -o with a pre-created file: under @@ -121,7 +121,7 @@ profile: (_banner "perf go" "profile") (_go-tool "dot" "github.com/forkcloser/do src=$1; png=$2; shift 2 go tool pprof -dot -nodecount=20 "$@" "$src" > "$png.dot" 2>/dev/null : > "$png" - .build/tools/dot -Tpng -o "$png" "$png.dot" + "$LIMEN_BUILD/tools/dot" -Tpng -o "$png" "$png.dot" rm -f "$png.dot" echo " -> $png" } diff --git a/.limen/just/security-go.just b/.limen/just/security-go.just index d61df338..e357d576 100644 --- a/.limen/just/security-go.just +++ b/.limen/just/security-go.just @@ -29,4 +29,4 @@ vuln: (_banner "security go" "vuln") fi just do security go _vuln -_vuln: (_go-tool "govulncheck" "golang.org/x/vuln/cmd/govulncheck") _vulndb (_per-platform '.build/tools/govulncheck -db "$(cat .build/vulndb.url)" ./...') +_vuln: (_go-tool "govulncheck" "golang.org/x/vuln/cmd/govulncheck") _vulndb (_per-platform '"$LIMEN_BUILD/tools/govulncheck" -db "$(cat "$LIMEN_BUILD/vulndb.url")" ./...') diff --git a/.limen/just/security.just b/.limen/just/security.just index 94c479ea..181f4a9e 100644 --- a/.limen/just/security.just +++ b/.limen/just/security.just @@ -91,7 +91,7 @@ links: (_banner "security" "links") binaries: (_banner "security" "binaries") (_go-tool "govulncheck" "golang.org/x/vuln/cmd/govulncheck") _vulndb #!/usr/bin/env bash set -euo pipefail - db=$(cat .build/vulndb.url) + db=$(cat "$LIMEN_BUILD/vulndb.url") bin="$(aqua root-dir)/bin" # A newline-delimited list, not an associative array: macOS ships bash 3. seen=$'\n' @@ -108,7 +108,7 @@ binaries: (_banner "security" "binaries") (_go-tool "govulncheck" "golang.org/x/ # A darwin universal binary (goreleaser ships one) is beyond govulncheck's # reader; the other platforms' legs scan that tool's thin binaries, so # the darwin leg says so and moves on rather than failing on a format. - if ! out=$(.build/tools/govulncheck -db "$db" -mode binary "$path" 2>&1); then + if ! out=$("$LIMEN_BUILD/tools/govulncheck" -db "$db" -mode binary "$path" 2>&1); then case "$out" in *"unrecognized binary format"*) echo " skipped: universal binary, scanned on the other platforms";; *) printf '%s\n' "$out"; fail=1;; diff --git a/.limen/just/test-go.just b/.limen/just/test-go.just index 3091cbc2..51118f6a 100644 --- a/.limen/just/test-go.just +++ b/.limen/just/test-go.just @@ -120,7 +120,7 @@ cover: (_banner "test go" "cover") #!/usr/bin/env bash set -euo pipefail export CGO_ENABLED="{{ go_cgo }}" - dir=.build/coverage + dir="$LIMEN_BUILD/coverage" mkdir -p "$dir" gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -coverprofile="$dir/coverage.out" ./... go tool cover -func="$dir/coverage.out" diff --git a/.limen/just/tools.just b/.limen/just/tools.just index 5d84af7e..421c8734 100644 --- a/.limen/just/tools.just +++ b/.limen/just/tools.just @@ -126,7 +126,7 @@ set pkg version: (_banner "tools" "set") if grep -qE "^(tool[[:space:]]+)?[[:space:]]*${esc}([[:space:]]|$)" ".limen/tools/${name}/go.mod" 2>/dev/null; then go -C ".limen/tools/${name}" get -tool '{{ pkg }}@{{ version }}' # moves the pin go -C ".limen/tools/${name}" mod tidy - mkdir -p .build/tools && GOOS='' GOARCH='' CGO_ENABLED=0 go -C ".limen/tools/${name}" build -o '../../../.build/tools/' '{{ pkg }}' + mkdir -p "$LIMEN_BUILD/tools" && GOOS='' GOARCH='' CGO_ENABLED=0 go -C ".limen/tools/${name}" build -o "$LIMEN_BUILD/tools/" '{{ pkg }}' exit 0 fi if ! grep -qE "^(tool[[:space:]]+)?[[:space:]]*${esc}([[:space:]]|$)" .limen/tools/go.mod 2>/dev/null; then @@ -192,7 +192,7 @@ update command="": (_banner "tools" "update") [ -n "$p" ] || return 0 go -C "$dir" get -tool "${p}@latest" go -C "$dir" mod tidy - mkdir -p .build/tools && GOOS='' GOARCH='' CGO_ENABLED=0 go -C "$dir" build -o '../../../.build/tools/' "$p" + mkdir -p "$LIMEN_BUILD/tools" && GOOS='' GOARCH='' CGO_ENABLED=0 go -C "$dir" build -o "$LIMEN_BUILD/tools/" "$p" } {{ project_aqua }} if [ -z '{{ command }}' ]; then @@ -243,7 +243,7 @@ remove pkg: (_banner "tools" "remove") # An isolated module (.limen/tools//go.mod) is the tool's alone: it # goes as a whole, go.sum included. if grep -qE "^(tool[[:space:]]+)?[[:space:]]*${esc}([[:space:]]|$)" ".limen/tools/${name}/go.mod" 2>/dev/null; then - rm -rf ".limen/tools/${name}" ".build/tools/${name}" ".build/tools/${name}.exe" + rm -rf ".limen/tools/${name}" "$LIMEN_BUILD/tools/${name}" "$LIMEN_BUILD/tools/${name}.exe" exit 0 fi if ! grep -qE "^(tool[[:space:]]+)?[[:space:]]*${esc}([[:space:]]|$)" .limen/tools/go.mod 2>/dev/null; then @@ -252,7 +252,7 @@ remove pkg: (_banner "tools" "remove") fi go -C .limen/tools get -tool '{{ pkg }}@none' # drops the directive go -C .limen/tools mod tidy # and the requirements only it needed - rm -f ".build/tools/${name}" ".build/tools/${name}.exe" + rm -f "$LIMEN_BUILD/tools/${name}" "$LIMEN_BUILD/tools/${name}.exe" exit 0 fi if ! grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc}([[:space:]]|@|$)" .aqua/aqua.yaml; then