From 14c7af39b7a9c246fe13ad5e9e5fd241571154c5 Mon Sep 17 00:00:00 2001 From: Fabian Wiles Date: Tue, 13 Aug 2019 21:58:15 +1200 Subject: [PATCH 1/3] feat(builtin): split node_loader into node_patcher --- internal/node/node.bzl | 29 +- internal/node/node_launcher.sh | 4 + internal/node/node_loader.js | 424 +--------------- internal/node/node_patcher.js | 459 ++++++++++++++++++ internal/node/test/child_process/BUILD.bazel | 11 + .../node/test/child_process/lib/BUILD.bazel | 9 + internal/node/test/child_process/lib/index.js | 1 + .../node/test/child_process/worker-main.js | 4 + .../node/test/child_process/worker-thread.js | 2 + internal/node/test/worker_threads/BUILD.bazel | 11 + .../node/test/worker_threads/lib/BUILD.bazel | 9 + .../node/test/worker_threads/lib/index.js | 1 + .../node/test/worker_threads/worker-main.js | 4 + .../node/test/worker_threads/worker-thread.js | 2 + 14 files changed, 543 insertions(+), 427 deletions(-) create mode 100644 internal/node/node_patcher.js create mode 100644 internal/node/test/child_process/BUILD.bazel create mode 100644 internal/node/test/child_process/lib/BUILD.bazel create mode 100644 internal/node/test/child_process/lib/index.js create mode 100644 internal/node/test/child_process/worker-main.js create mode 100644 internal/node/test/child_process/worker-thread.js create mode 100644 internal/node/test/worker_threads/BUILD.bazel create mode 100644 internal/node/test/worker_threads/lib/BUILD.bazel create mode 100644 internal/node/test/worker_threads/lib/index.js create mode 100644 internal/node/test/worker_threads/worker-main.js create mode 100644 internal/node/test/worker_threads/worker-thread.js diff --git a/internal/node/node.bzl b/internal/node/node.bzl index 82c624e352..211898521c 100644 --- a/internal/node/node.bzl +++ b/internal/node/node.bzl @@ -103,19 +103,28 @@ def _write_loader_script(ctx): template = ctx.file._loader_template, output = ctx.outputs.loader, substitutions = { - "TEMPLATED_bin_dir": ctx.bin_dir.path, "TEMPLATED_bootstrap": "\n " + ",\n ".join( ["\"" + d + "\"" for d in ctx.attr.bootstrap], ), "TEMPLATED_entry_point": entry_point_path, - "TEMPLATED_gen_dir": ctx.genfiles_dir.path, "TEMPLATED_install_source_map_support": str(ctx.attr.install_source_map_support).lower(), + "TEMPLATED_node_modules_root": node_modules_root, + "TEMPLATED_target": str(ctx.label), + }, + is_executable = True, + ) + + ctx.actions.expand_template( + template = ctx.file._patcher_template, + output = ctx.outputs.patcher, + substitutions = { + "TEMPLATED_bin_dir": ctx.bin_dir.path, + "TEMPLATED_gen_dir": ctx.genfiles_dir.path, "TEMPLATED_module_roots": "\n " + ",\n ".join(module_mappings), "TEMPLATED_node_modules_root": node_modules_root, "TEMPLATED_target": str(ctx.label), "TEMPLATED_user_workspace_name": ctx.workspace_name, }, - is_executable = True, ) def _short_path_to_manifest_path(ctx, short_path): @@ -149,11 +158,16 @@ def _nodejs_binary_impl(ctx): # Avoid writing non-normalized paths (workspace/../other_workspace/path) if ctx.outputs.loader.short_path.startswith("../"): script_path = ctx.outputs.loader.short_path[len("../"):] + patcher_path = ctx.outputs.patcher.short_path[len("../"):] else: script_path = "/".join([ ctx.workspace_name, ctx.outputs.loader.short_path, ]) + patcher_path = "/".join([ + ctx.workspace_name, + ctx.outputs.patcher.short_path, + ]) env_vars = "export BAZEL_TARGET=%s\n" % ctx.label for k in ctx.attr.configuration_env_vars: if k in ctx.var.keys(): @@ -184,6 +198,7 @@ def _nodejs_binary_impl(ctx): "TEMPLATED_env_vars": env_vars, "TEMPLATED_expected_exit_code": str(expected_exit_code), "TEMPLATED_node": node_tool, + "TEMPLATED_patcher_path": patcher_path, "TEMPLATED_repository_args": _short_path_to_manifest_path(ctx, ctx.file._repository_args.short_path), "TEMPLATED_script_path": script_path, } @@ -194,7 +209,7 @@ def _nodejs_binary_impl(ctx): is_executable = True, ) - runfiles = depset(node_tool_files + [ctx.outputs.loader, ctx.file._repository_args], transitive = [sources, node_modules]) + runfiles = depset(node_tool_files + [ctx.outputs.loader, ctx.outputs.patcher, ctx.file._repository_args], transitive = [sources, node_modules]) # entry point is only needed in runfiles if it is a .js file if ctx.file.entry_point.extension == "js": @@ -206,6 +221,7 @@ def _nodejs_binary_impl(ctx): transitive_files = runfiles, files = node_tool_files + [ ctx.outputs.loader, + ctx.outputs.patcher, ] + ctx.files._source_map_support_files + # We need this call to the list of Files. @@ -384,6 +400,10 @@ _NODEJS_EXECUTABLE_ATTRS = { default = Label("//internal/node:node_loader.js"), allow_single_file = True, ), + "_patcher_template": attr.label( + default = Label("//internal/node:node_patcher.js"), + allow_single_file = True, + ), "_repository_args": attr.label( default = Label("@nodejs//:bin/node_repo_args.sh"), allow_single_file = True, @@ -400,6 +420,7 @@ _NODEJS_EXECUTABLE_ATTRS = { _NODEJS_EXECUTABLE_OUTPUTS = { "loader": "%{name}_loader.js", + "patcher": "%{name}_patcher.js", "script": "%{name}.sh", } diff --git a/internal/node/node_launcher.sh b/internal/node/node_launcher.sh index 3a2bb376f2..3ac88f99ad 100644 --- a/internal/node/node_launcher.sh +++ b/internal/node/node_launcher.sh @@ -117,6 +117,7 @@ TEMPLATED_env_vars readonly node=$(rlocation "TEMPLATED_node") readonly repository_args=$(rlocation "TEMPLATED_repository_args") readonly script=$(rlocation "TEMPLATED_script_path") +readonly patcher=$(rlocation "TEMPLATED_patcher_path") source $repository_args @@ -130,6 +131,9 @@ for ARG in "${ALL_ARGS[@]}"; do esac done +# TODO: what happens if the user passed a require flag? +NODE_OPTIONS+=( "--require=./$patcher" ) + # The EXPECTED_EXIT_CODE lets us write bazel tests which assert that # a binary fails to run. Otherwise any failure would make such a test # fail before we could assert that we expected that failure. diff --git a/internal/node/node_loader.js b/internal/node/node_loader.js index 051d1b1eca..c923bac2e7 100644 --- a/internal/node/node_loader.js +++ b/internal/node/node_loader.js @@ -23,34 +23,19 @@ */ 'use strict'; var path = require('path'); -var fs = require('fs'); -const isWindows = /^win/i.test(process.platform); // Ensure that node is added to the path for any subprocess calls -process.env.PATH = [path.dirname(process.execPath), process.env.PATH].join(isWindows ? ';' : ':'); +process.env.PATH = [path.dirname(process.execPath), process.env.PATH].join(path.delimiter); const DEBUG = false; -/** - * The module roots as pairs of a RegExp to match the require path, and a - * module_root to substitute for the require path. - * Ordered by regex length, longest to smallest. - * @type {!Array<{module_name: RegExp, module_root: string}>} - */ -var MODULE_ROOTS = [ - TEMPLATED_module_roots -].sort((a, b) => b.module_name.toString().length - a.module_name.toString().length); - /** * Array of bootstrap modules that need to be loaded before the entry point. */ var BOOTSTRAP = [TEMPLATED_bootstrap]; -const USER_WORKSPACE_NAME = 'TEMPLATED_user_workspace_name'; const NODE_MODULES_ROOT = 'TEMPLATED_node_modules_root'; -const BIN_DIR = 'TEMPLATED_bin_dir'; const ENTRY_POINT = 'TEMPLATED_entry_point'; -const GEN_DIR = 'TEMPLATED_gen_dir'; const INSTALL_SOURCE_MAP_SUPPORT = TEMPLATED_install_source_map_support; const TARGET = 'TEMPLATED_target'; @@ -60,420 +45,13 @@ node_loader: running ${TARGET} with cwd: ${process.cwd()} runfiles: ${process.env.RUNFILES} - BIN_DIR: ${BIN_DIR} BOOTSTRAP: ${JSON.stringify(BOOTSTRAP, undefined, 2)} ENTRY_POINT: ${ENTRY_POINT} - GEN_DIR: ${GEN_DIR} INSTALL_SOURCE_MAP_SUPPORT: ${INSTALL_SOURCE_MAP_SUPPORT} - MODULE_ROOTS: ${JSON.stringify(MODULE_ROOTS, undefined, 2)} NODE_MODULES_ROOT: ${NODE_MODULES_ROOT} TARGET: ${TARGET} - USER_WORKSPACE_NAME: ${USER_WORKSPACE_NAME} `); -function resolveToModuleRoot(path) { - if (!path) { - throw new Error('resolveToModuleRoot missing path: ' + path); - } - - // We want all possible matches. - const orderedMatches = MODULE_ROOTS.filter(m => m.module_name.test(path)); - - if (orderedMatches.length === 0) { - return null; - } else { - // Longest regex wins when multiple match, and the list is already ordered by length. - const m = orderedMatches[0]; - return path.replace(m.module_name, m.module_root); - } -} - -/** - * The runfiles manifest maps from short_path - * https://docs.bazel.build/versions/master/skylark/lib/File.html#short_path - * to the actual location on disk where the file can be read. - * - * In a sandboxed execution, it does not exist. In that case, runfiles must be - * resolved from a symlink tree under the runfiles dir. - * See https://github.com/bazelbuild/bazel/issues/3726 - */ -function loadRunfilesManifest(manifestPath) { - if (DEBUG) console.error(`node_loader: using manifest ${manifestPath}`); - - // Create the manifest and reverse manifest maps. - const runfilesManifest = Object.create(null); - const reverseRunfilesManifest = Object.create(null); - const input = fs.readFileSync(manifestPath, {encoding: 'utf-8'}); - - // Absolute path that refers to the local workspace path. We need to determine the absolute - // path to the local workspace because it allows us to support absolute path resolving - // for runfiles. - let localWorkspacePath = null; - - for (const line of input.split('\n')) { - if (!line) continue; - const [runfilesPath, realPath] = line.split(' '); - runfilesManifest[runfilesPath] = realPath; - reverseRunfilesManifest[realPath] = runfilesPath; - - // We don't need to try determining the local workspace path for the current runfile - // mapping in case we already determined the local workspace path, the current - // runfile refers to a different workspace, or the current runfile resolves to a file - // in the bazel-out directory (bin/genfiles directory). - if (localWorkspacePath || !runfilesPath.startsWith(USER_WORKSPACE_NAME) || - realPath.includes(BIN_DIR) || realPath.includes(GEN_DIR)) { - continue; - } - - // Relative path for the runfile. We can compute that path by removing the leading - // workspace name. e.g. `my_workspace/src/my-runfile.js` becomes `src/my-runfile.js`. - const relativeWorkspacePath = runfilesPath.slice(USER_WORKSPACE_NAME.length + 1); - - // TODO(gregmagolan): should not be needed when --nolegacy_external_runfiles is default - if (relativeWorkspacePath.startsWith('external/')) { - continue; - } - - localWorkspacePath = realPath.slice(0, -relativeWorkspacePath.length); - } - - // Determine bin and gen root to convert absolute paths into runfile paths. - const binRootIdx = manifestPath.indexOf(BIN_DIR); - let binRoot, genRoot; - if (binRootIdx !== -1) { - const execRoot = manifestPath.slice(0, binRootIdx); - binRoot = `${execRoot}${BIN_DIR}/`; - genRoot = `${execRoot}${GEN_DIR}/`; - } - - if (DEBUG) console.error(`node_loader: using binRoot ${binRoot}`); - if (DEBUG) console.error(`node_loader: using genRoot ${genRoot}`); - if (DEBUG) console.error(`node_loader: using localWorkspacePath ${localWorkspacePath}`); - - return { runfilesManifest, reverseRunfilesManifest, binRoot, genRoot, localWorkspacePath }; -} -const { runfilesManifest, reverseRunfilesManifest, binRoot, genRoot, localWorkspacePath } = - // On Windows, Bazel sets RUNFILES_MANIFEST_ONLY=1. - // On every platform, Bazel also sets RUNFILES_MANIFEST_FILE, but on Linux - // and macOS it's faster to use the symlinks in RUNFILES_DIR rather than resolve - // through the indirection of the manifest file. - // We also need to construct a reverse map to resolve relative files from existing - // manifest entries. - process.env.RUNFILES_MANIFEST_ONLY === '1' && - loadRunfilesManifest(process.env.RUNFILES_MANIFEST_FILE); - -function isFile(res) { - try { - return fs.statSync(res).isFile(); - } catch (e) { - return false; - } -} - -function isDirectory(res) { - try { - return fs.statSync(res).isDirectory(); - } catch (e) { - return false; - } -} - -function readDir(dir) { - return fs.statSync(dir).isDirectory() ? - Array.prototype.concat(...fs.readdirSync(dir).map(f => readDir(path.join(dir, f)))) : - dir.replace(/\\/g, '/'); -} - -function loadAsFileSync(res) { - if (isFile(res)) { - return res; - } - if (isFile(res + '.js')) { - return res; - } - return null; -} - -function loadAsDirectorySync(res) { - const pkgfile = path.join(res, 'package.json'); - if (isFile(pkgfile)) { - try { - const pkg = JSON.parse(fs.readFileSync(pkgfile, 'UTF-8')); - const main = pkg['main']; - if (main) { - if (main === '.' || main === './') { - main = 'index'; - } - - let maybe = loadAsFileSync(path.resolve(res, main)); - if (maybe) { - return maybe; - } - - maybe = loadAsDirectorySync(path.resolve(res, main)); - if (maybe) { - return maybe; - } - } - } catch (e) { - } - } - return loadAsFileSync(path.resolve(res, 'index')); -} - -function resolveManifestFile(res) { - const maybe = runfilesManifest[res] || runfilesManifest[res + '.js']; - if (maybe) { - return maybe; - } - // Look for tree artifacts that match and update - // the runfiles with files that are in the tree artifact. - // Attempt to resolve again with the updated runfiles - // if a tree artifact matched. - let segments = res.split('/'); - segments.pop(); - while (segments.length) { - const test = segments.join('/'); - const tree = runfilesManifest[test]; - if (tree && isDirectory(tree)) { - // We have a tree artifact that matches - const files = readDir(tree).map(f => path.relative(tree, f).replace(/\\/g, '/')); - files.forEach(f => { - runfilesManifest[path.posix.join(test, f)] = path.posix.join(tree, f); - }) - return runfilesManifest[res] || runfilesManifest[res + '.js']; - } - segments.pop(); - } -} - -function resolveManifestDirectory(res) { - const pkgfile = runfilesManifest[path.posix.join(res, 'package.json')]; - if (pkgfile) { - try { - const pkg = JSON.parse(fs.readFileSync(pkgfile, 'UTF-8')); - const main = pkg['main']; - if (main) { - if (main === '.' || main === './') { - main = 'index'; - } - - let maybe = resolveManifestFile(path.posix.join(res, main)); - if (maybe) { - return maybe; - } - - maybe = resolveManifestDirectory(path.posix.join(res, main)); - if (maybe) { - return maybe; - } - } - } catch (e) { - } - } - return resolveManifestFile(path.posix.join(res, 'index')); -} - -function resolveRunfiles(parent, ...pathSegments) { - // Remove any empty strings from pathSegments - // Normalize to forward slash, because even on Windows the runfiles_manifest file - // is written with forward slash. - let runfilesEntry = pathSegments.filter(segment => segment).join('/').replace(/\\/g, '/'); - - // Trim `${USER_WORKSPACE_NAME}/external/` from start of runfilesEntry - const externalWorkspacePrefix = `${USER_WORKSPACE_NAME}/external/`; - if (runfilesEntry.startsWith(externalWorkspacePrefix)) { - runfilesEntry = runfilesEntry.slice(externalWorkspacePrefix.length); - } - - const runfilesPath = path.join(process.env.RUNFILES, runfilesEntry); - - if (runfilesManifest) { - if (parent && runfilesEntry.startsWith('.')) { - // Resolve relative paths from manifest files. - const normalizedParent = parent.replace(/\\/g, '/'); - const parentRunfile = reverseRunfilesManifest[normalizedParent]; - if (parentRunfile) { - runfilesEntry = path.join(path.dirname(parentRunfile), runfilesEntry); - } - } else if (runfilesEntry.startsWith(binRoot) || runfilesEntry.startsWith(genRoot) - || runfilesEntry.startsWith(localWorkspacePath)) { - // For absolute paths, replace binRoot, genRoot or localWorkspacePath with - // USER_WORKSPACE_NAME to enable lookups. - // It's OK to do multiple replacements because all of these are absolute paths with drive - // names (e.g. C:\), and on Windows you can't have drive names in the middle of paths. - runfilesEntry = runfilesEntry - .replace(binRoot, `${USER_WORKSPACE_NAME}/`) - .replace(genRoot, `${USER_WORKSPACE_NAME}/`) - .replace(localWorkspacePath, `${USER_WORKSPACE_NAME}/`); - } - - // Normalize and replace path separators to conform to the ones in the manifest. - runfilesEntry = path.normalize(runfilesEntry).replace(/\\/g, '/'); - - if (DEBUG) console.error('node_loader: try to resolve in runfiles manifest', runfilesEntry); - - let maybe = resolveManifestFile(runfilesEntry); - if (maybe) { - if (DEBUG) console.error('node_loader: resolved manifest file', maybe); - return maybe; - } - - maybe = resolveManifestDirectory(runfilesEntry); - if (maybe) { - if (DEBUG) console.error('node_loader: resolved via manifest directory', maybe); - return maybe; - } - } else { - if (DEBUG) console.error('node_loader: try to resolve in runfiles', runfilesPath); - - let maybe = loadAsFileSync(runfilesPath); - if (maybe) { - if (DEBUG) console.error('node_loader: resolved file', maybe); - return maybe; - } - - maybe = loadAsDirectorySync(runfilesPath); - if (maybe) { - if (DEBUG) console.error('node_loader: resolved via directory', maybe); - return maybe; - } - } - - return runfilesPath; -} - -var originalResolveFilename = module.constructor._resolveFilename; -module.constructor._resolveFilename = function(request, parent, isMain, options) { - const parentFilename = (parent && parent.filename) ? parent.filename : undefined; - if (DEBUG) console.error(`\n\nnode_loader: resolve ${request} from ${parentFilename}`); - - const failedResolutions = []; - - // Attempt to resolve to module root. - // This should be the first attempted resolution because: - // - it's fairly cheap to check (regex over a small array); - // - it is be very common when there are a lot of packages built from source; - if (!isMain) { - // Don't resolve to module root if this is the main entry point - // as the main entry point will always be fully qualified with the - // workspace name and full path. - // See https://github.com/bazelbuild/rules_nodejs/issues/834 - const moduleRoot = resolveToModuleRoot(request); - if (moduleRoot) { - const moduleRootInRunfiles = resolveRunfiles(undefined, moduleRoot); - const filename = module.constructor._findPath(moduleRootInRunfiles, []); - if (filename) { - return filename; - } else { - failedResolutions.push( - `module root ${moduleRoot} - No file ${request} found in module root ${moduleRoot}`); - } - } - } - - // Built-in modules, relative, absolute imports and npm dependencies - // can be resolved using request - try { - const resolved = originalResolveFilename(request, parent, isMain, options); - if (resolved === request || request.startsWith('.') || request.startsWith('/') || - request.match(/^[A-Z]\:[\\\/]/i)) { - if (DEBUG) - console.error( - `node_loader: resolved ${request} to built-in, relative or absolute import ` + - `${resolved} from ${parentFilename}` - ); - return resolved; - } else { - // Resolved is not a built-in module, relative or absolute import - // but also allow imports within npm packages that are within the parent files - // node_modules, meaning it is a dependency of the npm package making the import. - const parentSegments = parentFilename ? parentFilename.replace(/\\/g, '/').split('/') : []; - const parentNodeModulesSegment = parentSegments.indexOf('node_modules'); - if (parentNodeModulesSegment != -1) { - const parentRoot = parentSegments.slice(0, parentNodeModulesSegment).join('/'); - const relative = path.relative(parentRoot, resolved); - if (!relative.startsWith('..')) { - // Resolved within parent node_modules - if (DEBUG) - console.error( - `node_loader: resolved ${request} within parent node_modules to ` + - `${resolved} from ${parentFilename}` - ); - return resolved; - } else { - throw new Error( - `Resolved to ${resolved} outside of parent node_modules ${parentFilename}`); - } - } - throw new Error('Not a built-in module, relative or absolute import'); - } - } catch (e) { - failedResolutions.push(`built-in, relative, absolute, nested node_modules - ${e.toString()}`); - } - - // If the import is not a built-in module, an absolute, relative import or a - // dependency of an npm package, attempt to resolve against the runfiles location - try { - const resolved = originalResolveFilename(resolveRunfiles(parentFilename, request), parent, isMain, options); - if (DEBUG) - console.error( - `node_loader: resolved ${request} within runfiles to ${resolved} from ${parentFilename}` - ); - return resolved; - } catch (e) { - failedResolutions.push(`runfiles - ${e.toString()}`); - } - - // If the parent file is from an external repository, attempt to resolve against - // the external repositories node_modules (if they exist) - let relativeParentFilename = - parentFilename ? path.relative(process.env.RUNFILES, parent.filename) : undefined; - if (relativeParentFilename && !relativeParentFilename.startsWith('..')) { - // Remove leading USER_WORKSPACE_NAME/external so that external workspace name is - // always the first segment - // TODO(gregmagolan): should not be needed when --nolegacy_external_runfiles is default - const externalPrefix = `${USER_WORKSPACE_NAME}/external/`; - if (relativeParentFilename.startsWith(externalPrefix)) { - relativeParentFilename = relativeParentFilename.substr(externalPrefix.length); - } - const parentSegments = relativeParentFilename.split('/'); - if (parentSegments[0] !== USER_WORKSPACE_NAME) { - try { - const resolved = originalResolveFilename(resolveRunfiles(undefined, parentSegments[0], 'node_modules', request), parent, isMain, options); - if (DEBUG) - console.error( - `node_loader: resolved ${request} within node_modules ` + - `(${parentSegments[0]}/node_modules) to ${resolved} from ${relativeParentFilename}` - ); - return resolved; - } catch (e) { - failedResolutions.push(`${parentSegments[0]}/node_modules - ${e.toString()}`); - } - } - } - - // If import was not resolved above then attempt to resolve - // within the node_modules filegroup in use - try { - const resolved = originalResolveFilename(resolveRunfiles(undefined, NODE_MODULES_ROOT, request), parent, isMain, options); - if (DEBUG) - console.error( - `node_loader: resolved ${request} within node_modules (${NODE_MODULES_ROOT}) to ` + - `${resolved} from ${parentFilename}` - ); - return resolved; - } catch (e) { - failedResolutions.push(`node_modules attribute (${NODE_MODULES_ROOT}) - ${e.toString()}`); - } - - const error = new Error( - `${TARGET} cannot find module '${request}' required by '${parentFilename}'\n looked in:\n` + - failedResolutions.map(r => ` ${r}`).join('\n') + '\n'); - error.code = 'MODULE_NOT_FOUND'; - throw error; -} - // Before loading anything that might print a stack, install the // source-map-support. if (INSTALL_SOURCE_MAP_SUPPORT) { diff --git a/internal/node/node_patcher.js b/internal/node/node_patcher.js new file mode 100644 index 0000000000..da7712cb70 --- /dev/null +++ b/internal/node/node_patcher.js @@ -0,0 +1,459 @@ +/** + * @license + * Copyright 2017 The Bazel Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @fileoverview Patched NodeJS module loader for bazel. This template is + * expanded to contain module name -> path mappings and then patches the + * NodeJS require() function to substitute the appropriate paths. + * + * @see https://github.com/nodejs/node/blob/master/lib/module.js + */ +'use strict'; +var path = require('path'); +var fs = require('fs'); + +const DEBUG = false; + +/** + * The module roots as pairs of a RegExp to match the require path, and a + * module_root to substitute for the require path. + * Ordered by regex length, longest to smallest. + * @type {!Array<{module_name: RegExp, module_root: string}>} + */ +var MODULE_ROOTS = [ + TEMPLATED_module_roots +].sort((a, b) => b.module_name.toString().length - a.module_name.toString().length); + +/** + * Array of bootstrap modules that need to be loaded before the entry point. + */ +const USER_WORKSPACE_NAME = 'TEMPLATED_user_workspace_name'; +const NODE_MODULES_ROOT = 'TEMPLATED_node_modules_root'; +const BIN_DIR = 'TEMPLATED_bin_dir'; +const GEN_DIR = 'TEMPLATED_gen_dir'; +const TARGET = 'TEMPLATED_target'; + +if (DEBUG) + console.error(` +node_patcher: running ${TARGET} with + cwd: ${process.cwd()} + runfiles: ${process.env.RUNFILES} + + BIN_DIR: ${BIN_DIR} + GEN_DIR: ${GEN_DIR} + MODULE_ROOTS: ${JSON.stringify(MODULE_ROOTS, undefined, 2)} + NODE_MODULES_ROOT: ${NODE_MODULES_ROOT} + TARGET: ${TARGET} + USER_WORKSPACE_NAME: ${USER_WORKSPACE_NAME} +`); + +function resolveToModuleRoot(path) { + if (!path) { + throw new Error('resolveToModuleRoot missing path: ' + path); + } + + // We want all possible matches. + const orderedMatches = MODULE_ROOTS.filter(m => m.module_name.test(path)); + + if (orderedMatches.length === 0) { + return null; + } else { + // Longest regex wins when multiple match, and the list is already ordered by length. + const m = orderedMatches[0]; + return path.replace(m.module_name, m.module_root); + } +} + +/** + * The runfiles manifest maps from short_path + * https://docs.bazel.build/versions/master/skylark/lib/File.html#short_path + * to the actual location on disk where the file can be read. + * + * In a sandboxed execution, it does not exist. In that case, runfiles must be + * resolved from a symlink tree under the runfiles dir. + * See https://github.com/bazelbuild/bazel/issues/3726 + */ +function loadRunfilesManifest(manifestPath) { + if (DEBUG) console.error(`node_patcher: using manifest ${manifestPath}`); + + // Create the manifest and reverse manifest maps. + const runfilesManifest = Object.create(null); + const reverseRunfilesManifest = Object.create(null); + const input = fs.readFileSync(manifestPath, {encoding: 'utf-8'}); + + // Absolute path that refers to the local workspace path. We need to determine the absolute + // path to the local workspace because it allows us to support absolute path resolving + // for runfiles. + let localWorkspacePath = null; + + for (const line of input.split('\n')) { + if (!line) continue; + const [runfilesPath, realPath] = line.split(' '); + runfilesManifest[runfilesPath] = realPath; + reverseRunfilesManifest[realPath] = runfilesPath; + + // We don't need to try determining the local workspace path for the current runfile + // mapping in case we already determined the local workspace path, the current + // runfile refers to a different workspace, or the current runfile resolves to a file + // in the bazel-out directory (bin/genfiles directory). + if (localWorkspacePath || !runfilesPath.startsWith(USER_WORKSPACE_NAME) || + realPath.includes(BIN_DIR) || realPath.includes(GEN_DIR)) { + continue; + } + + // Relative path for the runfile. We can compute that path by removing the leading + // workspace name. e.g. `my_workspace/src/my-runfile.js` becomes `src/my-runfile.js`. + const relativeWorkspacePath = runfilesPath.slice(USER_WORKSPACE_NAME.length + 1); + + // TODO(gregmagolan): should not be needed when --nolegacy_external_runfiles is default + if (relativeWorkspacePath.startsWith('external/')) { + continue; + } + + localWorkspacePath = realPath.slice(0, -relativeWorkspacePath.length); + } + + // Determine bin and gen root to convert absolute paths into runfile paths. + const binRootIdx = manifestPath.indexOf(BIN_DIR); + let binRoot, genRoot; + if (binRootIdx !== -1) { + const execRoot = manifestPath.slice(0, binRootIdx); + binRoot = `${execRoot}${BIN_DIR}/`; + genRoot = `${execRoot}${GEN_DIR}/`; + } + + if (DEBUG) console.error(`node_patcher: using binRoot ${binRoot}`); + if (DEBUG) console.error(`node_patcher: using genRoot ${genRoot}`); + if (DEBUG) console.error(`node_patcher: using localWorkspacePath ${localWorkspacePath}`); + + return { runfilesManifest, reverseRunfilesManifest, binRoot, genRoot, localWorkspacePath }; +} +const { runfilesManifest, reverseRunfilesManifest, binRoot, genRoot, localWorkspacePath } = + // On Windows, Bazel sets RUNFILES_MANIFEST_ONLY=1. + // On every platform, Bazel also sets RUNFILES_MANIFEST_FILE, but on Linux + // and macOS it's faster to use the symlinks in RUNFILES_DIR rather than resolve + // through the indirection of the manifest file. + // We also need to construct a reverse map to resolve relative files from existing + // manifest entries. + process.env.RUNFILES_MANIFEST_ONLY === '1' && + loadRunfilesManifest(process.env.RUNFILES_MANIFEST_FILE); + +function isFile(res) { + try { + return fs.statSync(res).isFile(); + } catch (e) { + return false; + } +} + +function isDirectory(res) { + try { + return fs.statSync(res).isDirectory(); + } catch (e) { + return false; + } +} + +function readDir(dir) { + return fs.statSync(dir).isDirectory() ? + Array.prototype.concat(...fs.readdirSync(dir).map(f => readDir(path.join(dir, f)))) : + dir.replace(/\\/g, '/'); +} + +function loadAsFileSync(res) { + if (isFile(res)) { + return res; + } + if (isFile(res + '.js')) { + return res; + } + return null; +} + +function loadAsDirectorySync(res) { + const pkgfile = path.join(res, 'package.json'); + if (isFile(pkgfile)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgfile, 'UTF-8')); + const main = pkg['main']; + if (main) { + if (main === '.' || main === './') { + main = 'index'; + } + + let maybe = loadAsFileSync(path.resolve(res, main)); + if (maybe) { + return maybe; + } + + maybe = loadAsDirectorySync(path.resolve(res, main)); + if (maybe) { + return maybe; + } + } + } catch (e) { + } + } + return loadAsFileSync(path.resolve(res, 'index')); +} + +function resolveManifestFile(res) { + const maybe = runfilesManifest[res] || runfilesManifest[res + '.js']; + if (maybe) { + return maybe; + } + // Look for tree artifacts that match and update + // the runfiles with files that are in the tree artifact. + // Attempt to resolve again with the updated runfiles + // if a tree artifact matched. + let segments = res.split('/'); + segments.pop(); + while (segments.length) { + const test = segments.join('/'); + const tree = runfilesManifest[test]; + if (tree && isDirectory(tree)) { + // We have a tree artifact that matches + const files = readDir(tree).map(f => path.relative(tree, f).replace(/\\/g, '/')); + files.forEach(f => { + runfilesManifest[path.posix.join(test, f)] = path.posix.join(tree, f); + }) + return runfilesManifest[res] || runfilesManifest[res + '.js']; + } + segments.pop(); + } +} + +function resolveManifestDirectory(res) { + const pkgfile = runfilesManifest[path.posix.join(res, 'package.json')]; + if (pkgfile) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgfile, 'UTF-8')); + const main = pkg['main']; + if (main) { + if (main === '.' || main === './') { + main = 'index'; + } + + let maybe = resolveManifestFile(path.posix.join(res, main)); + if (maybe) { + return maybe; + } + + maybe = resolveManifestDirectory(path.posix.join(res, main)); + if (maybe) { + return maybe; + } + } + } catch (e) { + } + } + return resolveManifestFile(path.posix.join(res, 'index')); +} + +function resolveRunfiles(parent, ...pathSegments) { + // Remove any empty strings from pathSegments + // Normalize to forward slash, because even on Windows the runfiles_manifest file + // is written with forward slash. + let runfilesEntry = pathSegments.filter(segment => segment).join('/').replace(/\\/g, '/'); + + // Trim `${USER_WORKSPACE_NAME}/external/` from start of runfilesEntry + const externalWorkspacePrefix = `${USER_WORKSPACE_NAME}/external/`; + if (runfilesEntry.startsWith(externalWorkspacePrefix)) { + runfilesEntry = runfilesEntry.slice(externalWorkspacePrefix.length); + } + + const runfilesPath = path.join(process.env.RUNFILES, runfilesEntry); + + if (runfilesManifest) { + if (parent && runfilesEntry.startsWith('.')) { + // Resolve relative paths from manifest files. + const normalizedParent = parent.replace(/\\/g, '/'); + const parentRunfile = reverseRunfilesManifest[normalizedParent]; + if (parentRunfile) { + runfilesEntry = path.join(path.dirname(parentRunfile), runfilesEntry); + } + } else if (runfilesEntry.startsWith(binRoot) || runfilesEntry.startsWith(genRoot) + || runfilesEntry.startsWith(localWorkspacePath)) { + // For absolute paths, replace binRoot, genRoot or localWorkspacePath with + // USER_WORKSPACE_NAME to enable lookups. + // It's OK to do multiple replacements because all of these are absolute paths with drive + // names (e.g. C:\), and on Windows you can't have drive names in the middle of paths. + runfilesEntry = runfilesEntry + .replace(binRoot, `${USER_WORKSPACE_NAME}/`) + .replace(genRoot, `${USER_WORKSPACE_NAME}/`) + .replace(localWorkspacePath, `${USER_WORKSPACE_NAME}/`); + } + + // Normalize and replace path separators to conform to the ones in the manifest. + runfilesEntry = path.normalize(runfilesEntry).replace(/\\/g, '/'); + + if (DEBUG) console.error('node_patcher: try to resolve in runfiles manifest', runfilesEntry); + + let maybe = resolveManifestFile(runfilesEntry); + if (maybe) { + if (DEBUG) console.error('node_patcher: resolved manifest file', maybe); + return maybe; + } + + maybe = resolveManifestDirectory(runfilesEntry); + if (maybe) { + if (DEBUG) console.error('node_patcher: resolved via manifest directory', maybe); + return maybe; + } + } else { + if (DEBUG) console.error('node_patcher: try to resolve in runfiles', runfilesPath); + + let maybe = loadAsFileSync(runfilesPath); + if (maybe) { + if (DEBUG) console.error('node_patcher: resolved file', maybe); + return maybe; + } + + maybe = loadAsDirectorySync(runfilesPath); + if (maybe) { + if (DEBUG) console.error('node_patcher: resolved via directory', maybe); + return maybe; + } + } + + return runfilesPath; +} + +var originalResolveFilename = module.constructor._resolveFilename; +module.constructor._resolveFilename = function(request, parent, isMain, options) { + const parentFilename = (parent && parent.filename) ? parent.filename : undefined; + if (DEBUG) console.error(`\n\nnode_patcher: resolve ${request} from ${parentFilename}`); + + const failedResolutions = []; + + // Attempt to resolve to module root. + // This should be the first attempted resolution because: + // - it's fairly cheap to check (regex over a small array); + // - it is be very common when there are a lot of packages built from source; + if (!isMain) { + // Don't resolve to module root if this is the main entry point + // as the main entry point will always be fully qualified with the + // workspace name and full path. + // See https://github.com/bazelbuild/rules_nodejs/issues/834 + const moduleRoot = resolveToModuleRoot(request); + if (moduleRoot) { + const moduleRootInRunfiles = resolveRunfiles(undefined, moduleRoot); + const filename = module.constructor._findPath(moduleRootInRunfiles, []); + if (filename) { + return filename; + } else { + failedResolutions.push( + `module root ${moduleRoot} - No file ${request} found in module root ${moduleRoot}`); + } + } + } + + // Built-in modules, relative, absolute imports and npm dependencies + // can be resolved using request + try { + const resolved = originalResolveFilename(request, parent, isMain, options); + if (resolved === request || request.startsWith('.') || request.startsWith('/') || + request.match(/^[A-Z]\:[\\\/]/i)) { + if (DEBUG) + console.error( + `node_patcher: resolved ${request} to built-in, relative or absolute import ` + + `${resolved} from ${parentFilename}`); + return resolved; + } else { + // Resolved is not a built-in module, relative or absolute import + // but also allow imports within npm packages that are within the parent files + // node_modules, meaning it is a dependency of the npm package making the import. + const parentSegments = parentFilename ? parentFilename.replace(/\\/g, '/').split('/') : []; + const parentNodeModulesSegment = parentSegments.indexOf('node_modules'); + if (parentNodeModulesSegment != -1) { + const parentRoot = parentSegments.slice(0, parentNodeModulesSegment).join('/'); + const relative = path.relative(parentRoot, resolved); + if (!relative.startsWith('..')) { + // Resolved within parent node_modules + if (DEBUG) + console.error( + `node_patcher: resolved ${request} within parent node_modules to ` + + `${resolved} from ${parentFilename}`); + return resolved; + } else { + throw new Error( + `Resolved to ${resolved} outside of parent node_modules ${parentFilename}`); + } + } + throw new Error('Not a built-in module, relative or absolute import'); + } + } catch (e) { + failedResolutions.push(`built-in, relative, absolute, nested node_modules - ${e.toString()}`); + } + + // If the import is not a built-in module, an absolute, relative import or a + // dependency of an npm package, attempt to resolve against the runfiles location + try { + const resolved = originalResolveFilename(resolveRunfiles(parentFilename, request), parent, isMain, options); + if (DEBUG) + console.error(`node_patcher: resolved ${request} within runfiles to ${resolved} from ${ + parentFilename}`); + return resolved; + } catch (e) { + failedResolutions.push(`runfiles - ${e.toString()}`); + } + + // If the parent file is from an external repository, attempt to resolve against + // the external repositories node_modules (if they exist) + let relativeParentFilename = + parentFilename ? path.relative(process.env.RUNFILES, parent.filename) : undefined; + if (relativeParentFilename && !relativeParentFilename.startsWith('..')) { + // Remove leading USER_WORKSPACE_NAME/external so that external workspace name is + // always the first segment + // TODO(gregmagolan): should not be needed when --nolegacy_external_runfiles is default + const externalPrefix = `${USER_WORKSPACE_NAME}/external/`; + if (relativeParentFilename.startsWith(externalPrefix)) { + relativeParentFilename = relativeParentFilename.substr(externalPrefix.length); + } + const parentSegments = relativeParentFilename.split('/'); + if (parentSegments[0] !== USER_WORKSPACE_NAME) { + try { + const resolved = originalResolveFilename(resolveRunfiles(undefined, parentSegments[0], 'node_modules', request), parent, isMain, options); + if (DEBUG) + console.error( + `node_patcher: resolved ${request} within node_modules ` + + `(${parentSegments[0]}/node_modules) to ${resolved} from ${relativeParentFilename}`); + return resolved; + } catch (e) { + failedResolutions.push(`${parentSegments[0]}/node_modules - ${e.toString()}`); + } + } + } + + // If import was not resolved above then attempt to resolve + // within the node_modules filegroup in use + try { + const resolved = originalResolveFilename(resolveRunfiles(undefined, NODE_MODULES_ROOT, request), parent, isMain, options); + if (DEBUG) + console.error( + `node_patcher: resolved ${request} within node_modules (${NODE_MODULES_ROOT}) to ` + + `${resolved} from ${parentFilename}`); + return resolved; + } catch (e) { + failedResolutions.push(`node_modules attribute (${NODE_MODULES_ROOT}) - ${e.toString()}`); + } + + const error = new Error( + `${TARGET} cannot find module '${request}' required by '${parentFilename}'\n looked in:\n` + + failedResolutions.map(r => ` ${r}`).join('\n') + '\n'); + error.code = 'MODULE_NOT_FOUND'; + throw error; +} diff --git a/internal/node/test/child_process/BUILD.bazel b/internal/node/test/child_process/BUILD.bazel new file mode 100644 index 0000000000..48d02292f2 --- /dev/null +++ b/internal/node/test/child_process/BUILD.bazel @@ -0,0 +1,11 @@ +load("//:defs.bzl", "nodejs_test") + +nodejs_test( + name = "workers", + data = [ + ":worker-main.js", + ":worker-thread.js", + "//internal/node/test/child_process/lib", + ], + entry_point = ":worker-main.js", +) diff --git a/internal/node/test/child_process/lib/BUILD.bazel b/internal/node/test/child_process/lib/BUILD.bazel new file mode 100644 index 0000000000..251f083aeb --- /dev/null +++ b/internal/node/test/child_process/lib/BUILD.bazel @@ -0,0 +1,9 @@ +load("//internal/js_library:js_library.bzl", "js_library") + +package(default_visibility = ["//internal:__subpackages__"]) + +js_library( + name = "lib", + srcs = ["index.js"], + module_name = "lib", +) diff --git a/internal/node/test/child_process/lib/index.js b/internal/node/test/child_process/lib/index.js new file mode 100644 index 0000000000..8477ecd933 --- /dev/null +++ b/internal/node/test/child_process/lib/index.js @@ -0,0 +1 @@ +console.log('LIB') \ No newline at end of file diff --git a/internal/node/test/child_process/worker-main.js b/internal/node/test/child_process/worker-main.js new file mode 100644 index 0000000000..e6d8c643f6 --- /dev/null +++ b/internal/node/test/child_process/worker-main.js @@ -0,0 +1,4 @@ +const childProcess = require('child_process'); +console.log('WORKER-MAIN'); +require('lib'); +new childProcess.fork(__dirname + '/worker-thread.js'); diff --git a/internal/node/test/child_process/worker-thread.js b/internal/node/test/child_process/worker-thread.js new file mode 100644 index 0000000000..54d1ee4c11 --- /dev/null +++ b/internal/node/test/child_process/worker-thread.js @@ -0,0 +1,2 @@ +console.log('WORKER THREAD'); +require('lib'); \ No newline at end of file diff --git a/internal/node/test/worker_threads/BUILD.bazel b/internal/node/test/worker_threads/BUILD.bazel new file mode 100644 index 0000000000..65acf70099 --- /dev/null +++ b/internal/node/test/worker_threads/BUILD.bazel @@ -0,0 +1,11 @@ +# Disabled untill nodejs v12.x is the default +# nodejs_test( +# name = "workers", +# data = [ +# ":worker-thread.js", +# ":worker-main.js", +# "//internal/node/test/child_process/lib", +# ], +# entry_point = ":worker-main.js", +# templated_args = ["--node_options=--experimental-worker"], +# ) diff --git a/internal/node/test/worker_threads/lib/BUILD.bazel b/internal/node/test/worker_threads/lib/BUILD.bazel new file mode 100644 index 0000000000..251f083aeb --- /dev/null +++ b/internal/node/test/worker_threads/lib/BUILD.bazel @@ -0,0 +1,9 @@ +load("//internal/js_library:js_library.bzl", "js_library") + +package(default_visibility = ["//internal:__subpackages__"]) + +js_library( + name = "lib", + srcs = ["index.js"], + module_name = "lib", +) diff --git a/internal/node/test/worker_threads/lib/index.js b/internal/node/test/worker_threads/lib/index.js new file mode 100644 index 0000000000..8477ecd933 --- /dev/null +++ b/internal/node/test/worker_threads/lib/index.js @@ -0,0 +1 @@ +console.log('LIB') \ No newline at end of file diff --git a/internal/node/test/worker_threads/worker-main.js b/internal/node/test/worker_threads/worker-main.js new file mode 100644 index 0000000000..2fecce8962 --- /dev/null +++ b/internal/node/test/worker_threads/worker-main.js @@ -0,0 +1,4 @@ +const workerThreads = require('worker_threads'); +console.log('WORKER-MAIN'); +require('lib'); +new workerThreads.Worker(__dirname + '/worker-thread.js'); diff --git a/internal/node/test/worker_threads/worker-thread.js b/internal/node/test/worker_threads/worker-thread.js new file mode 100644 index 0000000000..54d1ee4c11 --- /dev/null +++ b/internal/node/test/worker_threads/worker-thread.js @@ -0,0 +1,2 @@ +console.log('WORKER THREAD'); +require('lib'); \ No newline at end of file From d52d55a3ba08dd2a3ff4aa146ef6b95141e81180 Mon Sep 17 00:00:00 2001 From: Fabian Wiles Date: Sun, 25 Aug 2019 08:27:19 +1200 Subject: [PATCH 2/3] fix(builtin): account for absolute --- internal/node/node_launcher.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/node/node_launcher.sh b/internal/node/node_launcher.sh index 3ac88f99ad..04e9af9f11 100644 --- a/internal/node/node_launcher.sh +++ b/internal/node/node_launcher.sh @@ -119,6 +119,11 @@ readonly repository_args=$(rlocation "TEMPLATED_repository_args") readonly script=$(rlocation "TEMPLATED_script_path") readonly patcher=$(rlocation "TEMPLATED_patcher_path") +echo $patcher +echo $patcher +echo $patcher +echo $patcher + source $repository_args ARGS=() @@ -132,7 +137,18 @@ for ARG in "${ALL_ARGS[@]}"; do done # TODO: what happens if the user passed a require flag? -NODE_OPTIONS+=( "--require=./$patcher" ) +# if [[ $patcher = /*]] +# then +# else +# fi + +if [[ $patcher = /* ]] +then + NODE_OPTIONS+=( "--require=$patcher" ) +else + # if it's a relative dir then prefix it with a ./ + NODE_OPTIONS+=( "--require=./$patcher" ) +fi # The EXPECTED_EXIT_CODE lets us write bazel tests which assert that # a binary fails to run. Otherwise any failure would make such a test From 64772d89db957577ecac160789bc81aff3905c88 Mon Sep 17 00:00:00 2001 From: Fabian Wiles Date: Sun, 25 Aug 2019 08:36:19 +1200 Subject: [PATCH 3/3] fix(builtin): remove debug --- internal/node/node.bzl | 19 ++++++- internal/node/node_launcher.sh | 22 +------- internal/node/node_loader.js | 8 ++- internal/node/node_proxy.sh | 55 +++++++++++++++++++ .../node/test/child_process/worker-main.js | 6 ++ 5 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 internal/node/node_proxy.sh diff --git a/internal/node/node.bzl b/internal/node/node.bzl index 211898521c..ef49f1253c 100644 --- a/internal/node/node.bzl +++ b/internal/node/node.bzl @@ -197,8 +197,7 @@ def _nodejs_binary_impl(ctx): ]), "TEMPLATED_env_vars": env_vars, "TEMPLATED_expected_exit_code": str(expected_exit_code), - "TEMPLATED_node": node_tool, - "TEMPLATED_patcher_path": patcher_path, + "TEMPLATED_node_proxy_path": _short_path_to_manifest_path(ctx, ctx.outputs.node_proxy.short_path), "TEMPLATED_repository_args": _short_path_to_manifest_path(ctx, ctx.file._repository_args.short_path), "TEMPLATED_script_path": script_path, } @@ -209,6 +208,16 @@ def _nodejs_binary_impl(ctx): is_executable = True, ) + ctx.actions.expand_template( + template = ctx.file._node_proxy_template, + output = ctx.outputs.node_proxy, + substitutions = { + "TEMPLATED_node": node_tool, + "TEMPLATED_patcher_path": patcher_path, + }, + is_executable = True, + ) + runfiles = depset(node_tool_files + [ctx.outputs.loader, ctx.outputs.patcher, ctx.file._repository_args], transitive = [sources, node_modules]) # entry point is only needed in runfiles if it is a .js file @@ -222,6 +231,7 @@ def _nodejs_binary_impl(ctx): files = node_tool_files + [ ctx.outputs.loader, ctx.outputs.patcher, + ctx.outputs.node_proxy, ] + ctx.files._source_map_support_files + # We need this call to the list of Files. @@ -400,6 +410,10 @@ _NODEJS_EXECUTABLE_ATTRS = { default = Label("//internal/node:node_loader.js"), allow_single_file = True, ), + "_node_proxy_template": attr.label( + default = Label("//internal/node:node_proxy.sh"), + allow_single_file = True, + ), "_patcher_template": attr.label( default = Label("//internal/node:node_patcher.js"), allow_single_file = True, @@ -420,6 +434,7 @@ _NODEJS_EXECUTABLE_ATTRS = { _NODEJS_EXECUTABLE_OUTPUTS = { "loader": "%{name}_loader.js", + "node_proxy": "%{name}_node_proxy/node", "patcher": "%{name}_patcher.js", "script": "%{name}.sh", } diff --git a/internal/node/node_launcher.sh b/internal/node/node_launcher.sh index 04e9af9f11..924cf8c02f 100644 --- a/internal/node/node_launcher.sh +++ b/internal/node/node_launcher.sh @@ -114,15 +114,9 @@ TEMPLATED_env_vars # This redirects to stderr so it doesn't interfere with Bazel's worker protocol # find . -name thingImLookingFor 1>&2 -readonly node=$(rlocation "TEMPLATED_node") +readonly node=$(rlocation "TEMPLATED_node_proxy_path") readonly repository_args=$(rlocation "TEMPLATED_repository_args") readonly script=$(rlocation "TEMPLATED_script_path") -readonly patcher=$(rlocation "TEMPLATED_patcher_path") - -echo $patcher -echo $patcher -echo $patcher -echo $patcher source $repository_args @@ -136,20 +130,6 @@ for ARG in "${ALL_ARGS[@]}"; do esac done -# TODO: what happens if the user passed a require flag? -# if [[ $patcher = /*]] -# then -# else -# fi - -if [[ $patcher = /* ]] -then - NODE_OPTIONS+=( "--require=$patcher" ) -else - # if it's a relative dir then prefix it with a ./ - NODE_OPTIONS+=( "--require=./$patcher" ) -fi - # The EXPECTED_EXIT_CODE lets us write bazel tests which assert that # a binary fails to run. Otherwise any failure would make such a test # fail before we could assert that we expected that failure. diff --git a/internal/node/node_loader.js b/internal/node/node_loader.js index c923bac2e7..c086fdf42f 100644 --- a/internal/node/node_loader.js +++ b/internal/node/node_loader.js @@ -25,7 +25,13 @@ var path = require('path'); // Ensure that node is added to the path for any subprocess calls -process.env.PATH = [path.dirname(process.execPath), process.env.PATH].join(path.delimiter); +let binDir = path.dirname(process.env.RULES_NODE_PROXY_SCRIPT); +process.env.PATH = [path.dirname(process.execPath), binDir].join(path.delimiter); + + +// resolve this path with the bazel module resolution +// if it's not resolved then it would fail to run the patcher on the child program +process.execPath = path.resolve(process.env.RULES_NODE_PROXY_SCRIPT) const DEBUG = false; diff --git a/internal/node/node_proxy.sh b/internal/node/node_proxy.sh new file mode 100644 index 0000000000..fe97009927 --- /dev/null +++ b/internal/node/node_proxy.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Immediately exit if any command fails. +set -e + +# --- begin runfiles.bash initialization --- +# Source the runfiles library: +# https://github.com/bazelbuild/bazel/blob/master/tools/bash/runfiles/runfiles.bash +# The runfiles library defines rlocation, which is a platform independent function +# used to lookup the runfiles locations. This code snippet is needed at the top +# of scripts that use rlocation to lookup the location of runfiles.bash and source it +if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + if [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + elif [[ -f "$0.runfiles/MANIFEST" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" + elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + export RUNFILES_DIR="$0.runfiles" + fi +fi +if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash" +elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \ + "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)" +else + echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash" + exit 1 +fi +# --- end runfiles.bash initialization --- + +readonly patcher=$(rlocation "TEMPLATED_patcher_path") +readonly node=$(rlocation "TEMPLATED_node") + +module_pather_path="./$patcher" +if [[ $patcher = /* ]] +then + module_pather_path="/$patcher" +fi + +export RULES_NODE_PROXY_SCRIPT=$0 +exec "$node" "--require=$module_pather_path" "$@" \ No newline at end of file diff --git a/internal/node/test/child_process/worker-main.js b/internal/node/test/child_process/worker-main.js index e6d8c643f6..7b83457cf9 100644 --- a/internal/node/test/child_process/worker-main.js +++ b/internal/node/test/child_process/worker-main.js @@ -1,4 +1,10 @@ const childProcess = require('child_process'); console.log('WORKER-MAIN'); require('lib'); + +// each one of these will throw if the patcher script has not been run on the child new childProcess.fork(__dirname + '/worker-thread.js'); +childProcess.execSync(`node -e 'console.log("exec1"); require("lib")'`, {stdio: 'inherit'}) +childProcess.spawnSync('node', ['-e', 'console.log("spawn1"); require("lib")'], {stdio: 'inherit'}) +childProcess.spawnSync( + process.execPath, ['-e', 'console.log("spawn2"); require("lib")'], {stdio: 'inherit'})