Skip to content

Commit 3be2902

Browse files
devversionalexeagle
authored andcommitted
fix(builtin): runfile resolution incorrect if entry starts similarly
Given the following runfile manifest: ``` src/lib/README.md /c/users/paul/projects/test/src/lib/README.md src/lib/README.md.update <..>/bazel-out/<..> ``` If a developer intends to resolve the `README.md` file using the `@bazel/runfiles` package, or the patched module resolution, they will end up with a wrong path (at least on Windows where runfiles are not symlinked). This happens because the `Runfiles.resolve` method continues looking for entries that start similar (even if we have a full-match). This logic helps with finding directories, but breaks resolution with exact matches. This commit also corrects the method name because technically this method is used for more than resolution of directories. BREAKING CHANGE: The `@bazel/runfiles` `lookupDirectory` method has been removed. Use the `resolve` method instead
1 parent 9aa390e commit 3be2902

File tree

8 files changed

+81
-21
lines changed

8 files changed

+81
-21
lines changed

internal/linker/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ function resolveWorkspaceNodeModules(workspace, startCwd, isExecroot, execroot,
145145
if (!execroot) {
146146
return path.resolve(`${startCwd}/../${targetManifestPath}`);
147147
}
148-
const fromManifest = runfiles.lookupDirectory(targetManifestPath);
148+
const fromManifest = runfiles.resolve(targetManifestPath);
149149
if (fromManifest) {
150150
return fromManifest;
151151
}

internal/linker/link_node_modules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ async function resolveWorkspaceNodeModules(
189189
// If we got a runfilesManifest map, look through it for a resolution
190190
// This will happen if we are running a binary that had some npm packages
191191
// "statically linked" into its runfiles
192-
const fromManifest = runfiles.lookupDirectory(targetManifestPath);
192+
const fromManifest = runfiles.resolve(targetManifestPath);
193193
if (fromManifest) {
194194
return fromManifest;
195195
} else {

internal/runfiles/index.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,28 @@ class Runfiles {
7575
this.package = target.split(':')[0].replace(/^\/\//, '');
7676
}
7777
}
78-
lookupDirectory(dir) {
78+
/** Resolves the given path from the runfile manifest. */
79+
_resolveFromManifest(searchPath) {
7980
if (!this.manifest)
8081
return undefined;
8182
let result;
8283
for (const [k, v] of this.manifest) {
8384
// Account for Bazel --legacy_external_runfiles
8485
// which pollutes the workspace with 'my_wksp/external/...'
85-
if (k.startsWith(`${dir}/external`))
86+
if (k.startsWith(`${searchPath}/external`))
8687
continue;
87-
// Entry looks like
88-
// k: npm/node_modules/semver/LICENSE
89-
// v: /path/to/external/npm/node_modules/semver/LICENSE
90-
// calculate l = length(`/semver/LICENSE`)
91-
if (k.startsWith(dir)) {
92-
const l = k.length - dir.length;
88+
// If the manifest entry fully matches, return the value path without
89+
// considering other manifest entries. We already have an exact match.
90+
if (k === searchPath) {
91+
return v;
92+
}
93+
// Consider a case where `npm/node_modules` is resolved, and we have the following
94+
// manifest: `npm/node_modules/semver/LICENSE /path/to/external/npm/node_modules/semver/LICENSE`
95+
// To resolve the directory, we look for entries that either fully match, or refer to contents
96+
// within the directory we are looking for. We can then subtract the child path to resolve the
97+
// directory. e.g. in the case above we subtract `length(`/semver/LICENSE`)` from the entry value.
98+
if (k.startsWith(`${searchPath}/`)) {
99+
const l = k.length - searchPath.length;
93100
const maybe = v.substring(0, v.length - l);
94101
if (maybe.match(paths.BAZEL_OUT_REGEX)) {
95102
return maybe;
@@ -166,7 +173,7 @@ class Runfiles {
166173
/** Helper for resolving a given module recursively in the runfiles. */
167174
_resolve(moduleBase, moduleTail) {
168175
if (this.manifest) {
169-
const result = this.lookupDirectory(moduleBase);
176+
const result = this._resolveFromManifest(moduleBase);
170177
if (result) {
171178
if (moduleTail) {
172179
const maybe = path__default['default'].join(result, moduleTail || '');

packages/runfiles/BUILD.bazel

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ ts_project(
1414
js_library(
1515
name = "bazel_runfiles",
1616
package_name = "@bazel/runfiles",
17-
visibility = ["//internal/linker:__subpackages__"],
17+
visibility = [
18+
"//internal/linker:__subpackages__",
19+
"//packages/runfiles/test:__pkg__",
20+
],
1821
deps = [":runfiles_lib"],
1922
)
2023

packages/runfiles/runfiles.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,29 @@ export class Runfiles {
5555
}
5656
}
5757

58-
lookupDirectory(dir: string): string|undefined {
58+
/** Resolves the given path from the runfile manifest. */
59+
private _resolveFromManifest(searchPath: string): string|undefined {
5960
if (!this.manifest) return undefined;
6061

6162
let result: string|undefined;
6263
for (const [k, v] of this.manifest) {
6364
// Account for Bazel --legacy_external_runfiles
6465
// which pollutes the workspace with 'my_wksp/external/...'
65-
if (k.startsWith(`${dir}/external`)) continue;
66+
if (k.startsWith(`${searchPath}/external`)) continue;
6667

67-
// Entry looks like
68-
// k: npm/node_modules/semver/LICENSE
69-
// v: /path/to/external/npm/node_modules/semver/LICENSE
70-
// calculate l = length(`/semver/LICENSE`)
71-
if (k.startsWith(dir)) {
72-
const l = k.length - dir.length;
68+
// If the manifest entry fully matches, return the value path without
69+
// considering other manifest entries. We already have an exact match.
70+
if (k === searchPath) {
71+
return v;
72+
}
73+
74+
// Consider a case where `npm/node_modules` is resolved, and we have the following
75+
// manifest: `npm/node_modules/semver/LICENSE /path/to/external/npm/node_modules/semver/LICENSE`
76+
// To resolve the directory, we look for entries that either fully match, or refer to contents
77+
// within the directory we are looking for. We can then subtract the child path to resolve the
78+
// directory. e.g. in the case above we subtract `length(`/semver/LICENSE`)` from the entry value.
79+
if (k.startsWith(`${searchPath}/`)) {
80+
const l = k.length - searchPath.length;
7381
const maybe = v.substring(0, v.length - l);
7482
if (maybe.match(BAZEL_OUT_REGEX)) {
7583
return maybe;
@@ -156,7 +164,7 @@ export class Runfiles {
156164
/** Helper for resolving a given module recursively in the runfiles. */
157165
private _resolve(moduleBase: string, moduleTail: string|undefined): string|undefined {
158166
if (this.manifest) {
159-
const result = this.lookupDirectory(moduleBase);
167+
const result = this._resolveFromManifest(moduleBase);
160168
if (result) {
161169
if (moduleTail) {
162170
const maybe = path.join(result, moduleTail || '');

packages/runfiles/test/BUILD.bazel

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
load("//packages/jasmine:index.bzl", "jasmine_node_test")
2+
3+
jasmine_node_test(
4+
name = "test",
5+
srcs = ["runfile_resolution.spec.js"],
6+
data = [
7+
"test_fixture.md",
8+
":test_fixture.md.generated_file_suffix",
9+
"//packages/runfiles:bazel_runfiles",
10+
],
11+
)
12+
13+
# Path of file must start similar to `test_fixture.md` in order to regression-test a
14+
# scenario where the runfile resolution would accidentally resolve the path to
15+
# `test_fixture.md` through a runfile manifest entry that starts similarly.
16+
genrule(
17+
name = "test_fixture.md.generated_file_suffix",
18+
outs = ["test_fixture.md.generated_file_suffix"],
19+
cmd = """echo "Generated" > $@""",
20+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const {join} = require('path')
2+
const {runfiles} = require('@bazel/runfiles');
3+
4+
describe('runfile resolution', () => {
5+
6+
it('should properly resolve the "test_fixture.md" file', () => {
7+
const testFixturePath = runfiles.resolve('build_bazel_rules_nodejs/packages/runfiles/test/test_fixture.md');
8+
const expectedPath = join(__dirname, 'test_fixture.md');
9+
10+
expect(normalizePath(testFixturePath)).toEqual(normalizePath(expectedPath),
11+
'Expected the test fixture to be resolved next to the spec source file.');
12+
});
13+
});
14+
15+
/**
16+
* Normalizes the delimiters within the specified path. This is useful for test assertions
17+
* where paths might be computed using different path delimiters.
18+
*/
19+
function normalizePath(value) {
20+
return value.replace(/\\/g, '/');
21+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is a file part of the sources

0 commit comments

Comments
 (0)