xref: /aosp_15_r20/build/bazel/scripts/incremental_build/finder.py (revision 7594170e27e0732bc44b93d1440d87a54b6ffe7c)
1*7594170eSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*7594170eSAndroid Build Coastguard Worker# Copyright (C) 2023 The Android Open Source Project
3*7594170eSAndroid Build Coastguard Worker#
4*7594170eSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
5*7594170eSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
6*7594170eSAndroid Build Coastguard Worker# You may obtain a copy of the License at
7*7594170eSAndroid Build Coastguard Worker#
8*7594170eSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
9*7594170eSAndroid Build Coastguard Worker#
10*7594170eSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
11*7594170eSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
12*7594170eSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*7594170eSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
14*7594170eSAndroid Build Coastguard Worker# limitations under the License.
15*7594170eSAndroid Build Coastguard Workerimport argparse
16*7594170eSAndroid Build Coastguard Workerimport glob
17*7594170eSAndroid Build Coastguard Workerimport os
18*7594170eSAndroid Build Coastguard Workerimport subprocess
19*7594170eSAndroid Build Coastguard Workerfrom pathlib import Path
20*7594170eSAndroid Build Coastguard Workerimport textwrap
21*7594170eSAndroid Build Coastguard Workerfrom typing import Generator, Iterator
22*7594170eSAndroid Build Coastguard Worker
23*7594170eSAndroid Build Coastguard Workerfrom util import get_out_dir
24*7594170eSAndroid Build Coastguard Workerfrom util import get_top_dir
25*7594170eSAndroid Build Coastguard Worker
26*7594170eSAndroid Build Coastguard Worker
27*7594170eSAndroid Build Coastguard Workerdef is_git_repo(p: Path) -> bool:
28*7594170eSAndroid Build Coastguard Worker    """checks if p is in a directory that's under git version control"""
29*7594170eSAndroid Build Coastguard Worker    git = subprocess.run(
30*7594170eSAndroid Build Coastguard Worker        args=f"git remote".split(),
31*7594170eSAndroid Build Coastguard Worker        cwd=p,
32*7594170eSAndroid Build Coastguard Worker        stdout=subprocess.DEVNULL,
33*7594170eSAndroid Build Coastguard Worker        stderr=subprocess.DEVNULL,
34*7594170eSAndroid Build Coastguard Worker    )
35*7594170eSAndroid Build Coastguard Worker    return git.returncode == 0
36*7594170eSAndroid Build Coastguard Worker
37*7594170eSAndroid Build Coastguard Worker
38*7594170eSAndroid Build Coastguard Workerdef confirm(root_dir: Path, *globs: str):
39*7594170eSAndroid Build Coastguard Worker    for g in globs:
40*7594170eSAndroid Build Coastguard Worker        disallowed = g.startswith("!")
41*7594170eSAndroid Build Coastguard Worker        if disallowed:
42*7594170eSAndroid Build Coastguard Worker            g = g[1:]
43*7594170eSAndroid Build Coastguard Worker        paths = glob.iglob(g, root_dir=root_dir, recursive=True)
44*7594170eSAndroid Build Coastguard Worker        path = next(paths, None)
45*7594170eSAndroid Build Coastguard Worker        if disallowed:
46*7594170eSAndroid Build Coastguard Worker            if path is not None:
47*7594170eSAndroid Build Coastguard Worker                raise RuntimeError(f"{root_dir}/{path} unexpected")
48*7594170eSAndroid Build Coastguard Worker        else:
49*7594170eSAndroid Build Coastguard Worker            if path is None:
50*7594170eSAndroid Build Coastguard Worker                raise RuntimeError(f"{root_dir}/{g} doesn't match")
51*7594170eSAndroid Build Coastguard Worker
52*7594170eSAndroid Build Coastguard Worker
53*7594170eSAndroid Build Coastguard Workerdef _should_visit(c: os.DirEntry) -> bool:
54*7594170eSAndroid Build Coastguard Worker    return c.is_dir() and not (
55*7594170eSAndroid Build Coastguard Worker        c.is_symlink()
56*7594170eSAndroid Build Coastguard Worker        or "." in c.name
57*7594170eSAndroid Build Coastguard Worker        or "test" in c.name
58*7594170eSAndroid Build Coastguard Worker        or Path(c.path) == get_out_dir()
59*7594170eSAndroid Build Coastguard Worker    )
60*7594170eSAndroid Build Coastguard Worker
61*7594170eSAndroid Build Coastguard Worker
62*7594170eSAndroid Build Coastguard Workerdef find_matches(root_dir: Path, *globs: str) -> Generator[Path, None, None]:
63*7594170eSAndroid Build Coastguard Worker    """
64*7594170eSAndroid Build Coastguard Worker    Finds sub-paths satisfying the patterns
65*7594170eSAndroid Build Coastguard Worker    :param root_dir the first directory to start searching from
66*7594170eSAndroid Build Coastguard Worker    :param globs glob patterns to require or disallow (if starting with "!")
67*7594170eSAndroid Build Coastguard Worker    :returns dirs satisfying the glbos
68*7594170eSAndroid Build Coastguard Worker    """
69*7594170eSAndroid Build Coastguard Worker    bfs: list[Path] = [root_dir]
70*7594170eSAndroid Build Coastguard Worker    while len(bfs) > 0:
71*7594170eSAndroid Build Coastguard Worker        first = bfs.pop(0)
72*7594170eSAndroid Build Coastguard Worker        if is_git_repo(first):
73*7594170eSAndroid Build Coastguard Worker            try:
74*7594170eSAndroid Build Coastguard Worker                confirm(first, *globs)
75*7594170eSAndroid Build Coastguard Worker                yield first
76*7594170eSAndroid Build Coastguard Worker            except RuntimeError:
77*7594170eSAndroid Build Coastguard Worker                pass
78*7594170eSAndroid Build Coastguard Worker        children = [Path(c.path) for c in os.scandir(first) if _should_visit(c)]
79*7594170eSAndroid Build Coastguard Worker        children.sort()
80*7594170eSAndroid Build Coastguard Worker        bfs.extend(children)
81*7594170eSAndroid Build Coastguard Worker
82*7594170eSAndroid Build Coastguard Worker
83*7594170eSAndroid Build Coastguard Workerdef main():
84*7594170eSAndroid Build Coastguard Worker    p = argparse.ArgumentParser(
85*7594170eSAndroid Build Coastguard Worker        formatter_class=argparse.RawTextHelpFormatter,
86*7594170eSAndroid Build Coastguard Worker        description=textwrap.dedent(
87*7594170eSAndroid Build Coastguard Worker            f"""\
88*7594170eSAndroid Build Coastguard Worker            A utility to find a directory that have descendants that satisfy
89*7594170eSAndroid Build Coastguard Worker            specified required glob patterns and have no descendent that
90*7594170eSAndroid Build Coastguard Worker            contradict any specified disallowed glob pattern.
91*7594170eSAndroid Build Coastguard Worker
92*7594170eSAndroid Build Coastguard Worker            Example:
93*7594170eSAndroid Build Coastguard Worker                {Path(__file__).name} '**/Android.bp'
94*7594170eSAndroid Build Coastguard Worker                {Path(__file__).name} '!**/BUILD' '**/Android.bp'
95*7594170eSAndroid Build Coastguard Worker
96*7594170eSAndroid Build Coastguard Worker            Don't forget to SINGLE QUOTE patterns to avoid shell glob expansion!
97*7594170eSAndroid Build Coastguard Worker            """
98*7594170eSAndroid Build Coastguard Worker        ),
99*7594170eSAndroid Build Coastguard Worker    )
100*7594170eSAndroid Build Coastguard Worker    p.add_argument(
101*7594170eSAndroid Build Coastguard Worker        "globs",
102*7594170eSAndroid Build Coastguard Worker        nargs="+",
103*7594170eSAndroid Build Coastguard Worker        help="""glob patterns to require or disallow(if preceded with "!")""",
104*7594170eSAndroid Build Coastguard Worker    )
105*7594170eSAndroid Build Coastguard Worker    p.add_argument(
106*7594170eSAndroid Build Coastguard Worker        "--root_dir",
107*7594170eSAndroid Build Coastguard Worker        "-r",
108*7594170eSAndroid Build Coastguard Worker        type=lambda s: Path(s).resolve(),
109*7594170eSAndroid Build Coastguard Worker        default=get_top_dir(),
110*7594170eSAndroid Build Coastguard Worker        help=textwrap.dedent(
111*7594170eSAndroid Build Coastguard Worker            """\
112*7594170eSAndroid Build Coastguard Worker                top dir to interpret the glob patterns relative to
113*7594170eSAndroid Build Coastguard Worker                defaults to %(default)s
114*7594170eSAndroid Build Coastguard Worker            """
115*7594170eSAndroid Build Coastguard Worker        ),
116*7594170eSAndroid Build Coastguard Worker    )
117*7594170eSAndroid Build Coastguard Worker    p.add_argument(
118*7594170eSAndroid Build Coastguard Worker        "--max",
119*7594170eSAndroid Build Coastguard Worker        "-m",
120*7594170eSAndroid Build Coastguard Worker        type=int,
121*7594170eSAndroid Build Coastguard Worker        default=1,
122*7594170eSAndroid Build Coastguard Worker        help=textwrap.dedent(
123*7594170eSAndroid Build Coastguard Worker            """\
124*7594170eSAndroid Build Coastguard Worker                maximum number of matching directories to show
125*7594170eSAndroid Build Coastguard Worker                defaults to %(default)s
126*7594170eSAndroid Build Coastguard Worker            """
127*7594170eSAndroid Build Coastguard Worker        ),
128*7594170eSAndroid Build Coastguard Worker    )
129*7594170eSAndroid Build Coastguard Worker    options = p.parse_args()
130*7594170eSAndroid Build Coastguard Worker    results = find_matches(options.root_dir, *options.globs)
131*7594170eSAndroid Build Coastguard Worker    max: int = options.max
132*7594170eSAndroid Build Coastguard Worker    while max > 0:
133*7594170eSAndroid Build Coastguard Worker        max -= 1
134*7594170eSAndroid Build Coastguard Worker        path = next(results, None)
135*7594170eSAndroid Build Coastguard Worker        if path is None:
136*7594170eSAndroid Build Coastguard Worker            break
137*7594170eSAndroid Build Coastguard Worker        print(f"{path.relative_to(get_top_dir())}")
138*7594170eSAndroid Build Coastguard Worker
139*7594170eSAndroid Build Coastguard Worker
140*7594170eSAndroid Build Coastguard Workerif __name__ == "__main__":
141*7594170eSAndroid Build Coastguard Worker    main()
142