Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runfiles): support bzlmod repo mappings #3771

Merged
merged 1 commit into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/runfiles/paths.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// 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\/)/;

// The runfiles root symlink under which the repository mapping can be found.
// https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/Runfiles.java;l=424
export const REPO_MAPPING_RLOCATION = '_repo_mapping';
43 changes: 43 additions & 0 deletions packages/runfiles/repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Utils for managing repository mappings.
*
* The majority of this code is ported from [rules_go](https://github.com/bazelbuild/rules_go/pull/3347).
*/

export interface RepoMappings {
[sourceRepo: string]: {
[targetRepoApparentName: string]: string;
};
}

const legacyExternalGeneratedFile =
/\/_main\/bazel-out\/[^/]+\/bin\/external\/([^/]+)\//;
const legacyExternalFile = /\/_main\/external\/([^/]+)\//;

// CurrentRepository returns the canonical name of the Bazel repository that
// contains the source file of the caller of CurrentRepository.
export function currentRepository(): string {
return callerRepositoryFromStack(1);
}

// CallerRepository returns the canonical name of the Bazel repository that
// contains the source file of the caller of the function that itself calls
// CallerRepository.
export function callerRepository(): string {
return callerRepositoryFromStack(2);
}

export function callerRepositoryFromStack(skip: number): string {
const stack = new Error().stack.split("\n");
const file = stack[skip + 2]; // 0 is the Error(msg), 1 is this method, 2 is the caller
const match =
file.match(legacyExternalGeneratedFile) || file.match(legacyExternalFile);

// If a file is not in an external repository, it is in the main repository,
// which has the empty string as its canonical name.
if (!match || match[0] == "_main") {
return "";
}

return match[0];
}
84 changes: 78 additions & 6 deletions packages/runfiles/runfiles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as fs from 'fs';
import * as path from 'path';

import {BAZEL_OUT_REGEX} from './paths';
import { BAZEL_OUT_REGEX, REPO_MAPPING_RLOCATION } from './paths';
import { RepoMappings, callerRepository } from './repository';

/**
* Class that provides methods for resolving Bazel runfiles.
Expand All @@ -17,8 +18,12 @@ export class Runfiles {
* If the environment gives us enough hints, we can know the package path
*/
package: string|undefined;
/**
* If the environment has repo mappings, we can use them to resolve repo relative paths.
*/
repoMappings: RepoMappings|undefined;

constructor(private _env: typeof process.env) {
constructor(private _env = 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
Expand All @@ -29,8 +34,10 @@ export class Runfiles {
this.manifest = this.loadRunfilesManifest(_env['RUNFILES_MANIFEST_FILE']!);
} else if (!!_env['RUNFILES_DIR']) {
this.runfilesDir = path.resolve(_env['RUNFILES_DIR']!);
this.repoMappings = this.parseRepoMapping(this.runfilesDir);
} else if (!!_env['RUNFILES']) {
this.runfilesDir = path.resolve(_env['RUNFILES']!);
this.repoMappings = this.parseRepoMapping(this.runfilesDir);
} else {
throw new Error(
'Every node program run under Bazel must have a $RUNFILES_DIR, $RUNFILES or $RUNFILES_MANIFEST_FILE environment variable');
Expand Down Expand Up @@ -116,15 +123,61 @@ export class Runfiles {
return runfilesEntries;
}

parseRepoMapping(runfilesDir: string): RepoMappings | undefined {
const repoMappingPath = path.join(runfilesDir, REPO_MAPPING_RLOCATION);

if (!fs.existsSync(repoMappingPath)) {
// The repo mapping manifest only exists with Bzlmod, so it's not an
// error if it's missing. Since any repository name not contained in the
// mapping is assumed to be already canonical, an empty map is
// equivalent to not applying any mapping.
return Object.create(null);
}

const repoMappings: RepoMappings = Object.create(null);
const mappings = fs.readFileSync(repoMappingPath, { encoding: "utf-8" });

// Each line of the repository mapping manifest has the form:
// canonical name of source repo,apparent name of target repo,target repo runfiles directory
// https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/RepoMappingManifestAction.java;l=117
for (const line of mappings.split("\n")) {
if (!line) continue;

const [sourceRepo, targetRepoApparentName, targetRepoDirectory] = line.split(",");

(repoMappings[sourceRepo] ??= Object.create(null))[targetRepoApparentName] = targetRepoDirectory;
}
return repoMappings;
}

/** Resolves the given module path. */
resolve(modulePath: string) {
resolve(modulePath: string, sourceRepo?: string): 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 (this.repoMappings) {
// Determine the repository which runfiles is being invoked from by default.
if (sourceRepo === undefined) {
sourceRepo = callerRepository();
}

// If the repository mappings were loaded ensure the source repository is valid.
if (!(sourceRepo in this.repoMappings)) {
throw new Error(
`source repository ${sourceRepo} not found in repo mappings: ${JSON.stringify(
this.repoMappings,
null,
2,
)}`,
);
}
}

const result = this._resolve(sourceRepo, modulePath, undefined);
if (result) {
return result;
}
Expand Down Expand Up @@ -175,7 +228,11 @@ export class Runfiles {
}

/** Helper for resolving a given module recursively in the runfiles. */
private _resolve(moduleBase: string, moduleTail: string|undefined): string|undefined {
private _resolve(
sourceRepo: string,
moduleBase: string,
moduleTail: string | undefined,
): string | undefined {
if (this.manifest) {
const result = this._resolveFromManifest(moduleBase);
if (result) {
Expand All @@ -189,6 +246,17 @@ export class Runfiles {
}
}
}

// Apply repo mappings to the moduleBase if it is a known repo.
if (this.repoMappings && moduleBase in this.repoMappings[sourceRepo]) {
const mappedRepo = this.repoMappings[sourceRepo][moduleBase];
if (mappedRepo !== moduleBase) {
const maybe = this._resolve(sourceRepo, mappedRepo, moduleTail);
if (maybe !== undefined) {
return maybe;
}
}
}
if (this.runfilesDir) {
const maybe = path.join(this.runfilesDir, moduleBase, moduleTail || '');
if (fs.existsSync(maybe)) {
Expand All @@ -200,6 +268,10 @@ export class Runfiles {
// no match
return undefined;
}
return this._resolve(dirname, path.join(path.basename(moduleBase), moduleTail || ''));
return this._resolve(
sourceRepo,
dirname,
path.join(path.basename(moduleBase), moduleTail || ""),
);
}
}