Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`main`, the release pipeline automatically replaces `[current]` with the next
version number before tagging the release.

## [current]

### Added

- **`ae build --size`** compiles with `-Os -g0` (`-Oz` under `--target`) and strips at link
(`-Wl,--strip-all -Wl,--gc-sections`), for a shipped artifact rather than a
debuggable one (#1729). Every other mode pointed at debugging — `--quick` is
`-O0 -g`, `--profile` is `-O2 -g -fno-omit-frame-pointer`, `--coverage` is
`-O0 -g --coverage` — so anyone shipping a library had to emit the C and
hand-compile it. It matters most under `--target`: `zig cc` emits DWARF **by
default** even at `-O2`, and the cross backend passed no `-g0`, so a
cross-compiled `--emit=lib` artifact was overwhelmingly debug information —
measured at **97.4%** of a two-function wasi library, which `--size` takes
from 956,573 to 24,942 bytes, a **38×** reduction with identical behaviour.
The equivalent native `.so` has zero `.debug*` sections, so this was a
cross-path problem rather than something every target shipped; native still
gains about 14%. Deliberately not the default, and deliberately not applied
to `--emit=obj`/`--emit=csrc`, whose symbols are what the next linker needs.

## [0.577.0]

### Fixed
Expand Down
49 changes: 49 additions & 0 deletions docs/build-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,55 @@ and adds only what the profiler needs to attribute it. It works under
`--coverage` takes precedence if both are passed: gcov's line attribution
needs `-O0`, which is a correctness requirement rather than a preference.

### `--size` for shipped artifacts

`--size` compiles with `-Os -g0` (`-Oz` under `--target`) and strips at link
time — `-Wl,--strip-all -Wl,--gc-sections` with GNU ld and LLD,
`-Wl,-x -Wl,-dead_strip` on Apple targets, whose linker rejects the GNU
spellings as unknown options:

```sh
ae build --target=wasm32-wasi --emit=lib mylib.ae -o lib.wasm # 956,573 bytes
ae build --target=wasm32-wasi --emit=lib --size mylib.ae -o lib.wasm # 24,942 bytes
```

Every other mode points at debugging — `--quick` is `-O0 -g`, `--profile` is
`-O2 -g -fno-omit-frame-pointer`, `--coverage` is `-O0 -g --coverage` — and the
default `-O2` sits between them. `--size` is the one that points at shipping.

**It matters most under `--target`.** `zig cc` emits DWARF **by default**, even
at `-O2`, and the cross backend passed no `-g0` — so a cross-compiled
`--emit=lib` artifact was overwhelmingly debug information. Measured on a
two-function wasi library, 97.4% of the module was `.debug*`/`name` sections;
code and data were the remaining 2.6%. The equivalent native `.so` has **zero**
`.debug*` sections, so this was a cross-path problem rather than something
every target shipped. Native builds still benefit, just far less: about 14% on
the same library, from `-Oz` and the symbol table.

Stripping is behaviour-preserving. The 24 KB module above still instantiates,
still exports every symbol, and still runs identically — including the
fail-stop panic path WASI has.

`-Os` rather than `-Oz` on the native path: gcc only gained `-Oz` in GCC 12,
and Ubuntu 22.04 — the CI baseline — ships GCC 11, where it is a hard error.
`-Os` is supported everywhere and gives nearly the same result. Cross builds do
use `-Oz`, because zig bundles its own clang and the version is not the host's
to vary.

Two things `--size` deliberately does not do:

- **It is not the default.** A 38× difference is discoverable; anyone shipping
to a browser will find the flag. Stripping every build by default would make
the first "why can't I get a stack trace from my wasm module" report
genuinely hard to diagnose.
- **It does not strip `--emit=obj` or `--emit=csrc`.** Neither links, and an
object file's symbols are exactly what whoever links it next needs. Those
modes still get `-Oz -g0` for the compile, but no link-time stripping.

`--profile` takes precedence if both are passed: asking for a small artifact
and a profileable one is contradictory, and the debug-oriented reading is the
safer one.

### Resolving the build target

`ae build` accepts either a path to a `.ae` file or a `[[bin]]` name from `aether.toml`. The two are equivalent:
Expand Down
1 change: 1 addition & 0 deletions tests/ae_sweep_prune.txt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ tests/integration/sealed_namespaces/
tests/integration/selective_import_alias/
tests/integration/selective_import_merge_order/
tests/integration/selective_import_shadow/
tests/integration/size_mode/
tests/integration/source_location/
tests/integration/source_location_default_capture/
tests/integration/spec_format_reporting/
Expand Down
18 changes: 18 additions & 0 deletions tests/integration/size_mode/sizelib.ae
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Library-shaped source for the --size test. Uses try/catch/panic so the
// panic machinery is linked in too -- a fixture that pulls in more of the
// runtime is a better size signal than one that pulls in almost none.

risky(x: int) -> int {
if x < 0 {
panic("negative")
}
return x * 2
}

safe(x: int) -> int {
try {
return risky(x)
} catch reason {
return 0
}
}
144 changes: 144 additions & 0 deletions tests/integration/size_mode/test_size_mode.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/bin/sh
# `ae build --size` produces a smaller artifact without breaking it.
#
# ae build had --quick (-O0 -g), --profile (-O2 -g -fno-omit-frame-pointer)
# and --coverage (-O0 -g --coverage) -- all debug-oriented -- and no mode
# pointing the other way. That mattered most on the cross path: `zig cc`
# emits DWARF by DEFAULT even at -O2, and nothing passed -g0, so a
# cross-compiled --emit=lib artifact was overwhelmingly debug information.
#
# Asserts, in rising order of what would actually break a user:
# - --size is accepted and the binary still runs (native exe)
# - --emit=lib under --size keeps its dynamic symbols (a stripped library
# with no symbols links fine and is useless)
# - --emit=obj under --size keeps its symbols (stripping an object would
# remove what whoever links it next needs)
# - the cross wasm artifact is dramatically smaller AND still valid
#
# The cross half is skipped without zig. Cost: ONE cross link (~90 TUs; no
# per-target archive cache), matching the convention in the neighbouring
# cross tests.

set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
AE="$ROOT/build/ae"

if [ ! -x "$AE" ]; then
echo " [SKIP] size_mode: ae not built"
exit 0
fi

TMPDIR_T="$(mktemp -d)"
cleanup() { rm -rf "$TMPDIR_T"; }
trap cleanup EXIT

LIB="$SCRIPT_DIR/sizelib.ae"

# ---- 1. a --size executable still runs -----------------------------------
cat > "$TMPDIR_T/app.ae" <<'AEEOF'
main() {
println("size mode ok")
}
AEEOF
# Keep the compiler's own message: a bad flag (say, one the host gcc is too
# old for) is invisible if this is swallowed, and "--size build failed" alone
# sends you looking in the wrong place.
if ! "$AE" build --size "$TMPDIR_T/app.ae" -o "$TMPDIR_T/app" \
>"$TMPDIR_T/build.log" 2>&1; then
echo " [FAIL] size_mode: --size build failed"
sed -n '1,15p' "$TMPDIR_T/build.log" | sed 's/^/ /'
exit 1
fi
OUT=$("$TMPDIR_T/app" 2>&1) || { echo " [FAIL] size_mode: --size binary did not run"; exit 1; }
[ "$OUT" = "size mode ok" ] || {
echo " [FAIL] size_mode: wrong output: $OUT"; exit 1; }

# ---- 2. --emit=lib keeps the symbols a consumer resolves against ---------
# Stripping removes the STATIC symbol table but must leave the dynamic /
# external ones; a library with neither would satisfy a size check and be
# unusable.
#
# The listing command is not portable. GNU nm spells it `nm -D`; BSD/macOS nm
# has no -D at all and spells defined-external `nm -gU`. Getting this wrong
# is not a harmless mismatch: the unsupported form exits non-zero with empty
# output, which reads exactly like "the symbols are gone" and fails the test
# on a perfectly good library. So pick by platform, and SKIP rather than fail
# if neither form works -- an inconclusive probe must not masquerade as a
# regression.
if "$AE" build --emit=lib --size "$LIB" -o "$TMPDIR_T/lib.so" >/dev/null 2>&1; then
SYMS=""
case "$(uname -s 2>/dev/null)" in
Darwin) SYMS=$(nm -gU "$TMPDIR_T/lib.so" 2>/dev/null || true) ;;
*) SYMS=$(nm -D --defined-only "$TMPDIR_T/lib.so" 2>/dev/null || true) ;;
esac
if [ -n "$SYMS" ]; then
if ! printf '%s' "$SYMS" | grep -q 'safe'; then
echo " [FAIL] size_mode: --emit=lib --size dropped the exported symbols"
echo " (listing was non-empty, so this is a real strip, not a"
echo " missing nm option)"
exit 1
fi
fi
fi

