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