xref: /aosp_15_r20/external/pigweed/pw_ide/ts/pigweed-vscode/src/project.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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