1// Copyright 2024 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15import * as fs from 'fs'; 16import * as path from 'path'; 17 18import { glob } from 'glob'; 19 20import type { Settings, WorkingDirStore } from './settings'; 21 22const PIGWEED_JSON = 'pigweed.json' as const; 23 24/** 25 * Find the path to pigweed.json by searching this directory and above. 26 27 * This starts looking in the current working directory, then recursively in 28 * each directory above the current working directory, until it finds a 29 * pigweed.json file or reaches the file system root. So invoking this anywhere 30 * within a Pigweed project directory should work. 31 * a path to pigweed.json searching this dir and all parent dirs. 32 */ 33export function findPigweedJsonAbove(workingDir: string): string | null { 34 const candidatePath = path.join(workingDir, PIGWEED_JSON); 35 36 // This dir is the Pigweed root. We're done. 37 if (fs.existsSync(candidatePath)) return workingDir; 38 39 // We've reached the root of the file system without finding a Pigweed root. 40 // We're done, but sadly. 41 if (path.dirname(workingDir) === workingDir) return null; 42 43 // Try again in the parent dir. 44 return findPigweedJsonAbove(path.dirname(workingDir)); 45} 46 47/** 48 * Find paths to pigweed.json in dirs below this one. 49 * 50 * This uses a glob search to find all paths to pigweed.json below the current 51 * working dir. Usually there shouldn't be more than one. 52 */ 53export async function findPigweedJsonBelow( 54 workingDir: string, 55): Promise<string[]> { 56 const candidatePaths = await glob(`**/${PIGWEED_JSON}`, { 57 absolute: true, 58 cwd: workingDir, 59 // Ignore the source file that pigwed.json is generated from. 60 ignore: '**/pw_env_setup/**', 61 }); 62 63 return candidatePaths.map((p) => path.dirname(p)); 64} 65 66/** 67 * Find the Pigweed root dir within the project. 68 * 69 * The presence of a pigweed.json file is the sentinel for the Pigweed root. 70 * The heuristic is to first search in the current directory or above ("are 71 * we inside of a Pigweed directory?"), and failing that, to search in the 72 * directories below ("does this project contain a Pigweed directory?"). 73 * 74 * Note that this logic presumes that there's only one Pigweed project 75 * directory. In a hypothetical project setup that contained multiple Pigweed 76 * projects, this would continue to work when invoked inside of one of those 77 * Pigweed directories, but would have inconsistent results when invoked 78 * in a parent directory. 79 */ 80export async function inferPigweedProjectRoot( 81 workingDir: string, 82): Promise<string | null> { 83 const rootAbove = findPigweedJsonAbove(workingDir); 84 if (rootAbove) return rootAbove; 85 86 const rootsBelow = await findPigweedJsonBelow(workingDir); 87 if (rootsBelow) return rootsBelow[0]; 88 89 return null; 90} 91 92/** 93 * Return the path to the Pigweed root dir. 94 * 95 * If the path is specified in the `pigweed.projectRoot` setting, that path 96 * will be returned regardless of whether it actually exists or not, or whether 97 * it actually is a Pigweed root dir. We trust the user! But if that setting is 98 * not set, we search for the Pigweed root by looking for the presence of 99 * pigweed.json. 100 */ 101export async function getPigweedProjectRoot( 102 settings: Settings, 103 workingDir: WorkingDirStore, 104): Promise<string | null> { 105 if (!settings.projectRoot()) { 106 return inferPigweedProjectRoot(workingDir.get()); 107 } 108 109 return settings.projectRoot()!; 110} 111 112const BOOTSTRAP_SH = 'bootstrap.sh' as const; 113 114export function getBootstrapScript(projectRoot: string): string { 115 return path.join(projectRoot, BOOTSTRAP_SH); 116} 117 118/** If a project has a bootstrap script, we treat it as a bootstrap project. */ 119export function isBootstrapProject(projectRoot: string): boolean { 120 return fs.existsSync(getBootstrapScript(projectRoot)); 121} 122 123const WORKSPACE = 'WORKSPACE' as const; 124const BZLMOD = 'MODULE.bazel' as const; 125 126export function getWorkspaceFile(projectRoot: string): string { 127 return path.join(projectRoot, WORKSPACE); 128} 129 130export function getBzlmodFile(projectRoot: string): string { 131 return path.join(projectRoot, BZLMOD); 132} 133 134/** 135 * It's a Bazel project if it has a `WORKSPACE` file or bzlmod file, but not 136 * a bootstrap script. 137 */ 138export function isBazelWorkspaceProject(projectRoot: string): boolean { 139 return ( 140 !isBootstrapProject(projectRoot) && 141 (fs.existsSync(getWorkspaceFile(projectRoot)) || 142 fs.existsSync(getBzlmodFile(projectRoot))) 143 ); 144} 145