fix(bins.linker): skip redundant .exe warning when hardlink is already correct#12284
Conversation
Code Review by Qodo
Context used 1. Unreliable inode identity check
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an inode/device identity check in a new ChangesWindows Binary Hardlink Deduplication
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Use truthy check on ino before comparing — on some Windows configurations stat.ino returns 0 for all files, which would make any two files appear identical. Fall through to the original warn-and-replace behavior when ino is not available. Addresses review feedback on pnpm#12284 --- Written by an agent (Claude Code, claude-sonnet-4-6).
6d4d50f to
99fa076
Compare
|
Code review by qodo was updated up to the latest commit 99fa076 |
Use truthy check on ino before comparing — on some Windows configurations stat.ino returns 0 for all files, which would make any two files appear identical. Fall through to the original warn-and-replace behavior when ino is not available. Addresses review feedback on pnpm#12284 --- Written by an agent (Claude Code, claude-sonnet-4-6).
99fa076 to
8a61866
Compare
|
Code review by qodo was updated up to the latest commit 8a61866 |
…ecision loss
NTFS 64-bit file IDs lose precision when read as a JS Number, which could
cause two distinct files to compare equal. Read inode/device with
{ bigint: true } so the hard-link identity check is exact.
Addresses review feedback on pnpm#12284
---
Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit 9239b97 |
Restrict the existing-exe early-return on Windows to the node.exe path so non-node commands still get their cmd-shims (re)generated. When the inode identity is unreliable (Windows often reports a zero inode), fall back to a size-then-content comparison so an identical node.exe is still detected without warning. Addresses review feedback on pnpm#12284 --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit 3a3303a |
Stream the content fallback in 64KB chunks and stop at the first mismatch, so an unreliable-inode comparison never buffers a whole executable in memory. Addresses review feedback on pnpm#12284 --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit f04be93 |
…ameFile comment Verify the regression test that the second linkBinsOfPackages call keeps node.exe sharing identity with the source binary, so a future delete+recreate regression is caught. Reword the isSameFile comment to describe the content fallback accurately (it covers any unprovable identity, not only zero inodes). Addresses review feedback on pnpm#12284 --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit ffb5f08 |
Port pnpm's same-file early-return for the Windows node.exe path: when the existing node.exe already refers to the source binary, skip the remove + hardlink churn on warm installs. Identity is checked cheaply via same_file::Handle (file index + volume serial), falling back to a chunked content comparison when the OS doesn't expose a reliable index. Mirrors pnpm#12284 on the TypeScript side. same-file is already a transitive dependency (via walkdir); promoted to a workspace dependency. --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit b8d6281 |
|
Pushed a round of changes addressing the bot review and extending the fix:
Written by an agent (Claude Code, claude-opus-4-8). |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #12284 +/- ##
==========================================
- Coverage 88.00% 88.00% -0.01%
==========================================
Files 308 308
Lines 41395 41418 +23
==========================================
+ Hits 36431 36451 +20
- Misses 4964 4967 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Treat a transient read error during the content comparison as 'not the same file' so bin linking falls back to the existing warn + remove + relink path instead of failing. Also relax the regression test to assert only the absence of the warning (the real regression signal, coupled with the rimraf in the same branch) rather than inode identity, which a copyFile fallback would not satisfy. Addresses review feedback on pnpm#12284 --- Written by an agent (Claude Code, claude-opus-4-8).
The same-file regression test treated a failed same_file::Handle lookup as a hard error, but the production code tolerates it and falls back to content comparison. Degrade to treating the files as distinct instead of unwrapping. Addresses review feedback on pnpm#12284 --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit 496432e |
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.27773792856,
"stddev": 0.23798258126383465,
"median": 4.20175115766,
"user": 3.1586060599999994,
"system": 2.7288637400000004,
"min": 3.92979734916,
"max": 4.711955507160001,
"times": [
4.21021896516,
4.07153721716,
4.61447473816,
4.33690864516,
4.711955507160001,
4.193283350160001,
4.17165002416,
4.36257335216,
4.17498013716,
3.92979734916
]
},
{
"command": "pacquet@main",
"mean": 4.14420756546,
"stddev": 0.2271294453410863,
"median": 4.156029331160001,
"user": 3.07991656,
"system": 2.71123024,
"min": 3.73524680416,
"max": 4.47862992916,
"times": [
4.1525737971600005,
4.1594848651600005,
4.15077321216,
4.47862992916,
4.240527585160001,
4.35969555616,
3.73524680416,
4.23614003516,
3.7970147431599997,
4.131989127160001
]
},
{
"command": "pnpr@HEAD",
"mean": 2.50774445786,
"stddev": 0.28004523104684165,
"median": 2.4150904316599995,
"user": 2.0734189599999997,
"system": 2.2568932399999997,
"min": 2.17799804516,
"max": 2.9564037541599997,
"times": [
2.3308354681599996,
2.85903262516,
2.31335353916,
2.4070059911599997,
2.64863733216,
2.76744808216,
2.1935548691599998,
2.4231748721599997,
2.9564037541599997,
2.17799804516
]
},
{
"command": "pnpr@main",
"mean": 2.54287908056,
"stddev": 0.3224493300259856,
"median": 2.51282959116,
"user": 2.0775401599999994,
"system": 2.2598755400000003,
"min": 2.12107491416,
"max": 3.10669542116,
"times": [
2.1767645011599996,
2.2488402431599996,
2.46882027116,
2.9450592951599996,
2.50903627416,
2.76568234916,
2.57019462816,
3.10669542116,
2.51662290816,
2.12107491416
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6986334734599999,
"stddev": 0.1569402599712927,
"median": 0.60713064906,
"user": 0.29282594,
"system": 1.02788558,
"min": 0.59492906756,
"max": 1.06427708756,
"times": [
0.87150976256,
0.59492906756,
0.71833476056,
0.5990238155600001,
0.72455324456,
0.61150449456,
0.60107597556,
0.60275680356,
1.06427708756,
0.59836972256
]
},
{
"command": "pacquet@main",
"mean": 0.6241160647599999,
"stddev": 0.04749729683495283,
"median": 0.60465570606,
"user": 0.29178993999999997,
"system": 1.02574648,
"min": 0.58462413656,
"max": 0.72697640056,
"times": [
0.69708068356,
0.58462413656,
0.6035524585600001,
0.60254649156,
0.59918925056,
0.60575895356,
0.61508507256,
0.60736118456,
0.72697640056,
0.59898601556
]
},
{
"command": "pnpr@HEAD",
"mean": 0.80501232686,
"stddev": 0.10734519104403198,
"median": 0.81088405056,
"user": 0.30668693999999996,
"system": 1.0723043799999998,
"min": 0.64473837456,
"max": 0.94717454256,
"times": [
0.8568632175600001,
0.64473837456,
0.92212231656,
0.94717454256,
0.8907211755600001,
0.87492509556,
0.75902141656,
0.71897398656,
0.67067825956,
0.76490488356
]
},
{
"command": "pnpr@main",
"mean": 0.8794818246600002,
"stddev": 0.11149963900664836,
"median": 0.88104700856,
"user": 0.30169974,
"system": 1.0605624800000002,
"min": 0.64488012956,
"max": 1.05364637056,
"times": [
0.86693613956,
0.83204158356,
1.0084431815600001,
0.82417083256,
0.64488012956,
0.9193644775600001,
0.83941662856,
1.05364637056,
0.91076102556,
0.89515787756
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.14575157414,
"stddev": 0.2067992754004891,
"median": 4.093687882739999,
"user": 2.9664041,
"system": 2.5964394799999995,
"min": 3.97269664874,
"max": 4.66551186774,
"times": [
4.11247114774,
4.17373657474,
3.97269664874,
3.98542955974,
4.66551186774,
4.00915466974,
4.00631254674,
4.07490461774,
4.24160214474,
4.21569596374
]
},
{
"command": "pacquet@main",
"mean": 4.088655215339999,
"stddev": 0.11610924320296122,
"median": 4.06025334674,
"user": 2.9435762999999997,
"system": 2.6086413799999995,
"min": 3.98476155974,
"max": 4.33470087374,
"times": [
4.06575707674,
4.02793922274,
3.9924653127400003,
4.03089028574,
4.05645067374,
4.33470087374,
4.06405601974,
3.98476155974,
4.06483562574,
4.2646955027399995
]
},
{
"command": "pnpr@HEAD",
"mean": 2.3532701125399997,
"stddev": 0.26528488538446066,
"median": 2.2630128122400004,
"user": 1.970158,
"system": 2.2067654799999996,
"min": 2.08395379774,
"max": 2.92058396474,
"times": [
2.28022451174,
2.21421216974,
2.5739409267399997,
2.28840494574,
2.08395379774,
2.18001866574,
2.12874617474,
2.61681485574,
2.92058396474,
2.24580111274
]
},
{
"command": "pnpr@main",
"mean": 2.4248103258400002,
"stddev": 0.15031386725372037,
"median": 2.44437726174,
"user": 1.9561575999999998,
"system": 2.22309938,
"min": 2.24229551074,
"max": 2.68578648974,
"times": [
2.24229551074,
2.53360666574,
2.24654910074,
2.49842003974,
2.33858251174,
2.51170597074,
2.26712450074,
2.68578648974,
2.53369798474,
2.3903344837400002
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.2308433120200002,
"stddev": 0.05322104091554628,
"median": 1.21695251062,
"user": 1.0780991199999999,
"system": 1.3554419599999998,
"min": 1.17960626562,
"max": 1.34484300062,
"times": [
1.21389566162,
1.2227412316200001,
1.17960626562,
1.21163675462,
1.22447435962,
1.20291056762,
1.30683432862,
1.34484300062,
1.18148159062,
1.2200093596200001
]
},
{
"command": "pacquet@main",
"mean": 1.2493833777200003,
"stddev": 0.1095469212887822,
"median": 1.21454855862,
"user": 1.06048072,
"system": 1.36226116,
"min": 1.18233671862,
"max": 1.54765956262,
"times": [
1.25330774662,
1.18233671862,
1.24066450862,
1.19044123862,
1.54765956262,
1.1909481156200001,
1.23653384962,
1.19256326762,
1.2708670346200002,
1.18851173462
]
},
{
"command": "pnpr@HEAD",
"mean": 0.73894071252,
"stddev": 0.2082910119514682,
"median": 0.6514677941200001,
"user": 0.26316532000000004,
"system": 1.0095050600000002,
"min": 0.6278028276200001,
"max": 1.27724605062,
"times": [
0.6419813916200001,
0.6470788406200001,
0.6278028276200001,
0.6598790666200001,
0.6558567476200001,
0.6434705796200001,
0.6346109216200001,
1.27724605062,
0.92262349062,
0.6788572086200001
]
},
{
"command": "pnpr@main",
"mean": 0.70977995952,
"stddev": 0.17009805454586824,
"median": 0.6373554226200001,
"user": 0.25737691999999995,
"system": 0.9919445600000001,
"min": 0.62152466362,
"max": 1.15137594062,
"times": [
0.6306244846200001,
0.6277072316200001,
0.6619750926200001,
0.65254233662,
0.6440863606200001,
0.6235183496200001,
0.62152466362,
0.6300187506200001,
0.8544263846200001,
1.15137594062
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.75323565496,
"stddev": 0.09666543919594382,
"median": 2.72261864586,
"user": 1.42148618,
"system": 1.5613922799999997,
"min": 2.64661263836,
"max": 2.95468115836,
"times": [
2.64661263836,
2.71168613736,
2.6880595343600002,
2.6905016303600005,
2.6664353803600003,
2.8209474993600003,
2.7693102303600003,
2.73355115436,
2.95468115836,
2.8505711863600003
]
},
{
"command": "pacquet@main",
"mean": 2.7706273933600007,
"stddev": 0.06386117713760264,
"median": 2.73393152686,
"user": 1.4376838799999998,
"system": 1.5783054799999996,
"min": 2.7134069373600003,
"max": 2.9069151003600004,
"times": [
2.72595662836,
2.8282904503600004,
2.7134069373600003,
2.7295039823600002,
2.7357600543600005,
2.7214540263600004,
2.7321029993600003,
2.79747374736,
2.9069151003600004,
2.81541000736
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6726953953600001,
"stddev": 0.051820707327420994,
"median": 0.64478914836,
"user": 0.2772349799999999,
"system": 1.00861018,
"min": 0.62642986236,
"max": 0.75384570436,
"times": [
0.64022154736,
0.62642986236,
0.62809488436,
0.66816809136,
0.75384570436,
0.7445020263600001,
0.64870449236,
0.73886047736,
0.63725306336,
0.64087380436
]
},
{
"command": "pnpr@main",
"mean": 0.6609638077600001,
"stddev": 0.04431550356460514,
"median": 0.63882340786,
"user": 0.2712273799999999,
"system": 0.99060608,
"min": 0.62488268936,
"max": 0.7362877023600001,
"times": [
0.63518773236,
0.7153712613600001,
0.7362877023600001,
0.72026460936,
0.6334991533600001,
0.64245908336,
0.64705919836,
0.62488268936,
0.62602957936,
0.6285970683600001
]
}
]
} |
|
| Branch | pr/12284 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,145.75 ms(-0.48%)Baseline: 4,165.92 ms | 4,999.11 ms (82.93%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,753.24 ms(-7.92%)Baseline: 2,990.12 ms | 3,588.15 ms (76.73%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,230.84 ms(-6.07%)Baseline: 1,310.44 ms | 1,572.53 ms (78.27%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,277.74 ms(+8.49%)Baseline: 3,943.00 ms | 4,731.60 ms (90.41%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 698.63 ms(+12.85%)Baseline: 619.08 ms | 742.90 ms (94.04%) |
|
| Branch | pr/12284 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,353.27 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 672.70 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 738.94 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,507.74 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 805.01 ms |
|
Congrats on merging your first pull request! 🎉🎉🎉 |
|
Thanks for reviewing and improving this fix. I learned a lot from the review process. Appreciate the detailed feedback and the additional hardening work! |
Closes pnpm/pnpm#12203.
On Windows,
nodeis linked by hardlinkingnode.exedirectly rather than through a cmd-shim. The early-exit inlinkBin()only recognized an existing cmd-shim, so on every warm install the existing (already correct)node.exewas treated as a conflict: pnpm warnedThe target bin directory already contains an exe called nodeand then removed and re-linked it. Because many commands re-linknode, the warning was spammed.@pnpm/bins.linkerBefore warning and replacing an existing
.exe, check whether it already refers to the link target via a newisSameFile()helper:BigIntto avoid the precision loss NTFS 64-bit file IDs suffer when cast to aNumber. A zero/unreliable inode (common on Windows) is not treated as a match.The early-return is scoped to the
node.exepath, so non-nodecommands still fall through to the existing warn + remove +cmdShim()regeneration and never end up with a partially populated bins directory.pacquet
pacquet never emitted this warning, but its Windows
link_node_binunconditionally removed and re-linkednode.exeon every install. Ported the same same-file early-return tocmd-shimso warm installs skip that churn, usingsame_file::Handlefor the cheap identity check (promoted from a transitive to a workspace dependency) with the same chunked content fallback.Tests
@pnpm/bins.linker: Windows regression tests covering the no-warning behavior on a repeat link and the identical-content-but-not-a-hardlink fallback.cmd-shimtest assertingnode.exeis left in place rather than removed and relinked.Written by an agent (Claude Code, claude-opus-4-8).