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