forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconvert-symlinks.mjs
More file actions
140 lines (121 loc) · 4.88 KB
/
Copy pathconvert-symlinks.mjs
File metadata and controls
140 lines (121 loc) · 4.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* @fileoverview Script that takes a directory and converts all its Unix symlinks
* to relative Windows-compatible symlinks. This is necessary because when building
* tests via Bazel inside WSL; the output cannot simply be used outside WSL to perform
* native Windows testing. This is a known limitation/bug of the WSL <> Windows interop.
*
* Symlinks are commonly used by Bazel inside the `.runfiles` directory, which is relevant
* for executing tests outside Bazel on the host machine. In addition, `rules_js` heavily
* relies on symlinks for node modules.
*
* Some more details in:
* - https://blog.trailofbits.com/2024/02/12/why-windows-cant-follow-wsl-symlinks/.
* - https://pnpm.io/symlinked-node-modules-structure.
*/
import childProcess from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
const [rootDir, cmdPath] = process.argv.slice(2);
const skipDirectories = [
// Modules that we don't need and would unnecessarily slow-down this.
'_windows_amd64/bin/nodejs/node_modules',
];
// Dereferencing can be parallelized and doesn't cause any WSL flakiness (no exe is invoked).
const dereferenceFns = [];
// Re-linking can be parallelized, but should only be in batched. WSL exe is involved and it can be flaky.
// Note: Relinking should not happen during removing & copying of dereference tasks.
const relinkFns = [];
async function transformDir(p) {
// We explore directories after all files were checked at this level.
const directoriesToVisit = [];
for (const file of await fs.readdir(p, { withFileTypes: true })) {
const subPath = path.join(p, file.name);
if (skipDirectories.some((d) => subPath.endsWith(d))) {
continue;
}
if (file.isSymbolicLink()) {
let realTarget = '';
let linkTarget = '';
try {
realTarget = await fs.realpath(subPath);
linkTarget = await fs.readlink(subPath);
} catch (e) {
throw new Error(`Skipping; cannot dereference & read link: ${subPath}: ${e}`);
}
// Transform relative links but preserve them.
// This is needed for pnpm.
if (!path.isAbsolute(linkTarget)) {
relinkFns.push(async () => {
const wslSubPath = path.relative(rootDir, subPath).replace(/\//g, '\\');
const linkTargetWindowsPath = linkTarget.replace(/\//g, '\\');
await fs.unlink(subPath);
if ((await fs.stat(realTarget)).isDirectory()) {
// This is a symlink to a directory, create a dir junction.
// Re-create this symlink on the Windows FS using the Windows mklink command.
await exec(`${cmdPath} /c mklink /d "${wslSubPath}" "${linkTargetWindowsPath}"`);
} else {
// This is a symlink to a file, create a file junction.
// Re-create this symlink on the Windows FS using the Windows mklink command.
await exec(`${cmdPath} /c mklink "${wslSubPath}" "${linkTargetWindowsPath}"`);
}
});
} else {
dereferenceFns.push(async () => {
await fs.unlink(subPath);
// Note: NodeJS `fs.cp` can have issues when sources are readonly.
await exec(`cp -R ${realTarget} ${subPath}`);
});
}
} else if (file.isDirectory()) {
directoriesToVisit.push(subPath);
}
}
await Promise.all(directoriesToVisit.map((d) => transformDir(d)));
}
function exec(cmd, maxRetries = 5, retryDelay = 200) {
return new Promise((resolve, reject) => {
childProcess.exec(cmd, { cwd: rootDir }, (error) => {
if (error !== null) {
// Windows command spawned within WSL (which is untypical) seem to be flaky rarely.
// This logic tries to make it fully stable by re-trying if this surfaces:
// See: https://github.com/microsoft/WSL/issues/8677.
if (
maxRetries > 0 &&
error.stderr !== undefined &&
error.stderr.includes(`accept4 failed 110`)
) {
// Add a small delay before the retry
setTimeout(() => {
resolve(exec(cmd, maxRetries - 1, retryDelay));
}, retryDelay);
return;
}
reject(error);
} else {
resolve();
}
});
});
}
try {
await transformDir(rootDir);
// Dereference first.
await Promise.all(dereferenceFns.map((fn) => fn()));
// Re-link symlinks to work inside Windows.
// This is done in batches to avoid flakiness due to WSL
// See: https://github.com/microsoft/WSL/issues/8677.
const batchSize = 75;
for (let i = 0; i < relinkFns.length; i += batchSize) {
await Promise.all(relinkFns.slice(i, i + batchSize).map((fn) => fn()));
}
} catch (err) {
console.error('Could not convert symlinks:', err);
process.exitCode = 1;
}