# 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 directories.""" load("//lib:paths.bzl", "paths") load(":providers.bzl", "DirectoryInfo", "create_directory_info") def _prefix_match(f, prefixes): for prefix in prefixes: if f.path.startswith(prefix): return prefix fail("Expected {path} to start with one of {prefixes}".format(path = f.path, prefixes = list(prefixes))) def _choose_path(prefixes): filtered = {prefix: example for prefix, example in prefixes.items() if example} if len(filtered) > 1: examples = list(filtered.values()) fail( "Your sources contain {} and {}.\n\n".format( examples[0], examples[1], ) + "Having both source and generated files in a single directory is " + "unsupported, since they will appear in two different " + "directories in the bazel execroot. You may want to consider " + "splitting your directory into one for source files and one for " + "generated files.", ) # If there's no entries, use the source path (it's always first in the dict) return list(filtered if filtered else prefixes)[0][:-1] def _directory_impl(ctx): # Declare a generated file so that we can get the path to generated files. f = ctx.actions.declare_file("_directory_rule_" + ctx.label.name) ctx.actions.write(f, "") source_prefix = ctx.label.package if ctx.label.workspace_root: source_prefix = ctx.label.workspace_root + "/" + source_prefix source_prefix = source_prefix.rstrip("/") + "/" # Mapping of a prefix to an arbitrary (but deterministic) file matching that path. # The arbitrary file is used to present error messages if we have both generated files and source files. prefixes = { source_prefix: None, f.dirname + "/": None, } root_metadata = struct( directories = {}, files = [], relative = "", human_readable = str(ctx.label), ) topological = [root_metadata] for src in ctx.files.srcs: prefix = _prefix_match(src, prefixes) prefixes[prefix] = src relative = src.path[len(prefix):].split("/") current_path = root_metadata for dirname in relative[:-1]: if dirname not in current_path.directories: dir_metadata = struct( directories = {}, files = [], relative = paths.join(current_path.relative, dirname), human_readable = paths.join(current_path.human_readable, dirname), ) current_path.directories[dirname] = dir_metadata topological.append(dir_metadata) current_path = current_path.directories[dirname] current_path.files.append(src) # The output DirectoryInfos. Key them by something arbitrary but unique. # In this case, we choose relative. out = {} root_path = _choose_path(prefixes) # By doing it in reversed topological order, we ensure that a child is # created before its parents. This means that when we create a provider, # we can always guarantee that a depset of its children will work. for dir_metadata in reversed(topological): directories = { dirname: out[subdir_metadata.relative] for dirname, subdir_metadata in sorted(dir_metadata.directories.items()) } entries = { file.basename: file for file in dir_metadata.files } entries.update(directories) transitive_files = depset( direct = sorted(dir_metadata.files, key = lambda f: f.basename), transitive = [ d.transitive_files for d in directories.values() ], order = "preorder", ) directory = create_directory_info( entries = {k: v for k, v in sorted(entries.items())}, transitive_files = transitive_files, path = paths.join(root_path, dir_metadata.relative) if dir_metadata.relative else root_path, human_readable = dir_metadata.human_readable, ) out[dir_metadata.relative] = directory root_directory = out[root_metadata.relative] return [ root_directory, DefaultInfo(files = root_directory.transitive_files), ] directory = rule( implementation = _directory_impl, attrs = { "srcs": attr.label_list( allow_files = True, ), }, provides = [DirectoryInfo], )