Skip to content

Commit 5925c17

Browse files
committed
Support remapping into runfiles tree when requested
1 parent 938480e commit 5925c17

File tree

2 files changed

+90
-15
lines changed

2 files changed

+90
-15
lines changed

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,8 +616,20 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
616616
) {
617617
const bindir = process.env.BAZEL_BINDIR;
618618
const execroot = process.env.JS_BINARY__EXECROOT;
619+
620+
let runfiles: string | undefined;
621+
// If requested, remap paths to the runfiles tree in the sandbox instead of the bindir
622+
// directly. This allows `js_binary` and `js_test` rules to invoke the Angular CLI against
623+
// their runfiles.
624+
if (
625+
process.env.BAZEL_SANDBOX_PLUGIN_REMAP_TO_RUNFILES === '1' ||
626+
process.env.BAZEL_SANDBOX_PLUGIN_REMAP_TO_RUNFILES === 'true'
627+
) {
628+
runfiles = process.env.JS_BINARY__RUNFILES;
629+
}
630+
619631
if (bindir && execroot) {
620-
plugins.push(createBazelSandboxPlugin({ bindir, execroot }));
632+
plugins.push(createBazelSandboxPlugin({ bindir, execroot, runfiles }));
621633
}
622634
}
623635

packages/angular/build/src/tools/esbuild/sandbox-plugin-bazel.ts

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,23 @@
1212

1313
import type { OnResolveResult, Plugin, PluginBuild, ResolveOptions } from 'esbuild';
1414
import { stat } from 'node:fs/promises';
15-
import { join } from 'node:path';
15+
import path, { join } from 'node:path';
1616

1717
export interface CreateBazelSandboxPluginOptions {
1818
bindir: string;
1919
execroot: string;
20+
runfiles?: string;
2021
}
2122

22-
// Under Bazel, esbuild will follow symlinks out of the sandbox when the sandbox is enabled. See https://github.com/aspect-build/rules_esbuild/issues/58.
23-
// This plugin using a separate resolver to detect if the the resolution has left the execroot (which is the root of the sandbox
24-
// when sandboxing is enabled) and patches the resolution back into the sandbox.
23+
// Under Bazel, esbuild will follow symlinks out of the sandbox when the sandbox
24+
// is enabled. See https://github.com/aspect-build/rules_esbuild/issues/58. This
25+
// plugin using a separate resolver to detect if the the resolution has left the
26+
// execroot (which is the root of the sandbox when sandboxing is enabled) and
27+
// patches the resolution back into the sandbox.
2528
export function createBazelSandboxPlugin({
2629
bindir,
2730
execroot,
31+
runfiles,
2832
}: CreateBazelSandboxPluginOptions): Plugin {
2933
return {
3034
name: 'bazel-sandbox',
@@ -40,7 +44,14 @@ export function createBazelSandboxPlugin({
4044
}
4145
otherOptions.pluginData.executedSandboxPlugin = true;
4246

43-
return await resolveInExecroot({ build, bindir, execroot, importPath, otherOptions });
47+
return await resolveInExecroot({
48+
build,
49+
bindir,
50+
execroot,
51+
runfiles,
52+
importPath,
53+
otherOptions,
54+
});
4455
});
4556
},
4657
};
@@ -50,14 +61,31 @@ interface ResolveInExecrootOptions {
5061
build: PluginBuild;
5162
bindir: string;
5263
execroot: string;
64+
runfiles?: string;
5365
importPath: string;
5466
otherOptions: ResolveOptions;
5567
}
5668

