xref: /aosp_15_r20/external/bazel-skylib/rules/directory/private/glob.bzl (revision bcb5dc7965af6ee42bf2f21341a2ec00233a8c8a)
1# Copyright 2024 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://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,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Skylib module containing glob operations on directories."""
16
17_NO_GLOB_MATCHES = "{glob} failed to match any files in {dir}"
18
19def transitive_entries(directory):
20    """Returns the files and directories contained within a directory transitively.
21
22    Args:
23        directory: (DirectoryInfo) The directory to look at
24
25    Returns:
26        List[Either[DirectoryInfo, File]] The entries contained within.
27    """
28    entries = [directory]
29    stack = [directory]
30    for _ in range(99999999):
31        if not stack:
32            return entries
33        d = stack.pop()
34        for entry in d.entries.values():
35            entries.append(entry)
36            if type(entry) != "File":
37                stack.append(entry)
38
39    fail("Should never get to here")
40
41def directory_glob_chunk(directory, chunk):
42    """Given a directory and a chunk of a glob, returns possible candidates.
43
44    Args:
45        directory: (DirectoryInfo) The directory to look relative from.
46        chunk: (string) A chunk of a glob to look at.
47
48    Returns:
49        List[Either[DirectoryInfo, File]]] The candidate next entries for the chunk.
50    """
51    if chunk == "*":
52        return directory.entries.values()
53    elif chunk == "**":
54        return transitive_entries(directory)
55    elif "*" not in chunk:
56        if chunk in directory.entries:
57            return [directory.entries[chunk]]
58        else:
59            return []
60    elif chunk.count("*") > 2:
61        fail("glob chunks with more than two asterixes are unsupported. Got", chunk)
62
63    if chunk.count("*") == 2:
64        left, middle, right = chunk.split("*")
65    else:
66        middle = ""
67        left, right = chunk.split("*")
68    entries = []
69    for name, entry in directory.entries.items():
70        if name.startswith(left) and name.endswith(right) and len(left) + len(right) <= len(name) and middle in name[len(left):len(name) - len(right)]:
71            entries.append(entry)
72    return entries
73
74def directory_single_glob(directory, glob):
75    """Calculates all files that are matched by a glob on a directory.
76
77    Args:
78        directory: (DirectoryInfo) The directory to look relative from.
79        glob: (string) A glob to match.
80
81    Returns:
82        List[File] A list of files that match.
83    """
84
85    # Treat a glob as a nondeterministic finite state automata. We can be in
86    # multiple places at the one time.
87    candidate_dirs = [directory]
88    candidate_files = []
89    for chunk in glob.split("/"):
90        next_candidate_dirs = {}
91        candidate_files = {}
92        for candidate in candidate_dirs:
93            for e in directory_glob_chunk(candidate, chunk):
94                if type(e) == "File":
95                    candidate_files[e] = None
96                else:
97                    next_candidate_dirs[e.human_readable] = e
98        candidate_dirs = next_candidate_dirs.values()
99
100    return list(candidate_files)
101
102def glob(directory, include, exclude = [], allow_empty = False):
103    """native.glob, but for DirectoryInfo.
104
105    Args:
106        directory: (DirectoryInfo) The directory to look relative from.
107        include: (List[string]) A list of globs to match.
108        exclude: (List[string]) A list of globs to exclude.
109        allow_empty: (bool) Whether to allow a glob to not match any files.
110
111    Returns:
112        depset[File] A set of files that match.
113    """
114    include_files = []
115    for g in include:
116        matches = directory_single_glob(directory, g)
117        if not matches and not allow_empty:
118            fail(_NO_GLOB_MATCHES.format(
119                glob = repr(g),
120                dir = directory.human_readable,
121            ))
122        include_files.extend(matches)
123
124    if not exclude:
125        return depset(include_files)
126
127    include_files = {k: None for k in include_files}
128    for g in exclude:
129        matches = directory_single_glob(directory, g)
130        if not matches and not allow_empty:
131            fail(_NO_GLOB_MATCHES.format(
132                glob = repr(g),
133                dir = directory.human_readable,
134            ))
135        for f in matches:
136            include_files.pop(f, None)
137    return depset(include_files.keys())
138