blob: df96444699d7e7a9cc1cadfd1943f2ca139490a3 [file] [log] [blame]
# 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())