# ---- 3. --emit=obj keeps its symbols -------------------------------------
# An object file is linked later by someone else, so stripping it would
# remove exactly what they need. --size must not apply link-time stripping
# to a mode that does not link.
# Plain `nm` (no -D) works on both toolchains for an object file, but the
# same "empty means inconclusive" rule applies.
if "$AE" build --emit=obj --size "$LIB" -o "$TMPDIR_T/lib.o" >/dev/null 2>&1; then
OSYMS=$(nm "$TMPDIR_T/lib.o" 2>/dev/null || true)
if [ -n "$OSYMS" ]; then
if ! printf '%s' "$OSYMS" | grep -q 'safe'; then
echo " [FAIL] size_mode: --emit=obj --size stripped the object's symbols"
exit 1
fi
fi
fi

# ---- 4. the cross case, which is what this mode is for -------------------
if ! command -v zig >/dev/null 2>&1; then
echo " [PASS] size_mode: native checks (cross skipped: zig not on PATH)"
exit 0
fi

BASE="$TMPDIR_T/base.wasm"
SIZED="$TMPDIR_T/sized.wasm"
"$AE" build --target=wasm32-wasi --emit=lib "$LIB" -o "$BASE" >/dev/null 2>&1 \
|| { echo " [FAIL] size_mode: baseline wasm build failed"; exit 1; }
"$AE" build --target=wasm32-wasi --emit=lib --size "$LIB" -o "$SIZED" >/dev/null 2>&1 \
|| { echo " [FAIL] size_mode: --size wasm build failed"; exit 1; }

