Merge commit '40acb2db542afbb20164ff40794796127e9378a5' into native/testflight
11 files changed,
+224
−107
.amadan/README.md+8 −0.gitignore+1 −0Makefile+23 −0cmd/docs/release_test.go+20 −4core/tests/wasm_trap_containment.rs+7 −35internal/core/core_internal_test.go+57 −59internal/core/testdata/docs_core.wasmbinaryinternal/docs/app.go+12 −1internal/docs/native_stream_test.go+27 −4scripts/build-release.sh+22 −2scripts/build-wasm.sh+47 −2
CI passed — run details
diff --git a/.amadan/README.md b/.amadan/README.md| index 4e8ba82..fc429c4 100644 |
| --- a/.amadan/README.md |
| +++ b/.amadan/README.md |
| @@ -73,3 +73,11 @@ fail-closed properties that matter now |
| development`, a fixed port, and a scratch database — one command to see |
| the app in a browser, signed in for real via a magic link logged to the |
| terminal instead of mailed. |
| + |
| +The embedded WASM is built on Linux amd64 using fixed `/work`, |
| +`/usr/local/cargo` and `/usr/local/rustup` paths. Rust/Cargo's crate identities |
| +otherwise change code ordering across hosts, even with remapped source paths. |
| +Linux amd64 uses unprivileged bubblewrap when available; other hosts use the |
| +pinned Rust Docker image. Install Docker on development Macs. Both the artifact |
| +builder and containment tests use this path; the freshness comparison remains |
| +byte-for-byte. Docker caches and writable targets are separate from source. |
diff --git a/.gitignore b/.gitignore| index 0d54ff6..a5d87be 100644 |
| --- a/.gitignore |
| +++ b/.gitignore |
| @@ -52,3 +52,4 @@ Thumbs.db |
| # Python native review/build helpers. |
| __pycache__/ |
| *.pyc |
| +/releases/ |
diff --git a/Makefile b/Makefile| new file mode 100644 |
| index 0000000..2e12f11 |
| --- /dev/null |
| +++ b/Makefile |
| @@ -0,0 +1,23 @@ |
| +# docs is script-driven, not make-driven: the gate is .amadan/ci.d/10-gate |
| +# and the release build is scripts/build-release.sh, which builds from a |
| +# `git archive` of the revision rather than from the working tree — so a |
| +# release can never pick up an uncommitted edit, and there is no |
| +# dirty-tree case to refuse. |
| +# |
| +# This Makefile exists so that `make release` means the same thing in |
| +# docs as it does in calendar, home, meet, memoria, sheets and slides. |
| +# It delegates rather than restating the build: two definitions of how |
| +# docs ships would drift, and the one in the script is the one CI uses. |
| + |
| +APP := docs |
| +RELEASE_GOOS ?= linux |
| +RELEASE_GOARCH ?= arm64 |
| +RELEASE_BIN := releases/$(APP)-$(RELEASE_GOOS)-$(RELEASE_GOARCH) |
| + |
| +.PHONY: release |
| + |
| +release: |
| + @mkdir -p releases |
| + GOOS=$(RELEASE_GOOS) GOARCH=$(RELEASE_GOARCH) ./scripts/build-release.sh $(RELEASE_BIN) $(REVISION) |
| + @echo "publish it with:" |
| + @echo " amadan release create -repo oficina/$(APP) -notes-file NOTES.md <tag> $(RELEASE_BIN)" |
diff --git a/cmd/docs/release_test.go b/cmd/docs/release_test.go| index f3d594f..a4e079d 100644 |
| --- a/cmd/docs/release_test.go |
| +++ b/cmd/docs/release_test.go |
| @@ -15,15 +15,31 @@ import ( |
| "time" |
| ) |
| -func TestReleaseBinaryReportsItsSourceCommit(t *testing.T) { |
| +// TestReleaseBinaryReportsItsVersion builds the real release artifact and |
| +// asks the running process what it is. The failure this guards against is |
| +// a release that answers "dev": rastrillo's BuildVersion default, which |
| +// the platform's deploy check compares against the release it believes it |
| +// adopted — so an unstamped binary makes that check structurally unable to |
| +// fail while still reporting green. |
| +// |
| +// The expected value is whatever scripts/build-release.sh stamps, computed |
| +// the same way it computes it: the tag when this revision carries one, and |
| +// the abbreviated sha when it does not. Asserting the full sha instead |
| +// would pass only for untagged builds, which is the case that matters |
| +// least. |
| +func TestReleaseBinaryReportsItsVersion(t *testing.T) { |
| root, err := filepath.Abs("../..") |
| if err != nil { |
| t.Fatal(err) |
| } |
| - commit, err := exec.Command("git", "-C", root, "rev-parse", "HEAD").Output() |
| + described, err := exec.Command("git", "-C", root, "describe", "--tags", "--always", "HEAD").Output() |
| if err != nil { |
| t.Fatal(err) |
| } |
| + want := strings.TrimSpace(string(described)) |
| + if want == "" || want == "dev" { |
| + t.Fatalf("git describe gave %q, which is not a version this test can check for", want) |
| + } |
| dir := t.TempDir() |
| binary := filepath.Join(dir, "docs") |
| ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) |
| @@ -64,8 +80,8 @@ func TestReleaseBinaryReportsItsSourceCommit(t *testing.T) { |
| if readErr != nil { |
| t.Fatal(readErr) |
| } |
| - if response.StatusCode != http.StatusOK || strings.TrimSpace(string(body)) != strings.TrimSpace(string(commit)) { |
| - t.Fatalf("version response %d %q; want commit %q", response.StatusCode, body, strings.TrimSpace(string(commit))) |
| + if response.StatusCode != http.StatusOK || strings.TrimSpace(string(body)) != want { |
| + t.Fatalf("version response %d %q; want %q", response.StatusCode, body, want) |
| } |
| return |
| } |
diff --git a/core/tests/wasm_trap_containment.rs b/core/tests/wasm_trap_containment.rs| index 87e70aa..6732034 100644 |
| --- a/core/tests/wasm_trap_containment.rs |
| +++ b/core/tests/wasm_trap_containment.rs |
| @@ -263,42 +263,14 @@ fn build_wasm() -> Vec<u8> { |
| let workspace_root = core_dir |
| .parent() |
| .expect("core/ always has a parent (the repo root)"); |
| - // Must match scripts/build-wasm.sh's RUSTFLAGS exactly: this test |
| - // writes to the same `target/wasm32-unknown-unknown/release/ |
| - // docs_core.wasm` path the gate's `cmp` checks against the committed |
| - // testdata copy. A cargo build here with different flags (even just |
| - // "none", the default before this fix) would fingerprint-mismatch |
| - // against build-wasm.sh's build and silently overwrite that path with |
| - // differently-flagged bytes -- reintroducing embedded absolute paths |
| - // (and failing `cmp`) any time `cargo test` runs after `build-wasm.sh`. |
| - let cargo_home = std::env::var("CARGO_HOME") |
| - .map(PathBuf::from) |
| - .unwrap_or_else(|_| { |
| - PathBuf::from(std::env::var("HOME").expect("HOME must be set")).join(".cargo") |
| - }); |
| - let registry_root = cargo_home.join("registry").join("src"); |
| - let rustflags = format!( |
| - "--cfg getrandom_backend=\"custom\" --remap-path-prefix={}=/cargo-registry-src --remap-path-prefix={}=/docs-core", |
| - registry_root.display(), |
| - workspace_root.display(), |
| - ); |
| - let status = Command::new("cargo") |
| - .args([ |
| - "build", |
| - "--release", |
| - "--target", |
| - "wasm32-unknown-unknown", |
| - "--locked", |
| - "--lib", |
| - ]) |
| - .env("RUSTFLAGS", rustflags) |
| - .current_dir(&core_dir) |
| + // Use the same canonical host and flags as the committed artifact. Rebuild |
| + // current source, without recursively linting the test that invoked us. |
| + let status = Command::new("bash") |
| + .args(["scripts/build-wasm.sh", "--build-only"]) |
| + .current_dir(workspace_root) |
| .status() |
| - .expect("failed to run cargo build for wasm32-unknown-unknown"); |
| - assert!( |
| - status.success(), |
| - "cargo build --target wasm32-unknown-unknown failed" |
| - ); |
| + .expect("failed to run canonical wasm builder"); |
| + assert!(status.success(), "canonical wasm build failed"); |
| let artifact = core_dir.join("target/wasm32-unknown-unknown/release/docs_core.wasm"); |
| std::fs::read(&artifact).unwrap_or_else(|e| panic!("reading {artifact:?}: {e}")) |
| } |
diff --git a/internal/core/core_internal_test.go b/internal/core/core_internal_test.go| index dfe7251..a765d70 100644 |
| --- a/internal/core/core_internal_test.go |
| +++ b/internal/core/core_internal_test.go |
| @@ -54,6 +54,8 @@ import ( |
| "path/filepath" |
| "testing" |
| "time" |
| + |
| + "github.com/tetratelabs/wazero/api" |
| ) |
| // rng and mutate below are a byte-for-byte port of |
| @@ -305,54 +307,60 @@ func TestSlowUpdateIsCutOffByGuestCallTimeout(t *testing.T) { |
| t.Fatalf("expected the default callTimeout (%v), got %v", defaultGuestCallTimeout, rt.callTimeout) |
| } |
| - mod, err := rt.instantiate(ctx) |
| - if err != nil { |
| - t.Fatalf("instantiate: %v", err) |
| - } |
| + warm := func() (api.Module, uint64, []byte) { |
| + t.Helper() |
| + mod, err := rt.instantiate(ctx) |
| + if err != nil { |
| + t.Fatalf("instantiate: %v", err) |
| + } |
| - fixtureNames := []string{"hello", "unicode", "formatted", "concurrent"} |
| - fixtures := make([][]byte, len(fixtureNames)) |
| - for i, name := range fixtureNames { |
| - fixtures[i] = loadFixtureBytes(t, name) |
| - } |
| + fixtureNames := []string{"hello", "unicode", "formatted", "concurrent"} |
| + fixtures := make([][]byte, len(fixtureNames)) |
| + for i, name := range fixtureNames { |
| + fixtures[i] = loadFixtureBytes(t, name) |
| + } |
| - // Warm up: replay trials 0..1132 exactly as |
| - // wasm_trap_containment.rs's fuzz loop does (fixture pick, mutate, |
| - // doc_new, apply), ignoring the outcome of each — most are clean |
| - // oks or clean rejections, and this loop must not go through |
| - // Handle.Apply (which would auto-rebuild on any genuine trap here |
| - // and undo the accumulated memory growth this test depends on). |
| - r := &rng{x: 0xC0FFEE} |
| - for i := 0; i < 1133; i++ { |
| + // Warm up: replay trials 0..1132 exactly as |
| + // wasm_trap_containment.rs's fuzz loop does (fixture pick, mutate, |
| + // doc_new, apply), ignoring the outcome of each — most are clean |
| + // oks or clean rejections, and this loop must not go through |
| + // Handle.Apply (which would auto-rebuild on any genuine trap here |
| + // and undo the accumulated memory growth this test depends on). |
| + r := &rng{x: 0xC0FFEE} |
| + for i := 0; i < 1133; i++ { |
| + fi := r.nextUsize(len(fixtures)) |
| + mutated := mutate(r, fixtures[fi]) |
| + docHandle, err := createDoc(ctx, rt.callTimeout, mod, fmt.Sprintf("warmup-%d", i), uint64(2_000_000+i)) |
| + if err != nil { |
| + t.Fatalf("warm-up trial %d: doc_new failed unexpectedly: %v", i, err) |
| + } |
| + _ = applyOn(ctx, rt.callTimeout, mod, docHandle, mutated) // outcome irrelevant; only the memory growth matters |
| + } |
| + |
| + // Trial 1133 itself: the exact reproducer documented in |
| + // core/tests/wasm_trap_containment.rs ("a 3-byte-flipped `concurrent` |
| + // fixture ... ran for 163 seconds of real CPU time"). Confirm the RNG |
| + // sequence still lands on the documented bytes before relying on them. |
| fi := r.nextUsize(len(fixtures)) |
| mutated := mutate(r, fixtures[fi]) |
| - docHandle, err := createDoc(ctx, rt.callTimeout, mod, fmt.Sprintf("warmup-%d", i), uint64(2_000_000+i)) |
| - if err != nil { |
| - t.Fatalf("warm-up trial %d: doc_new failed unexpectedly: %v", i, err) |
| + const wantHex = "02010200040107636f6e6f656e7403424293010100040107636f6e74656e74034141418a" |
| + if got := hex.EncodeToString(mutated); got != wantHex { |
| + t.Fatalf("RNG/mutate drifted from core/tests/wasm_trap_containment.rs: trial 1133 produced "+ |
| + "fixture=%s bytes=%s, want the documented bytes=%s — this test no longer exercises the "+ |
| + "known-slow reproducer", fixtureNames[fi], got, wantHex) |
| + } |
| + if fixtureNames[fi] != "concurrent" { |
| + t.Fatalf("expected trial 1133 to mutate the concurrent fixture, got %s", fixtureNames[fi]) |
| } |
| - _ = applyOn(ctx, rt.callTimeout, mod, docHandle, mutated) // outcome irrelevant; only the memory growth matters |
| - } |
| - // Trial 1133 itself: the exact reproducer documented in |
| - // core/tests/wasm_trap_containment.rs ("a 3-byte-flipped `concurrent` |
| - // fixture ... ran for 163 seconds of real CPU time"). Confirm the RNG |
| - // sequence still lands on the documented bytes before relying on them. |
| - fi := r.nextUsize(len(fixtures)) |
| - mutated := mutate(r, fixtures[fi]) |
| - const wantHex = "02010200040107636f6e6f656e7403424293010100040107636f6e74656e74034141418a" |
| - if got := hex.EncodeToString(mutated); got != wantHex { |
| - t.Fatalf("RNG/mutate drifted from core/tests/wasm_trap_containment.rs: trial 1133 produced "+ |
| - "fixture=%s bytes=%s, want the documented bytes=%s — this test no longer exercises the "+ |
| - "known-slow reproducer", fixtureNames[fi], got, wantHex) |
| - } |
| - if fixtureNames[fi] != "concurrent" { |
| - t.Fatalf("expected trial 1133 to mutate the concurrent fixture, got %s", fixtureNames[fi]) |
| - } |
| + docHandle, err := createDoc(ctx, rt.callTimeout, mod, "trial-1133", 2_000_000+1133) |
| + if err != nil { |
| + t.Fatalf("creating the document under test: %v", err) |
| + } |
| - docHandle, err := createDoc(ctx, rt.callTimeout, mod, "trial-1133", 2_000_000+1133) |
| - if err != nil { |
| - t.Fatalf("creating the document under test: %v", err) |
| + return mod, docHandle, mutated |
| } |
| + mod, docHandle, mutated := warm() |
| // Sanity: confirm this input is genuinely slow (not just a fast trap) |
| // under wazero's compiler engine at this memory-growth state, with a |
| @@ -375,31 +383,21 @@ func TestSlowUpdateIsCutOffByGuestCallTimeout(t *testing.T) { |
| "it returned in %v instead — the reproducer may have stopped reproducing, which would make "+ |
| "the capped assertion below meaningless", sanityElapsed) |
| } |
| - t.Logf("uncapped: pathological update ran %v before failing on its own (%v)", sanityElapsed, sanityErr) |
| - |
| - // The actual assertion: a fresh document on the same (already grown) |
| - // instance, wired through the exported Handle so this exercises the |
| - // real Apply()/rebuild() path, called with a plain context carrying no |
| - // deadline of its own — so what cuts this call off, if anything does, |
| - // is exactly rt.callTimeout (the default, 1s), not a contrived |
| - // caller-side budget. That is the point fix-round-1 finding 2 raised: |
| - // the previous 5s default was close enough to this input's ~4.34s |
| - // natural running time that the cap wasn't reliably doing anything for |
| - // its own motivating case. 1s should cut it off at roughly a quarter |
| - // of that. |
| - docHandle2, err := createDoc(ctx, rt.callTimeout, mod, "trial-1133b", 2_000_000+11330) |
| - if err != nil { |
| - t.Fatalf("creating the second document under test: %v", err) |
| - } |
| - handle := &Handle{rt: rt, guid: "trial-1133b", clientID: 2_000_000 + 11330, mod: mod, handle: docHandle2} |
| + t.Logf("sanity probe: pathological update failed or reached its 20s bound after %v (%v)", sanityElapsed, sanityErr) |
| + _ = mod.Close(ctx) |
| + |
| + // Repeat the exact warm-up independently: a timed-out sanity probe closes |
| + // its module, and a fresh but unwarmed module would lose the reproducer. |
| + mod, docHandle, mutated = warm() |
| + handle := &Handle{rt: rt, guid: "trial-1133", clientID: 2_000_000 + 1133, mod: mod, handle: docHandle} |
| oldMod := handle.mod |
| start := time.Now() |
| applyErr := handle.Apply(ctx, mutated) |
| elapsed := time.Since(start) |
| - if applyErr == nil { |
| - t.Fatalf("expected the capped Apply to fail (it should have been cut off before completing), got nil error after %v", elapsed) |
| + if !errors.Is(applyErr, context.DeadlineExceeded) { |
| + t.Fatalf("expected the capped Apply to reach its deadline, got %v after %v", applyErr, elapsed) |
| } |
| // Generous upper bound: the call must not have been allowed to run |
| // anywhere near its natural ~4.34s completion time. |
diff --git a/internal/core/testdata/docs_core.wasm b/internal/core/testdata/docs_core.wasm| index c03208f..53decb9 100755 |
| Binary files a/internal/core/testdata/docs_core.wasm and b/internal/core/testdata/docs_core.wasm differ |
diff --git a/internal/docs/app.go b/internal/docs/app.go| index 01e27e3..ccfb4e0 100644 |
| --- a/internal/docs/app.go |
| +++ b/internal/docs/app.go |
| @@ -23,6 +23,7 @@ import ( |
| "amadan.net/oficina/home/handoff" |
| nativeauth "amadan.net/rastrillo/native/server" |
| "github.com/carlosframework/docs/internal/docs/linkstore" |
| + "github.com/carlosframework/rastrillo" |
| "github.com/carlosframework/rastrillo/db" |
| "github.com/carlosframework/rastrillo/mail" |
| "github.com/carlosframework/rastrillo/migrate" |
| @@ -492,7 +493,17 @@ func New(cfg Config) (*App, error) { |
| if home == nil { |
| return authPlugin.RequireSession(next) |
| } |
| - return home.bridge.sessions.Require(home.bridge.resolver.middleware("/signin/local")(next)) |
| + current := home.bridge.sessions.Require(home.bridge.resolver.middleware("/signin/local")(next)) |
| + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| + // A live stream retains its opening context. Require trusts that cached |
| + // session, so re-read the cookie before admitting each stream write. |
| + session, ok := home.bridge.sessions.From(r) |
| + if !ok { |
| + http.Error(w, rastrillo.T(r, "docs.error.generic"), http.StatusUnauthorized) |
| + return |
| + } |
| + current.ServeHTTP(w, sessions.WithSession(r, session)) |
| + }) |
| } |
| nativeSessions, err := sessions.New(sessions.Config{DB: d.Writer(), Origin: cfg.Origin, SigninPath: "/signin", Logger: logger}) |
| if err != nil { |
diff --git a/internal/docs/native_stream_test.go b/internal/docs/native_stream_test.go| index f760928..d65e355 100644 |
| --- a/internal/docs/native_stream_test.go |
| +++ b/internal/docs/native_stream_test.go |
| @@ -14,7 +14,20 @@ func TestNativeStreamStopsAfterSessionRevocation(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - owner := app.TestPerson(t, "stream@example.com") |
| + authenticated := app.TestRequest(t, "stream@example.com", "GET", "/", nil) |
| + assertNativeStreamRevoked(t, app, "stream@example.com", authenticated) |
| +} |
| + |
| +func TestNativeHomeStreamStopsAfterSessionRevocation(t *testing.T) { |
| + f := newRouterHomeFixture(t) |
| + browser := f.browser() |
| + browser.localSignin(t, "alice@example.com") |
| + assertNativeStreamRevoked(t, f.app, "alice@example.com", browser.request("GET", "/", "")) |
| +} |
| + |
| +func assertNativeStreamRevoked(t *testing.T, app *App, address string, authenticated *http.Request) { |
| + t.Helper() |
| + owner := app.TestPerson(t, address) |
| doc, err := app.Store().Create(t.Context(), owner, "Live fixture") |
| if err != nil { |
| t.Fatal(err) |
| @@ -22,26 +35,36 @@ func TestNativeStreamStopsAfterSessionRevocation(t *testing.T) { |
| streamDurations(t, app, 20*time.Millisecond, time.Second) |
| server := httptest.NewServer(app.Handler()) |
| defer server.Close() |
| - authenticated := app.TestRequest(t, "stream@example.com", "GET", "/", nil) |
| req, _ := http.NewRequest("GET", server.URL+"/d/"+doc.Sqid+"/stream", nil) |
| + req.Host = authenticated.Host |
| for _, cookie := range authenticated.Cookies() { |
| req.AddCookie(cookie) |
| } |
| - response, err := server.Client().Do(req) |
| + client := server.Client() |
| + client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } |
| + response, err := client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer response.Body.Close() |
| + if response.StatusCode != http.StatusOK || response.Header.Get("Content-Type") != "text/event-stream" { |
| + t.Fatalf("stream did not open: status=%d location=%q", response.StatusCode, response.Header.Get("Location")) |
| + } |
| reader := bufio.NewReader(response.Body) |
| if name, _ := nextEvent(t, reader, time.Second); name != "update" { |
| t.Fatalf("no initial event: %s", name) |
| } |
| signout := httptest.NewRequest("POST", "/signout", nil) |
| + signout.Host = authenticated.Host |
| signout.Header.Set("Sec-Fetch-Site", "same-origin") |
| for _, cookie := range authenticated.Cookies() { |
| signout.AddCookie(cookie) |
| } |
| - app.Handler().ServeHTTP(httptest.NewRecorder(), signout) |
| + signedOut := httptest.NewRecorder() |
| + app.Handler().ServeHTTP(signedOut, signout) |
| + if signedOut.Code != http.StatusSeeOther { |
| + t.Fatalf("signout failed: %d", signedOut.Code) |
| + } |
| closed := make(chan error, 1) |
| go func() { _, err := io.Copy(io.Discard, reader); closed <- err }() |
| select { |
diff --git a/scripts/build-release.sh b/scripts/build-release.sh| index 1869b7e..5ecafa1 100755 |
| --- a/scripts/build-release.sh |
| +++ b/scripts/build-release.sh |
| @@ -4,14 +4,34 @@ export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}amadan.net/oficina/estilo.git" |
| test "$#" -ge 1 |
| test "$#" -le 2 |
| -docs_output=$(realpath -m -- "$1") |
| +# The output path has to be absolute before the cd into the archive |
| +# below, and it has to resolve even though the file does not exist yet. |
| +# `realpath -m` does that on Linux, where this has always run, but BSD |
| +# realpath has no -m, so it fails on a Mac. dirname + pwd is the same |
| +# thing in POSIX, once the directory is there to cd into. |
| +docs_output_dir=$(dirname -- "$1") |
| +mkdir -p -- "$docs_output_dir" |
| +docs_output=$(cd -- "$docs_output_dir" && pwd)/$(basename -- "$1") |
| cd "$(dirname "$0")/.." |
| docs_revision=$(git rev-parse --verify "${2:-HEAD}^{commit}") |
| +# What the built binary answers GET /api/version with. rastrillo's |
| +# BuildVersion defaults to "dev", and the platform's deploy check |
| +# compares that field against the release it believes it adopted — so an |
| +# unstamped binary makes the check incapable of failing. |
| +# |
| +# git describe --tags --always resolves to the tag when the revision |
| +# carries one and to the same abbreviated sha when it does not, so a |
| +# published release reports the name it was published under and an |
| +# ordinary build still says something true about itself. Name it |
| +# yourself for a one-off: DOCS_VERSION=v0.1.0-wip scripts/build-release.sh out |
| +docs_version="${DOCS_VERSION:-$(git describe --tags --always "$docs_revision")}" |
| docs_source=$(mktemp -d) |
| trap 'rm -rf -- "$docs_source"' EXIT |
| git archive "$docs_revision" | tar -x -C "$docs_source" |
| cd "$docs_source" |
| CGO_ENABLED=0 GOOS="${GOOS:-linux}" GOARCH="${GOARCH:-arm64}" \ |
| go build -mod=readonly -trimpath \ |
| - -ldflags "-X github.com/carlosframework/rastrillo.BuildVersion=$docs_revision" \ |
| + -ldflags "-s -w -X github.com/carlosframework/rastrillo.BuildVersion=$docs_version" \ |
| -o "$docs_output" ./cmd/docs |
| + |
| +echo "built $docs_output reporting version $docs_version" |
diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh| index 522f045..d277b38 100755 |
| --- a/scripts/build-wasm.sh |
| +++ b/scripts/build-wasm.sh |
| @@ -8,7 +8,50 @@ cd "$(dirname "$0")/.." |
| # Doc::with_client_id() both draw entropy this build has no safe source |
| # for. A doc comment alone has been wrong, or gotten around, more than |
| # once; this is the actual enforcement. |
| -cargo clippy --manifest-path core/Cargo.toml --all-targets -- -D warnings -D clippy::disallowed_methods |
| +if [[ "${1:-}" != "--build-only" && "${1:-}" != "--canonical-build" ]]; then |
| + cargo clippy --manifest-path core/Cargo.toml --all-targets -- -D warnings -D clippy::disallowed_methods |
| +fi |
| +# Rust/Cargo crate identities affect code ordering even after path remapping. |
| +# Build on Linux amd64 at fixed source/toolchain paths on every host. |
| +if [[ "${1:-}" == "--canonical-build" ]]; then |
| + [[ "$(uname -s)/$(uname -m)" == "Linux/x86_64" && "$(pwd)" == /work \ |
| + && "$CARGO_HOME" == /usr/local/cargo && "$RUSTUP_HOME" == /usr/local/rustup ]] \ |
| + || { echo "build-wasm: invalid canonical build environment" >&2; exit 1; } |
| + export CARGO_TARGET_DIR=/work/core/target RUSTUP_TOOLCHAIN=1.98.0 |
| +else |
| + out="core/target/wasm32-unknown-unknown/release/docs_core.wasm" |
| + mkdir -p "$(dirname "$out")" |
| + temporary=$(mktemp "${out}.XXXXXX") |
| + trap 'rm -f "$temporary"' EXIT |
| + if [[ "$(uname -s)/$(uname -m)" == "Linux/x86_64" ]] && command -v bwrap >/dev/null; then |
| + cargo_home=$(cd "${CARGO_HOME:-$HOME/.cargo}" && pwd) |
| + rustup_home=$(cd "${RUSTUP_HOME:-$HOME/.rustup}" && pwd) |
| + mkdir -p core/target/canonical-linux-amd64 |
| + bwrap --tmpfs / \ |
| + --ro-bind /usr /usr --tmpfs /usr/local \ |
| + --ro-bind /bin /bin --ro-bind /lib /lib --ro-bind /lib64 /lib64 \ |
| + --ro-bind /etc /etc --bind /tmp /tmp --proc /proc --dev /dev \ |
| + --ro-bind "$(pwd)" /work \ |
| + --bind "$(pwd)/core/target/canonical-linux-amd64" /work/core/target \ |
| + --bind "$cargo_home" /usr/local/cargo --ro-bind "$rustup_home" /usr/local/rustup \ |
| + --chdir /work --setenv CARGO_HOME /usr/local/cargo --setenv RUSTUP_HOME /usr/local/rustup \ |
| + --setenv PATH /usr/local/cargo/bin:/usr/local/bin:/usr/bin:/bin \ |
| + bash -ec './scripts/build-wasm.sh --canonical-build >&2; cat core/target/wasm32-unknown-unknown/release/docs_core.wasm' > "$temporary" |
| + else |
| + command -v docker >/dev/null || { echo "build-wasm: install Docker, or bubblewrap on Linux amd64" >&2; exit 1; } |
| + target_key=$(printf %s "$(pwd)" | shasum -a 256 | cut -c1-16) |
| + docker run --rm --platform linux/amd64 \ |
| + --mount "type=bind,source=$(pwd),target=/work,readonly" \ |
| + --mount "type=volume,source=oficina-docs-wasm-target-${target_key},target=/work/core/target" \ |
| + --mount type=volume,source=oficina-docs-wasm-cargo,target=/usr/local/cargo \ |
| + --mount type=volume,source=oficina-docs-wasm-rustup,target=/usr/local/rustup \ |
| + --workdir /work \ |
| + rust@sha256:8aea3a845c48caac4091a8d0e6494d213061f8f014dc2f5a5f1a88a1a8c9f301 \ |
| + bash -ec 'rustup target add wasm32-unknown-unknown >&2; ./scripts/build-wasm.sh --canonical-build >&2; cat core/target/wasm32-unknown-unknown/release/docs_core.wasm' > "$temporary" |
| + fi |
| + mv "$temporary" "$out" |
| + exit 0 |
| +fi |
| # --lib only: the wasm module shipped to wazero is the library crate. Native |
| # tooling under src/bin (e.g. emit_update, used from the host side of |
| # wire-gate.sh) is never part of that artifact and does not define the |
| @@ -36,7 +79,9 @@ cargo clippy --manifest-path core/Cargo.toml --all-targets -- -D warnings -D cli |
| # it has to be repeated here alongside the remap flags. |
| registry_root="${CARGO_HOME:-$HOME/.cargo}/registry/src" |
| workspace_root="$(pwd)" |
| -export RUSTFLAGS="--cfg getrandom_backend=\"custom\" --remap-path-prefix=${registry_root}=/cargo-registry-src --remap-path-prefix=${workspace_root}=/docs-core" |
| +# Cargo also puts local path dependency identities into mangled debug names. |
| +# Remove that optional symbol section so identical code reproduces across worktrees. |
| +export RUSTFLAGS="-C strip=symbols --cfg getrandom_backend=\"custom\" --remap-path-prefix=${registry_root}=/cargo-registry-src --remap-path-prefix=${workspace_root}=/docs-core" |
| cargo build --release --manifest-path core/Cargo.toml --target wasm32-unknown-unknown --locked --lib |
| out="core/target/wasm32-unknown-unknown/release/docs_core.wasm" |
| [ -f "$out" ] || { echo "build-wasm: no artifact at $out" >&2; exit 1; } |