diff --git a/BUILD.bazel b/BUILD.bazel index a9f13e57c0..ad9444fb1f 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -89,6 +89,7 @@ pkg_npm( "//internal/pkg_npm:package_contents", "//internal/pkg_web:package_contents", "//internal/providers:package_contents", + "//internal/runfiles:package_contents", "//third_party/github.com/bazelbuild/bazel:package_contents", "//third_party/github.com/bazelbuild/bazel-skylib:package_contents", "//third_party/github.com/bazelbuild/bazel/tools/bash/runfiles:package_contents", diff --git a/commitlint.config.js b/commitlint.config.js index 251b43aac7..647f900263 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -17,6 +17,7 @@ module.exports = { 'labs', 'protractor', 'rollup', + 'runfiles', 'terser', 'typescript', 'worker', diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index bfc6c2db94..e06717a600 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -65,6 +65,9 @@ example_integration_test( example_integration_test( name = "examples_create-react-app", + npm_packages = { + "//packages/runfiles:npm_package": "@bazel/runfiles", + }, ) example_integration_test( @@ -92,6 +95,9 @@ example_integration_test( example_integration_test( name = "examples_closure", + npm_packages = { + "//packages/runfiles:npm_package": "@bazel/runfiles", + }, ) example_integration_test( diff --git a/examples/closure/BUILD.bazel b/examples/closure/BUILD.bazel index 4448d81422..d0a0fc3ea8 100644 --- a/examples/closure/BUILD.bazel +++ b/examples/closure/BUILD.bazel @@ -16,6 +16,9 @@ google_closure_compiler( nodejs_test( name = "test", - data = ["bundle.js"], + data = [ + "bundle.js", + "@npm//@bazel/runfiles", + ], entry_point = "test.js", ) diff --git a/examples/closure/package.json b/examples/closure/package.json index b8d7cb2481..bab0e25f27 100644 --- a/examples/closure/package.json +++ b/examples/closure/package.json @@ -1,6 +1,8 @@ { "private": true, + "//comment": "TODO: Change runfiles dependency to an actual version once released.", "dependencies": { + "@bazel/runfiles": "latest", "google-closure-compiler": "20190729.0.0" } } diff --git a/examples/closure/test.js b/examples/closure/test.js index 30b80c3597..26f18b10b5 100644 --- a/examples/closure/test.js +++ b/examples/closure/test.js @@ -1,4 +1,4 @@ -const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']); +const {runfiles} = require('@bazel/runfiles'); const closureOutput = runfiles.resolve('examples_closure/bundle.js'); diff --git a/examples/create-react-app/BUILD.bazel b/examples/create-react-app/BUILD.bazel index a1ca6cac08..05ff4f65e2 100644 --- a/examples/create-react-app/BUILD.bazel +++ b/examples/create-react-app/BUILD.bazel @@ -59,7 +59,10 @@ react_scripts( nodejs_test( name = "build_smoke_test", - data = ["build"], + data = [ + "build", + "@npm//@bazel/runfiles", + ], entry_point = "build_smoke_test.js", ) diff --git a/examples/create-react-app/build_smoke_test.js b/examples/create-react-app/build_smoke_test.js index 40172bf3d5..3080dd184f 100644 --- a/examples/create-react-app/build_smoke_test.js +++ b/examples/create-react-app/build_smoke_test.js @@ -1,6 +1,6 @@ const assert = require('assert'); const fs = require('fs'); -const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']); +const {runfiles} = require('@bazel/runfiles'); // Make sure there's a file like build/static/js/main.12345678.chunk.js const jsDir = runfiles.resolvePackageRelative('build/static/js'); diff --git a/examples/create-react-app/package.json b/examples/create-react-app/package.json index 808806b5ba..4a7ce72dc9 100644 --- a/examples/create-react-app/package.json +++ b/examples/create-react-app/package.json @@ -16,8 +16,10 @@ "react-scripts": "3.4.1", "typescript": "~3.7.2" }, + "//comment": "TODO: Change runfiles dependency to an actual version once released.", "devDependencies": { "@bazel/ibazel": "^0.15.6", + "@bazel/runfiles": "latest", "patch-package": "^6.2.2" }, "scripts": { diff --git a/internal/js_library/js_library.bzl b/internal/js_library/js_library.bzl index b7d526b75c..cce74445db 100644 --- a/internal/js_library/js_library.bzl +++ b/internal/js_library/js_library.bzl @@ -38,10 +38,10 @@ _ATTRS = { ), "deps": attr.label_list(), "external_npm_package": attr.bool( - doc = """Internal use only. Indictates that this js_library target is one or more external npm packages in node_modules. + doc = """Internal use only. Indicates that this js_library target is one or more external npm packages in node_modules. This is used by the yarn_install & npm_install repository rules for npm dependencies installed by yarn & npm. When true, js_library will provide ExternalNpmPackageInfo. - + It can also be used for user-managed npm dependencies if node_modules is layed out outside of bazel. For example, diff --git a/internal/linker/BUILD.bazel b/internal/linker/BUILD.bazel index 3cdd4d684b..69c7bdeac7 100644 --- a/internal/linker/BUILD.bazel +++ b/internal/linker/BUILD.bazel @@ -1,5 +1,6 @@ # BEGIN-INTERNAL load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("//:index.bzl", "js_library") load("//packages/typescript:checked_in_ts_project.bzl", "checked_in_ts_project") # We can't bootstrap the ts_library rule using the linker itself, @@ -12,7 +13,17 @@ checked_in_ts_project( src = "link_node_modules.ts", checked_in_js = "index.js", visibility = ["//internal/linker:__subpackages__"], - deps = ["@npm//@types/node"], + deps = [ + "//packages/runfiles:bazel_runfiles", + "@npm//@types/node", + ], +) + +js_library( + name = "linker_js", + srcs = ["index.js"], + visibility = ["//internal/linker/test:__pkg__"], + deps = ["//internal/runfiles:runfiles_js"], ) bzl_library( @@ -24,7 +35,6 @@ bzl_library( # END-INTERNAL exports_files([ "index.js", - "runfiles_helper.js", ]) filegroup( diff --git a/internal/linker/index.js b/internal/linker/index.js index 99d783c870..126337f110 100644 --- a/internal/linker/index.js +++ b/internal/linker/index.js @@ -11,8 +11,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge Object.defineProperty(exports, "__esModule", { value: true }); const fs = require("fs"); const path = require("path"); +const { runfiles: _defaultRunfiles, _BAZEL_OUT_REGEX } = require('../runfiles/index.js'); const VERBOSE_LOGS = !!process.env['VERBOSE_LOGS']; -const BAZEL_OUT_REGEX = /(\/bazel-out\/|\/bazel-~1\/x64_wi~1\/)/; function log_verbose(...m) { if (VERBOSE_LOGS) console.error('[link_node_modules.js]', ...m); @@ -20,16 +20,6 @@ function log_verbose(...m) { function log_error(error) { console.error('[link_node_modules.js] An error has been reported:', error, error.stack); } -function panic(m) { - throw new Error(`Internal error! Please run again with - --define=VERBOSE_LOG=1 -and file an issue: https://github.com/bazelbuild/rules_nodejs/issues/new?template=bug_report.md -Include as much of the build output as you can without disclosing anything confidential. - - Error: - ${m} - `); -} function mkdirp(p) { return __awaiter(this, void 0, void 0, function* () { if (p && !(yield exists(p))) { @@ -141,125 +131,6 @@ function resolveExternalWorkspacePath(workspace, startCwd, isExecroot, execroot, } }); } -class Runfiles { - constructor(env) { - if (!!env['RUNFILES_MANIFEST_FILE']) { - this.manifest = this.loadRunfilesManifest(env['RUNFILES_MANIFEST_FILE']); - } - else if (!!env['RUNFILES_DIR']) { - this.dir = path.resolve(env['RUNFILES_DIR']); - } - else { - panic('Every node program run under Bazel must have a $RUNFILES_DIR or $RUNFILES_MANIFEST_FILE environment variable'); - } - if (env['RUNFILES_MANIFEST_ONLY'] === '1' && !env['RUNFILES_MANIFEST_FILE']) { - log_verbose(`Workaround https://github.com/bazelbuild/bazel/issues/7994 - RUNFILES_MANIFEST_FILE should have been set but wasn't. - falling back to using runfiles symlinks. - If you want to test runfiles manifest behavior, add - --spawn_strategy=standalone to the command line.`); - } - this.workspace = env['BAZEL_WORKSPACE'] || undefined; - const target = env['BAZEL_TARGET']; - if (!!target && !target.startsWith('@')) { - this.package = target.split(':')[0].replace(/^\/\//, ''); - } - } - lookupDirectory(dir) { - if (!this.manifest) - return undefined; - let result; - for (const [k, v] of this.manifest) { - if (k.startsWith(`${dir}/external`)) - continue; - if (k.startsWith(dir)) { - const l = k.length - dir.length; - const maybe = v.substring(0, v.length - l); - if (maybe.match(BAZEL_OUT_REGEX)) { - return maybe; - } - else { - result = maybe; - } - } - } - return result; - } - loadRunfilesManifest(manifestPath) { - log_verbose(`using runfiles manifest ${manifestPath}`); - const runfilesEntries = new Map(); - const input = fs.readFileSync(manifestPath, { encoding: 'utf-8' }); - for (const line of input.split('\n')) { - if (!line) - continue; - const [runfilesPath, realPath] = line.split(' '); - runfilesEntries.set(runfilesPath, realPath); - } - return runfilesEntries; - } - resolve(modulePath) { - if (path.isAbsolute(modulePath)) { - return modulePath; - } - const result = this._resolve(modulePath, undefined); - if (result) { - return result; - } - const e = new Error(`could not resolve module ${modulePath}`); - e.code = 'MODULE_NOT_FOUND'; - throw e; - } - _resolve(moduleBase, moduleTail) { - if (this.manifest) { - const result = this.lookupDirectory(moduleBase); - if (result) { - if (moduleTail) { - const maybe = path.join(result, moduleTail || ''); - if (fs.existsSync(maybe)) { - return maybe; - } - } - else { - return result; - } - } - } - if (exports.runfiles.dir) { - const maybe = path.join(exports.runfiles.dir, moduleBase, moduleTail || ''); - if (fs.existsSync(maybe)) { - return maybe; - } - } - const dirname = path.dirname(moduleBase); - if (dirname == '.') { - return undefined; - } - return this._resolve(dirname, path.join(path.basename(moduleBase), moduleTail || '')); - } - resolveWorkspaceRelative(modulePath) { - if (!this.workspace) { - throw new Error('workspace could not be determined from the environment; make sure BAZEL_WORKSPACE is set'); - } - return this.resolve(path.posix.join(this.workspace, modulePath)); - } - resolvePackageRelative(modulePath) { - if (!this.workspace) { - throw new Error('workspace could not be determined from the environment; make sure BAZEL_WORKSPACE is set'); - } - if (this.package === undefined) { - throw new Error('package could not be determined from the environment; make sure BAZEL_TARGET is set'); - } - return this.resolve(path.posix.join(this.workspace, this.package, modulePath)); - } - patchRequire() { - const requirePatch = process.env['BAZEL_NODE_PATCH_REQUIRE']; - if (!requirePatch) { - throw new Error('require patch location could not be determined from the environment'); - } - require(requirePatch); - } -} -exports.Runfiles = Runfiles; function exists(p) { return __awaiter(this, void 0, void 0, function* () { return ((yield gracefulLstat(p)) !== null); @@ -372,7 +243,7 @@ function findExecroot(startCwd) { if (existsSync(`${startCwd}/bazel-out`)) { return startCwd; } - const bazelOutMatch = startCwd.match(BAZEL_OUT_REGEX); + const bazelOutMatch = startCwd.match(_BAZEL_OUT_REGEX); return bazelOutMatch ? startCwd.slice(0, bazelOutMatch.index) : undefined; } function main(args, runfiles) { @@ -511,7 +382,7 @@ function main(args, runfiles) { try { target = runfiles.resolve(runfilesPath); if (runfiles.manifest && modulePath.startsWith(`${bin}/`)) { - if (!target.match(BAZEL_OUT_REGEX)) { + if (!target.match(_BAZEL_OUT_REGEX)) { const e = new Error(`could not resolve module ${runfilesPath} in output tree`); e.code = 'MODULE_NOT_FOUND'; throw e; @@ -565,7 +436,6 @@ function main(args, runfiles) { }); } exports.main = main; -exports.runfiles = new Runfiles(process.env); 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}`); @@ -575,7 +445,7 @@ if (require.main === module) { } (() => __awaiter(void 0, void 0, void 0, function* () { try { - process.exitCode = yield main(process.argv.slice(2), exports.runfiles); + process.exitCode = yield main(process.argv.slice(2), _defaultRunfiles); } catch (e) { log_error(e); diff --git a/internal/linker/link_node_modules.ts b/internal/linker/link_node_modules.ts index 558408099e..df2e1320e4 100644 --- a/internal/linker/link_node_modules.ts +++ b/internal/linker/link_node_modules.ts @@ -6,13 +6,17 @@ 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']; -// NB: on windows thanks to legacy 8-character path segments it might be like -// c:/b/ojvxx6nx/execroot/build_~1/bazel-~1/x64_wi~1/bin/internal/npm_in~1/test -const BAZEL_OUT_REGEX = /(\/bazel-out\/|\/bazel-~1\/x64_wi~1\/)/; - function log_verbose(...m: string[]) { if (VERBOSE_LOGS) console.error('[link_node_modules.js]', ...m); } @@ -21,17 +25,6 @@ function log_error(error: Error) { console.error('[link_node_modules.js] An error has been reported:', error, error.stack); } -function panic(m: string) { - throw new Error(`Internal error! Please run again with - --define=VERBOSE_LOG=1 -and file an issue: https://github.com/bazelbuild/rules_nodejs/issues/new?template=bug_report.md -Include as much of the build output as you can without disclosing anything confidential. - - Error: - ${m} - `); -} - /** * Create a new directory and any necessary subdirectories * if they do not exist. @@ -183,177 +176,6 @@ async function resolveExternalWorkspacePath( return path.resolve(`${startCwd}/../${workspace}`) } } -export class Runfiles { - manifest: Map|undefined; - dir: string|undefined; - /** - * If the environment gives us enough hints, we can know the workspace name - */ - workspace: string|undefined; - /** - * If the environment gives us enough hints, we can know the package path - */ - package: string|undefined; - - constructor(env: typeof process.env) { - // If Bazel sets a variable pointing to a runfiles manifest, - // we'll always use it. - // Note that this has a slight performance implication on Mac/Linux - // where we could use the runfiles tree already laid out on disk - // but this just costs one file read for the external npm/node_modules - // and one for each first-party module, not one per file. - if (!!env['RUNFILES_MANIFEST_FILE']) { - this.manifest = this.loadRunfilesManifest(env['RUNFILES_MANIFEST_FILE']!); - } else if (!!env['RUNFILES_DIR']) { - this.dir = path.resolve(env['RUNFILES_DIR']!); - } else { - panic( - 'Every node program run under Bazel must have a $RUNFILES_DIR or $RUNFILES_MANIFEST_FILE environment variable'); - } - // Under --noenable_runfiles (in particular on Windows) - // Bazel sets RUNFILES_MANIFEST_ONLY=1. - // When this happens, we need to read the manifest file to locate - // inputs - if (env['RUNFILES_MANIFEST_ONLY'] === '1' && !env['RUNFILES_MANIFEST_FILE']) { - log_verbose(`Workaround https://github.com/bazelbuild/bazel/issues/7994 - RUNFILES_MANIFEST_FILE should have been set but wasn't. - falling back to using runfiles symlinks. - If you want to test runfiles manifest behavior, add - --spawn_strategy=standalone to the command line.`); - } - // Bazel starts actions with pwd=execroot/my_wksp or pwd=runfiles/my_wksp - this.workspace = env['BAZEL_WORKSPACE'] || undefined; - // If target is from an external workspace such as @npm//rollup/bin:rollup - // resolvePackageRelative is not supported since package is in an external - // workspace. - const target = env['BAZEL_TARGET']; - if (!!target && !target.startsWith('@')) { - // //path/to:target -> path/to - this.package = target.split(':')[0].replace(/^\/\//, ''); - } - } - - lookupDirectory(dir: string): string|undefined { - if (!this.manifest) return undefined; - - let result: string|undefined; - for (const [k, v] of this.manifest) { - // Account for Bazel --legacy_external_runfiles - // which pollutes the workspace with 'my_wksp/external/...' - if (k.startsWith(`${dir}/external`)) continue; - - // Entry looks like - // k: npm/node_modules/semver/LICENSE - // v: /path/to/external/npm/node_modules/semver/LICENSE - // calculate l = length(`/semver/LICENSE`) - if (k.startsWith(dir)) { - const l = k.length - dir.length; - const maybe = v.substring(0, v.length - l); - if (maybe.match(BAZEL_OUT_REGEX)) { - return maybe; - } else { - result = maybe; - } - } - } - return result; - } - - - /** - * 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 - */ - loadRunfilesManifest(manifestPath: string) { - log_verbose(`using runfiles manifest ${manifestPath}`); - - const runfilesEntries = new Map(); - const input = fs.readFileSync(manifestPath, {encoding: 'utf-8'}); - - for (const line of input.split('\n')) { - if (!line) continue; - const [runfilesPath, realPath] = line.split(' '); - runfilesEntries.set(runfilesPath, realPath); - } - - return runfilesEntries; - } - - resolve(modulePath: string) { - if (path.isAbsolute(modulePath)) { - return modulePath; - } - const result = this._resolve(modulePath, undefined); - if (result) { - return result; - } - const e = new Error(`could not resolve module ${modulePath}`); - (e as any).code = 'MODULE_NOT_FOUND'; - throw e; - } - - _resolve(moduleBase: string, moduleTail: string|undefined): string|undefined { - if (this.manifest) { - const result = this.lookupDirectory(moduleBase); - if (result) { - if (moduleTail) { - const maybe = path.join(result, moduleTail || ''); - if (fs.existsSync(maybe)) { - return maybe; - } - } else { - return result; - } - } - } - if (runfiles.dir) { - const maybe = path.join(runfiles.dir, moduleBase, moduleTail || ''); - if (fs.existsSync(maybe)) { - return maybe; - } - } - const dirname = path.dirname(moduleBase); - if (dirname == '.') { - // no match - return undefined; - } - return this._resolve(dirname, path.join(path.basename(moduleBase), moduleTail || '')); - } - - resolveWorkspaceRelative(modulePath: string) { - if (!this.workspace) { - throw new Error( - 'workspace could not be determined from the environment; make sure BAZEL_WORKSPACE is set'); - } - return this.resolve(path.posix.join(this.workspace, modulePath)); - } - - resolvePackageRelative(modulePath: string) { - if (!this.workspace) { - throw new Error( - 'workspace could not be determined from the environment; make sure BAZEL_WORKSPACE is set'); - } - // NB: this.package may be '' if at the root of the workspace - if (this.package === undefined) { - throw new Error( - 'package could not be determined from the environment; make sure BAZEL_TARGET is set'); - } - return this.resolve(path.posix.join(this.workspace, this.package, modulePath)); - } - - patchRequire() { - const requirePatch = process.env['BAZEL_NODE_PATCH_REQUIRE']; - if (!requirePatch) { - throw new Error('require patch location could not be determined from the environment'); - } - require(requirePatch); - } -} // TypeScript lib.es5.d.ts has a mistake: JSON.parse does accept Buffer. declare global { @@ -544,7 +366,7 @@ function findExecroot(startCwd: string): string|undefined { // 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); + const bazelOutMatch = startCwd.match(_BAZEL_OUT_REGEX); return bazelOutMatch ? startCwd.slice(0, bazelOutMatch.index) : undefined; } @@ -763,7 +585,7 @@ export async function main(args: string[], runfiles: Runfiles) { 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)) { + 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; @@ -838,8 +660,6 @@ export async function main(args: string[], runfiles: Runfiles) { return code; } -export const runfiles = new Runfiles(process.env); - 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 ${ @@ -850,7 +670,7 @@ if (require.main === module) { } (async () => { try { - process.exitCode = await main(process.argv.slice(2), runfiles); + process.exitCode = await main(process.argv.slice(2), _defaultRunfiles); } catch (e) { log_error(e); process.exitCode = 1; diff --git a/internal/linker/runfiles_helper.js b/internal/linker/runfiles_helper.js deleted file mode 100644 index 0a73f0a1b5..0000000000 --- a/internal/linker/runfiles_helper.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./index.js').runfiles; diff --git a/internal/linker/test/BUILD.bazel b/internal/linker/test/BUILD.bazel index aba3c6bd9a..d14bc19823 100644 --- a/internal/linker/test/BUILD.bazel +++ b/internal/linker/test/BUILD.bazel @@ -6,6 +6,7 @@ ts_library( srcs = glob(["*.ts"]), deps = [ "//internal/linker:linker_lib", + "//packages/runfiles:bazel_runfiles", "@npm//@types/jasmine", "@npm//@types/node", ], @@ -14,7 +15,7 @@ ts_library( jasmine_node_test( name = "unit_tests", srcs = ["test_lib"], - # NB: for better dev round-trip, we test against the ts_library target - # rather than update the index.js it's transpiled to. - data = ["//internal/linker:linker_lib"], + data = [ + "//internal/linker:linker_js", + ], ) diff --git a/internal/linker/test/integration/BUILD.bazel b/internal/linker/test/integration/BUILD.bazel index 06a7ace30c..3bd092b084 100644 --- a/internal/linker/test/integration/BUILD.bazel +++ b/internal/linker/test/integration/BUILD.bazel @@ -39,12 +39,13 @@ sh_binary( # "runfiles" module mappings from `:some_program` in `:run_program` under the ":some_program", "//internal/linker:index.js", + "//internal/runfiles:index.js", + "//internal/runfiles:runfile_helper_main.js", "//internal/linker/test/integration/static_linked_pkg", "//internal/linker/test/integration/static_linked_scoped_pkg", "//internal/linker/test/integration/absolute_import:index.js", "//third_party/github.com/bazelbuild/bazel/tools/bash/runfiles", "//toolchains/node:node_bin", - "//internal/linker:runfiles_helper.js", ], ) diff --git a/internal/linker/test/integration/run_program.sh b/internal/linker/test/integration/run_program.sh index 12855e79b9..b5526cc91e 100755 --- a/internal/linker/test/integration/run_program.sh +++ b/internal/linker/test/integration/run_program.sh @@ -36,7 +36,7 @@ export VERBOSE_LOGS=1 # export NODE_DEBUG=module # Export the location of the runfiles helpers script -export BAZEL_NODE_RUNFILES_HELPER=$(rlocation "build_bazel_rules_nodejs/internal/linker/runfiles_helper.js") +export BAZEL_NODE_RUNFILES_HELPER=$(rlocation "build_bazel_rules_nodejs/internal/runfiles/runfile_helper_main.js") if [[ "${BAZEL_NODE_RUNFILES_HELPER}" != /* ]] && [[ ! "${BAZEL_NODE_RUNFILES_HELPER}" =~ ^[A-Z]:[\\/] ]]; then export BAZEL_NODE_RUNFILES_HELPER=$(pwd)/${BAZEL_NODE_RUNFILES_HELPER} fi diff --git a/internal/linker/test/link_node_modules.spec.ts b/internal/linker/test/link_node_modules.spec.ts index 76dcfb2079..af91d7acd2 100644 --- a/internal/linker/test/link_node_modules.spec.ts +++ b/internal/linker/test/link_node_modules.spec.ts @@ -1,9 +1,16 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as linker from '../link_node_modules'; import {LinkerAliases, LinkerTreeElement} from '../link_node_modules'; +// The linker is imported through it's checked-in file. We do this because the import to +// the runfile helpers from the linker file will always resolve to the checked-in file too. +const linker: typeof import('../link_node_modules') = require('../index.js'); +// As seen above, the linker file always loads the checked-in runfile helpers. We don't want +// to have a mix of checked-in files and sources, so we import runfile helpers from the +// checked-in file too, but use the types provided by the `@bazel/runfiles` package. +const {Runfiles}: typeof import('@bazel/runfiles') = require('../../runfiles/index.js'); + const BIN_DIR = `bazel-out/my-platform-fastbuild/bin`; function mkdirp(p: string) { @@ -378,7 +385,7 @@ describe('link_node_modules', () => { }); // TODO(alexeagle): test should control the environment, not just pass through - await linker.main(['manifest.json'], new linker.Runfiles(process.env)); + await linker.main(['manifest.json'], new Runfiles(process.env)); // The linker expects to run as its own process, so it changes the wd process.chdir(path.join()); @@ -420,7 +427,7 @@ describe('link_node_modules', () => { roots: {} }); - await linker.main(['manifest.json'], new linker.Runfiles({ + await linker.main(['manifest.json'], new Runfiles({ // This test assumes an environment where runfiles are not symlinked. Hence // we pass a runfile manifest file. 'RUNFILES_MANIFEST_FILE': 'runfiles.mf', @@ -449,7 +456,7 @@ describe('link_node_modules', () => { // In the second run, we added a mapping for `@angular/cdk`. This means // that the linker would need to clean up the previous `cdk/bidi` link // in order to be able to create a link for `@angular/cdk`. - await linker.main(['manifest.json'], new linker.Runfiles({ + await linker.main(['manifest.json'], new Runfiles({ // This test assumes an environment where runfiles are not symlinked. Hence // we pass a runfile manifest file. 'RUNFILES_MANIFEST_FILE': 'runfiles.mf', @@ -487,7 +494,7 @@ describe('link_node_modules', () => { }); // TODO(alexeagle): test should control the environment, not just pass through - await linker.main(['manifest.json'], new linker.Runfiles(process.env)); + await linker.main(['manifest.json'], new Runfiles(process.env)); // The linker expects to run as its own process, so it changes the wd process.chdir(path.join()); @@ -520,7 +527,7 @@ describe('link_node_modules', () => { }); // TODO(alexeagle): test should control the environment, not just pass through - await linker.main(['manifest.json'], new linker.Runfiles(process.env)); + await linker.main(['manifest.json'], new Runfiles(process.env)); // The linker expects to run as its own process, so it changes the wd process.chdir(path.join()); @@ -553,7 +560,7 @@ describe('link_node_modules', () => { }); // TODO(alexeagle): test should control the environment, not just pass through - await linker.main(['manifest.json'], new linker.Runfiles(process.env)); + await linker.main(['manifest.json'], new Runfiles(process.env)); // The linker expects to run as its own process, so it changes the wd process.chdir(path.join()); @@ -579,7 +586,7 @@ describe('link_node_modules', () => { }); writeRunfiles(runfilesManifest); - await linker.main(['manifest.json'], new linker.Runfiles({ + await linker.main(['manifest.json'], new Runfiles({ 'RUNFILES_MANIFEST_FILE': 'runfiles.mf', })); diff --git a/internal/node/node.bzl b/internal/node/node.bzl index c9408093e9..c0ae1f472e 100644 --- a/internal/node/node.bzl +++ b/internal/node/node.bzl @@ -225,7 +225,8 @@ fi node_tool_files.extend(ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo.tool_files) node_tool_files.append(ctx.file._link_modules_script) - node_tool_files.append(ctx.file._runfiles_helper_script) + node_tool_files.append(ctx.file._runfile_helpers_bundle) + node_tool_files.append(ctx.file._runfile_helpers_main) node_tool_files.append(ctx.file._node_patches_script) node_tool_files.append(ctx.file._lcov_merger_script) node_tool_files.append(node_modules_manifest) @@ -288,7 +289,7 @@ fi "TEMPLATED_node_patches_script": _to_manifest_path(ctx, ctx.file._node_patches_script), "TEMPLATED_repository_args": _to_manifest_path(ctx, ctx.file._repository_args), "TEMPLATED_require_patch_script": _to_manifest_path(ctx, ctx.outputs.require_patch_script), - "TEMPLATED_runfiles_helper_script": _to_manifest_path(ctx, ctx.file._runfiles_helper_script), + "TEMPLATED_runfiles_helper_script": _to_manifest_path(ctx, ctx.file._runfile_helpers_main), "TEMPLATED_vendored_node": "" if is_builtin else strip_external(ctx.file._node.path), } @@ -356,7 +357,7 @@ By default, Bazel always runs in the workspace root. Due to implementation details, this argument must be underneath this package directory. To run in the directory containing the `nodejs_binary` / `nodejs_test`, use - + chdir = package_name() (or if you're in a macro, use `native.package_name()`) @@ -574,8 +575,12 @@ Predefined genrule variables are not supported in this context. default = Label("//internal/node:require_patch.js"), allow_single_file = True, ), - "_runfiles_helper_script": attr.label( - default = Label("//internal/linker:runfiles_helper.js"), + "_runfile_helpers_bundle": attr.label( + default = Label("//internal/runfiles:index.js"), + allow_single_file = True, + ), + "_runfile_helpers_main": attr.label( + default = Label("//internal/runfiles:runfile_helper_main.js"), allow_single_file = True, ), "_source_map_support_files": attr.label_list( diff --git a/internal/runfiles/BUILD.bazel b/internal/runfiles/BUILD.bazel new file mode 100644 index 0000000000..ff3d2b0978 --- /dev/null +++ b/internal/runfiles/BUILD.bazel @@ -0,0 +1,49 @@ +# BEGIN-INTERNAL +load("//:index.bzl", "generated_file_test", "js_library") +load("//packages/rollup:index.bzl", "rollup_bundle") + +rollup_bundle( + name = "runfiles_pkg_bundled", + config_file = "rollup.config.js", + entry_points = { + "//packages/runfiles:index.js": "index_bundled", + }, + format = "cjs", + sourcemap = "false", + deps = [ + "//packages/runfiles:runfiles_lib", + "@npm//@rollup/plugin-commonjs", + "@npm//@rollup/plugin-node-resolve", + ], +) + +js_library( + name = "runfiles_js", + srcs = ["index.js"], + visibility = ["//internal/linker:__pkg__"], +) + +# Assert that we keep the `index.js` file up-to-date when the +# unfile helpers change. +generated_file_test( + name = "runfiles_checked_in", + src = "index.js", + generated = "index_bundled.js", +) + +# END-INTERNAL + +filegroup( + name = "package_contents", + srcs = [ + "BUILD.bazel", + "index.js", + "runfile_helper_main.js", + ], + visibility = ["//:__pkg__"], +) + +exports_files([ + "index.js", + "runfile_helper_main.js", +]) diff --git a/internal/runfiles/index.js b/internal/runfiles/index.js new file mode 100644 index 0000000000..7c672c456c --- /dev/null +++ b/internal/runfiles/index.js @@ -0,0 +1,204 @@ +// clang-format off +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +var path = require('path'); +var fs = require('fs'); + +function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } + +var path__default = /*#__PURE__*/_interopDefaultLegacy(path); +var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); + +// NB: on windows thanks to legacy 8-character path segments it might be like +// c:/b/ojvxx6nx/execroot/build_~1/bazel-~1/x64_wi~1/bin/internal/npm_in~1/test +var BAZEL_OUT_REGEX = /(\/bazel-out\/|\/bazel-~1\/x64_wi~1\/)/; + +var paths = /*#__PURE__*/Object.defineProperty({ + BAZEL_OUT_REGEX: BAZEL_OUT_REGEX +}, '__esModule', {value: true}); + +/** + * Class that provides methods for resolving Bazel runfiles. + */ +class Runfiles$1 { + constructor(_env) { + this._env = _env; + // If Bazel sets a variable pointing to a runfiles manifest, + // we'll always use it. + // Note that this has a slight performance implication on Mac/Linux + // where we could use the runfiles tree already laid out on disk + // but this just costs one file read for the external npm/node_modules + // and one for each first-party module, not one per file. + if (!!_env['RUNFILES_MANIFEST_FILE']) { + this.manifest = this.loadRunfilesManifest(_env['RUNFILES_MANIFEST_FILE']); + } + else if (!!_env['RUNFILES_DIR']) { + this.runfilesDir = path__default['default'].resolve(_env['RUNFILES_DIR']); + } + else { + throw new Error('Every node program run under Bazel must have a $RUNFILES_DIR or $RUNFILES_MANIFEST_FILE environment variable'); + } + // Under --noenable_runfiles (in particular on Windows) + // Bazel sets RUNFILES_MANIFEST_ONLY=1. + // When this happens, we need to read the manifest file to locate + // inputs + if (_env['RUNFILES_MANIFEST_ONLY'] === '1' && !_env['RUNFILES_MANIFEST_FILE']) { + console.warn(`Workaround https://github.com/bazelbuild/bazel/issues/7994 + RUNFILES_MANIFEST_FILE should have been set but wasn't. + falling back to using runfiles symlinks. + If you want to test runfiles manifest behavior, add + --spawn_strategy=standalone to the command line.`); + } + // Bazel starts actions with pwd=execroot/my_wksp or pwd=runfiles/my_wksp + this.workspace = _env['BAZEL_WORKSPACE'] || undefined; + // If target is from an external workspace such as @npm//rollup/bin:rollup + // resolvePackageRelative is not supported since package is in an external + // workspace. + const target = _env['BAZEL_TARGET']; + if (!!target && !target.startsWith('@')) { + // //path/to:target -> path/to + this.package = target.split(':')[0].replace(/^\/\//, ''); + } + } + lookupDirectory(dir) { + if (!this.manifest) + return undefined; + let result; + for (const [k, v] of this.manifest) { + // Account for Bazel --legacy_external_runfiles + // which pollutes the workspace with 'my_wksp/external/...' + if (k.startsWith(`${dir}/external`)) + continue; + // Entry looks like + // k: npm/node_modules/semver/LICENSE + // v: /path/to/external/npm/node_modules/semver/LICENSE + // calculate l = length(`/semver/LICENSE`) + if (k.startsWith(dir)) { + const l = k.length - dir.length; + const maybe = v.substring(0, v.length - l); + if (maybe.match(paths.BAZEL_OUT_REGEX)) { + return maybe; + } + else { + result = maybe; + } + } + } + return result; + } + /** + * 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 + */ + loadRunfilesManifest(manifestPath) { + const runfilesEntries = new Map(); + const input = fs__default['default'].readFileSync(manifestPath, { encoding: 'utf-8' }); + for (const line of input.split('\n')) { + if (!line) + continue; + const [runfilesPath, realPath] = line.split(' '); + runfilesEntries.set(runfilesPath, realPath); + } + return runfilesEntries; + } + /** Resolves the given module path. */ + resolve(modulePath) { + if (path__default['default'].isAbsolute(modulePath)) { + return modulePath; + } + const result = this._resolve(modulePath, undefined); + if (result) { + return result; + } + const e = new Error(`could not resolve module ${modulePath}`); + e.code = 'MODULE_NOT_FOUND'; + throw e; + } + /** Resolves the given path relative to the current Bazel workspace. */ + resolveWorkspaceRelative(modulePath) { + if (!this.workspace) { + throw new Error('workspace could not be determined from the environment; make sure BAZEL_WORKSPACE is set'); + } + return this.resolve(path__default['default'].posix.join(this.workspace, modulePath)); + } + /** Resolves the given path relative to the current Bazel package. */ + resolvePackageRelative(modulePath) { + if (!this.workspace) { + throw new Error('workspace could not be determined from the environment; make sure BAZEL_WORKSPACE is set'); + } + // NB: this.package may be '' if at the root of the workspace + if (this.package === undefined) { + throw new Error('package could not be determined from the environment; make sure BAZEL_TARGET is set'); + } + return this.resolve(path__default['default'].posix.join(this.workspace, this.package, modulePath)); + } + /** + * Patches the default NodeJS resolution to support runfile resolution. + * @deprecated Use the runfile helpers directly instead. + **/ + patchRequire() { + const requirePatch = this._env['BAZEL_NODE_PATCH_REQUIRE']; + if (!requirePatch) { + throw new Error('require patch location could not be determined from the environment'); + } + require(requirePatch); + } + /** Helper for resolving a given module recursively in the runfiles. */ + _resolve(moduleBase, moduleTail) { + if (this.manifest) { + const result = this.lookupDirectory(moduleBase); + if (result) { + if (moduleTail) { + const maybe = path__default['default'].join(result, moduleTail || ''); + if (fs__default['default'].existsSync(maybe)) { + return maybe; + } + } + else { + return result; + } + } + } + if (this.runfilesDir) { + const maybe = path__default['default'].join(this.runfilesDir, moduleBase, moduleTail || ''); + if (fs__default['default'].existsSync(maybe)) { + return maybe; + } + } + const dirname = path__default['default'].dirname(moduleBase); + if (dirname == '.') { + // no match + return undefined; + } + return this._resolve(dirname, path__default['default'].join(path__default['default'].basename(moduleBase), moduleTail || '')); + } +} +var Runfiles_1 = Runfiles$1; + +var runfiles$1 = /*#__PURE__*/Object.defineProperty({ + Runfiles: Runfiles_1 +}, '__esModule', {value: true}); + +var Runfiles = runfiles$1.Runfiles; + +var _BAZEL_OUT_REGEX = paths.BAZEL_OUT_REGEX; +/** Instance of the runfile helpers. */ +var runfiles_2 = new runfiles$1.Runfiles(process.env); + +var runfiles = /*#__PURE__*/Object.defineProperty({ + Runfiles: Runfiles, + _BAZEL_OUT_REGEX: _BAZEL_OUT_REGEX, + runfiles: runfiles_2 +}, '__esModule', {value: true}); + +exports.Runfiles = Runfiles; +exports._BAZEL_OUT_REGEX = _BAZEL_OUT_REGEX; +exports.default = runfiles; +exports.runfiles = runfiles_2; diff --git a/internal/runfiles/rollup.config.js b/internal/runfiles/rollup.config.js new file mode 100755 index 0000000000..b59bded3b1 --- /dev/null +++ b/internal/runfiles/rollup.config.js @@ -0,0 +1,17 @@ +const commonjs = require('@rollup/plugin-commonjs'); +const {nodeResolve} = require('@rollup/plugin-node-resolve'); + +module.exports = { + output: { + // Since we check-in the bundle, add a comment that disables + // clang-format for the checked-in file. + banner: '// clang-format off', + }, + + plugins: [ + nodeResolve({preferBuiltins: true}), + // The runfile helpers use a dynamic import for loading the + // NodeJS patch script. We want to preserve such dynamic imports. + commonjs({ignoreDynamicRequires: true}), + ], +}; diff --git a/internal/runfiles/runfile_helper_main.js b/internal/runfiles/runfile_helper_main.js new file mode 100644 index 0000000000..53502cd078 --- /dev/null +++ b/internal/runfiles/runfile_helper_main.js @@ -0,0 +1,9 @@ +/** + * File that re-exports the runfile helpers. This is the entry-point for the runfile helper + * script that can be accessed through the `BAZEL_NODE_RUNFILES_HELPER` environment variable. + * + * ```ts + * require(process.env['BAZEL_NODE_RUNFILES_HELPER']) + * ``` + */ +module.exports = require('./index.js').runfiles; diff --git a/packages/index.bzl b/packages/index.bzl index 985a1db464..c7ed4242a5 100644 --- a/packages/index.bzl +++ b/packages/index.bzl @@ -23,6 +23,7 @@ NPM_PACKAGES = ["@bazel/%s" % pkg for pkg in [ "labs", "protractor", "rollup", + "runfiles", "terser", "typescript", "worker", diff --git a/packages/runfiles/BUILD.bazel b/packages/runfiles/BUILD.bazel new file mode 100644 index 0000000000..767224d3ce --- /dev/null +++ b/packages/runfiles/BUILD.bazel @@ -0,0 +1,30 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("@build_bazel_rules_nodejs//:tools/defaults.bzl", "pkg_npm") +load("//packages/typescript:index.bzl", "ts_project") + +ts_project( + name = "runfiles_lib", + srcs = glob(["*.ts"]), + declaration = True, + tsconfig = "tsconfig.json", + visibility = ["//internal/runfiles:__pkg__"], + deps = ["@npm//@types/node"], +) + +js_library( + name = "bazel_runfiles", + package_name = "@bazel/runfiles", + visibility = ["//internal/linker:__subpackages__"], + deps = [":runfiles_lib"], +) + +pkg_npm( + name = "npm_package", + srcs = [ + "README.md", + "package.json", + ], + deps = [ + ":runfiles_lib", + ], +) diff --git a/packages/runfiles/README.md b/packages/runfiles/README.md new file mode 100644 index 0000000000..39822c94ba --- /dev/null +++ b/packages/runfiles/README.md @@ -0,0 +1,7 @@ +# @bazel/runfiles + +This package provides a basic set of utilities for resovling runfiles within NodeJS scripts +executed through `nodejs_binary` or `nodejs_test`. + +Runfile resolution is desirable if your workspace intends to support users that cannot rely +on runfile forest symlinking (most commonly affected are Windows machines). diff --git a/packages/runfiles/index.ts b/packages/runfiles/index.ts new file mode 100755 index 0000000000..684281e0b2 --- /dev/null +++ b/packages/runfiles/index.ts @@ -0,0 +1,13 @@ +import {Runfiles} from './runfiles'; +import {BAZEL_OUT_REGEX} from './paths'; + +// Re-export the `Runfiles` class. This class if the runfile helpers need to be +// mocked for testing purposes. This is used by the linker but also publicly exposed. +export {Runfiles}; + +// Re-export a RegExp for matching `bazel-out` paths. This is used by the linker +// but not intended for public use. +export {BAZEL_OUT_REGEX as _BAZEL_OUT_REGEX}; + +/** Instance of the runfile helpers. */ +export const runfiles = new Runfiles(process.env); diff --git a/packages/runfiles/package.json b/packages/runfiles/package.json new file mode 100644 index 0000000000..5d1dbaa397 --- /dev/null +++ b/packages/runfiles/package.json @@ -0,0 +1,22 @@ +{ + "name": "@bazel/runfiles", + "description": "NodeJS Runfile helpers for Bazel", + "license": "Apache-2.0", + "version": "0.0.0-PLACEHOLDER", + "repository": { + "type" : "git", + "url" : "https://github.com/bazelbuild/rules_nodejs.git", + "directory": "packages/runfiles" + }, + "bugs": { + "url": "https://github.com/bazelbuild/rules_nodejs/issues" + }, + "keywords": [ + "bazel", + "runfiles", + "runfiles helpers" + ], + "main": "index.js", + "types": "index.d.ts", + "dependencies": {} +} diff --git a/packages/runfiles/paths.ts b/packages/runfiles/paths.ts new file mode 100755 index 0000000000..e43ec09cf2 --- /dev/null +++ b/packages/runfiles/paths.ts @@ -0,0 +1,3 @@ +// NB: on windows thanks to legacy 8-character path segments it might be like +// c:/b/ojvxx6nx/execroot/build_~1/bazel-~1/x64_wi~1/bin/internal/npm_in~1/test +export const BAZEL_OUT_REGEX = /(\/bazel-out\/|\/bazel-~1\/x64_wi~1\/)/; diff --git a/packages/runfiles/runfiles.ts b/packages/runfiles/runfiles.ts new file mode 100755 index 0000000000..0b20517eac --- /dev/null +++ b/packages/runfiles/runfiles.ts @@ -0,0 +1,184 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import {BAZEL_OUT_REGEX} from './paths'; + +/** + * Class that provides methods for resolving Bazel runfiles. + */ +export class Runfiles { + manifest: Map|undefined; + runfilesDir: string|undefined; + /** + * If the environment gives us enough hints, we can know the workspace name + */ + workspace: string|undefined; + /** + * If the environment gives us enough hints, we can know the package path + */ + package: string|undefined; + + constructor(private _env: typeof process.env) { + // If Bazel sets a variable pointing to a runfiles manifest, + // we'll always use it. + // Note that this has a slight performance implication on Mac/Linux + // where we could use the runfiles tree already laid out on disk + // but this just costs one file read for the external npm/node_modules + // and one for each first-party module, not one per file. + if (!!_env['RUNFILES_MANIFEST_FILE']) { + this.manifest = this.loadRunfilesManifest(_env['RUNFILES_MANIFEST_FILE']!); + } else if (!!_env['RUNFILES_DIR']) { + this.runfilesDir = path.resolve(_env['RUNFILES_DIR']!); + } else { + throw new Error( + 'Every node program run under Bazel must have a $RUNFILES_DIR or $RUNFILES_MANIFEST_FILE environment variable'); + } + // Under --noenable_runfiles (in particular on Windows) + // Bazel sets RUNFILES_MANIFEST_ONLY=1. + // When this happens, we need to read the manifest file to locate + // inputs + if (_env['RUNFILES_MANIFEST_ONLY'] === '1' && !_env['RUNFILES_MANIFEST_FILE']) { + console.warn(`Workaround https://github.com/bazelbuild/bazel/issues/7994 + RUNFILES_MANIFEST_FILE should have been set but wasn't. + falling back to using runfiles symlinks. + If you want to test runfiles manifest behavior, add + --spawn_strategy=standalone to the command line.`); + } + // Bazel starts actions with pwd=execroot/my_wksp or pwd=runfiles/my_wksp + this.workspace = _env['BAZEL_WORKSPACE'] || undefined; + // If target is from an external workspace such as @npm//rollup/bin:rollup + // resolvePackageRelative is not supported since package is in an external + // workspace. + const target = _env['BAZEL_TARGET']; + if (!!target && !target.startsWith('@')) { + // //path/to:target -> path/to + this.package = target.split(':')[0].replace(/^\/\//, ''); + } + } + + lookupDirectory(dir: string): string|undefined { + if (!this.manifest) return undefined; + + let result: string|undefined; + for (const [k, v] of this.manifest) { + // Account for Bazel --legacy_external_runfiles + // which pollutes the workspace with 'my_wksp/external/...' + if (k.startsWith(`${dir}/external`)) continue; + + // Entry looks like + // k: npm/node_modules/semver/LICENSE + // v: /path/to/external/npm/node_modules/semver/LICENSE + // calculate l = length(`/semver/LICENSE`) + if (k.startsWith(dir)) { + const l = k.length - dir.length; + const maybe = v.substring(0, v.length - l); + if (maybe.match(BAZEL_OUT_REGEX)) { + return maybe; + } else { + result = maybe; + } + } + } + return result; + } + + + /** + * 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 + */ + loadRunfilesManifest(manifestPath: string) { + const runfilesEntries = new Map(); + const input = fs.readFileSync(manifestPath, {encoding: 'utf-8'}); + + for (const line of input.split('\n')) { + if (!line) continue; + const [runfilesPath, realPath] = line.split(' '); + runfilesEntries.set(runfilesPath, realPath); + } + + return runfilesEntries; + } + + /** Resolves the given module path. */ + resolve(modulePath: string) { + if (path.isAbsolute(modulePath)) { + return modulePath; + } + const result = this._resolve(modulePath, undefined); + if (result) { + return result; + } + const e = new Error(`could not resolve module ${modulePath}`); + (e as any).code = 'MODULE_NOT_FOUND'; + throw e; + } + + /** Resolves the given path relative to the current Bazel workspace. */ + resolveWorkspaceRelative(modulePath: string) { + if (!this.workspace) { + throw new Error( + 'workspace could not be determined from the environment; make sure BAZEL_WORKSPACE is set'); + } + return this.resolve(path.posix.join(this.workspace, modulePath)); + } + + /** Resolves the given path relative to the current Bazel package. */ + resolvePackageRelative(modulePath: string) { + if (!this.workspace) { + throw new Error( + 'workspace could not be determined from the environment; make sure BAZEL_WORKSPACE is set'); + } + // NB: this.package may be '' if at the root of the workspace + if (this.package === undefined) { + throw new Error( + 'package could not be determined from the environment; make sure BAZEL_TARGET is set'); + } + return this.resolve(path.posix.join(this.workspace, this.package, modulePath)); + } + + /** + * Patches the default NodeJS resolution to support runfile resolution. + * @deprecated Use the runfile helpers directly instead. + **/ + patchRequire() { + const requirePatch = this._env['BAZEL_NODE_PATCH_REQUIRE']; + if (!requirePatch) { + throw new Error('require patch location could not be determined from the environment'); + } + require(requirePatch); + } + + /** Helper for resolving a given module recursively in the runfiles. */ + private _resolve(moduleBase: string, moduleTail: string|undefined): string|undefined { + if (this.manifest) { + const result = this.lookupDirectory(moduleBase); + if (result) { + if (moduleTail) { + const maybe = path.join(result, moduleTail || ''); + if (fs.existsSync(maybe)) { + return maybe; + } + } else { + return result; + } + } + } + if (this.runfilesDir) { + const maybe = path.join(this.runfilesDir, moduleBase, moduleTail || ''); + if (fs.existsSync(maybe)) { + return maybe; + } + } + const dirname = path.dirname(moduleBase); + if (dirname == '.') { + // no match + return undefined; + } + return this._resolve(dirname, path.join(path.basename(moduleBase), moduleTail || '')); + } +} diff --git a/packages/runfiles/tsconfig.json b/packages/runfiles/tsconfig.json new file mode 100755 index 0000000000..813286d8fa --- /dev/null +++ b/packages/runfiles/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "commonjs", + "target": "ES2015", + "types": ["node"] + }, +}