BASE_SZ=$(wc -c < "$BASE" | tr -d '[:space:]')
SIZED_SZ=$(wc -c < "$SIZED" | tr -d '[:space:]')

# The measured ratio is ~38x; assert a conservative 4x so a partial
# regression (say, -g0 lost but stripping kept) still trips this.
if [ "$SIZED_SZ" -ge $((BASE_SZ / 4)) ]; then
echo " [FAIL] size_mode: --size wasm not meaningfully smaller"
echo " baseline=$BASE_SZ sized=$SIZED_SZ (wanted < baseline/4)"
exit 1
fi

# Smaller is worthless if it is no longer a wasm module.
case "$(file -b "$SIZED" 2>/dev/null)" in
*WebAssembly*) ;;
*)
echo " [FAIL] size_mode: --size output is not a wasm module:"
echo " $(file -b "$SIZED" 2>/dev/null)"
exit 1
;;
esac

# ...and worthless again if the exports it exists to provide are gone.
for sym in aether_risky aether_safe; do
if ! strings "$SIZED" | grep -q "$sym"; then
echo " [FAIL] size_mode: $sym missing from the --size module"
exit 1
fi
done

echo " [PASS] size_mode: wasm ${BASE_SZ} -> ${SIZED_SZ} bytes, exports and symbols intact"
77 changes: 71 additions & 6 deletions tools/ae.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading