Repository navigation
Fix NuGet lock discovery for member and named locks (#353, #514) - #1340
Mikola Lysenko (mikolalysenko) wants to merge 15 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Vendored NuGet now reads the source keys and finds the <packageSources>, <packageSourceMapping> and <configuration> anchors through formats::nuget::parse_config, the reader that hosted, upstream restore and VEX already use. The private substring scanner (blank_comments, parse_config_source_keys, attr_value, self_closing_package_sources, insert_at_line) is deleted. User impact: - A close tag written with whitespace (</packageSources >) is now the section that gets extended; vendor used to append a second section NuGet ignores, so restore failed NU1100/NU1403 (#685). - An empty <packageSourceMapping /> is expanded in place instead of left beside a second mapping section. - A section opened and closed on one line receives the source inside it, not before its open tag. - Catch-all keys are written XML-encoded, so a key with & or a quote keeps its identity. - Malformed XML or a repeated section is refused with "malformed XML or a repeated section; not wired" instead of being spliced at the first substring match, as hosted already does. Output bytes for well-formed configs are unchanged. Fixes #685 Refs #594 Assisted-by: Claude Code:claude-opus-5-5
…-config' into agent/v5-nuget-locks
…-config' into agent/v5-nuget-member-locks
Draft placeholder while the fix is written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Draft placeholder while the fix is written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hosted NuGet rewrote every packages.lock.json entry of the patched id, whatever version it resolved, so a multi-targeting project silently got the patched version's bytes in another framework (#593). Vendored only pinned the matching version but still routed every version of the id to a feed serving one, so the other framework failed NU1102. Both modes now refuse such a lock (redirect_ / vendor_nuget_lock_other_version) before writing anything, and only entries at the patched version are re-pinned. A lock starting with a UTF-8 BOM, which dotnet restores fine, made hosted skip the redirect with exit 0 and vendored fail apply (#623). The lock is now read past the BOM, and the BOM and layout are kept on write. All four lock walkers (vendored pin, hosted redirect, upstream restore, VEX) now share one reader in formats::nuget::lock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… agent/v5-nuget-member-locks
vendor / scan --mode vendored and scan --mode hosted wired the root nuget.config, which every project under the root inherits, but only re-pinned <root>/packages.lock.json. A member project's own lock (#353) or a per-project packages.<Project>.lock.json (#514) kept the upstream contentHash, so every fresh restore failed NU1403 while the run reported success, and vendored even claimed the lockfile setting was off. Both modes now discover the locks the projects under the root restore into (formats::nuget::lock::governed_locks over a walk of the project files) and pin all of them: the root lock, member locks, named locks and a literal NuGetLockFilePath. A NuGetLockFilePath that cannot be evaluated is refused (vendor_ / redirect_nuget_lock_path_unresolved) instead of leaving a lock unpinned. Vendored records one wiring entry per lock path and reverts each; hosted rewrites them under their own paths; the hosted unwind and VEX read the same set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
BugBot review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issues.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0168763. Configure here.
| Err(detail) => { | ||
| result.success = false; | ||
| result.error = Some(detail); | ||
| result.error = Some(format!("{}: {detail}", lock.rel)); |
There was a problem hiding this comment.
Rebuild leaves member locks half-pinned
Medium Severity
The stale-artifact rebuild path writes each governed lock in turn and returns on the first write or parse failure without putting earlier locks back. The first-run path rolls those writes back via unwind_locks, but this path does not. After a multi-lock rebuild fails, some member locks pin the new nupkg hash and others keep the old one, so a later --locked-mode restore hits NU1403 in the unpinned projects.
Reviewed by Cursor Bugbot for commit 0168763. Configure here.
There was a problem hiding this comment.
[agent] Fixed in d99550f: the rebuild path now tracks the locks it re-pinned and puts them back (unwind_locks) on a later write or parse failure, like the first-run path.
| rels.push(rel); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
VEX drops member locks on walk failure
Medium Severity
load_lock takes governed_locks_on_disk only on Ok and never inspects unresolved. A walk error or an unevaluable NuGetLockFilePath silently falls back to the root packages.lock.json. Hosted and vendored writers refuse those cases. For a member-lock-only tree that is the #353 layout, discovery then treats the pin as lockless, so VEX cannot attest it and rollback/remove that key off lock evidence will not see the member hashes.
Reviewed by Cursor Bugbot for commit 0168763. Configure here.
There was a problem hiding this comment.
[agent] Fixed in d99550f: a walk error or an unevaluable NuGetLockFilePath now diagnoses lockfile_unreadable and makes the lock unusable (no ref read off a partial lock set), matching the writers' refusal. Covered in hosted_version_and_pin_come_from_a_member_lock.
| }; | ||
| let Ok(kind) = entry.file_type() else { | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
Project walk fail-opens on unreadable entries
Medium Severity
project_files is documented to fail closed when it cannot see the whole tree, but a non-UTF-8 name, a file_type() error, and a symlink-to-.csproj are skipped with continue. Those projects never enter governed_locks, so the root mapping can still be written while their locks keep the upstream contentHash and restore fails NU1403.
Reviewed by Cursor Bugbot for commit 0168763. Configure here.
There was a problem hiding this comment.
[agent] Fixed in d99550f: a non-UTF-8 project or directory name and a file_type() error now fail the walk closed; a symlinked project file is read through the link (symlinked directories stay unentered). Test: project_walk_follows_project_links_and_skips_output.
The hosted engine put every project file's text into the candidate files so the rewriter could re-derive the locks; the confirmation probe then searched that text too, and nuget/rescan regressed 15% in the scan benchmark. The engine now hands the rewriter the walk's answer (lock paths, an unevaluable NuGetLockFilePath, or why the tree could not be listed) through a synthetic key instead. Review fixes (Bugbot on #1340): the stale-artifact rebuild puts back the locks it already re-pinned when a later one fails; VEX reports a tree whose locks it cannot all locate instead of reading the root lock alone; the project walk fails closed on a project or directory name that is not UTF-8 and on an unreadable entry, and reads a symlinked project file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hosted engine and VEX discovery asked the project view for its raw disk root to walk the project files. On a re-scan that ends the read cache's recording, so the discovery it guards was redone and nuget/rescan regressed about 14% in the scan benchmark. The walk now lists, reads and probes through the view (governed_locks_in), which fingerprints them like its own reads. Local perf compare against main: nuget/rescan +1.6% (noise), nuget/hosted +3.6%. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eader # Conflicts: # crates/socket-patch-core/src/vendor/nuget_feed.rs
… agent/v5-nuget-member-locks
|
[final reviewer] Auto-merge is off. Tanmay Singla (@Tanmay182003), one non-merge commit landed after your approval at
Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| "more than {WALK_DIR_BUDGET} directories under the project root" | ||
| )); | ||
| } | ||
| let entries = view.list_dir(&rel).await.map_err(|e| { |
There was a problem hiding this comment.
[agent] This walk goes through ProjectView::list_dir → list_disk_dir (lock_inventory/view.rs:631), which silently skips non-UTF-8 names and stops on a mid-listing read error (while let Ok(Some(..))), where project_files failed closed on both. Scenario: a member project under a non-UTF-8 dir name (or an I/O error while listing): the hosted engine (hosted/engine.rs:645) builds the governed-locks set without that member's packages.lock.json, the redirect wires the root nuget.config source/mapping but leaves that lock on its upstream contentHash → NU1403 on restore while the ledger records the redirect; VEX discovery (vex/discover/nuget.rs:303) has the same gap, and rollback/nuget_feed still use the strict disk walk so the two disagree. Fix: give the view a strict listing that errors on non-UTF-8 names and read errors, and use it here.


LLM Description written by Claude Code:claude-opus-5-5
Fixes #353
Fixes #514
Summary
Vendored and hosted NuGet now pin every lock that the root
nuget.configgoverns, not just<root>/packages.lock.json:src/App/packages.lock.json, Vendored and hosted NuGet leave member-project packages.lock.json unpinned in a solution layout, so every fresh restore fails NU1403 #353);packages.<Project>.lock.json(Vendored and hosted NuGet ignore a per-project packages.<project>.lock.json, so the lock is never re-pinned and every later restore fails NU1403 while VEX attests the patch #514);NuGetLockFilePath(from the Vendored and hosted NuGet ignore a per-project packages.<project>.lock.json, so the lock is never re-pinned and every later restore fails NU1403 while VEX attests the patch #514 probe comment).Root cause
Both writers wire the root
nuget.config, which every project below it inherits. But lock discovery was hard-coded to<root>/packages.lock.json:PACKAGES_LOCKinvendor/nuget_feed.rsandfiles["packages.lock.json"]in the hostedrewrite_nuget. Any other lock kept the upstreamcontentHash, so every fresh restore failed NU1403. Hosted reportedredirected: 1with no warning. Vendored reportedvendor_nuget_no_lockfile("RestorePackagesWithLockFile is off"), which was false.Fix
formats::nuget::lock::governed_locks(pure) takes the project files (.csproj/.fsproj/.vbproj) and returns each one's lock as NuGet picks it: a literalNuGetLockFilePath, elsepackages.<Name>.lock.json(spaces become_) when it exists, elsepackages.lock.json. The root lock is included too. ANuGetLockFilePathit can't evaluate (property or item references, aCondition, an absolute path or one outside the root) is reported as unresolved.vendor::nuget_config::project_fileswalks the tree. It skipsbin,obj,packages,node_modules, hidden dirs and symlinked dirs, and fails closed when it can't see the whole tree.nuget_lock_entrywiring per lock path. A failure rolls back the locks already written and the config. Revert restores each lock (the recorded path must passis_safe_multi_segment). The hot path checks every lock. An unevaluable path is refused withvendor_nuget_lock_path_unresolved. The no-lock warning no longer claims the setting is off.<socket-patch:nuget-locks>: the lock paths, an unevaluable path, or a walk error, like sbt's resolution key). The rewriter rewrites each lock under its own path. It warns and skips the nuget redirect, writing nothing, for an unevaluable path (redirect_nuget_lock_path_unresolved) or an incomplete walk (redirect_nuget_lock_unreadable). Without the key (in memory) only the root lock counts. The project files stay out of the candidate texts: a first version put them there, and the confirmation probe's extra search regressednuget/rescanby 15% in the scan benchmark. A local perf compare is now +1.2% CPU, within noise.remove/rollback) restores every lock the root config governs. VEX reads the same set, so a member-lock-only hosted pin is still attributed.Stacked on #1339 (shared lock reader; its commit is included here) and #1288.
Per-issue tests
vendor::nuget_feed::tests::member_project_lock_is_pinned_and_reverted,patch::redirect::tests::nuget_member_and_named_locks_are_repinned,upstream::nuget::tests::a_member_project_lock_is_restored_with_the_root_config,vex::discover::nuget::tests::hosted_version_and_pin_come_from_a_member_lock. Real SDK:e2e_nuget_dotnet_build::nuget_vendored_solution_member_and_named_locks_restore(fresh checkout, cold cache,--locked-modein each project → patched bytes).vendor::nuget_feed::tests::named_project_lock_is_pinned,unresolvable_lock_file_path_is_refused,patch::redirect::tests::nuget_unknowable_locks_skip_the_redirect,formats::nuget::lock::tests::*, plus the same real-SDK test (packages.named.lock.json).revert_refuses_an_unsafe_lock_path,vendor::nuget_config::tests::project_walk_follows_project_links_and_skips_output, and the VEX test's unlocatable-lock case. Review fixes (Bugbot): the rebuild path puts back the locks it already re-pinned on failure; VEX reports a tree whose locks it can't all locate instead of reading the root lock alone; the walk fails closed on non-UTF-8 names and unreadable entries.Commands run
cargo test -p socket-patch-core --no-fail-fast(lib + every integration suite): greencargo test -p socket-patch-cli --all-features --test e2e_nuget(21): greencargo test -p socket-patch-cli --all-features --test e2e_nuget_dotnet_build -- --ignored(3, local SDK 8.0.129): greencargo clippy --workspace --all-features -- -D warnings: clean.cargo fmt --check: clean for touched files.🤖 Generated with Claude Code