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