From e1ad6ec1aa2ca867fa563a40786196bb8b3bf37e Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:57:25 -0400 Subject: [PATCH] SMLib links prebuilt: one build per commit, pinned by SHA-256 tangency-labs/smlib's prebuilt workflow builds the C API once per commit for every Rust target SolidCraft builds (Linux x86_64 and aarch64, macOS arm64 and x86_64, Windows x64, x86 and ARM64, wasm32 with wasi-sdk's C++ runtime) and releases it as prebuilt-. SMLIB_PREBUILT pins each archive's SHA-256. scripts/fetch-smlib.sh now downloads the host's archive (or the named targets'), checks its hash and the commit it was built from, and falls back to the sources for a target with no archive; --source fetches the sources, --pin writes SMLIB_PREBUILT from the release. build.rs links a fetched archive whose hash matches the pin, else compiles as before, now with -ffp-contract=off like the archives: every platform does the same arithmetic. No network access in build.rs. SMLIB_REV moves to 8b57e7e: the workflow, and fixes for MSVC (no directive inside a macro argument, SM_PI for M_PI), the no-fused-multiply-add build and a test that needed a POSIX shell. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs --- crates/smlib/SMLIB_PREBUILT | 10 ++++ crates/smlib/SMLIB_REV | 2 +- crates/smlib/build.rs | 68 ++++++++++++++++++++--- scripts/fetch-smlib.sh | 107 ++++++++++++++++++++++++++++++------ 4 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 crates/smlib/SMLIB_PREBUILT diff --git a/crates/smlib/SMLIB_PREBUILT b/crates/smlib/SMLIB_PREBUILT new file mode 100644 index 0000000000..4d57ce5ac7 --- /dev/null +++ b/crates/smlib/SMLIB_PREBUILT @@ -0,0 +1,10 @@ +# Rust target, SHA-256 of smlib-.tar.gz in the release prebuilt- of +# tangency-labs/smlib. Written by scripts/fetch-smlib.sh --pin; never edited by hand. +aarch64-apple-darwin 09ce8b7cc7598f2600ab2b117b9d1a5ebfecddaad0ce21b80c731bc6671b3673 +aarch64-pc-windows-msvc e36e4810b4cfdcdcf7fae1d30f5f25313aba92668fcda68452415fb30174d4a3 +aarch64-unknown-linux-gnu 1e6d149632160bc298b3b7c43b029e4155569c8172c4e59c9b8d614ca78893e1 +i686-pc-windows-msvc 5c731edbd96e7d7b608aad755bbb0cc18c2bd90699862cf227cc3338ec22dc3b +wasm32-unknown-unknown 9dceb0fb9b9253e231cc5b929f93924c4ee80566685302bf6bbb906988ade46e +x86_64-apple-darwin a30676477cd12e7f9e3c98744fb6f96a27b8fcbfe6dfb2478a6c79e6dac7a29a +x86_64-pc-windows-msvc 72f3ba519fe7a76c278cda772c00a6189c211f52d4c4c1584f7dc2da8c7fc2cb +x86_64-unknown-linux-gnu 4c00ac33f3feb547fb151104580d56c291e7879c4773805539b0033fe27a4962 diff --git a/crates/smlib/SMLIB_REV b/crates/smlib/SMLIB_REV index ca36ab7eb5..84e9f578b5 100644 --- a/crates/smlib/SMLIB_REV +++ b/crates/smlib/SMLIB_REV @@ -1 +1 @@ -a15b5a2885384a9d10e76a68ba8b79526faccb09 +8b57e7e8ace9e7886ce9936cfe45f3ed947c4006 diff --git a/crates/smlib/build.rs b/crates/smlib/build.rs index 98baa8bed3..f9eb2f3119 100644 --- a/crates/smlib/build.rs +++ b/crates/smlib/build.rs @@ -1,15 +1,20 @@ -//! Builds SMLib, SM_API and their C API (tangency-labs/smlib, at the commit in `SMLIB_REV`) into -//! static libraries, when the `native` feature is on. The sources come from -//! `SOLIDCRAFT_SMLIB_DIR`, or from `target/smlib-` as `scripts/fetch-smlib.sh` leaves them. +//! Links SMLib, SM_API and their C API (tangency-labs/smlib, at the commit in `SMLIB_REV`) as +//! static libraries, when the `native` feature is on. In order: +//! - `SOLIDCRAFT_SMLIB_DIR`: a checkout, compiled here; +//! - the prebuilt library for the target, as `scripts/fetch-smlib.sh` leaves it in +//! `target/smlib--prebuilt/`, when its SHA-256 is the one in `SMLIB_PREBUILT`; +//! - the sources in `target/smlib-` (`scripts/fetch-smlib.sh --source`), compiled here. //! -//! The file lists, defines and include order follow the fork's `capi/CMakeLists.txt`: SMLib/inc -//! and SM_API/inc each have a StdAfx.h, so each part must find its own first. +//! The prebuilt libraries come from the fork's `capi/CMakeLists.txt`. The compiled path follows +//! it: the same file lists, defines and include order (SMLib/inc and SM_API/inc each have a +//! StdAfx.h, so each part must find its own first). use std::path::{Path, PathBuf}; fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=SMLIB_REV"); + println!("cargo:rerun-if-changed=SMLIB_PREBUILT"); println!("cargo:rerun-if-env-changed=SOLIDCRAFT_SMLIB_DIR"); println!("cargo:rerun-if-env-changed=WASI_SDK_PATH"); if std::env::var_os("CARGO_FEATURE_NATIVE").is_none() { @@ -25,14 +30,26 @@ fn main() { fn build() -> Result<(), String> { let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").map_err(|e| e.to_string())?); let rev = std::fs::read_to_string(manifest.join("SMLIB_REV")).map_err(|e| format!("SMLIB_REV: {e}"))?.trim().to_string(); + let rev12 = rev.get(..12).unwrap_or(&rev); + let target = std::env::var("TARGET").unwrap_or_default(); + let fetched = manifest.join("../../target"); let root = match std::env::var_os("SOLIDCRAFT_SMLIB_DIR") { Some(d) => PathBuf::from(d), - None => manifest.join("../../target").join(format!("smlib-{}", rev.get(..12).unwrap_or(&rev))), + None => { + let prebuilt = fetched.join(format!("smlib-{rev12}-prebuilt")).join(&target); + if let Some(sha) = pinned(&manifest, &target)? { + println!("cargo:rerun-if-changed={}", prebuilt.join(".sha256").display()); + let have = std::fs::read_to_string(prebuilt.join(".sha256")).unwrap_or_default(); + if have.trim() == sha { + return link_prebuilt(&prebuilt, &target); + } + } + fetched.join(format!("smlib-{rev12}")) + } }; if !root.join("capi/include/smlib.h").is_file() { return Err(format!( - "the SMLib sources are not at {} (feature `native`): run scripts/fetch-smlib.sh, or set SOLIDCRAFT_SMLIB_DIR to a tangency-labs/smlib checkout at {rev}", - root.display() + "SMLib {rev} for {target} is not fetched (feature `native`): run scripts/fetch-smlib.sh {target}, or set SOLIDCRAFT_SMLIB_DIR to a tangency-labs/smlib checkout at that commit" )); } println!("cargo:rerun-if-changed={}", root.join("capi").display()); @@ -41,7 +58,6 @@ fn build() -> Result<(), String> { let src = root.join("source"); let core = sources(&src.join("SMLib/src"), &["StdAfx.cpp", "SmDllMain.cpp"])?; let api = sources(&src.join("SM_API/src"), &["StdAfx.cpp"])?; - let target = std::env::var("TARGET").unwrap_or_default(); let msvc = target.contains("msvc"); let wasi = if target == "wasm32-unknown-unknown" { Some(wasi_sdk()?) } else { None }; @@ -51,6 +67,10 @@ fn build() -> Result<(), String> { // Rust links the MSVC runtime as a DLL; the cc crate's default (/MD) matches. if msvc { b.flag("/EHsc").flag("/bigobj"); + } else { + // No fused multiply-add, as the prebuilt libraries (capi/CMakeLists.txt): SMLib's + // tangent cases decide on the last bits. MSVC does not contract by default. + b.flag("-ffp-contract=off"); } if let Some(sdk) = &wasi { b.compiler(sdk.join("bin/clang++")).archiver(sdk.join("bin/llvm-ar")).target(WASI_TARGET); @@ -100,6 +120,36 @@ fn build() -> Result<(), String> { Ok(()) } +/// The SHA-256 `SMLIB_PREBUILT` pins for `target`'s prebuilt archive, if there is one. +fn pinned(manifest: &Path, target: &str) -> Result, String> { + let pins = std::fs::read_to_string(manifest.join("SMLIB_PREBUILT")).map_err(|e| format!("SMLIB_PREBUILT: {e}"))?; + Ok(pins.lines().filter(|l| !l.starts_with('#')).find_map(|l| { + let mut f = l.split_whitespace(); + (f.next() == Some(target)).then(|| f.next().map(str::to_string)).flatten() + })) +} + +/// Links the prebuilt C API in `dir` (one archive, `smlib`) and the C++ runtime it was compiled +/// against: the system's, or for wasm wasi-sdk's, which the archive carries. +fn link_prebuilt(dir: &Path, target: &str) -> Result<(), String> { + let lib = dir.join("lib"); + if !lib.is_dir() { + return Err(format!("{} has no lib folder: run scripts/fetch-smlib.sh {target} again", dir.display())); + } + println!("cargo:rustc-link-search=native={}", lib.display()); + println!("cargo:rustc-link-lib=static=smlib"); + if target == "wasm32-unknown-unknown" { + for l in ["c++", "c++abi", "unwind", "c"] { + println!("cargo:rustc-link-lib=static={l}"); + } + } else if target.contains("apple") { + println!("cargo:rustc-link-lib=c++"); + } else if !target.contains("msvc") { + println!("cargo:rustc-link-lib=stdc++"); + } + Ok(()) +} + /// The C target SMLib is compiled for inside a `wasm32-unknown-unknown` build: wasi-sdk's, the /// one with a C++ standard library. const WASI_TARGET: &str = "wasm32-wasip1"; diff --git a/scripts/fetch-smlib.sh b/scripts/fetch-smlib.sh index d73143db01..8fb37dcfc9 100755 --- a/scripts/fetch-smlib.sh +++ b/scripts/fetch-smlib.sh @@ -1,20 +1,95 @@ #!/usr/bin/env bash -# Fetches the SMLib sources solidcraft-smlib builds (feature `native`): tangency-labs/smlib at -# the commit in crates/smlib/SMLIB_REV, only the parts the build reads (source/SMLib, -# source/SM_API, capi), into target/smlib-. OpenCASCADE is not part of the fork. +# Fetches SMLib for solidcraft-smlib (feature `native`), at the commit in crates/smlib/SMLIB_REV. +# +# scripts/fetch-smlib.sh [TARGET...] the prebuilt C API library for each Rust target (default: +# the host's), checked against crates/smlib/SMLIB_PREBUILT, +# into target/smlib--prebuilt/; the sources +# for a target with no prebuilt library +# scripts/fetch-smlib.sh --source the sources (source/SMLib, source/SM_API, capi), into +# target/smlib- or SOLIDCRAFT_SMLIB_DIR, to compile +# scripts/fetch-smlib.sh --pin writes SMLIB_PREBUILT from the release of SMLIB_REV +# +# The libraries are the release prebuilt- of tangency-labs/smlib (its prebuilt.yml). +# OpenCASCADE is not part of the fork. set -euo pipefail root="$(cd "$(dirname "$0")/.." && pwd)" rev="$(tr -d '[:space:]' < "$root/crates/smlib/SMLIB_REV")" -dest="${SOLIDCRAFT_SMLIB_DIR:-$root/target/smlib-${rev:0:12}}" -if [ -f "$dest/capi/include/smlib.h" ] && [ "$(git -C "$dest" rev-parse HEAD 2>/dev/null)" = "$rev" ]; then - echo "SMLib $rev already at $dest" - exit 0 -fi -rm -rf "$dest" -mkdir -p "$dest" -git -C "$dest" init -q -git -C "$dest" remote add origin https://github.com/tangency-labs/smlib.git -git -C "$dest" sparse-checkout set --no-cone /source/SMLib/ /source/SM_API/ /capi/ /LICENSE -git -C "$dest" fetch -q --depth 1 --filter=blob:none origin "$rev" -git -C "$dest" checkout -q FETCH_HEAD -echo "SMLib $rev at $dest" +pins="$root/crates/smlib/SMLIB_PREBUILT" +release="https://github.com/tangency-labs/smlib/releases/download/prebuilt-$rev" + +sha256() { + if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi +} + +fetch_source() { + local dest="${SOLIDCRAFT_SMLIB_DIR:-$root/target/smlib-${rev:0:12}}" + if [ -f "$dest/capi/include/smlib.h" ] && [ "$(git -C "$dest" rev-parse HEAD 2>/dev/null)" = "$rev" ]; then + echo "SMLib sources $rev already at $dest" + return + fi + rm -rf "$dest" + mkdir -p "$dest" + git -C "$dest" init -q + git -C "$dest" remote add origin https://github.com/tangency-labs/smlib.git + git -C "$dest" sparse-checkout set --no-cone /source/SMLib/ /source/SM_API/ /capi/ /LICENSE + git -C "$dest" fetch -q --depth 1 --filter=blob:none origin "$rev" + git -C "$dest" checkout -q FETCH_HEAD + echo "SMLib sources $rev at $dest" +} + +fetch_prebuilt() { + local target="$1" want dest tmp + want="$(awk -v t="$target" '$1 == t { print $2 }' "$pins")" + if [ -z "$want" ]; then + echo "no prebuilt SMLib for $target: fetching the sources to compile" + fetch_source + return + fi + dest="$root/target/smlib-${rev:0:12}-prebuilt/$target" + if [ "$(cat "$dest/.sha256" 2>/dev/null)" = "$want" ]; then + echo "SMLib $rev for $target already at $dest" + return + fi + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' RETURN + curl -fsSL --retry 3 -o "$tmp/smlib.tar.gz" "$release/smlib-$target.tar.gz" + if [ "$(sha256 "$tmp/smlib.tar.gz")" != "$want" ]; then + echo "smlib-$target.tar.gz does not match its SHA-256 in crates/smlib/SMLIB_PREBUILT" >&2 + exit 1 + fi + tar -xzf "$tmp/smlib.tar.gz" -C "$tmp" + if [ "$(tr -d '[:space:]' < "$tmp/smlib-$target/REV")" != "$rev" ]; then + echo "smlib-$target.tar.gz was built from another commit than SMLIB_REV" >&2 + exit 1 + fi + rm -rf "$dest" + mkdir -p "$(dirname "$dest")" + mv "$tmp/smlib-$target" "$dest" + echo "$want" > "$dest/.sha256" + echo "SMLib $rev for $target at $dest" +} + +case "${1:-}" in + --source) + fetch_source + ;; + --pin) + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + curl -fsSL --retry 3 -o "$tmp/SHA256SUMS" "$release/SHA256SUMS" + { + echo "# Rust target, SHA-256 of smlib-.tar.gz in the release prebuilt- of" + echo "# tangency-labs/smlib. Written by scripts/fetch-smlib.sh --pin; never edited by hand." + sed -E 's/^([0-9a-f]{64}) +smlib-(.*)\.tar\.gz$/\2 \1/' "$tmp/SHA256SUMS" | sort + } > "$pins" + echo "pinned $(grep -vc '^#' "$pins") prebuilt targets of $rev" + ;; + *) + if [ $# -eq 0 ]; then + set -- "$(rustc -vV | sed -n 's/^host: //p')" + fi + for target in "$@"; do + fetch_prebuilt "$target" + done + ;; +esac