blob: 144bafd45b4a8b927248ce278aeaf59a4adf3bb5 [file]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Functions for helping with globbing for files in starlark rules"""
visibility([
"//bazel/build_defs",
"//bazel/module_extensions/toolchains/files",
])
_NO_MATCH_FMT = """The following entries failed to match anything. If this is \
expected, consider setting allow_entry to True in the filter_files invocation.
{globs}
Example file: "{f}"
"""
def glob_matches(path, glob):
"""Determines whether or not a glob matches a given path.
This works the same as the glob function in bazel, supporting both * and **.
Args:
path: (List[str]): The path to check, encoded as a list
eg. a/b/c -> ["a", "b", "c"].
glob: (List[str]): The glob to try, encoded as a list
eg. a/**/b*/*d* -> ["a", "**", "b*", "*d*"].
Returns:
Whether or not the glob matches the path.
"""
# Thanks to the '**' operator, we need to treat this as a nondeterministic
# finite state machine, where we can be in multiple states at the same time.
# Otherwise, we would fail the test "multi_star_with_multiple_options".
path_uptos = [0]
for chunk in glob:
new_uptos = []
for upto in path_uptos:
if upto == len(path):
continue
dir = path[upto]
if chunk == "**":
new_uptos = range(upto, len(path) + 1)
elif "*" in chunk:
segments = chunk.split("*")
if not dir.startswith(segments[0]) or not dir.endswith(segments[-1]):
continue
if len(segments) == 2:
new_uptos.append(upto + 1)
elif len(segments) == 3 and segments[1] in dir[len(segments[0]):len(dir) - len(segments[2])]:
new_uptos.append(upto + 1)
elif len(segments) > 3:
fail("Unsupported number of stars")
elif chunk == dir:
new_uptos.append(upto + 1)
if not new_uptos:
return False
path_uptos = new_uptos
return len(path) in path_uptos
def glob(file_map, include, exclude, allow_empty):
"""Filters a file map to just the specified glob.
Args:
file_map: (Dict[str, File]) A mapping from human-readable name to file
include: (List[str]) See native.glob.include
exclude: (List[str]) See native.glob.exclude
allow_empty: (bool) See native.glob.allow_empty
Returns:
(Dict[str, File]) A subset of file_map containing just the files that
matched the glob.
"""
include = [tuple(glob.split("/")) for glob in include]
exclude = [tuple(glob.split("/")) for glob in exclude]
matched = {k: False for k in include}
filtered = {}
for path_str, f in file_map.items():
path = path_str.split("/")
for glob in include:
if glob_matches(path, glob):
if not any([glob_matches(path, g) for g in exclude]):
filtered[path_str] = f
matched[glob] = True
break
missing_matches = [k for k, v in matched.items() if not v]
if missing_matches and not allow_empty:
fail(_NO_MATCH_FMT.format(
globs = ["/".join(glob) for glob in missing_matches],
f = "/".join(path),
))
return filtered
def _glob_check_impl(ctx):
path = ctx.attr.path.split("/")
glob = ctx.attr.glob.split("/")
got = glob_matches(path, glob)
want = ctx.attr.want
if got != want:
fail("When checking {check}, _matches_glob({path}, {glob}) returned {got}, wanted {want}".format(
check = ctx.label.name,
path = path,
glob = glob,
got = got,
want = want,
))
glob_check = rule(
implementation = _glob_check_impl,
attrs = dict(
path = attr.string(mandatory = True),
glob = attr.string(mandatory = True),
want = attr.bool(mandatory = True),
),
)