#!/usr/bin/env bash # Build the ArgyllCMS helpers the app ships (spotread, oeminst) from the pinned # upstream source, unmodified, and stage them with their license notices. # # This replaces the old Homebrew route (copy /opt/homebrew/bin/spotread, then # rewrite its dylib load paths). Homebrew is not part of the shipping pipeline # any more; a developer can still point SPOTREAD_PATH / OEMINST_PATH at a # Homebrew install for local experiments. # # What it does, in order: # 1. Gets the pinned Argyll source zip and the pinned Argyll-Jam tgz # (argyll_pins.sh): a local file you pass, else the cache, else ONE https # download. The SHA-256 is always checked; a mismatch stops the build. # 2. Unpacks both into a fresh work dir and builds `jam0` from the Argyll-Jam # source with the system compiler (its own common.mk rule). # 3. Runs Argyll's own Jam build for exactly `spotread` and `oeminst`, with a # scrubbed environment (`env -i`), the flags upstream's own release script # (makepackagebin.sh) uses for the bundled libraries, and: # - MACOSX_DEPLOYMENT_TARGET=13.0, arm64 only (the app's floor and arch); # - LDFLAGS=-Wl,-S (no debug map, which would embed absolute .o paths # and timestamps) and ZERO_AR_DATE=1, so the build is reproducible. # Argyll's bundled libraries are compiled from the same zip and linked # statically. OpenSSL is never used: BUILTIN_SSL picks Argyll's bundled # axTLS, which only the ChromeCast code needs, and neither program links # that code at all. # 4. Checks each binary: arm64 only, every load command under /System/ or # /usr/lib/, minimum macOS 13.0, a valid (linker) signature, `-?` prints # the pinned version, and its code digest equals the pinned reference # (ARGYLL_ALLOW_DIGEST_DRIFT=1 downgrades a mismatch to a warning, for a # deliberate toolchain bump). # 5. Stages Contents/Helpers/{spotread,oeminst} and # Contents/Resources/licenses/argyllcms/ into the sidecar staging tree, # where prepare_sidecars.sh copies them into the app, then runs # tool/verify_argyll_helpers.sh over the staging tree. # # No source file is edited. The one compile flag added to jam0 # (`-include unistd.h`) only declares functions the old jam sources call # without a prototype, which current clang rejects. # # Usage: # build_argyll.sh [--src-zip FILE] [--jam-tgz FILE] [--if-needed] [--no-stage] # --src-zip FILE use this Argyll_V_src.zip instead of cache/download # --jam-tgz FILE use this ajam tgz instead of cache/download # --if-needed do nothing when the staged helpers already match the pins # (prepare_sidecars.sh uses this on every Xcode build) # --no-stage build and check only; leave staging alone # # Env: # SIDECAR_STAGING staging dir (default: /staging) # ARGYLL_BUILD_DIR work + output dir (default: packages/app/build/argyll) # ARGYLL_CACHE_DIR download cache (default: $ARGYLL_BUILD_DIR/cache) # ARGYLL_NO_DOWNLOAD=1 never touch the network; need local or cached files # ARGYLL_ALLOW_DIGEST_DRIFT=1 stage even if the code digest differs from the pin # ARGYLL_PINS_FILE tests only: an alternative pins file # # Writes $ARGYLL_BUILD_DIR/out/build-record.json (toolchain, input and output # digests). Performs NO Developer ID signing; prepare_sidecars.sh signs. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$HERE/../../../.." && pwd)" ARGYLL_BUILD_DIR="${ARGYLL_BUILD_DIR:-$ROOT/packages/app/build/argyll}" export ARGYLL_CACHE_DIR="${ARGYLL_CACHE_DIR:-$ARGYLL_BUILD_DIR/cache}" # shellcheck source=argyll_lib.sh source "$HERE/argyll_lib.sh" STAGING="${SIDECAR_STAGING:-$HERE/staging}" VERIFY="$ROOT/tool/verify_argyll_helpers.sh" SRC_ZIP_ARG="" JAM_TGZ_ARG="" IF_NEEDED=0 STAGE=1 while [[ $# -gt 0 ]]; do case "$1" in --src-zip) SRC_ZIP_ARG="${2:?--src-zip needs a file}"; shift 2 ;; --jam-tgz) JAM_TGZ_ARG="${2:?--jam-tgz needs a file}"; shift 2 ;; --if-needed) IF_NEEDED=1; shift ;; --no-stage) STAGE=0; shift ;; -h|--help) sed -n '2,60p' "$0"; exit 0 ;; *) echo "error: unknown argument $1" >&2; exit 2 ;; esac done say() { echo "==> build_argyll: $*"; } die() { echo "error: build_argyll: $*" >&2; exit 1; } if [[ "$IF_NEEDED" -eq 1 && "$STAGE" -eq 1 ]]; then # During a deliberate toolchain bump (ARGYLL_ALLOW_DIGEST_DRIFT=1) the staged # bytes differ from the pin on purpose; accept them here too, or every Xcode # build would rebuild Argyll from scratch. IF_NEEDED_ARGS=(--quiet) [[ "${ARGYLL_ALLOW_DIGEST_DRIFT:-0}" == "1" ]] && IF_NEEDED_ARGS+=(--allow-digest-drift) if "$VERIFY" "${IF_NEEDED_ARGS[@]}" "$STAGING" >/dev/null 2>&1; then say "staged Argyll $ARGYLL_VERSION helpers already match the pins — nothing to do" exit 0 fi say "staged Argyll helpers missing or not the pinned build — building" fi # --- 0. Host --------------------------------------------------------------- [[ "$(uname -s)" == "Darwin" ]] || die "macOS only" [[ "$(uname -m)" == "$ARGYLL_ARCH" ]] || die "needs an $ARGYLL_ARCH Mac (this host is $(uname -m))" if [[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" == "1" ]]; then die "running under Rosetta; run natively on $ARGYLL_ARCH" fi for tool in /usr/bin/cc /usr/bin/make otool lipo nm codesign shasum unzip tar perl; do command -v "$tool" >/dev/null 2>&1 || die "$tool not found (install the Xcode command line tools)" done # --- 1. Inputs ------------------------------------------------------------- SRC_ZIP="$(argyll_fetch "$ARGYLL_SRC_URL" "$ARGYLL_SRC_NAME" "$ARGYLL_SRC_SHA256" "$SRC_ZIP_ARG")" \ || die "could not get a verified $ARGYLL_SRC_NAME" JAM_TGZ="$(argyll_fetch "$ARGYLL_JAM_URL" "$ARGYLL_JAM_NAME" "$ARGYLL_JAM_SHA256" "$JAM_TGZ_ARG")" \ || die "could not get a verified $ARGYLL_JAM_NAME" say "source $SRC_ZIP (sha256 $ARGYLL_SRC_SHA256)" say "jam $JAM_TGZ (sha256 $ARGYLL_JAM_SHA256)" # --- 2. Fresh work dir + jam0 ---------------------------------------------- WORK="$ARGYLL_BUILD_DIR/work" OUT="$ARGYLL_BUILD_DIR/out" rm -rf "$WORK" "$OUT" mkdir -p "$WORK" "$OUT" (cd "$WORK" && unzip -q "$SRC_ZIP" && tar -xzf "$JAM_TGZ") [[ -f "$WORK/$ARGYLL_SRC_DIR/Jambase" ]] || die "$ARGYLL_SRC_NAME did not unpack to $ARGYLL_SRC_DIR/" [[ -f "$WORK/$ARGYLL_JAM_DIR/common.mk" ]] || die "$ARGYLL_JAM_NAME did not unpack to $ARGYLL_JAM_DIR/" CLEAN_ENV=(env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin HOME="$HOME") say "building jam0" (cd "$WORK/$ARGYLL_JAM_DIR" && "${CLEAN_ENV[@]}" /usr/bin/make -s -f common.mk jam0 \ CC=/usr/bin/cc TARGET="-o jam0" CFLAGS="-O2 -include unistd.h") \ > "$OUT/jam0.log" 2>&1 || { tail -40 "$OUT/jam0.log" >&2; die "jam0 build failed (log: $OUT/jam0.log)"; } JAM0="$WORK/$ARGYLL_JAM_DIR/jam0" [[ -x "$JAM0" ]] || die "jam0 was not produced" # --- 3. Argyll --------------------------------------------------------------- TARGETS=() for h in $ARGYLL_HELPERS; do TARGETS+=("$h") done # Serial on purpose: a parallel jam build archives the static libraries in # completion order, which shuffles spotread's GOT slots and its code digest from # run to run (2026-10-04: two -j10 builds on one host differed by 193 bytes; # serial builds reproduce the pin every time). A serial build takes about 80 s. JOBS=1 say "building $ARGYLL_HELPERS (Argyll $ARGYLL_VERSION, $JOBS jobs)" (cd "$WORK/$ARGYLL_SRC_DIR" && "${CLEAN_ENV[@]}" \ OSTYPE=darwin MACHTYPE="$ARGYLL_ARCH-apple-darwin" HOSTTYPE="$ARGYLL_ARCH" \ MACOSX_DEPLOYMENT_TARGET="$ARGYLL_MACOS_MIN" ZERO_AR_DATE=1 LDFLAGS="-Wl,-S" \ "$JAM0" -q -fJambase -j"$JOBS" \ -sBUILTIN_TIFF=true -sBUILTIN_JPEG=true -sBUILTIN_PNG=true \ -sBUILTIN_Z=true -sBUILTIN_SSL=true \ "${TARGETS[@]}") \ > "$OUT/argyll.log" 2>&1 || { tail -60 "$OUT/argyll.log" >&2; die "Argyll build failed (log: $OUT/argyll.log)"; } # --- 4. Check each binary ---------------------------------------------------- TOOLCHAIN_XCODE="$(xcodebuild -version 2>/dev/null | tr '\n' ' ' | sed 's/ *$//' || true)" TOOLCHAIN_CC="$(/usr/bin/cc --version 2>/dev/null | head -1 || true)" TOOLCHAIN_SDK="$(xcrun --show-sdk-version 2>/dev/null || true)" DRIFT=0 RECORD_HELPERS="" for h in $ARGYLL_HELPERS; do bin="$WORK/$ARGYLL_SRC_DIR/spectro/$h" [[ -f "$bin" ]] || die "$h was not produced" archs="$(lipo -archs "$bin")" [[ "$archs" == "$ARGYLL_ARCH" ]] || die "$h has architectures '$archs', expected '$ARGYLL_ARCH'" nonsystem="$(otool -L "$bin" | tail -n +2 | awk 'NF{print $1}' | grep -vE '^(/usr/lib/|/System/)' || true)" [[ -z "$nonsystem" ]] || die "$h loads non-system libraries: $(echo "$nonsystem" | tr '\n' ' ')" minos="$(otool -l "$bin" | awk '/cmd LC_BUILD_VERSION/{f=1} f && $1=="minos"{print $2; exit}')" [[ "$minos" == "$ARGYLL_MACOS_MIN" ]] || die "$h has minimum macOS '$minos', expected $ARGYLL_MACOS_MIN" codesign --verify --strict "$bin" 2>/dev/null || die "$h does not carry a valid linker signature" # NOTICE.txt says neither program contains OpenSSL, libusb, libtiff, libjpeg, # libpng or zlib code. The build also compiles axTLS (BUILTIN_SSL), the # ChromeCast client, yajl and mxml archives, none of whose members either # program pulls in, and the notice does not cover them either. Any of their # symbols here means the license notices need revisiting before this can ship. syms="$(nm "$bin" 2>/dev/null || true)" grep -qE ' T _main$' <<<"$syms" || die "$h has no readable symbol table; cannot check its third-party contents" third="$(grep -E '_TIFF|_jpeg_|_png_|zlibVersion|_libusb_|_ssl_|_SSL_|_yajl_|_mxml|_ccast' <<<"$syms" | head -5 || true)" [[ -z "$third" ]] || die "$h now links third-party library code the notices do not cover: $(echo "$third" | tr '\n' ' ')" # Usage output is the cheapest proof the binary runs. Argyll exits 1 after # printing usage, so the text is checked, not the status. perl's alarm is a # timeout: spotread lists instrument ports in its usage and must not hang. usage="$(perl -e 'alarm 60; exec @ARGV' "$bin" '-?' 2>&1 || true)" printf '%s\n' "$usage" > "$OUT/$h-usage.txt" grep -q "Version $ARGYLL_VERSION" <<<"$usage" \ || die "$h -? did not print 'Version $ARGYLL_VERSION' (see $OUT/$h-usage.txt)" file_sha="$(argyll_sha256 "$bin")" code_sha="$(argyll_code_sha256 "$bin")" || die "could not compute the code digest of $h" ref="$(argyll_ref_code_sha256 "$h")" if [[ "$code_sha" == "$ref" ]]; then match=true say "$h ok: $archs, system libraries only, macOS $minos+, code digest matches the pin" else match=false DRIFT=1 echo "warning: build_argyll: $h code digest $code_sha differs from the pinned $ref" >&2 echo " (pinned toolchain: $ARGYLL_REF_TOOLCHAIN; this host: $TOOLCHAIN_XCODE)" >&2 fi cp "$bin" "$OUT/$h" otool -L "$bin" > "$OUT/$h-otool-L.txt" RECORD_HELPERS+="$(printf ' "%s": {"sha256": "%s", "codeSha256": "%s", "referenceCodeSha256": "%s", "matchesReference": %s}' \ "$h" "$file_sha" "$code_sha" "$ref" "$match"),"$'\n' done RECORD_HELPERS="${RECORD_HELPERS%,$'\n'}" json_str() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; } cat > "$OUT/build-record.json" < "$LIC_DIR/SOURCE.txt" <