69+
const EXTERNAL_PREFIX = 'external/';
70+
71+
function removeExternalPathPrefix(filePath: string): string {
72+
// Normalize to relative path without leading slash.
73+
if (filePath.startsWith('/')) {
74+
filePath = filePath.substring(1);
75+
}
76+
// Remove the EXTERNAL_PREFIX if present.
77+
if (filePath.startsWith(EXTERNAL_PREFIX)) {
78+
filePath = filePath.substring(EXTERNAL_PREFIX.length);
79+
}
80+
81+
return filePath;
82+
}
83+
5784
async function resolveInExecroot({
5885
build,
5986
bindir,
6087
execroot,
88+
runfiles,
6189
importPath,
6290
otherOptions,
6391
}: ResolveInExecrootOptions): Promise<OnResolveResult> {
@@ -73,20 +101,53 @@ async function resolveInExecroot({
73101
!result.path.startsWith('/') &&
74102
!result.path.startsWith('\\')
75103
) {
76-
// Not a relative or absolute path. Likely a module resolution that is marked "external"
104+
// Not a relative or absolute path. Likely a module resolution that is
105+
// marked "external"
77106
return result;
78107
}
79108

80-
// If esbuild attempts to leave the execroot, map the path back into the execroot.
109+
// If esbuild attempts to leave the execroot, map the path back into the
110+
// execroot.
81111
if (!result.path.startsWith(execroot)) {
82112
// If it tried to leave bazel-bin, error out completely.
83113
if (!result.path.includes(bindir)) {
84114
throw new Error(
85115
`Error: esbuild resolved a path outside of BAZEL_BINDIR (${bindir}): ${result.path}`,
86116
);
87117
}
88-
// Otherwise remap the bindir-relative path
89-
const correctedPath = join(execroot, result.path.substring(result.path.indexOf(bindir)));
118+
// Get the path under the bindir for the file. This allows us to map into
119+
// the execroot or the runfiles directory (if present).
120+
// Example:
121+
// bindir = bazel-out/<arch>/bin
122+
// result.path = <base>/execroot/bazel-out/<arch>/bin/external/repo+/path/file.ts
123+
// binDirRelativePath = external/repo+/path/file.ts
124+
const binDirRelativePath = result.path.substring(
125+
result.path.indexOf(bindir) + bindir.length + 1,
126+
);
127+
// We usually remap into the bindir. However, when sources are provided
128+
// as `data` (runfiles), they will be in the runfiles root instead. The
129+
// runfiles path is absolute and under the bindir, so we don't need to
130+
// join anything to it. The execroot does not include the bindir, so there
131+
// we add it again after previously removing it from the result path.
132+
const remapBase = runfiles ?? path.join(execroot, bindir);
133+
// The path relative to the remapBase also differs between runfiles and
134+
// bindir, but only if the file is in an external repository. External
135+
// repositories appear under `external/repo+` in the bindir, whereas they
136+
// are directly under `repo+` in the runfiles tree. This difference needs
137+
// to be accounted for by removing a potential `external/` prefix when
138+
// mapping into runfiles.
139+
const remapBaseRelativePath = runfiles
140+
? removeExternalPathPrefix(binDirRelativePath)
141+
: binDirRelativePath;
142+
// Join the paths back together. The results will look slightly different
143+
// between runfiles and bindir, but this is intentional.
144+
// Source path:
145+
// <bin>/external/repo+/path/file.ts
146+
// Example in bindir:
147+
// <sandbox-bin>/external/repo+/path/file.ts
148+
// Example in runfiles:
149+
// <sandbox-bin>/path/bin.runfiles/repo+/path/file.ts
150+
const correctedPath = join(remapBase, remapBaseRelativePath);
90151
if (process.env.JS_BINARY__LOG_DEBUG) {
91152
// eslint-disable-next-line no-console
92153
console.error(
@@ -95,12 +156,14 @@ async function resolveInExecroot({
95156
}
96157
result.path = correctedPath;
97158

98-
// Fall back to `.js` file if resolved `.ts` file does not exist in the changed path.
159+
// Fall back to `.js` file if resolved `.ts` file does not exist in the
160+
// changed path.
99161
//
100-
// It's possible that a `.ts` file exists outside the sandbox and esbuild resolves it. It's not
101-
// guaranteed that the sandbox also contains the same file. One example might be that the build
102-
// depend on a compiled version of the file and the sandbox will only contain the corresponding
103-
// `.js` and `.d.ts` files.
162+
// It's possible that a `.ts` file exists outside the sandbox and esbuild
163+
// resolves it. It is not guaranteed that the sandbox also contains the same
164+
// file. One example might be that the build depends on a compiled version
165+
// of the file and the sandbox will only contain the corresponding `.js` and
166+
// `.d.ts` files.
104167
if (result.path.endsWith('.ts')) {
105168
try {
106169
await stat(result.path);

0 commit comments

Comments
 (0)