diff --git a/rules/directories/subdirectory.bzl b/rules/directories/subdirectory.bzl new file mode 100644 index 00000000..dac76f26 --- /dev/null +++ b/rules/directories/subdirectory.bzl @@ -0,0 +1,50 @@ +# 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 rules to create metadata about subdirectories.""" + +load(":providers.bzl", "DirectoryInfo") + +visibility("public") + +_DIR_NOT_FOUND = """{dir} does not contain a directory named {name}. +Instead, it contains the directories {children}.""" + +def _subdirectory_impl(ctx): + dir = ctx.attr.parent[DirectoryInfo] + if ctx.attr.path.startswith("/"): + fail("Invalid path. Path must be relative to the parent directory") + + for dirname in ctx.attr.path.split("/"): + if dirname not in dir.directories: + fail(_DIR_NOT_FOUND.format( + dir = dir.source_path, + name = repr(dirname), + children = repr(sorted(dir.directories)), + )) + dir = dir.directories[dirname] + + return [ + dir, + DefaultInfo(files = dir.transitive_files), + ] + +subdirectory = rule( + implementation = _subdirectory_impl, + attrs = { + "parent": attr.label(providers = [DirectoryInfo], mandatory = True), + "path": attr.string(mandatory = True), + }, + provides = [DirectoryInfo], +) diff --git a/tests/directories/BUILD b/tests/directories/BUILD index 271d5af4..9552948e 100644 --- a/tests/directories/BUILD +++ b/tests/directories/BUILD @@ -2,7 +2,9 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load( ":directory_test.bzl", "directory_test", + "nonexistent_subdirectory_test", "outside_testdata_test", + "subdirectory_test", ) analysis_test( @@ -28,3 +30,21 @@ analysis_test( impl = outside_testdata_test, target = "//tests/directories/testdata:outside_testdata", ) + +analysis_test( + name = "subdirectory_test", + impl = subdirectory_test, + targets = { + "root": "//tests/directories/testdata:root", + "dir": "//tests/directories/testdata:dir", + "subdir": "//tests/directories/testdata:subdir", + "f2": "//tests/directories/testdata:f2_filegroup", + }, +) + +analysis_test( + name = "nonexistent_subdirectory_test", + expect_failure = True, + impl = nonexistent_subdirectory_test, + target = "//tests/directories/testdata:nonexistent_subdirectory", +) diff --git a/tests/directories/directory_test.bzl b/tests/directories/directory_test.bzl index 57f20ea3..54a7b85a 100644 --- a/tests/directories/directory_test.bzl +++ b/tests/directories/directory_test.bzl @@ -83,3 +83,31 @@ def directory_test(env, targets): newdir.direct_files().contains_exactly([f3]) newdir.transitive_files().contains_exactly([f3]).in_order() env.expect.that_str(newdir.actual.generated_path + "/f3").equals(f3.path) + +_NONEXISTENT_ERR = """tests/directories/testdata does not contain a directory named "nonexistent". +Instead, it contains the directories ["dir", "newdir"].""" + +# buildifier: disable=function-docstring +def nonexistent_subdirectory_test(env, target): + env.expect.that_target(target).failures().contains_exactly_predicates([ + matching.contains(_NONEXISTENT_ERR), + ]) + +# buildifier: disable=function-docstring +def subdirectory_test(env, targets): + f2 = targets.f2.files.to_list()[0] + + root = targets.root[DirectoryInfo] + want_dir = root.directories["dir"] + want_subdir = want_dir.directories["subdir"] + + # Use that_str because it supports equality checks. They're not strings. + env.expect.that_str(targets.dir[DirectoryInfo]).equals(want_dir) + env.expect.that_str(targets.subdir[DirectoryInfo]).equals(want_subdir) + + env.expect.that_collection( + targets.dir.files.to_list(), + ).contains_exactly([f2]) + env.expect.that_collection( + targets.subdir.files.to_list(), + ).contains_exactly([f2]) diff --git a/tests/directories/testdata/BUILD b/tests/directories/testdata/BUILD index ca19ebec..3db1d3e6 100644 --- a/tests/directories/testdata/BUILD +++ b/tests/directories/testdata/BUILD @@ -1,6 +1,7 @@ load("@rules_testing//lib:util.bzl", "util") load("//rules:copy_file.bzl", "copy_file") load("//rules/directories:directory.bzl", "directory") +load("//rules/directories:subdirectory.bzl", "subdirectory") package(default_visibility = ["//tests/directories:__pkg__"]) @@ -36,3 +37,22 @@ filegroup( name = "f2_filegroup", srcs = ["dir/subdir/f2"], ) + +subdirectory( + name = "dir", + parent = ":root", + path = "dir", +) + +subdirectory( + name = "subdir", + parent = ":root", + path = "dir/subdir", +) + +util.helper_target( + subdirectory, + name = "nonexistent_subdirectory", + parent = ":root", + path = "nonexistent", +)