From 84429c006cfa0089446e24998e418b038a4cf377 Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Sat, 19 Sep 2026 10:35:29 +0100 Subject: [PATCH] feat(ae add): require .sha256 on binary packages, add --target (#2105, #2108) Two follow-ups on the ae add binary-package path, both scoped to the aether side (the publisher-side aether.toml wiring in #2105 is a downstream release change). #2105 checksum strictness: a binary package now REQUIRES a published .sha256. The bare-lib path previously matched the archive path -- warn and install unverified when no checksum sidecar was published. A raw shared library fetched over the network deserves mandatory verification (stricter than a git tag or a source archive), so ae_try_binary_package now treats a missing .sha256 as fatal (refuses to install). A published-but-mismatched checksum stays fatal as before. #2108 --target: fetch a FOREIGN platform's binary package instead of the host's, for cross-platform bundling / release CI. `ae add pkg@v --target macos-arm64` fetches --macos-arm64.dylib using the TRIPLE's OS extension (new ae_shlib_ext_for_triple), not the host's. With an explicit --target the binary package is the only correct path -- a host source archive/git clone would build the wrong platform -- so a non-binary-package target is a hard error, not a fall-through. --target and --source are mutually exclusive. (--all-targets and --source dual-mode from #2108 remain tracked-future; only --target now.) ae_try_release_asset takes a target param (NULL = host); the flag is parsed in cmd_add and the usage text documents both. The common leaf-consumer host path is unchanged. Test: ae_add_binary_package gains 4b (missing .sha256 is refused), 4c (--target fetches the foreign lib under the right ext; --target+--source rejected). Existing binary_import + dep_resolution unaffected. 3/3 stable. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 20 +++- .../test_ae_add_binary_package.sh | 52 ++++++++- tools/ae.c | 100 ++++++++++++++---- 3 files changed, 150 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f73a0450..46386e19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,25 @@ version number before tagging the release. ## [current] -## [0.695.0] +### Added + +- **`ae add --target ` fetches a foreign platform's binary package.** + The binary-package path fetched only the host's shared lib; `--target + macos-arm64` (or any release triple — `linux-x86_64`, `freebsd-x86_64`, …) now + fetches that platform's lib instead, with the triple's own extension + (`.dylib`/`.dll`/`.so`), for cross-platform bundling / release CI. A `--target` + that names a package with no binary declaration is a hard error, not a + host-source fall-through, and `--target` with `--source` is rejected. (#2108, + the leaf-consumer host case is unchanged.) + +### Changed + +- **A binary package now REQUIRES a published `.sha256`.** The bare-lib path + previously warned and installed unverified when no checksum sidecar was + published (matching the archive path); it now refuses. A raw shared library + downloaded over the network deserves mandatory verification — stricter than a + git tag or a source archive. A published-but-mismatched checksum stays fatal + as before. (#2105.) ### Fixed diff --git a/tests/integration/ae_add_binary_package/test_ae_add_binary_package.sh b/tests/integration/ae_add_binary_package/test_ae_add_binary_package.sh index bd252679..ebd55948 100755 --- a/tests/integration/ae_add_binary_package/test_ae_add_binary_package.sh +++ b/tests/integration/ae_add_binary_package/test_ae_add_binary_package.sh @@ -18,6 +18,11 @@ # through to the archive path (here: to git, which fails on the fake forge; # we assert it did NOT install as a binary package). # 4. a MISMATCHED checksum on the lib is fatal — nothing installed. +# 4b. a MISSING .sha256 on a binary package is fatal too (stricter than the +# archive path): a raw downloaded shared library must be verifiable. +# 4c. `--target ` fetches a FOREIGN platform's binary (right OS ext), +# and `--target` with `--source` is rejected. +# 5. end-to-end: a real installed lib actually imports + runs after ae add. # # HOME is redirected per-case so the real package cache is never touched. @@ -86,14 +91,26 @@ mk_binpkg() { head -c 4096 /dev/urandom > "$d/$asset" if [ "$mode" = "badsum" ]; then echo "0000000000000000000000000000000000000000000000000000000000000000 $asset" > "$d/$asset.sha256" + elif [ "$mode" = "nosum" ]; then + : # lib present, NO .sha256 sidecar — a binary package must refuse this else ( cd "$d" && $SHA "$asset" | awk -v n="$asset" '{print $1" "n}' > "$asset.sha256" ) fi + # A FOREIGN-platform lib for the same release, so `ae add --target ` + # has something to fetch. Pick a triple that is NOT this host's. + case "$TRIPLE" in + linux-x86_64) FT="macos-arm64"; FEXT=".dylib" ;; + *) FT="linux-x86_64"; FEXT=".so" ;; + esac + fasset="$STEM-$tag-$FT$FEXT" + head -c 4096 /dev/urandom > "$d/$fasset" + ( cd "$d" && $SHA "$fasset" | awk -v n="$fasset" '{print $1" "n}' > "$fasset.sha256" ) } mk_binpkg v1.0.0 ok mk_binpkg v2.0.0 nolib mk_binpkg v3.0.0 nobinkey mk_binpkg v4.0.0 badsum +mk_binpkg v5.0.0 nosum # ---- serve on a free loopback port --------------------------------------- PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()') @@ -156,6 +173,39 @@ fi grep -qi "checksum MISMATCH" "$TMP/badsum.log" || fail "mismatched checksum was not reported" "$TMP/badsum.log" [ -d "$P/home/.aether/packages/$PKG" ] && fail "a mismatched-checksum install left files behind" +# ---- Property 4b: a MISSING .sha256 on a binary package is FATAL ---------- +# A binary package REQUIRES a published checksum (stricter than the archive +# path, which warns and installs unverified): a raw downloaded shared library +# must be verifiable (#2105). +P="$(new_proj nosum)" +if ( cd "$P" && HOME="$P/home" AE_RELEASE_BASE_URL="$BASE" "$AE" add "$PKG@v5.0.0" ) \ + >"$TMP/nosum.log" 2>&1; then + fail "a binary package with no .sha256 should have been refused" "$TMP/nosum.log" +fi +grep -qi "refusing to install an" "$TMP/nosum.log" \ + || fail "missing-checksum was not refused with the expected message" "$TMP/nosum.log" +[ -d "$P/home/.aether/packages/$PKG" ] && fail "a no-checksum install left files behind" + +# ---- Property 4c: --target fetches a FOREIGN platform's binary ------------ +# `--target ` names another platform's lib (for cross-platform bundling), +# using the triple's OS extension, not the host's. It installs it; and --target +# on a package that isn't a binary package is an error, not a host fall-through. +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) FT="macos-arm64" ; FE=".dylib" ;; + *) FT="linux-x86_64"; FE=".so" ;; +esac +P="$(new_proj target)" +( cd "$P" && HOME="$P/home" AE_RELEASE_BASE_URL="$BASE" "$AE" add "$PKG@v1.0.0" --target "$FT" ) \ + >"$TMP/target.log" 2>&1 || fail "--target fetch of a foreign binary failed" "$TMP/target.log" +[ -f "$P/home/.aether/packages/$PKG/$STEM$FE" ] \ + || fail "--target did not install the foreign lib under $STEM$FE" "$TMP/target.log" +# --target with --source is rejected +if ( cd "$(new_proj tconf)" && HOME="$TMP/proj_tconf/home" AE_RELEASE_BASE_URL="$BASE" \ + "$AE" add "$PKG@v1.0.0" --target "$FT" --source ) >"$TMP/tconf.log" 2>&1; then + fail "--target --source together should be rejected" "$TMP/tconf.log" +fi +grep -qi "mutually exclusive" "$TMP/tconf.log" || fail "--target/--source conflict not reported" "$TMP/tconf.log" + # ---- Property 5: END-TO-END — a real installed lib actually IMPORTS + RUNS - # Properties 1-4 use a random payload (they exercise fetch/verify/install). This # one builds a REAL importable shared lib, publishes it as a binary package, and @@ -190,5 +240,5 @@ case "$RUN" in exit 1 ;; esac -echo " [PASS] ae_add_binary_package: bare lib + released aether.toml, verified, imports+runs, no fallback ladder" +echo " [PASS] ae_add_binary_package: install+import+run, require-checksum, --target foreign fetch, no fallback ladder" exit 0 diff --git a/tools/ae.c b/tools/ae.c index 788f0900..cdb9e4a2 100644 --- a/tools/ae.c +++ b/tools/ae.c @@ -7590,6 +7590,17 @@ static const char* ae_host_shlib_ext(void) { #endif } +/* The shared-lib extension a release TRIPLE implies, by its OS prefix: + * macos-* -> .dylib, windows-* -> .dll, everything else (linux-*, freebsd-*, + * other ELF) -> .so. Used for `ae add --target `, where the artifact + * to fetch is a FOREIGN platform's, so the host's own extension is wrong. */ +static const char* ae_shlib_ext_for_triple(const char* triple) { + if (!triple) return ae_host_shlib_ext(); + if (strncmp(triple, "macos-", 6) == 0) return ".dylib"; + if (strncmp(triple, "windows-", 8) == 0) return ".dll"; + return ".so"; +} + /* Last path component of "github.com/user/repo" -> "repo". */ static const char* ae_pkg_basename(const char* package) { const char* slash = strrchr(package, '/'); @@ -7689,7 +7700,8 @@ static int ae_verify_sha256(const char* archive, const char* url_base, */ static int ae_try_binary_package(const char* package, const char* tag, const char* triple, const char* url_base, - const char* tmp_dir, const char* pkg_dir) { + const char* tmp_dir, const char* pkg_dir, + int foreign_target) { /* 1. Fetch the released aether.toml (a normal named asset). */ char toml_url[2048], toml_tmp[1024]; if (ae_sprintf(toml_url, sizeof(toml_url), "%s/aether.toml", url_base) != 0 || @@ -7714,8 +7726,10 @@ static int ae_try_binary_package(const char* package, const char* tag, snprintf(stem, sizeof(stem), "%s", stem_raw); toml_free_document(doc); - /* 3. Name the host's lib asset: --. */ - const char* ext = ae_host_shlib_ext(); + /* 3. Name the lib asset for the requested triple: --. + * The extension follows the TRIPLE's OS (not the host's), so `--target + * macos-arm64` on a Linux host names `--macos-arm64.dylib`. */ + const char* ext = ae_shlib_ext_for_triple(triple); char lib_asset[512], lib_url[2048], lib_path[1024]; if (ae_sprintf(lib_asset, sizeof(lib_asset), "%s-%s-%s%s", stem, tag, triple, ext) != 0 || ae_sprintf(lib_url, sizeof(lib_url), "%s/%s", url_base, lib_asset) != 0 || @@ -7725,18 +7739,23 @@ static int ae_try_binary_package(const char* package, const char* tag, } remove(lib_path); if (ae_download(lib_url, lib_path) != 0 || !path_exists(lib_path)) { - /* The manifest declared a binary package but the host's lib is not - * published — a real error for this host, not a reason to try a source + /* The manifest declared a binary package but the requested platform's + * lib is not published — a real error, not a reason to try a source * archive that would land the uncompilable tree. */ fprintf(stderr, - "Error: %s declares a binary package but publishes no %s for this host.\n", - package, lib_asset); + "Error: %s declares a binary package but publishes no %s for %s.\n", + package, lib_asset, foreign_target ? triple : "this host"); remove(toml_tmp); return -1; } printf("Found binary package %s\n", lib_asset); - /* 4. Verify the lib against its published .sha256 (same policy as archives). */ + /* 4. Verify the lib against its published .sha256. A binary package REQUIRES + * the checksum — stricter than the archive path (which warns and installs + * unverified on a missing sum), because this is a raw executable shared + * library downloaded over the network: refusing an unverifiable one is the + * right supply-chain default (aether#2105). A mismatch is fatal on both + * paths. */ int v = ae_verify_sha256(lib_path, url_base, lib_asset, tmp_dir); if (v < 0) { fprintf(stderr, "Error: checksum MISMATCH for %s — refusing to install.\n", lib_asset); @@ -7744,10 +7763,14 @@ static int ae_try_binary_package(const char* package, const char* tag, return -1; } if (v == 0) { - fprintf(stderr, "Warning: %s publishes no .sha256 — installing unverified.\n", lib_asset); - } else { - printf("Checksum verified.\n"); + fprintf(stderr, + "Error: binary package %s publishes no %s.sha256 — refusing to install an\n" + " unverified shared library. Ask the publisher to attach the checksum.\n", + package, lib_asset); + remove(lib_path); remove(toml_tmp); + return -1; } + printf("Checksum verified.\n"); /* 5. Install the lib + the aether.toml into pkg_dir. The lib is staged under * `` (dropping the -- the ASSET name carries), because @@ -7782,8 +7805,11 @@ static int ae_try_binary_package(const char* package, const char* tag, * Returns 1 when the package was installed from an artifact, 0 when no * matching artifact exists (caller falls back to git). */ static int ae_try_release_asset(const char* package, const char* version, - const char* pkg_dir) { - const char* triple = ae_host_triple(); + const char* pkg_dir, const char* target) { + /* `target` (from --target ) fetches a FOREIGN platform's binary + * package instead of the host's; NULL means "this host". */ + int foreign = (target != NULL); + const char* triple = target ? target : ae_host_triple(); if (!triple) return 0; /* unpublished host → clone */ if (!version) return 0; /* artifacts are per-tag */ @@ -7815,9 +7841,20 @@ static int ae_try_release_asset(const char* package, const char* version, /* Binary-package path first: a released aether.toml with a `binary` key is * the explicit, declared signal (no fallback ladder — see the function). A * 0 means "not a binary package"; carry on to the archive path below. */ - int bp = ae_try_binary_package(package, tag, triple, url_base, tmp_dir, pkg_dir); + int bp = ae_try_binary_package(package, tag, triple, url_base, tmp_dir, pkg_dir, foreign); if (bp != 0) return bp; /* 1 installed, -1 fatal */ + /* --target names a FOREIGN platform's binary. A host source archive or a + * git clone would build the WRONG platform, so with an explicit target the + * binary package is the only correct path: fail rather than fall through. */ + if (foreign) { + fprintf(stderr, + "Error: %s is not a binary package (no [package] binary in its released\n" + " aether.toml), so there is nothing to fetch for --target %s.\n", + package, target); + return -1; + } + /* tar.gz first (the POSIX default), then zip (what the Windows * releases publish). */ const char* exts[2] = { "tar.gz", "zip" }; @@ -7875,22 +7912,45 @@ static void ae_add_rmrf(const char* dir) { static int cmd_add(int argc, char** argv) { if (argc < 1 || argv[0][0] == '-') { - fprintf(stderr, "Usage: ae add //[@version] [--source]\n"); + fprintf(stderr, "Usage: ae add //[@version] [--source] [--target ]\n"); fprintf(stderr, "Examples:\n"); fprintf(stderr, " ae add github.com/user/repo\n"); fprintf(stderr, " ae add github.com/user/repo@v1.2.0\n"); fprintf(stderr, " ae add gitlab.com/user/repo\n"); + fprintf(stderr, " ae add github.com/user/repo@v1.2.0 --target macos-arm64\n"); fprintf(stderr, "\nWith @version, a matching release artifact is preferred when the\n"); fprintf(stderr, "package publishes one: a binary package (a released aether.toml with\n"); fprintf(stderr, "a `binary` key naming a bare per-triple shared lib), else a source\n"); - fprintf(stderr, "archive. --source forces the git clone.\n"); + fprintf(stderr, "archive. A binary package REQUIRES a published .sha256 (an\n"); + fprintf(stderr, "unverifiable shared library is refused). --source forces the git\n"); + fprintf(stderr, "clone. --target fetches a FOREIGN platform's binary package\n"); + fprintf(stderr, "(e.g. linux-x86_64, macos-arm64, freebsd-x86_64) instead of the\n"); + fprintf(stderr, "host's, for cross-platform bundling.\n"); return 1; } - /* --source forces the historical git-clone path (#1360). */ + /* --source forces the historical git-clone path (#1360). + * --target fetches a FOREIGN platform's binary package instead of + * the host's (for cross-platform bundling / release CI). */ bool force_source = false; + const char* target = NULL; for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--source") == 0) force_source = true; + if (strcmp(argv[i], "--source") == 0) { + force_source = true; + } else if (strcmp(argv[i], "--target") == 0 && i + 1 < argc) { + target = argv[++i]; + } else if (strncmp(argv[i], "--target=", 9) == 0) { + target = argv[i] + 9; + } + } + if (force_source && target) { + fprintf(stderr, "Error: --source and --target are mutually exclusive " + "(--source forces the git clone; --target fetches a binary).\n"); + return 1; + } + if (target && !*target) { + fprintf(stderr, "Error: --target needs a release triple, e.g. --target macos-arm64.\n"); + return 1; } // Parse package@version @@ -7945,8 +8005,8 @@ static int cmd_add(int argc, char** argv) { * and pins against an immutable asset rather than a movable tag. * Falls back to the clone when nothing is published. */ if (!force_source) { - int r = ae_try_release_asset(package, version, pkg_dir); - if (r < 0) return 1; /* checksum mismatch — already reported */ + int r = ae_try_release_asset(package, version, pkg_dir, target); + if (r < 0) return 1; /* checksum mismatch / target error — reported */ if (r > 0) goto write_toml; /* installed from artifact */ } printf("Downloading...\n");