| /** |
| * @fileoverview Creates a node_modules directory in the current working directory |
| * and symlinks in the node modules needed to run a program. |
| * This replaces the need for custom module resolution logic inside the process. |
| */ |
| import * as fs from 'fs'; |
| import * as path from 'path'; |
| |
| // We cannot rely from the linker on the `@bazel/runfiles` package, hence we import from |
| // the runfile helper through a checked-in file from `internal/runfiles`. In order to still |
| // have typings we use a type-only import to the `@bazel/runfiles` package that is the source |
| // of truth for the checked-in file. |
| const {runfiles: _defaultRunfiles, _BAZEL_OUT_REGEX}: |
| typeof import('@bazel/runfiles') = require('../runfiles/index.js') |
| import {Runfiles} from '@bazel/runfiles'; |
| |
| // Run Bazel with --define=VERBOSE_LOGS=1 to enable this logging |
| const VERBOSE_LOGS = !!process.env['VERBOSE_LOGS']; |
| |
| function log_verbose(...m: string[]) { |
| if (VERBOSE_LOGS) console.error('[link_node_modules.js]', ...m); |
| } |
| |
| function log_error(error: Error) { |
| console.error('[link_node_modules.js] An error has been reported:', error, error.stack); |
| } |
| |
| /** |
| * Create a new directory and any necessary subdirectories |
| * if they do not exist. |
| */ |
| async function mkdirp(p: string) { |
| if (p && !await exists(p)) { |
| await mkdirp(path.dirname(p)); |
| log_verbose(`creating directory ${p} in ${process.cwd()}`); |
| try { |
| await fs.promises.mkdir(p); |
| } catch (e) { |
| if (e.code !== 'EEXIST') { |
| // can happen if path being created exists via a symlink |
| throw e; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Gets the `lstat` results for a given path. Returns `null` if the path |
| * does not exist on disk. |
| */ |
| async function gracefulLstat(path: string): Promise<fs.Stats|null> { |
| try { |
| return await fs.promises.lstat(path); |
| } catch (e) { |
| if (e.code === 'ENOENT') { |
| return null; |
| } |
| throw e; |
| } |
| } |
| |
| /** |
| * Resolves a symlink to its linked path for a given path. Returns `null` if the path |
| * does not exist on disk. |
| */ |
| function gracefulReadlink(path: string): string|null { |
| try { |
| return fs.readlinkSync(path); |
| } catch (e) { |
| if (e.code === 'ENOENT') { |
| return null; |
| } |
| throw e; |
| } |
| } |
| |
| /** |
| * Lists the names of files and directories that exist in the given path. Returns an empty |
| * array if the path does not exist on disk. |
| */ |
| async function gracefulReaddir(path: string): Promise<string[]> { |
| try { |
| return await fs.promises.readdir(path); |
| } catch (e) { |
| if (e.code === 'ENOENT') { |
| return []; |
| } |
| throw e; |
| } |
| } |
| |
| /** |
| * Deletes the given module name from the current working directory (i.e. symlink root). |
| * If the module name resolves to a directory, the directory is deleted. Otherwise the |
| * existing file or junction is unlinked. |
| */ |
| async function unlink(moduleName: string) { |
| const stat = await gracefulLstat(moduleName); |
| if (stat === null) { |
| return; |
| } |
| log_verbose(`unlink( ${moduleName} )`); |
| if (stat.isDirectory()) { |
| await deleteDirectory(moduleName); |
| } else { |
| log_verbose("Deleting file: ", moduleName); |
| await fs.promises.unlink(moduleName); |
| } |
| } |
| |
| /** Asynchronously deletes a given directory (with contents). */ |
| async function deleteDirectory(p: string) { |
| log_verbose("Deleting children of", p); |
| for (let entry of await gracefulReaddir(p)) { |
| const childPath = path.join(p, entry); |
| const stat = await gracefulLstat(childPath); |
| if (stat === null) { |
| log_verbose(`File does not exist, but is listed as directory entry: ${childPath}`); |
| continue; |
| } |
| if (stat.isDirectory()) { |
| await deleteDirectory(childPath); |
| } else { |
| log_verbose("Deleting file", childPath); |
| await fs.promises.unlink(childPath); |
| } |
| } |
| log_verbose("Cleaning up dir", p); |
| await fs.promises.rmdir(p); |
| } |
| |
| async function symlink(target: string, p: string): Promise<boolean> { |
| if (!path.isAbsolute(target)) { |
| target = path.resolve(process.cwd(), target); |
| } |
| log_verbose(`creating symlink ${p} -> ${target}`); |
| |
| // Use junction on Windows since symlinks require elevated permissions. |
| // We only link to directories so junctions work for us. |
| try { |
| await fs.promises.symlink(target, p, 'junction'); |
| return true; |
| } catch (e) { |
| if (e.code !== 'EEXIST') { |
| throw e; |
| } |
| // We assume here that the path is already linked to the correct target. |
| // Could add some logic that asserts it here, but we want to avoid an extra |
| // filesystem access so we should only do it under some kind of strict mode. |
| |
| if (VERBOSE_LOGS) { |
| // Be verbose about creating a bad symlink |
| // Maybe this should fail in production as well, but again we want to avoid |
| // any unneeded file I/O |
| if (!await exists(p)) { |
| log_verbose( |
| 'ERROR\n***\nLooks like we created a bad symlink:' + |
| `\n pwd ${process.cwd()}\n target ${target}\n path ${p}\n***`); |
| } |
| } |
| return false; |
| } |
| } |
| |
| /** Determines an absolute path to the given workspace if it contains node modules. */ |
| async function resolveWorkspaceNodeModules( |
| workspace: string, startCwd: string, isExecroot: boolean, execroot: string|undefined, |
| runfiles: Runfiles) { |
| const targetManifestPath = `${workspace}/node_modules`; |
| |
| if (isExecroot) { |
| // Under execroot, the npm workspace will be under an external folder from the startCwd |
| // `execroot/my_wksp`. For example, `execroot/my_wksp/external/npm/node_modules`. If there is no |
| // npm workspace, which will be the case if there are no third-party modules dependencies for |
| // this target, npmWorkspace the root to `execroot/my_wksp/node_modules`. |
| return `${execroot}/external/${targetManifestPath}`; |
| } |
| |
| if (!execroot) { |
| // This can happen if we are inside a nodejs_image or a nodejs_binary is run manually. |
| // Resolve as if we are in runfiles in a sandbox. |
| return path.resolve(`${startCwd}/../${targetManifestPath}`) |
| } |
| |
| // Under runfiles, the linker should symlink node_modules at `execroot/my_wksp` |
| // so that when there are no runfiles (default on Windows) and scripts run out of |
| // `execroot/my_wksp` they can resolve node_modules with standard node_module resolution |
| |
| // If we got a runfilesManifest map, look through it for a resolution |
| // This will happen if we are running a binary that had some npm packages |
| // "statically linked" into its runfiles |
| const fromManifest = runfiles.resolve(targetManifestPath); |
| if (fromManifest) { |
| return fromManifest; |
| } else { |
| const maybe = path.resolve(`${execroot}/external/${targetManifestPath}`); |
| if (await exists(maybe)) { |
| // Under runfiles, when not in the sandbox we must symlink node_modules down at the execroot |
| // `execroot/my_wksp/external/npm/node_modules` since `runfiles/npm/node_modules` will be a |
| // directory and not a symlink back to the root node_modules where we expect |
| // to resolve from. This case is tested in internal/linker/test/local. |
| return maybe; |
| } |
| // However, when in the sandbox, `execroot/my_wksp/external/npm/node_modules` does not exist, |
| // so we must symlink into `runfiles/npm/node_modules`. This directory exists whether legacy |
| // external runfiles are on or off. |
| return path.resolve(`${startCwd}/../${targetManifestPath}`) |
| } |
| } |
| |
| // TypeScript lib.es5.d.ts has a mistake: JSON.parse does accept Buffer. |
| declare global { |
| interface JSON { |
| parse(b: {toString: () => string}): any; |
| } |
| } |
| |
| // There is no fs.promises.exists function because |
| // node core is of the opinion that exists is always too racey to rely on. |
| async function exists(p: string) { |
| return (await gracefulLstat(p) !== null); |
| } |
| |
| function existsSync(p: string|undefined) { |
| if (!p) { |
| return false; |
| } |
| try { |
| fs.lstatSync(p); |
| return true; |
| } catch (e) { |
| if (e.code === 'ENOENT') { |
| return false; |
| } |
| throw e; |
| } |
| } |
| |
| /** |
| * Given a set of module aliases returns an array of recursive `LinkerTreeElement`. |
| * |
| * The tree nodes represent the FS links required to represent the module aliases. |
| * Each node of the tree hierarchy depends on its parent node having been setup first. |
| * Each sibling node can be processed concurrently. |
| * |
| * The number of symlinks is minimized in situations such as: |
| * |
| * Shared parent path to lowest common denominator: |
| * `@foo/b/c => /path/to/a/b/c` |
| * |
| * can be represented as |
| * |
| * `@foo => /path/to/a` |
| * |
| * Shared parent path across multiple module names: |
| * `@foo/p/a => /path/to/x/a` |
| * `@foo/p/c => /path/to/x/a` |
| * |
| * can be represented as a single parent |
| * |
| * `@foo/p => /path/to/x` |
| */ |
| export function reduceModules(modules: LinkerAliases): LinkerTreeElement[] { |
| return buildModuleHierarchy(Object.keys(modules).sort(), modules, '/').children || []; |
| } |
| |
| function buildModuleHierarchy( |
| moduleNames: string[], modules: LinkerAliases, elementPath: string): LinkerTreeElement { |
| let element: LinkerTreeElement = { |
| name: elementPath.slice(0, -1), |
| link: modules[elementPath.slice(0, -1)], |
| children: [], |
| }; |
| |
| for (let i = 0; i < moduleNames.length;) { |
| const moduleName = moduleNames[i]; |
| const next = moduleName.indexOf('/', elementPath.length + 1); |
| const moduleGroup = (next === -1) ? (moduleName + '/') : moduleName.slice(0, next + 1); |
| |
| // An exact match (direct child of element) then it is the element parent, skip it |
| if (next === -1) { |
| i++; |
| } |
| |
| const siblings: string[] = []; |
| while (i < moduleNames.length && moduleNames[i].startsWith(moduleGroup)) { |
| siblings.push(moduleNames[i++]); |
| } |
| |
| let childElement = buildModuleHierarchy(siblings, modules, moduleGroup); |
| |
| for (let cur = childElement; (cur = liftElement(childElement)) !== childElement;) { |
| childElement = cur; |
| } |
| |
| element.children!.push(childElement); |
| } |
| |
| // Cleanup empty children+link |
| if (!element.link) { |
| delete element.link; |
| } |
| if (!element.children || element.children.length === 0) { |
| delete element.children; |
| } |
| |
| return element; |
| } |
| |
| function liftElement(element: LinkerTreeElement): LinkerTreeElement { |
| let {name, link, children} = element; |
| |
| if (!children || !children.length) { |
| return element; |
| } |
| |
| // This element has a link and all the child elements have aligning links |
| // => this link alone represents that structure |
| if (link && allElementsAlignUnder(name, link, children)) { |
| return {name, link}; |
| } |
| |
| return element; |
| } |
| |
| function allElementsAlignUnder( |
| parentName: string, parentLink: Link, elements: LinkerTreeElement[]) { |
| for (const {name, link, children} of elements) { |
| if (!link || children) { |
| return false; |
| } |
| |
| if (!isDirectChildPath(parentName, name)) { |
| return false; |
| } |
| |
| if (!isDirectChildLink(parentLink, link)) { |
| return false; |
| } |
| |
| if (!isNameLinkPathTopAligned(name, link)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| function isDirectChildPath(parent: string, child: string) { |
| return parent === path.dirname(child); |
| } |
| |
| function isDirectChildLink(parentLink: Link, childLink: Link) { |
| return parentLink === path.dirname(childLink); |
| } |
| |
| function isNameLinkPathTopAligned(namePath: string, linkPath: Link) { |
| return path.basename(namePath) === path.basename(linkPath); |
| } |
| |
| async function visitDirectoryPreserveLinks( |
| dirPath: string, visit: (filePath: string, stat: fs.Stats) => Promise<void>) { |
| for (const entry of await fs.promises.readdir(dirPath)) { |
| const childPath = path.join(dirPath, entry); |
| const stat = await gracefulLstat(childPath); |
| if (stat === null) { |
| continue; |
| } |
| if (stat.isDirectory()) { |
| await visitDirectoryPreserveLinks(childPath, visit); |
| } else { |
| await visit(childPath, stat); |
| } |
| } |
| } |
| |
| export type Link = string; |
| export type LinkerTreeElement = { |
| name: string, |
| link?: Link, |
| children?: LinkerTreeElement[], |
| }; |
| export type LinkerAliases = { |
| [name: string]: Link |
| }; |
| |
| function findExecroot(startCwd: string): string|undefined { |
| // We can derive if the process is being run in the execroot if there is a bazel-out folder |
| if (existsSync(`${startCwd}/bazel-out`)) { |
| return startCwd; |
| } |
| |
| // Look for bazel-out which is used to determine the the path to `execroot/my_wksp`. This works in |
| // all cases including on rbe where the execroot is a path such as `/b/f/w`. For example, when in |
| // runfiles on rbe, bazel runs the process in a directory such as |
| // `/b/f/w/bazel-out/k8-fastbuild/bin/path/to/pkg/some_test.sh.runfiles/my_wksp`. From here we can |
| // determine the execroot `b/f/w` by finding the first instance of bazel-out. |
| // NB: If we are inside nodejs_image or a nodejs_binary run manually there may be no execroot |
| // found. |
| const bazelOutMatch = startCwd.match(_BAZEL_OUT_REGEX); |
| return bazelOutMatch ? startCwd.slice(0, bazelOutMatch.index) : undefined; |
| } |
| |
| export async function main(args: string[], runfiles: Runfiles) { |
| if (!args || args.length < 1) throw new Error('requires one argument: modulesManifest path'); |
| |
| const [modulesManifest] = args; |
| log_verbose('manifest file:', modulesManifest); |
| |
| let {workspace, bin, roots, module_sets} = JSON.parse(fs.readFileSync(modulesManifest)); |
| log_verbose('manifest contents:', JSON.stringify({workspace, bin, roots, module_sets}, null, 2)); |
| roots = roots || {}; |
| module_sets = module_sets || {}; |
| |
| // Bazel starts actions with pwd=execroot/my_wksp when under execroot or pwd=runfiles/my_wksp |
| // when under runfiles. |
| // Normalize the slashes in startCwd for easier matching and manipulation. |
| const startCwd = process.cwd().replace(/\\/g, '/'); |
| log_verbose('startCwd:', startCwd); |
| |
| const execroot = findExecroot(startCwd); |
| log_verbose('execroot:', execroot ? execroot : 'not found'); |
| |
| const isExecroot = startCwd == execroot; |
| log_verbose('isExecroot:', isExecroot.toString()); |
| |
| if (!isExecroot && execroot) { |
| // If we're not in the execroot and we've found one then change to the execroot |
| // directory to create the node_modules symlinks |
| process.chdir(execroot); |
| log_verbose('changed directory to execroot', execroot); |
| } |
| |
| async function symlinkWithUnlink( |
| target: string, p: string, stats: fs.Stats|null = null): Promise<boolean> { |
| if (!path.isAbsolute(target)) { |
| target = path.resolve(process.cwd(), target); |
| } |
| if (stats === null) { |
| stats = await gracefulLstat(p); |
| } |
| // Check if this an an old out-of-date symlink |
| // If we are running without a runfiles manifest (i.e. in sandbox or with symlinked runfiles), |
| // then this is guaranteed to be not an artifact from a previous linker run. If not we need to |
| // check. |
| if (runfiles.manifest && execroot && stats !== null && stats.isSymbolicLink()) { |
| // Although `stats` suggests that the file exists as a symlink, it may have been deleted by |
| // another process. Only proceed unlinking if the file actually still exists. |
| const symlinkPathRaw = gracefulReadlink(p); |
| if (symlinkPathRaw !== null) { |
| const symlinkPath = symlinkPathRaw.replace(/\\/g, '/'); |
| if (path.relative(symlinkPath, target) != '' && |
| !path.relative(execroot, symlinkPath).startsWith('..')) { |
| // Left-over out-of-date symlink from previous run. This can happen if switching between |
| // root configuration options such as `--noenable_runfiles` and/or |
| // `--spawn_strategy=standalone`. It can also happen if two different targets link the |
| // same module name to different targets in a non-sandboxed environment. The latter will |
| // lead to undeterministic behavior. |
| // TODO: can we detect the latter case and throw an apprioriate error? |
| log_verbose(`Out-of-date symlink for ${p} to ${symlinkPath} detected. Target should be ${ |
| target}. Unlinking.`); |
| await unlink(p); |
| } else { |
| log_verbose(`The symlink at ${p} no longer exists, so no need to unlink it.`); |
| } |
| } |
| } |
| return symlink(target, p); |
| } |
| |
| // Symlink all node_modules roots defined. These are 3rd party deps in external npm workspaces |
| // lined to node_modules folders at the root or in sub-directories |
| for (const packagePath of Object.keys(roots)) { |
| const workspace = roots[packagePath]; |
| if (workspace) { |
| const workspaceNodeModules = await resolveWorkspaceNodeModules( |
| workspace, startCwd, isExecroot, execroot, runfiles); |
| log_verbose(`resolved ${workspace} workspace node modules path to ${workspaceNodeModules}`); |
| |
| if (packagePath) { |
| // sub-directory node_modules |
| if (await exists(workspaceNodeModules)) { |
| // in some cases packagePath may not exist in sandbox if there are no source deps |
| // and only generated file deps. we create it so that we that we can link to |
| // packagePath/node_modules since packagePathBin/node_modules is a symlink to |
| // packagePath/node_modules and is unguarded in launcher.sh as we allow symlinks to fall |
| // through to from output tree to source tree to prevent resolving the same npm package to |
| // multiple locations on disk |
| await mkdirp(packagePath); |
| await symlinkWithUnlink(workspaceNodeModules, `${packagePath}/node_modules`); |
| if (!isExecroot) { |
| // Under runfiles, we symlink into the package in runfiles as well. |
| // When inside the sandbox, the execroot location will not exist to symlink to. |
| const runfilesPackagePath = `${startCwd}/${packagePath}`; |
| if (await exists(runfilesPackagePath)) { |
| await symlinkWithUnlink( |
| `${packagePath}/node_modules`, `${runfilesPackagePath}/node_modules`); |
| } |
| } |
| const packagePathBin = `${bin}/${packagePath}`; |
| if (await exists(packagePathBin)) { |
| // if bin path exists, symlink bin/package/node_modules -> package/node_modules |
| // NB: this location is unguarded in launcher.sh to allow symlinks to fall-throught |
| // package/node_modules to prevent resolving the same npm package to multiple locations |
| // on disk |
| await symlinkWithUnlink( |
| `${packagePath}/node_modules`, `${packagePathBin}/node_modules`); |
| } |
| } else { |
| // Special case if there no target to symlink to for the root workspace, create a |
| // root node_modules folder for 1st party deps |
| log_verbose(`no npm workspace node_modules folder under ${ |
| packagePath} to link to; creating node_modules directories in ${process.cwd()} for ${ |
| packagePath} 1p deps`); |
| await mkdirp(`${packagePath}/node_modules`); |
| if (!isExecroot) { |
| // Under runfiles, we symlink into the package in runfiles as well. |
| // When inside the sandbox, the execroot location will not exist to symlink to. |
| const runfilesPackagePath = `${startCwd}/${packagePath}`; |
| await mkdirp(`${runfilesPackagePath}/node_modules`); |
| await symlinkWithUnlink( |
| `${packagePath}/node_modules`, `${runfilesPackagePath}/node_modules`); |
| } |
| const packagePathBin = `${bin}/${packagePath}`; |
| await mkdirp(`${packagePathBin}/node_modules`); |
| await symlinkWithUnlink(`${packagePath}/node_modules`, `${packagePathBin}/node_modules`); |
| } |
| } else { |
| // root node_modules |
| if (await exists(workspaceNodeModules)) { |
| await symlinkWithUnlink(workspaceNodeModules, `node_modules`); |
| } else { |
| // Special case if there no target to symlink to for the root workspace, create a |
| // root node_modules folder for 1st party deps |
| log_verbose( |
| `no root npm workspace node_modules folder to link to; creating node_modules directory in ${ |
| process.cwd()}`); |
| await mkdirp('node_modules'); |
| } |
| } |
| } else { |
| if (packagePath) { |
| // Special case if there for first party node_modules at root only |
| log_verbose(`no 3p deps at ${packagePath}; creating node_modules directories in ${ |
| process.cwd()} for ${packagePath} 1p deps`); |
| await mkdirp(`${packagePath}/node_modules`); |
| if (!isExecroot) { |
| // Under runfiles, we symlink into the package in runfiles as well. |
| // When inside the sandbox, the execroot location will not exist to symlink to. |
| const runfilesPackagePath = `${startCwd}/${packagePath}`; |
| await mkdirp(`${runfilesPackagePath}/node_modules`); |
| await symlinkWithUnlink( |
| `${packagePath}/node_modules`, `${runfilesPackagePath}/node_modules`); |
| } |
| const packagePathBin = `${bin}/${packagePath}`; |
| await mkdirp(`${packagePathBin}/node_modules`); |
| await symlinkWithUnlink(`${packagePath}/node_modules`, `${packagePathBin}/node_modules`); |
| } else { |
| // Special case if there for first party node_modules at root only |
| log_verbose(`no 3p deps at root; creating node_modules directory in ${ |
| process.cwd()} for root 1p deps`); |
| await mkdirp('node_modules'); |
| } |
| } |
| } |
| |
| /** |
| * Whether the given module resolves to a directory that has been created by a previous linker |
| * run purely to make space for deep module links. e.g. consider a mapping for `my-pkg/a11y`. |
| * The linker will create folders like `node_modules/my-pkg/` so that the `a11y` symbolic |
| * junction can be created. The `my-pkg` folder is then considered a leftover from a previous |
| * linker run as it only contains symbolic links and no actual source files. |
| */ |
| async function isLeftoverDirectoryFromLinker(stats: fs.Stats, modulePath: string) { |
| // If we are running without a runfiles manifest (i.e. in sandbox or with symlinked runfiles), |
| // then this is guaranteed to be not an artifact from a previous linker run. |
| if (runfiles.manifest === undefined) { |
| return false; |
| } |
| if (!stats.isDirectory()) { |
| return false; |
| } |
| let isLeftoverFromPreviousLink = true; |
| // If the directory contains actual files, this cannot be a leftover from a previous |
| // linker run. The linker only creates directories in the node modules that hold |
| // symbolic links for configured module mappings. |
| await visitDirectoryPreserveLinks(modulePath, async (childPath, childStats) => { |
| if (!childStats.isSymbolicLink()) { |
| isLeftoverFromPreviousLink = false; |
| } |
| }); |
| return isLeftoverFromPreviousLink; |
| } |
| |
| /** |
| * Creates a symlink for the given module. Existing child symlinks which are part of |
| * the module are preserved in order to not cause race conditions in non-sandbox |
| * environments where multiple actions rely on the same node modules root. |
| * |
| * To avoid unexpected resource removal, a new temporary link for the target is created. |
| * Then all symlinks from the existing module are cloned. Once done, the existing module |
| * is unlinked while the temporary link takes place for the given module. This ensures |
| * that the module link is never removed at any time (causing race condition failures). |
| */ |
| async function createSymlinkAndPreserveContents(stats: fs.Stats, modulePath: string, |
| target: string) { |
| const tmpPath = `${modulePath}__linker_tmp`; |
| log_verbose(`createSymlinkAndPreserveContents( ${modulePath} )`); |
| |
| await symlink(target, tmpPath); |
| await visitDirectoryPreserveLinks(modulePath, async (childPath, stat) => { |
| if (stat.isSymbolicLink()) { |
| const targetPath = path.join(tmpPath, path.relative(modulePath, childPath)); |
| log_verbose(`Cloning symlink into temporary created link ( ${childPath} )`); |
| await mkdirp(path.dirname(targetPath)); |
| await symlink(targetPath, await fs.promises.realpath(childPath)); |
| } |
| }); |
| |
| log_verbose(`Removing existing module so that new link can take place ( ${modulePath} )`); |
| await unlink(modulePath); |
| await fs.promises.rename(tmpPath, modulePath); |
| } |
| |
| async function linkModules(package_path: string, m: LinkerTreeElement) { |
| const symlinkIn = package_path ? `${package_path}/node_modules` : 'node_modules'; |
| |
| // ensure the parent directory exist |
| if (path.dirname(m.name)) { |
| await mkdirp(`${symlinkIn}/${path.dirname(m.name)}`); |
| } |
| |
| if (m.link) { |
| const modulePath = m.link; |
| let target: string|undefined; |
| if (isExecroot) { |
| // If we're running out of the execroot, try the execroot path first. |
| // If the dependency came in exclusively from a transitive binary target |
| // then the module won't be at this path but in the runfiles of the binary. |
| // In that case we'll fallback to resolving via runfiles below. |
| target = `${startCwd}/${modulePath}`; |
| } |
| if (!isExecroot || !existsSync(target)) { |
| // Transform execroot path to the runfiles manifest path so that |
| // it can be resolved with runfiles.resolve() |
| let runfilesPath = modulePath; |
| if (runfilesPath.startsWith(`${bin}/`)) { |
| runfilesPath = runfilesPath.slice(bin.length + 1); |
| } else if (runfilesPath === bin) { |
| runfilesPath = ''; |
| } |
| const externalPrefix = 'external/'; |
| if (runfilesPath.startsWith(externalPrefix)) { |
| runfilesPath = runfilesPath.slice(externalPrefix.length); |
| } else { |
| runfilesPath = `${workspace}/${runfilesPath}`; |
| } |
| try { |
| target = runfiles.resolve(runfilesPath); |
| // if we're resolving from a manifest then make sure we don't resolve |
| // into the source tree when we are expecting the output tree |
| if (runfiles.manifest && modulePath.startsWith(`${bin}/`)) { |
| // Check for BAZEL_OUT_REGEX and not /${bin}/ since resolution |
| // may be in the `/bazel-out/host` if cfg = "host" |
| if (!target.match(_BAZEL_OUT_REGEX)) { |
| const e = new Error(`could not resolve module ${runfilesPath} in output tree`); |
| (e as any).code = 'MODULE_NOT_FOUND'; |
| throw e; |
| } |
| } |
| } catch (err) { |
| target = undefined; |
| log_verbose(`runfiles resolve failed for module '${m.name}': ${err.message}`); |
| } |
| } |
| // Ensure target path absolute for consistency |
| if (target && !path.isAbsolute(target)) { |
| target = path.resolve(process.cwd(), target); |
| } |
| |
| const symlinkFile = `${symlinkIn}/${m.name}`; |
| |
| // In environments where runfiles are not symlinked (e.g. Windows), existing linked |
| // modules are preserved. This could cause issues when a link is created at higher level |
| // as a conflicting directory is already on disk. e.g. consider in a previous run, we |
| // linked the modules `my-pkg/overlay`. Later on, in another run, we have a module mapping |
| // for `my-pkg` itself. The linker cannot create `my-pkg` because the directory `my-pkg` |
| // already exists. To ensure that the desired link is generated, we create the new desired |
| // link and move all previous nested links from the old module into the new link. Read more |
| // about this in the description of `createSymlinkAndPreserveContents`. |
| const stats = await gracefulLstat(symlinkFile); |
| const isLeftOver = |
| (stats !== null && await isLeftoverDirectoryFromLinker(stats, symlinkFile)); |
| |
| // Check if the target exists before creating the symlink. |
| // This is an extra filesystem access on top of the symlink but |
| // it is necessary for the time being. |
| if (target && await exists(target)) { |
| if (stats !== null && isLeftOver) { |
| await createSymlinkAndPreserveContents(stats, symlinkFile, target); |
| } else { |
| await symlinkWithUnlink(target, symlinkFile, stats); |
| } |
| } else { |
| if (!target) { |
| log_verbose(`no symlink target found for module ${m.name}`); |
| } else { |
| // This can happen if a module mapping is propogated from a dependency |
| // but the target that generated the mapping in not in the deps. We don't |
| // want to create symlinks to non-existant targets as this will |
| // break any nested symlinks that may be created under the module name |
| // after this. |
| log_verbose(`potential target ${target} does not exists for module ${m.name}`); |
| } |
| if (isLeftOver) { |
| // Remove left over directory if it exists |
| await unlink(symlinkFile); |
| } |
| } |
| } |
| |
| // Process each child branch concurrently |
| if (m.children) { |
| await Promise.all(m.children.map(m => linkModules(package_path, m))); |
| } |
| } |
| |
| const links = []; |
| for (const package_path of Object.keys(module_sets)) { |
| const modules = module_sets[package_path] |
| log_verbose(`modules for package path '${package_path}':\n${JSON.stringify(modules, null, 2)}`); |
| const moduleHierarchy = reduceModules(modules); |
| log_verbose(`mapping hierarchy for package path '${package_path}':\n${ |
| JSON.stringify(moduleHierarchy)}`); |
| // Process each root branch concurrently |
| links.push(...moduleHierarchy.map(m => linkModules(package_path, m))) |
| } |
| |
| let code = 0; |
| await Promise.all(links).catch(e => { |
| log_error(e); |
| code = 1; |
| }); |
| |
| return code; |
| } |
| |
| if (require.main === module) { |
| if (Number(process.versions.node.split('.')[0]) < 10) { |
| console.error(`ERROR: rules_nodejs linker requires Node v10 or greater, but is running on ${ |
| process.versions.node}`); |
| console.error('Note that earlier Node versions are no longer in long-term-support, see'); |
| console.error('https://nodejs.org/en/about/releases/'); |
| process.exit(1); |
| } |
| (async () => { |
| try { |
| process.exitCode = await main(process.argv.slice(2), _defaultRunfiles); |
| } catch (e) { |
| log_error(e); |
| process.exitCode = 1; |
| } |
| })(); |
| } |