Skip to content

Single-file build -o silently overwrites existing files (including the .spx source) and merges into non-empty directories #37

Closed

Description

@wsdt

Summary

The single-file route of semaprax build has none of the fresh-destination guards the project route enforces:

  1. build <file>.spx --target native -o <existing file> silently overwrites the file with the executable and exits 0. -o pointing at the .spx source itself replaces the source with a Mach-O binary.
  2. build <file>.spx --target web|wasm -o <existing non-empty directory> writes into the directory, overwriting same-named files (index.html, package.json, …) and exits 0. Two concurrent single-file web builds into one -o both "succeed".
  3. Ordinary -o path errors (missing parent, read-only parent, trailing slash) surface as raw ld:/clang: linker output under SPX-B102 instead of the SPX-I301/SPX-I307 diagnostics the project route emits before compiling.

The project route refuses all of these (SPX-I307 … destination already exists), and docs/PROJECT-MANIFEST-V1.md ("CLI and artifacts") promises a native build never clobbers an existing file. Identical -o spellings therefore behave oppositely depending on whether the input is a file or a manifest. This is data loss on an agent-facing command.

Observed on semaprax 0.3.0 built from main at 5c6e7a4b.

Reproducers

cp examples/calculator.spx calc.spx
echo precious > victim
semaprax build calc.spx --target native -o victim; echo "rc=$?"
# built native executable victim   rc=0
file victim                            # Mach-O 64-bit executable arm64

semaprax build calc.spx --target native -o calc.spx; echo "rc=$?"
# built native executable calc.spx  rc=0
semaprax check calc.spx                # error[SPX-I001]: cannot read calc.spx: stream did not contain valid UTF-8

mkdir ne; echo mine > ne/index.html; echo keep > ne/keep.txt
semaprax build examples/calculator.spx --target web -o ne; echo "rc=$?"
# built web package ne   rc=0        ne/index.html replaced, ne/keep.txt kept

# project route, for comparison
echo x > v2; semaprax build examples/calculator-project --target native -o v2
# error[SPX-I307]: Project v1 native executable destination already exists: v2   rc=1

Root cause

  • src/cli_driver.rs::build_source → codegen::build → src/codegen/native_emit/mod.rs ~line 2174 write_and_compile_c_with_runner passes -o <output> straight to clang, which truncates and overwrites.
  • src/wasm.rs ~line 1678 build_web calls std::fs::create_dir_all(output) and then unconditionally fs::writes each package file.
  • The project route's guard lives in src/project/mod.rs ~line 789 (SPX-I307) and the scalar-export package has its own fresh-destination check in src/wasm.rs ~2643-2690; neither is applied to BuildInput::Source.

Suggested fix

  1. Apply the same create-new contract to the single-file route: refuse if the destination exists (symlink_metadata), compile/link into a temporary sibling in the destination's parent, then rename with create-new semantics so concurrent builds cannot both succeed. Refuse -o that resolves to any input source path explicitly.
  2. For web/wasm, use fs::create_dir (create-new) or reuse the scalar-export package's fresh-destination check.
  3. Pre-check the parent directory (exists, writable, not a file) and emit SPX-I301/SPX-I307 before invoking clang, so raw linker text never reaches the user.
  4. Regression tests in the harness that owns single-file build (tests/compiler.rs module tree): existing file, existing directory, the source path itself, missing parent, read-only parent; assert exit 1, the SPX code, and that the pre-existing bytes are untouched.
  5. Document the contract in docs/CLI-GUIDE.md ("build" section) and CHANGELOG.md.

Activity

  1. added
    bugSomething isn't working
    on Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions