angular-cli/scripts/windows-testing/convert-symlinks.mjs
Paul Gschwendtner 5fd1cb56ab build: update dev-infra and rework windows native testing
As part of go/ng:windows-dev-future, we are changing how our
infrastructure supports Windows build & testing. Clearly:

- we will still support contributors on Windows, and we believe we will
  be improving and streamlining the experience here
- we will continue testing the Angular CLI for our Windows users. We are
  aware of the many Windows users using the `ng` CLI.

What is changing? We are no longer actively working towards a Bazel infrastructure
that supports native Windows building and testing. There are currently
two ways to contribute to Angular on Windows. That is via WSL, or via
e.g. native Windows cmd.exe, with Git Bash on top. We acknowledge that
the latter worked sometimes, but we also realize it very often breaks as
nobody on our team uses, verifies it, and it introduces extra complexity
because Bazel on Windows is quite disconnected from Linux/Mac (e.g. no
sandboxing). Going forward, to improve our team's effectiveness, and
improve our stability guarantees for Windows (and Windows contributors),
we are actively discouraging the use of Git Bash for contributing to
Angular; but instead ask for WSL to be used. I can speak as one of the
few long-term team members that have worked on Windows (without WSL) most
of my time, that WSL is great and the contributing experience is much
smoother and also easier to "guide". It's a positive change because we
won't be suggesting "two ways to contribute on Windows", where in
reality one is very brittle and can break at any time!

---

For testing of the Angular CLI: We will continue to maintain the
capability to cross-compile via Bazel with Windows as the target
platform. This allows us to build the e2e tests for Windows, and run
them natively outside WSL to ensure native Windows `ng` CLI testing!
This is what this change mostly does.

Notably, two things are missing here and will be followed up:

- caching of the e2e tests on Windows is not properly functioning yet.
- caching of the WSL node modules + nvm is not working properly yet.

Other than that, we are seeing very similar timing and results of the
Windows tests, so this change unblocks our `rules_js` migration.
2025-03-03 21:44:50 +01:00

159 lines
5.3 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) {
return new Promise((resolve, reject) => {
childProcess.exec(cmd, { cwd: rootDir }, (error) => {
if (error !== null) {
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;
}