@@ -532,6 +532,24 @@ begin
532532 Result := Trim(NodeStoragePage.Values[0 ]);
533533end ;
534534
535+ { DataRoot is the parent of InstallRoot (.shim / .nodejs / .cache live here). }
536+ { Default InstallRoot={app} \installs keeps DataRoot={ app} . Custom storage splits them. }
537+ function GetDataRoot (Param: String): String;
538+ var
539+ InstallRoot: String;
540+ begin
541+ InstallRoot := NormalizePath(GetInstallRoot(' ' ));
542+ if InstallRoot = ' ' then
543+ begin
544+ Result := ExpandConstant(' {app}' );
545+ Exit;
546+ end ;
547+
548+ Result := NormalizePath(ExtractFileDir(InstallRoot));
549+ if Result = ' ' then
550+ Result := ExpandConstant(' {app}' );
551+ end ;
552+
535553function GetLegacySettingValue (const SettingsText: String; const Key: String): String;
536554var
537555 Normalized: String;
@@ -1038,6 +1056,7 @@ var
10381056 NormalizedLegacyNodePath: String;
10391057 NormalizedCurrentHome: String;
10401058 NormalizedCurrentNodePath: String;
1059+ NormalizedProgramNodePath: String;
10411060 I: Integer;
10421061 RemoveLegacyHome: Boolean;
10431062 RemoveLegacyNodePath: Boolean;
@@ -1051,7 +1070,9 @@ begin
10511070 NormalizedLegacyHome := NormalizePath(Trim(LegacyInstallDir));
10521071 NormalizedLegacyNodePath := NormalizePath(Trim(LegacySettingsPath));
10531072 NormalizedCurrentHome := NormalizePath(ExpandConstant(' {app}' ));
1054- NormalizedCurrentNodePath := NormalizePath(ExpandConstant(' {app}\\.nodejs' ));
1073+ { PATH node entry must track DataRoot (.shim lives there), not always {app} . }
1074+ NormalizedCurrentNodePath := NormalizePath(AddBackslash(GetDataRoot(' ' )) + ' .nodejs' );
1075+ NormalizedProgramNodePath := NormalizePath(ExpandConstant(' {app}\\.nodejs' ));
10551076
10561077 RemoveLegacyHome := RemoveLegacyEntries and (NormalizedLegacyHome <> ' ' ) and
10571078 (CompareText(NormalizedLegacyHome, NormalizedCurrentHome) <> 0 );
@@ -1072,7 +1093,8 @@ begin
10721093 if (CompareText(Segment , ' %NVM_HOME%' ) = 0 ) or
10731094 (CompareText(Segment , ' %NVM_HOME%\\.nodejs' ) = 0 ) or
10741095 IsSameExpandedPath(Segment , NormalizedCurrentHome) or
1075- IsSameExpandedPath(Segment , NormalizedCurrentNodePath) then
1096+ IsSameExpandedPath(Segment , NormalizedCurrentNodePath) or
1097+ IsSameExpandedPath(Segment , NormalizedProgramNodePath) then
10761098 Continue;
10771099 end ;
10781100
@@ -1224,7 +1246,7 @@ begin
12241246 if LegacySymlinkPath = ' ' then
12251247 Exit;
12261248
1227- NewNodePath := NormalizePath(ExpandConstant( ' {app}\\ .nodejs' ) );
1249+ NewNodePath := NormalizePath(AddBackslash(GetDataRoot( ' ' )) + ' .nodejs' );
12281250 if CompareText(LegacySymlinkPath, NewNodePath) = 0 then
12291251 Exit;
12301252
@@ -1289,7 +1311,7 @@ begin
12891311 if (LegacySettingsPath <> ' ' ) and
12901312 not SameText(
12911313 RemoveBackslashUnlessRoot(Trim(LegacySettingsPath)),
1292- RemoveBackslashUnlessRoot(ExpandConstant( ' {app}\ .nodejs' ) )
1314+ RemoveBackslashUnlessRoot(AddBackslash(GetDataRoot( ' ' )) + ' .nodejs' )
12931315 ) then
12941316 UpgraderArgs := UpgraderArgs + ' --symlink-path "' + LegacySettingsPath + ' "' ;
12951317
@@ -2264,43 +2286,71 @@ end;
22642286function PrepareToInstall (var NeedsRestart: Boolean): String;
22652287var
22662288 ShimDir: String;
2289+ DataShimDir: String;
22672290 ResultCode: Integer;
22682291 UserGrant: String;
22692292begin
22702293 Result := ' ' ;
22712294 NeedsRestart := False;
22722295 ShimDir := ExpandConstant(' {app}\.shim' );
2273- if not DirExists(ShimDir) then
2274- Exit;
2296+ DataShimDir := AddBackslash(GetDataRoot(' ' )) + ' .shim' ;
22752297
22762298 { Runtime LockShimDirectory makes .shim a protected read-only DACL.
2277- Community ProgramRoot == DataRoot, so reinstall extracts node.exe here
2278- and Inno reports "create a file in the destination directory: Access is denied."
2279- User still has WRITE_DAC, so icacls /reset works without elevation. }
2280- if Exec(
2281- ExpandConstant(' {sys}\icacls.exe' ),
2282- ' "' + ShimDir + ' " /reset /T /C /Q' ,
2283- ' ' ,
2284- SW_HIDE,
2285- ewWaitUntilTerminated,
2286- ResultCode
2287- ) then
2288- Log(' PrepareToInstall: reset .shim ACL, icacls exit=' + IntToStr(ResultCode))
2289- else
2290- Log(' PrepareToInstall: icacls /reset failed to start for ' + ShimDir);
2299+ When DataRoot == {app} , reinstall extracts node.exe here and Inno reports
2300+ " create a file in the destination directory: Access is denied."
2301+ User still has WRITE_DAC, so icacls /reset works without elevation.
2302+ When DataRoot differs, unlock both ProgramRoot and DataRoot shim dirs. }
2303+ if DirExists(ShimDir) then
2304+ begin
2305+ if Exec(
2306+ ExpandConstant(' {sys}\icacls.exe' ),
2307+ ' "' + ShimDir + ' " /reset /T /C /Q' ,
2308+ ' ' ,
2309+ SW_HIDE,
2310+ ewWaitUntilTerminated,
2311+ ResultCode
2312+ ) then
2313+ Log(' PrepareToInstall: reset .shim ACL, icacls exit=' + IntToStr(ResultCode))
2314+ else
2315+ Log(' PrepareToInstall: icacls /reset failed to start for ' + ShimDir);
22912316
2292- UserGrant := ' "' + ShimDir + ' " /grant:r "' + GetUserNameString + ' :(OI)(CI)(F)" /T /C /Q' ;
2293- if Exec(
2294- ExpandConstant(' {sys}\icacls.exe' ),
2295- UserGrant,
2296- ' ' ,
2297- SW_HIDE,
2298- ewWaitUntilTerminated,
2299- ResultCode
2300- ) then
2301- Log(' PrepareToInstall: grant .shim write, icacls exit=' + IntToStr(ResultCode))
2302- else
2303- Log(' PrepareToInstall: icacls /grant failed to start for ' + ShimDir);
2317+ UserGrant := ' "' + ShimDir + ' " /grant:r "' + GetUserNameString + ' :(OI)(CI)(F)" /T /C /Q' ;
2318+ if Exec(
2319+ ExpandConstant(' {sys}\icacls.exe' ),
2320+ UserGrant,
2321+ ' ' ,
2322+ SW_HIDE,
2323+ ewWaitUntilTerminated,
2324+ ResultCode
2325+ ) then
2326+ Log(' PrepareToInstall: grant .shim write, icacls exit=' + IntToStr(ResultCode))
2327+ else
2328+ Log(' PrepareToInstall: icacls /grant failed to start for ' + ShimDir);
2329+ end ;
2330+
2331+ if (CompareText(NormalizePath(ShimDir), NormalizePath(DataShimDir)) <> 0 ) and DirExists(DataShimDir) then
2332+ begin
2333+ if Exec(
2334+ ExpandConstant(' {sys}\icacls.exe' ),
2335+ ' "' + DataShimDir + ' " /reset /T /C /Q' ,
2336+ ' ' ,
2337+ SW_HIDE,
2338+ ewWaitUntilTerminated,
2339+ ResultCode
2340+ ) then
2341+ Log(' PrepareToInstall: reset DataRoot .shim ACL, icacls exit=' + IntToStr(ResultCode));
2342+
2343+ UserGrant := ' "' + DataShimDir + ' " /grant:r "' + GetUserNameString + ' :(OI)(CI)(F)" /T /C /Q' ;
2344+ if Exec(
2345+ ExpandConstant(' {sys}\icacls.exe' ),
2346+ UserGrant,
2347+ ' ' ,
2348+ SW_HIDE,
2349+ ewWaitUntilTerminated,
2350+ ResultCode
2351+ ) then
2352+ Log(' PrepareToInstall: grant DataRoot .shim write, icacls exit=' + IntToStr(ResultCode));
2353+ end ;
23042354end ;
23052355
23062356procedure InitializeWizard ;
@@ -2407,6 +2457,7 @@ begin
24072457 Result :=
24082458 ' NVM for Windows will be installed with the following settings:' + NewLine + NewLine +
24092459 Space + ' Node.js storage: ' + GetInstallRoot(' ' ) + NewLine +
2460+ Space + ' Runtime data root: ' + GetDataRoot(' ' ) + NewLine +
24102461 Space + ' Operating mode: ' + Mode + NewLine + NewLine +
24112462 Space + ' Keep downloaded Node.js setup files (cache for reinstall): ' ;
24122463 if (PreferencesPage <> nil ) and PreferencesPage.Values[0 ] then
@@ -2515,10 +2566,90 @@ begin
25152566 BroadcastEnvironmentChange();
25162567end ;
25172568
2569+ { When InstallRoot is customized, DataRoot leaves {app} . Seed runtime .shim + proxy }
2570+ { so PATH/.nodejs match where nvm use / reshim write shims (closes #1388). }
2571+ procedure EnsureDataRootRuntimeLayout ();
2572+ var
2573+ DataRoot: String;
2574+ ProgramRoot: String;
2575+ ProgramShim: String;
2576+ DataShim: String;
2577+ ProgramProxy: String;
2578+ DataProxy: String;
2579+ ResultCode: Integer;
2580+ begin
2581+ DataRoot := NormalizePath(GetDataRoot(' ' ));
2582+ ProgramRoot := NormalizePath(ExpandConstant(' {app}' ));
2583+ ProgramShim := AddBackslash(ProgramRoot) + ' .shim' ;
2584+ DataShim := AddBackslash(DataRoot) + ' .shim' ;
2585+ ProgramProxy := ExpandConstant(' {app}\utils\proxy.exe' );
2586+ DataProxy := AddBackslash(DataRoot) + ' proxy.exe' ;
2587+
2588+ AppendInstallLog(' EnsureDataRootRuntimeLayout: ProgramRoot=' + ProgramRoot);
2589+ AppendInstallLog(' EnsureDataRootRuntimeLayout: DataRoot=' + DataRoot);
2590+
2591+ if not ForceDirectories(DataShim) then
2592+ begin
2593+ AppendInstallLogWarn(' EnsureDataRootRuntimeLayout: could not create ' + DataShim);
2594+ Exit;
2595+ end ;
2596+
2597+ if CompareText(ProgramRoot, DataRoot) = 0 then
2598+ begin
2599+ AppendInstallLog(' EnsureDataRootRuntimeLayout: DataRoot equals ProgramRoot, no seed copy needed' );
2600+ Exit;
2601+ end ;
2602+
2603+ if DirExists(ProgramShim) then
2604+ begin
2605+ if not Exec(
2606+ ' robocopy.exe' ,
2607+ ' "' + ProgramShim + ' " "' + DataShim + ' " /E /MT:8 /R:1 /W:1 /NP /NFL /NDL /NJH /NJS' ,
2608+ ' ' ,
2609+ SW_HIDE,
2610+ ewWaitUntilTerminated,
2611+ ResultCode
2612+ ) then
2613+ AppendInstallLogWarn(' EnsureDataRootRuntimeLayout: robocopy seed failed to start' )
2614+ else if ResultCode >= 8 then
2615+ AppendInstallLogWarn(' EnsureDataRootRuntimeLayout: robocopy seed exit=' + IntToStr(ResultCode))
2616+ else
2617+ AppendInstallLog(' EnsureDataRootRuntimeLayout: seeded DataRoot .shim from ProgramRoot' );
2618+ end
2619+ else
2620+ AppendInstallLogWarn(' EnsureDataRootRuntimeLayout: missing ProgramRoot .shim: ' + ProgramShim);
2621+
2622+ { Hardlinks to proxy require same volume; certified also keeps proxy on DataRoot. }
2623+ if FileExists(ProgramProxy) then
2624+ begin
2625+ if not FileCopy(ProgramProxy, DataProxy, False) then
2626+ AppendInstallLogWarn(' EnsureDataRootRuntimeLayout: failed to copy proxy.exe to DataRoot' )
2627+ else
2628+ AppendInstallLog(' EnsureDataRootRuntimeLayout: copied proxy.exe to ' + DataProxy);
2629+ end ;
2630+
2631+ { Drop stale ProgramRoot .nodejs so PATH/docs don't point at the empty seed tree. }
2632+ if DirExists(AddBackslash(ProgramRoot) + ' .nodejs' ) then
2633+ begin
2634+ if not Exec(
2635+ ExpandConstant(' {cmd}' ),
2636+ ' /C rmdir "' + AddBackslash(ProgramRoot) + ' .nodejs"' ,
2637+ ' ' ,
2638+ SW_HIDE,
2639+ ewWaitUntilTerminated,
2640+ ResultCode
2641+ ) then
2642+ AppendInstallLogWarn(' EnsureDataRootRuntimeLayout: failed to remove stale ProgramRoot .nodejs' )
2643+ else
2644+ AppendInstallLog(' EnsureDataRootRuntimeLayout: removed stale ProgramRoot .nodejs junction' );
2645+ end ;
2646+ end ;
2647+
25182648function EnsureShimModeNodePathJunction (): Boolean;
25192649var
25202650 NodePath: String;
25212651 ShimPath: String;
2652+ DataRoot: String;
25222653 CommandLine: String;
25232654 ResultCode: Integer;
25242655begin
@@ -2527,8 +2658,9 @@ begin
25272658 if not IsShimModeSelected() then
25282659 Exit;
25292660
2530- NodePath := ExpandConstant(' {app}\.nodejs' );
2531- ShimPath := ExpandConstant(' {app}\.shim' );
2661+ DataRoot := NormalizePath(GetDataRoot(' ' ));
2662+ NodePath := AddBackslash(DataRoot) + ' .nodejs' ;
2663+ ShimPath := AddBackslash(DataRoot) + ' .shim' ;
25322664 AppendInstallLog(' EnsureShimModeNodePathJunction: source=' + ShimPath + ' target=' + NodePath);
25332665
25342666 if not DirExists(ShimPath) then
@@ -2561,7 +2693,7 @@ begin
25612693 Exit;
25622694 end ;
25632695
2564- AppendInstallLog(' EnsureShimModeNodePathJunction: created hidden .nodejs -> .shim' );
2696+ AppendInstallLog(' EnsureShimModeNodePathJunction: created hidden .nodejs -> .shim under DataRoot ' );
25652697end ;
25662698
25672699procedure RegisterInstalledVersionsInWindowsApps ();
@@ -2611,10 +2743,18 @@ var
26112743 ResultCode: Integer;
26122744 ShimPath: String;
26132745 ProxyPath: String;
2746+ DataRoot: String;
2747+ DataShim: String;
26142748 CommandLine: String;
26152749begin
2616- ShimPath := ExpandConstant(' {app}\.shim\' + ShimBaseName + ' .exe' );
2617- ProxyPath := ExpandConstant(' {app}\utils\proxy.exe' );
2750+ DataRoot := NormalizePath(GetDataRoot(' ' ));
2751+ DataShim := AddBackslash(DataRoot) + ' .shim' ;
2752+ ShimPath := AddBackslash(DataShim) + ShimBaseName + ' .exe' ;
2753+
2754+ { Prefer DataRoot proxy when present (required for cross-volume hardlinks). }
2755+ ProxyPath := AddBackslash(DataRoot) + ' proxy.exe' ;
2756+ if not FileExists(ProxyPath) then
2757+ ProxyPath := ExpandConstant(' {app}\utils\proxy.exe' );
26182758
26192759 if FileExists(ShimPath) then
26202760 Exit;
@@ -2625,9 +2765,9 @@ begin
26252765 Exit;
26262766 end ;
26272767
2628- if not DirExists(ExpandConstant( ' {app}\.shim ' ) ) then
2768+ if not DirExists(DataShim ) then
26292769 begin
2630- AppendInstallLogWarn(' EnsureShimHardlinkForPrewarm: missing .shim directory' );
2770+ AppendInstallLogWarn(' EnsureShimHardlinkForPrewarm: missing .shim directory: ' + DataShim );
26312771 Exit;
26322772 end ;
26332773
@@ -2650,10 +2790,12 @@ end;
26502790procedure TryPrewarmShim (const ShimBaseName: String);
26512791var
26522792 ResultCode: Integer;
2793+ ShimPath: String;
26532794begin
26542795 ResultCode := -1 ;
2796+ ShimPath := AddBackslash(GetDataRoot(' ' )) + ' .shim\' + ShimBaseName + ' .exe' ;
26552797 if Exec(
2656- ExpandConstant( ' {app}\.shim\ ' + ShimBaseName + ' .exe ' ) ,
2798+ ShimPath ,
26572799 ' --version' ,
26582800 ' ' ,
26592801 SW_HIDE,
@@ -2738,6 +2880,7 @@ begin
27382880 AppendInstallLog(' CurStepChanged: ssPostInstall start' );
27392881 AppendInstallLog(' CurStepChanged: app=' + ExpandConstant(' {app}' ));
27402882 AppendInstallLog(' CurStepChanged: install root=' + GetInstallRoot(' ' ));
2883+ AppendInstallLog(' CurStepChanged: data root=' + GetDataRoot(' ' ));
27412884 AppendInstallLog(' CurStepChanged: LegacyInstallDir=' + LegacyInstallDir);
27422885 AppendInstallLog(' CurStepChanged: LegacySettingsRoot=' + LegacySettingsRoot);
27432886 AppendInstallLog(' CurStepChanged: LegacySettingsPath=' + LegacySettingsPath);
@@ -2777,6 +2920,7 @@ begin
27772920
27782921 FinalizingStep := FinalizingStep + 1 ;
27792922 UpdateFinalizingProgress(FinalizingPage, ' Configuring internal Node.js shim junction...' , FinalizingStep, FinalizingTotal);
2923+ EnsureDataRootRuntimeLayout();
27802924 if not EnsureShimModeNodePathJunction() then
27812925 MsgBox(
27822926 ' NVM for Windows could not create the internal .nodejs junction required for shim mode.' + #13 #10 + #13 #10 +
0 commit comments