xref: /aosp_15_r20/external/bazel-skylib/rules/directory/directory.bzl (revision bcb5dc7965af6ee42bf2f21341a2ec00233a8c8a)
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 rules to create metadata about directories."""
16
17load("//lib:paths.bzl", "paths")
18load(":providers.bzl", "DirectoryInfo", "create_directory_info")
19
20def _prefix_match(f, prefixes):
21    for prefix in prefixes:
22        if f.path.startswith(prefix):
23            return prefix
24    fail("Expected {path} to start with one of {prefixes}".format(path = f.path, prefixes = list(prefixes)))
25
26def _choose_path(prefixes):
27    filtered = {prefix: example for prefix, example in prefixes.items() if example}
28    if len(filtered) > 1:
29        examples = list(filtered.values())
30        fail(
31            "Your sources contain {} and {}.\n\n".format(
32                examples[0],
33                examples[1],
34            ) +
35            "Having both source and generated files in a single directory is " +
36            "unsupported, since they will appear in two different " +
37            "directories in the bazel execroot. You may want to consider " +
38            "splitting your directory into one for source files and one for " +
39            "generated files.",
40        )
41
42    # If there's no entries, use the source path (it's always first in the dict)
43    return list(filtered if filtered else prefixes)[0][:-1]
44
45def _directory_impl(ctx):
46    # Declare a generated file so that we can get the path to generated files.
47    f = ctx.actions.declare_file("_directory_rule_" + ctx.label.name)
48    ctx.actions.write(f, "")
49
50    source_prefix = ctx.label.package
51    if ctx.label.workspace_root:
52        source_prefix = ctx.label.workspace_root + "/" + source_prefix
53    source_prefix = source_prefix.rstrip("/") + "/"
54
55    # Mapping of a prefix to an arbitrary (but deterministic) file matching that path.
56    # The arbitrary file is used to present error messages if we have both generated files and source files.
57    prefixes = {
58        source_prefix: None,
59        f.dirname + "/": None,
60    }
61
62    root_metadata = struct(
63        directories = {},
64        files = [],
65        relative = "",
66        human_readable = str(ctx.label),
67    )
68
69    topological = [root_metadata]
70    for src in ctx.files.srcs:
71        prefix = _prefix_match(src, prefixes)
72        prefixes[prefix] = src
73        relative = src.path[len(prefix):].split("/")
74        current_path = root_metadata
75        for dirname in relative[:-1]:
76            if dirname not in current_path.directories:
77                dir_metadata = struct(
78                    directories = {},
79                    files = [],
80                    relative = paths.join(current_path.relative, dirname),
81                    human_readable = paths.join(current_path.human_readable, dirname),
82                )
83                current_path.directories[dirname] = dir_metadata
84                topological.append(dir_metadata)
85
86            current_path = current_path.directories[dirname]
87
88        current_path.files.append(src)
89
90    # The output DirectoryInfos. Key them by something arbitrary but unique.
91    # In this case, we choose relative.
92    out = {}
93
94    root_path = _choose_path(prefixes)
95
96    # By doing it in reversed topological order, we ensure that a child is
97    # created before its parents. This means that when we create a provider,
98    # we can always guarantee that a depset of its children will work.
99    for dir_metadata in reversed(topological):
100        directories = {
101            dirname: out[subdir_metadata.relative]
102            for dirname, subdir_metadata in sorted(dir_metadata.directories.items())
103        }
104        entries = {
105            file.basename: file
106            for file in dir_metadata.files
107        }
108        entries.update(directories)
109
110        transitive_files = depset(
111            direct = sorted(dir_metadata.files, key = lambda f: f.basename),
112            transitive = [
113                d.transitive_files
114                for d in directories.values()
115            ],
116            order = "preorder",
117        )
118        directory = create_directory_info(
119            entries = {k: v for k, v in sorted(entries.items())},
120            transitive_files = transitive_files,
121            path = paths.join(root_path, dir_metadata.relative) if dir_metadata.relative else root_path,
122            human_readable = dir_metadata.human_readable,
123        )
124        out[dir_metadata.relative] = directory
125
126    root_directory = out[root_metadata.relative]
127
128    return [
129        root_directory,
130        DefaultInfo(files = root_directory.transitive_files),
131    ]
132
133directory = rule(
134    implementation = _directory_impl,
135    attrs = {
136        "srcs": attr.label_list(
137            allow_files = True,
138        ),
139    },
140    provides = [DirectoryInfo],
141)
142