Skip to content

fix: byte-stable, non-destructive generate (#674) - #802

Merged
apotema merged 4 commits into
mainfrom
fix/674-byte-stable-generate
Sep 29, 2026
Merged

apotema merged 4 commits into
mainfrom
fix/674-byte-stable-generate

Conversation

@apotema

@apotema apotema commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #674.

What changes

If nothing in the inputs changed, a second generate now writes nothing. Every file under .labelle/<target>/ and .labelle/deps/ keeps its bytes, inode and mtime, and deps/ is no longer wiped.

  • src/write_if_changed.zig: compares against the file on disk before writing. Used by scanner.writeFile (build.zig, build.zig.zon, main.zig, game.zig, __tests_root.zig, …), the mirrored copies (copyAndScan*, copyDir, copyDirRecursive* for assets), app icon/ico/rc, the staged plugin params/build-hook modules, the constants/i18n/flow modules, the v2 hook import and the csproj.
  • src/deps_sync.zig replaces the deleteTree(deps) + hardlinkTree pass. Each staged package is reconciled in place:
    • A file that is still the source file (hardlink: same inode, size and mtime) or holds the same bytes is kept.
    • A changed file is relinked; an orphan is swept.
    • Tool-created zig-pkg//.zig-cache/ inside a stage are kept. The scripting declare step's zig build fetches into them, and sweeping them would mean re-fetching on every generate.
    • A link/junction overlay placed over a source dir (stageNativeSources) is kept and never descended into.
  • Top-level prune (DepsLinkOptions.prune/keep): the exe pass sweeps dropped packages but keeps the tests target's backend link, so the two passes don't fight.
  • rewriteZonPaths now reads the package's source zon, not the staged copy, and writes only on change. It is idempotent, so the old "additive pass must not re-rewrite" special case is gone and both passes run the same code.
  • Pack prefab/hook rewrites materialize from the source instead of copy-then-rewrite. Before, those files were written twice on every generate.
  • minimum_zig_version in the generated zon comes from the assembler's own build.zig.zon (build option). It was hardcoded "0.15.2".
  • Fingerprint: a test pins Zig's manifest rule (fp >> 32 == Crc32(.name), id not 0 or 0xffffffff) on the emitted zon. The CLI probe can be deleted: Remove the post-generate fingerprint probe (fixFingerprints): the assembler's fingerprint is authoritative labelle-cli#507.

Tests (mechanism, not just bytes)

  • test/generate_stability_tests.zig:
    • Two REAL generate runs into the same dir leave every file (generated sources plus staged deps) with the same inode and mtime.
    • A second staging pass keeps every staged file, including the rewritten plugin zon.
    • The tests-target pass doesn't double-rewrite.
    • Tool-made zig-pkg and keep entries survive; only dropped top-level deps are swept.
    • A replaced source file reaches the stage while its siblings keep their identity.
    • minimum_zig_version and the fingerprint rule.
  • Unit tests in deps_sync.zig (kept/linked/removed counters) and write_if_changed.zig. The rewriteZonPaths tests now assert that a second call doesn't touch the staged file.

Manual acceptance (Windows, Zig 0.16.0)

Generated each example, then generated twice more, snapshotting sha256 + mtime + inode of every entry under .labelle between runs:

example differences after a 2nd generate
plugin-controllers 4 of 589, all .labelle/ root metadata (below)
scripting-smoke (lua, declare tool) the same 4, plus zig's own declare-tool/zig-cache timestamp
packs-demo, flows-smoke, hook-order, null the same 4

Before this change, plugin-controllers showed 96 differences, deps/ included. zig build --list-steps on the generated tree exits 0 with no use this value:.

Not covered (issues filed)

Local test run

zig build test on this branch fails the same 3 tests as main on this machine: language_policy … REAL generate output is byte-identical (Windows REPARSE_POINT_NOT_RESOLVED) and the two MANIFEST_V2_GENERATE_CUTOVER tests. Everything else passes (3718/3760, 39 skipped).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Caqyi5Snr6yKssztYFztzZ


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • Improvements
    • Repeated builds avoid rewriting generated files and unchanged dependency files, helping preserve timestamps and reduce unnecessary file changes.
    • Dependency staging now updates packages in place, keeps configured dependencies, and removes stale entries when pruning is enabled.
  • Bug Fixes
    • Generated package manifests now reflect the project’s minimum Zig version instead of a fixed version.
    • Hook files are retained in generated output even when a pack has no events.

A second generate with unchanged inputs now rewrites nothing: every
generated file keeps its bytes, inode and mtime, and .labelle/deps/ is
no longer wiped.

- write_if_changed: generated-file writes (scanner.writeFile, mirrored
  copies, app icon, params/build-hook staging, constants/i18n/flow
  modules, asset copies) compare before writing.
- deps_sync: staged packages are reconciled in place (hardlink identity
  or equal bytes = kept; changed = relinked; orphans swept). Tool-made
  zig-pkg/.zig-cache in a stage and link overlays (native scripting)
  survive. Top-level prune keeps the tests target's backend.
- rewriteZonPaths reads the source zon and writes only on change, so it
  is idempotent and runs on both passes.
- pack prefab/hook rewrites materialize from source instead of
  copy-then-rewrite.
- generated build.zig.zon minimum_zig_version comes from the assembler's
  own build.zig.zon (was hardcoded 0.15.2); fingerprint rule is tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Caqyi5Snr6yKssztYFztzZ
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: d674fa57-3881-4ebf-9fe5-3e174e4baa19

📥 Commits

Reviewing files that changed from the base of the PR and between 3279202 and fc830e2.

📒 Files selected for processing (19)
  • build.zig
  • src/app_icon.zig
  • src/build_files.zig
  • src/build_files/build_zig_zon.zig
  • src/codegen/manifest_v2_splice/common.zig
  • src/constants_phase.zig
  • src/deps_linker.zig
  • src/deps_sync.zig
  • src/flow_scanner.zig
  • src/i18n_phase.zig
  • src/plugin_build_hook.zig
  • src/plugin_params.zig
  • src/root.zig
  • src/root/pack_scan.zig
  • src/scanner.zig
  • src/scripting_splice.zig
  • src/templates/build_zig_zon.txt
  • src/write_if_changed.zig
  • test/generate_stability_tests.zig

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


📝 Walkthrough

Walkthrough

The generator now derives the generated manifest’s minimum Zig version from package build options. Dependency staging reconciles existing directories instead of recreating them. Generated and mirrored files use conditional writes, and new tests check staging and output stability.

Changes

Generation Stability

Layer / File(s) Summary
Manifest minimum Zig version
build.zig, src/build_files.zig, src/build_files/build_zig_zon.zig, src/templates/build_zig_zon.txt, src/root.zig, test/generate_stability_tests.zig
Build options obtain minimum_zig_version from the package manifest and pass it to generated zon manifests. Tests check the emitted version and package-name fingerprints.
In-place dependency synchronization
src/build_files/build_zig_zon.zig, src/deps_linker.zig, src/deps_sync.zig, src/root.zig, src/scripting_splice.zig, test/generate_stability_tests.zig
Dependency staging syncs package trees in place, with configurable pruning and retained entries. Staging preserves rewritten manifests, removes stale dependencies when pruning is enabled, and handles missing source packages. Tests cover sync behavior and repeated staging passes.
Conditional output writes and generation checks
src/write_if_changed.zig, src/scanner.zig, src/root/pack_scan.zig, src/app_icon.zig, src/codegen/manifest_v2_splice/common.zig, src/constants_phase.zig, src/flow_scanner.zig, src/i18n_phase.zig, src/plugin_build_hook.zig, src/plugin_params.zig, src/scripting_splice.zig, build.zig, src/root.zig, test/generate_stability_tests.zig
Generated and mirrored files are written only when their contents differ. Pack rewriting reads source files separately from generated destinations. Tests check conditional writes and unchanged output trees across repeated generation.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant BuildZigZonGenerator
  participant createDepsLinks
  participant syncTree
  participant Filesystem
  BuildZigZonGenerator->>createDepsLinks: pass prune and keep options
  createDepsLinks->>syncTree: reconcile each staged package
  syncTree->>Filesystem: stage, retain, or remove entries
  syncTree-->>createDepsLinks: return sync results or errors
Loading

Merge Risk: ⚪ Minimal · up to fc830

No identified issue blocks merging after normal checks. The reported .labelle/ root metadata changes remain outside this PR’s stated stability scope.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #674 requirements are partly implemented. writeIfChanged, in-place dependency synchronization, pruning, minimum-version derivation, and stability tests are present. The reported repeated runs … Make all remaining generated .labelle/ root metadata writes conditional on content changes, then extend the repeated-generation test to cover the complete generated tree and its dependency inode identity. Add a test that validates the emi…
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: making generation byte-stable and non-destructive.
Out of Scope Changes check ✅ Passed The changed files support issue #674. They implement write-if-changed behavior, dependency reconciliation, fingerprint generation, minimum Zig version propagation, and automated stability tests. No un…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Full details: Linked Issues check

Explanation

Issue #674 requirements are partly implemented. writeIfChanged, in-place dependency synchronization, pruning, minimum-version derivation, and stability tests are present. The reported repeated runs still change four .labelle/ root metadata files in most examples, while #674 requires every emitted file to remain byte- and timestamp-stable. The fingerprint test checks the formula and reserved ID values, but it does not assert that the emitted manifest passes std.zon.Manifest validation as specified by #674.

Resolution

Make all remaining generated .labelle/ root metadata writes conditional on content changes, then extend the repeated-generation test to cover the complete generated tree and its dependency inode identity. Add a test that validates the emitted manifest with the pinned Zig manifest validator, or an equivalent direct validation that proves the stated std.zon.Manifest requirement.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

A rabbit checks each file in place
And leaves unchanged bytes with grace
The staged deps keep what should stay
New versions find their path today
The manifest marks the Zig floor
Then hops away to write no more

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-09-29T00:59:41.904870Z fc830e2 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62bd41e037

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/deps_linker.zig
// Never wiped (#674): each package below is reconciled in place, and
// only top-level entries no pass stages are swept (`prune`).
try cwd.createDirPath(io, deps_dir);
if (opts.prune) try pruneStaleDeps(allocator, deps_dir, deps.items, opts.keep);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Delete staged packages whose sources vanished

When a previously staged local plugin or backend is deleted or moved, pruneStaleDeps treats it as live because it remains in deps.items, and the later missing-source branch merely warns and skips synchronization. The generated zon therefore continues pointing at the old .labelle/deps/<name> tree, allowing builds to silently use stale package code instead of surfacing the intended missing-package error. Remove that destination when its source fails the existence check, or exclude missing sources from the prune keep-set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9b7665d, with a regression test (deps_sync.zig / generate_stability_tests.zig).

Comment thread src/deps_sync.zig Outdated
Comment on lines +107 to +110
if (isLink(dest_sub)) {
// Overlay placed by another phase — keep, never descend.
stats.kept += 1;
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore native placeholders after scripts disappear

When a native-language project has already overlaid the plugin's placeholder directory and the user subsequently removes scripts/ or all native source files, this unconditional link preservation keeps the old overlay. stageNativeSources returns early for exactly those zero-source cases, so nothing replaces it: a deleted directory leaves a broken link, while a now Zig-only directory remains mounted without the required native module root and breaks the plugin's native build. Preserve the overlay only while the current generation still requires native source staging, or explicitly restore the source placeholder on the no-op path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9b7665d, with a regression test (deps_sync.zig / generate_stability_tests.zig).

Comment thread src/deps_sync.zig Outdated
Comment on lines +215 to +216
if (ss.inode == ds.inode and ss.mtime.nanoseconds == ds.mtime.nanoseconds) return true;
return write_if_changed.sameFiles(io, cwd, src, cwd, dest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile file permissions for copied dependencies

On POSIX systems where hardlinking falls back to copying, this byte-only equality check treats files as current even when their permission bits changed. For example, updating a plugin version solely to mark a packaged build helper executable leaves the staged copy non-executable because its bytes still match; the inverse can also retain permissions that were intentionally removed. Compare the relevant permissions before returning true so a mode-only source change refreshes the staged file.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9b7665d, with a regression test (deps_sync.zig / generate_stability_tests.zig).

Comment thread src/deps_sync.zig Outdated
Comment on lines +215 to +216
if (ss.inode == ds.inode and ss.mtime.nanoseconds == ds.mtime.nanoseconds) return true;
return write_if_changed.sameFiles(io, cwd, src, cwd, dest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Relink identical files when the dependency source changes

When a dependency pin is switched to a different source tree whose files initially have identical bytes—common when moving between local checkouts or forks—this returns true and leaves each staged file hardlinked to the old tree. Subsequent edits to the newly selected local dependency do not reach .labelle/deps or wake the long-lived build watcher, while edits to the old checkout can unexpectedly alter the staged package. Byte equality is sufficient only for a known copy fallback; a hardlink-backed destination must be relinked when it is not linked to the currently resolved source.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9b7665d, with a regression test (deps_sync.zig / generate_stability_tests.zig).

@apotema

apotema commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 62bd41e037

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…perms (#674)

- A staged package whose source vanished is deleted (the zon still
  points there; a stale tree must not build silently).
- A link overlay over a source dir is unlinked and the dir restored;
  stageNativeSources re-places it while native sources exist, so the
  placeholder comes back when they are gone.
- Byte equality only keeps a standalone copy (nlink == 1) with equal
  permissions; a stage hardlinked to another tree is relinked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Caqyi5Snr6yKssztYFztzZ
@apotema

apotema commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b7665d0b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/deps_sync.zig Outdated
if (ds.kind != .file) return false;
const ss = cwd.statFile(io, src, .{}) catch return false;
if (ss.size != ds.size) return false;
if (ss.inode == ds.inode and ss.mtime.nanoseconds == ds.mtime.nanoseconds) return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include filesystem identity when recognizing hardlinks

When a local dependency pin moves between filesystems, inode numbers are only unique within each filesystem, so the new source and the staged file can coincidentally have the same inode, size, and preserved mtime without being the same file. This early return then leaves the stage hardlinked to the old checkout, so later edits to the newly selected source are missed. Fresh evidence after the earlier fix is that this implementation still compares inode without a device/filesystem identifier; use a same-file primitive or include device identity in this check.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not addressable portably on Zig 0.16: File.Stat has no device id and std has no same-file primitive. A collision also needs the same size and the same nanosecond mtime. Tracked in #806.

Comment thread src/scanner.zig
// (assets routinely exceed any fixed readFileAlloc limit).
// Skipped when the copy is already current (#674): an
// unchanged asset keeps its identity and mtime.
if (write_if_changed.sameFiles(io, src_dir, entry.name, dst_dir, entry.name)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile permissions before skipping recursive copies

On POSIX, a mode-only source change is ignored because sameFiles compares only size and bytes. For a recursively copied pack asset or fallback-staged directory, changing a helper from non-executable to executable therefore leaves the generated copy with its old permissions, whereas the previous unconditional copyFile refreshed them. Compare the relevant permissions before taking this fast path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f67727e: sameFiles now compares permissions too, so both the recursive copy and the deps copy-fallback refresh on a mode-only change.

@apotema

apotema commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f67727e9a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/deps_sync.zig Outdated
Comment on lines +221 to +222
if (ds.nlink != 1) return false;
return write_if_changed.sameFiles(io, cwd, src, cwd, dest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Relink detached single-link stages

When a source file is atomically replaced with identical bytes and permissions, the old staged hardlink loses the source pathname and its link count drops to 1. This branch then mistakes that detached hardlink for a copy fallback and keeps it, so subsequent in-place edits to the new source no longer reach .labelle/deps or wake a long-lived build watcher. Fresh evidence beyond the earlier source-switch fix is that nlink == 1 does not prove the destination was copied: unlinking/replacing the source side of a hardlink produces exactly that state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc830e2 by simplifying: a stage counts as current only when it IS the source file (same inode, size and mtime). Anything else is re-hardlinked (link to a temp name, then rename over). Byte and permission equality is consulted only when hardlinking fails (the copy fallback). Regression test: syncTree: a stage detached from an atomically replaced source (same bytes) is relinked.

…for the copy fallback (#674)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Caqyi5Snr6yKssztYFztzZ
@apotema

apotema commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc830e2927

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/deps_sync.zig
Comment on lines +226 to +228
const tmp = try std.fmt.allocPrint(allocator, "{s}.labelle-link", .{dest});
defer allocator.free(tmp);
cwd.deleteFile(io, tmp) catch {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a collision-proof staging path

When a dependency contains both a regular file x and a legitimate file named x.labelle-link, and directory iteration visits the suffixed file first, staging x deletes the already-staged sibling here before using that pathname as scratch space. Because both source names were added to seen, the orphan sweep does not restore the deleted file, so the first generated dependency tree is incomplete. Use a unique temporary path outside the mirrored package namespace, or otherwise ensure it cannot collide with a source entry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid but very unlikely: it needs a package file literally named *.labelle-link, and the next generate self-heals it. Tracked in #809.

Comment thread src/deps_sync.zig
Comment on lines +108 to +109
if (isLink(dest_sub)) {
try removeEntry(dest_sub);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve native overlays while they remain active

For every native-language project that still has source files, the destination directory here is the live stageNativeSources symlink or junction from the preceding generation, but this branch unconditionally removes it and mirrors the plugin placeholder before stageNativeSources recreates the overlay later in generate. Thus even an unchanged generation replaces the native source link (twice when the normal and tests targets both run), defeating the non-destructive guarantee and potentially dropping a long-lived build watch on that staged source directory. The sync needs to preserve a currently required overlay and restore the placeholder only on the no-native-sources path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Known trade-off, taken deliberately in the previous round: correctness first, since the placeholder must come back when native sources go away. Keeping the overlay stable needs the has-sources predicate hoisted out of stageNativeSources. Tracked in #808.

@apotema
apotema merged commit 5d0f86d into main Sep 29, 2026
5 checks passed
@apotema
apotema deleted the fix/674-byte-stable-generate branch September 29, 2026 01:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zig 0.17 prep: make generate byte-stable and non-destructive (no deps/ wipe, no fingerprint patch dance)

1 participant