# Copyright 2024 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Skylib module containing glob operations on directories.""" _NO_GLOB_MATCHES = "{glob} failed to match any files in {dir}" def transitive_entries(directory): """Returns the files and directories contained within a directory transitively. Args: directory: (DirectoryInfo) The directory to look at Returns: List[Either[DirectoryInfo, File]] The entries contained within. """ entries = [directory] stack = [directory] for _ in range(99999999): if not stack: return entries d = stack.pop() for entry in d.entries.values(): entries.append(entry) if type(entry) != "File": stack.append(entry) fail("Should never get to here") def directory_glob_chunk(directory, chunk): """Given a directory and a chunk of a glob, returns possible candidates. Args: directory: (DirectoryInfo) The directory to look relative from. chunk: (string) A chunk of a glob to look at. Returns: List[Either[DirectoryInfo, File]]] The candidate next entries for the chunk. """ if chunk == "*": return directory.entries.values() elif chunk == "**": return transitive_entries(directory) elif "*" not in chunk: if chunk in directory.entries: return [directory.entries[chunk]] else: return [] elif chunk.count("*") > 2: fail("glob chunks with more than two asterixes are unsupported. Got", chunk) if chunk.count("*") == 2: left, middle, right = chunk.split("*") else: middle = "" left, right = chunk.split("*") entries = [] for name, entry in directory.entries.items(): 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)]: entries.append(entry) return entries def directory_single_glob(directory, glob): """Calculates all files that are matched by a glob on a directory. Args: directory: (DirectoryInfo) The directory to look relative from. glob: (string) A glob to match. Returns: List[File] A list of files that match. """ # Treat a glob as a nondeterministic finite state automata. We can be in # multiple places at the one time. candidate_dirs = [directory] candidate_files = [] for chunk in glob.split("/"): next_candidate_dirs = {} candidate_files = {} for candidate in candidate_dirs: for e in directory_glob_chunk(candidate, chunk): if type(e) == "File": candidate_files[e] = None else: next_candidate_dirs[e.human_readable] = e candidate_dirs = next_candidate_dirs.values() return list(candidate_files) def glob(directory, include, exclude = [], allow_empty = False): """native.glob, but for DirectoryInfo. Args: directory: (DirectoryInfo) The directory to look relative from. include: (List[string]) A list of globs to match. exclude: (List[string]) A list of globs to exclude. allow_empty: (bool) Whether to allow a glob to not match any files. Returns: depset[File] A set of files that match. """ include_files = [] for g in include: matches = directory_single_glob(directory, g) if not matches and not allow_empty: fail(_NO_GLOB_MATCHES.format( glob = repr(g), dir = directory.human_readable, )) include_files.extend(matches) if not exclude: return depset(include_files) include_files = {k: None for k in include_files} for g in exclude: matches = directory_single_glob(directory, g) if not matches and not allow_empty: fail(_NO_GLOB_MATCHES.format( glob = repr(g), dir = directory.human_readable, )) for f in matches: include_files.pop(f, None) return depset(include_files.keys())