import upath from 'upath'; import { logger } from '../../../logger/index.ts'; import { isNotNullOrUndefined } from '../../../util/array.ts'; import * as fs from '../../../util/fs/index.ts'; import { regEx } from '../../../util/regex.ts'; const importRegex = regEx(`^(?(?:try-)?import)\\s+(?\\S+)$`); const optionRegex = regEx( `^(?\\w+)(:(?\\S+))?\\s+(?.*)$`, ); const spaceRegex = regEx(`\\s+`); export class ImportEntry { readonly entryType = 'import'; readonly path: string; readonly isTry: boolean; constructor(path: string, isTry: boolean) { this.path = path; this.isTry = isTry; } } export class BazelOption { readonly name: string; readonly value?: string; constructor(name: string, value?: string) { this.name = name; this.value = value; } static parse(input: string): BazelOption[] { const options: BazelOption[] = []; const parts = input.split(spaceRegex); for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (!part.startsWith('--')) { continue; } const nameStartIdx = 2; // Check for --option_name=option_value const equalSignIdx = part.indexOf('='); if (equalSignIdx >= 0) { const name = part.substring(nameStartIdx, equalSignIdx); const value = part.substring(equalSignIdx + 1); options.push(new BazelOption(name, value)); continue; } const name = part.substring(nameStartIdx); const nextIdx = i + 1; // Check for --option_name OR --option_name option_value const value = nextIdx < parts.length && !parts[nextIdx].startsWith('--') ? parts[nextIdx] : undefined; options.push(new BazelOption(name, value)); } return options; } } export class CommandEntry { readonly entryType = 'command'; readonly command: string; readonly options: BazelOption[]; readonly config?: string; constructor(command: string, options: BazelOption[], config?: string) { this.command = command; this.options = options; this.config = config; } getOption(name: string): BazelOption | undefined { return this.options.find((bo) => bo.name === name); } } type BazelrcEntries = ImportEntry | CommandEntry; function shouldProcessLine(line: string): boolean { if (line.length === 0) { return false; } return !line.startsWith('#'); } function createEntry(line: string): BazelrcEntries | undefined { const importResult = importRegex.exec(line); if (importResult?.groups) { const irGroups = importResult.groups; return new ImportEntry(irGroups.path, irGroups.type === 'try-import'); } const optionResult = optionRegex.exec(line); if (optionResult?.groups) { const orGroups = optionResult.groups; return new CommandEntry( orGroups.command, BazelOption.parse(orGroups.options), orGroups.config, ); } return undefined; } export function expandWorkspacePath( value: string, workspaceDir: string, ): string | null { if (!value.includes('%workspace%')) { return value; } const absolutePath = upath.resolve(workspaceDir); const expandedPath = value.replace('%workspace%', absolutePath); if (!fs.isValidLocalPath(expandedPath)) { return null; } return expandedPath; } export function sanitizeOptions( options: BazelOption[], workspaceDir: string, ): BazelOption[] { return options .map((option) => { if (!option.value) { return option; } const expandedPath = expandWorkspacePath(option.value, workspaceDir); if (!expandedPath) { logger.debug( `Skipping invalid workspace path: ${option.value} in ${workspaceDir}`, ); return null; } return new BazelOption(option.name, expandedPath); }) .filter(isNotNullOrUndefined); } export function parse(contents: string): BazelrcEntries[] { return contents .split('\n') .map((l) => l.trim()) .filter(shouldProcessLine) .map(createEntry) .filter(isNotNullOrUndefined); } async function readFile( file: string, workspaceDir: string, readFiles: Set, ): Promise { if (readFiles.has(file)) { throw new Error( `Attempted to read a bazelrc multiple times. file: ${file}`, ); } readFiles.add(file); const contents = await fs.readLocalFile(file, 'utf8'); if (!contents) { return []; } const entries = parse(contents); const results: CommandEntry[] = []; for (const entry of entries) { if (entry.entryType === 'command') { const sanitizedOptions = sanitizeOptions(entry.options, workspaceDir); results.push( new CommandEntry(entry.command, sanitizedOptions, entry.config), ); continue; } const importFile = upath.normalize( entry.path.replace('%workspace%', workspaceDir), ); if (fs.isValidLocalPath(importFile)) { const importEntries = await readFile(importFile, workspaceDir, readFiles); results.push(...importEntries); } else { logger.debug(`Skipping non-local .bazelrc import ${importFile}`); } } return results; } export async function read(workspaceDir: string): Promise { const bazelrcPath = upath.join(workspaceDir, '.bazelrc'); const readFiles = new Set(); return await readFile(bazelrcPath, workspaceDir, readFiles); }