mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-18 03:23:57 +08:00
For our native Windows tests (verifying `ng` CLI outside WSL), we currently build the test binaries inside WSL, leveraging bazel remote execution. The scripts involved in moving the built binaries to outside WSL rarely flake due to a WSL interop bug when we invoke Windows commands from inside WSL (which is an untypical thing to do; but a needed trick given some WSL limitation). This commit tries to stabilize/avoid this flake by retrying if we recognize this.
171 lines
5.8 KiB
JavaScript
171 lines
5.8 KiB
JavaScript
/**
|
|
* @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 path from 'node:path';
|
|
import fs from 'node:fs/promises';
|
|
import childProcess from 'node:child_process';
|
|
|
|
const [rootDir, cmdPath] = process.argv.slice(2);
|
|
|
|
// GitHub actions can set this environment variable when pressing the "re-run" button.
|
|
const debug = process.env.ACTIONS_STEP_DEBUG === 'true';
|
|
const skipDirectories = [
|
|
// Modules that we don't need and would unnecessarily slow-down this.
|
|
'_windows_amd64/bin/nodejs/node_modules',
|
|
];
|
|
|
|
const workspaceRootPaths = [/.*\.runfiles\/angular_cli\//, /^.*-fastbuild\/bin\//];
|
|
|
|
// Copying can be parallelized and doesn't cause any WSL flakiness (no exe is invoked).
|
|
const parallelCopyTasks = [];
|
|
|
|
async function transformDir(p) {
|
|
// We perform all command executions in parallel here to speed up.
|
|
// Note that we can't parallelize for the full recursive directory,
|
|
// as WSL and its interop would otherwise end up with some flaky errors.
|
|
// See: https://github.com/microsoft/WSL/issues/8677.
|
|
const tasks = [];
|
|
// 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()) {
|
|
// Allow for parallel processing of directory entries.
|
|
tasks.push(
|
|
(async () => {
|
|
let target = '';
|
|
try {
|
|
target = await fs.realpath(subPath);
|
|
} catch (e) {
|
|
if (debug) {
|
|
console.error('Skipping', subPath);
|
|
}
|
|
return;
|
|
}
|
|
|
|
await fs.rm(subPath);
|
|
|
|
const subPathId = relativizeForSimilarWorkspacePaths(subPath);
|
|
const targetPathId = relativizeForSimilarWorkspacePaths(target);
|
|
const isSelfLink = subPathId === targetPathId;
|
|
|
|
// This is an actual file that needs to be copied. Copy contents.
|
|
// - the target path is equivalent to the link. This is a self-link from `.runfiles` to `bin/`.
|
|
// - the target path is outside any of our workspace roots.
|
|
if (isSelfLink || targetPathId.startsWith('..')) {
|
|
parallelCopyTasks.push(exec(`cp -Rf ${target} ${subPath}`));
|
|
return;
|
|
}
|
|
|
|
const relativeSubPath = relativizeToRoot(subPath);
|
|
const targetAtDestination = path.relative(path.dirname(subPathId), targetPathId);
|
|
const targetAtDestinationWindowsPath = targetAtDestination.replace(/\//g, '\\');
|
|
|
|
const wslSubPath = relativeSubPath.replace(/\//g, '\\');
|
|
|
|
if (debug) {
|
|
console.log({
|
|
targetAtDestination,
|
|
subPath,
|
|
relativeSubPath,
|
|
target,
|
|
targetPathId,
|
|
subPathId,
|
|
});
|
|
}
|
|
|
|
if ((await fs.stat(target)).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}" "${targetAtDestinationWindowsPath}"`,
|
|
);
|
|
} 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}" "${targetAtDestinationWindowsPath}"`);
|
|
}
|
|
})(),
|
|
);
|
|
} else if (file.isDirectory()) {
|
|
directoriesToVisit.push(subPath);
|
|
}
|
|
}
|
|
|
|
// Wait for all commands/tasks to complete, executed in parallel.
|
|
await Promise.all(tasks);
|
|
|
|
// Descend into other directories, sequentially to avoid WSL interop errors.
|
|
for (const d of directoriesToVisit) {
|
|
await transformDir(d);
|
|
}
|
|
}
|
|
|
|
function exec(cmd, maxRetries = 2) {
|
|
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`)
|
|
) {
|
|
resolve(exec(cmd, maxRetries - 1));
|
|
return;
|
|
}
|
|
|
|
reject(error);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function relativizeForSimilarWorkspacePaths(p) {
|
|
const workspaceRootMatch = workspaceRootPaths.find((r) => r.test(p));
|
|
if (workspaceRootMatch !== undefined) {
|
|
return p.replace(workspaceRootMatch, '');
|
|
}
|
|
|
|
return path.relative(rootDir, p);
|
|
}
|
|
|
|
function relativizeToRoot(p) {
|
|
const res = path.relative(rootDir, p);
|
|
if (!res.startsWith('..')) {
|
|
return res;
|
|
}
|
|
|
|
throw new Error('Could not relativize to root: ' + p);
|
|
}
|
|
|
|
try {
|
|
await transformDir(rootDir);
|
|
await Promise.all(parallelCopyTasks);
|
|
} catch (err) {
|
|
console.error('Could not convert symlinks:', err);
|
|
process.exitCode = 1;
|
|
}
|