#!/bin/sh # The cross-compile gate, one target per shard. # # Why it exists at all is worth keeping next to it: readDiskFreeBytes in # internal/host/probes.go multiplied an int64 by syscall.Statfs_t.Bsize, # which is int64 on Linux and uint32 on darwin. That is a compile error # on macOS for every GOARCH, internal/host is linked into the carlos CLI # people run on Macs, and it sat in main undetected because everyone who # built it was on Linux. Cross-compiling catches it in seconds and needs # no Mac. # # Windows is a CLIENT target: `carlos deploy` from a Windows workstation # against a Linux fleet. The box-side packages still have to COMPILE # there, because the CLI is the same binary as the agent, so unix-only # syscalls live behind per-OS files (internal/edge/purge_listen_*.go, # internal/host/{statfs,flock}_*.go). These two shards are what stops # the next syscall.Foo from silently un-shipping the Windows build. # # The @5 is amadan's whole sharding interface: it runs this script five # times concurrently and tells each copy which one it is. One target per # shard rather than five builds in a row, because they are independent # and this box has the cores — and because a failing target then names # itself in the shard that failed, instead of being the first line of a # combined log. set -eu shard=${AMADAN_SHARD:?this step must be run by amadan CI, which sets AMADAN_SHARD} shards=${AMADAN_SHARD_COUNT:?this step must be run by amadan CI, which sets AMADAN_SHARD_COUNT} # The fence. The targets below are a fixed list, and the shard count in # this file's name is what deals them out — so the two MUST agree. Rename # this to @4 and, without this check, one platform silently stops being # compiled while CI stays green. That is the exact shape of the bug the # job exists to catch, arriving through the job that catches it. if [ "$shards" -ne 5 ]; then echo "this step deals 5 fixed targets but was run with $shards shards —" echo "rename the file back to 20-cross@5, or add a target below" exit 1 fi case "$shard" in 0) GOOS=darwin GOARCH=arm64 ;; 1) GOOS=darwin GOARCH=amd64 ;; 2) GOOS=linux GOARCH=arm64 ;; 3) GOOS=windows GOARCH=amd64 ;; 4) GOOS=windows GOARCH=arm64 ;; *) echo "no target for shard $shard"; exit 1 ;; esac echo "cross-compiling $GOOS/$GOARCH" # CGO_ENABLED=0 is already the default when cross-compiling; explicit # here so that adding a cgo dependency fails loudly rather than quietly # changing what this covers. The tree is pure Go today. GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=0 go build ./... # The size ceiling, on the one shard that builds the shipping target. # # The platform binary is what every box downloads and what the update gate # waits on, so its size is a cost to the fleet. On 2026-10-13 it doubled from # about 45 MB to 77.7 MB linux/arm64 with no one deciding that (discussion # #275). Cause: html/template, which the edge and console pages use, puts the # Go linker in a conservative mode that keeps EVERY exported method of any # concrete type stored in an interface value. internal/awsinv's factoryFor put # *ec2.Client (~2,400 methods, 14 MB) and six more SDK clients into narrow # interfaces, and the whole lot came along. The fix is to store METHOD VALUES # (a struct or func type bound to the few calls actually made) and not the # client; wrapping the client in a struct does not help. # # The ceiling is the measured size after that fix (45,678,754 bytes, 45.7 MB) # plus 6 MB of headroom for ordinary growth, rounded up to a whole MB: # 52 MB (decimal, 52000000 bytes). It is a ceiling and not an equality on # purpose. If you are here because it fired, look for an SDK client stored in # an interface, which html/template's method retention then drags in whole: # # go build -ldflags=-dumpdep -o /dev/null . 2>&1 | # grep 'go:itab.*aws-sdk-go-v2/service' # # names the conversions. Two are inherent and are not the cause: s3.New and # config.assumeWebIdentity convert their own clients inside the SDK, so S3 # and STS stay whole whatever this repo does. Raise the number only for # growth you meant, and say why in the commit. if [ "$GOOS/$GOARCH" = "linux/arm64" ]; then max_bytes=52000000 size_dir=$(mktemp -d) trap 'rm -rf "$size_dir"' EXIT GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "-s -w" -o "$size_dir/carlos" . # No pipeline here: `wc | tr` would report tr's exit status, an unreadable # binary would leave size empty, and the comparison below would error # inside `if` and let the step pass. Fail on the read, then on anything # that is not a number (BSD wc pads with spaces, which $(( )) accepts). size=$(wc -c <"$size_dir/carlos") || { echo "cannot read the built binary $size_dir/carlos"; exit 1; } case $size in '' | *[!0-9' ']*) echo "could not measure the linux/arm64 binary (wc said '$size')"; exit 1 ;; esac size=$((size)) echo "linux/arm64 carlos binary: $size bytes (ceiling $max_bytes)" if [ "$size" -gt "$max_bytes" ]; then echo "the linux/arm64 binary is $size bytes, over the $max_bytes ceiling." echo "look for an SDK client stored in an interface under html/template's" echo "method retention (every exported method of the client is kept):" echo "store method values instead. See discussion #275 and the comment in" echo ".amadan/ci.d/20-cross@5." exit 1 fi fi