/** * @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; }