-
Notifications
You must be signed in to change notification settings - Fork 519
/
runfiles.ts
executable file
·203 lines (188 loc) · 7.78 KB
/
runfiles.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
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<string, string>|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 if (!!_env['RUNFILES']) {
this.runfilesDir = path.resolve(_env['RUNFILES']!);
} else {
throw new Error(
'Every node program run under Bazel must have a $RUNFILES_DIR, $RUNFILES 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'] || _env['JS_BINARY__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.
let target = _env['BAZEL_TARGET'] || _env['JS_BINARY__TARGET'];
if (!!target && !target.startsWith('@')) {
// //path/to:target -> path/to
this.package = target.split(':')[0].replace(/^\/\//, '');
}
}
/** Resolves the given path from the runfile manifest. */
private _resolveFromManifest(searchPath: 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(`${searchPath}/external`)) continue;
// If the manifest entry fully matches, return the value path without
// considering other manifest entries. We already have an exact match.
if (k === searchPath) {
return v;
}
// Consider a case where `npm/node_modules` is resolved, and we have the following
// manifest: `npm/node_modules/semver/LICENSE /path/to/external/npm/node_modules/semver/LICENSE`
// To resolve the directory, we look for entries that either fully match, or refer to contents
// within the directory we are looking for. We can then subtract the child path to resolve the
// directory. e.g. in the case above we subtract `length(`/semver/LICENSE`)` from the entry value.
if (k.startsWith(`${searchPath}/`)) {
const l = k.length - searchPath.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/main/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) {
// Normalize path by converting to forward slashes and removing all trailing
// forward slashes
modulePath = modulePath.replace(/\\/g, '/').replace(/\/+$/g, '')
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) {
// Normalize path by converting to forward slashes and removing all trailing
// forward slashes
modulePath = modulePath.replace(/\\/g, '/').replace(/\/+$/g, '')
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) {
// Normalize path by converting to forward slashes and removing all trailing
// forward slashes
modulePath = modulePath.replace(/\\/g, '/').replace(/\/+$/g, '')
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._resolveFromManifest(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 || ''));
}
}