""" Copyright 2023 The Android Open Source Project 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. """ load("@bazel_skylib//lib:paths.bzl", "paths") def _remove_extension(p): """Removes the extension from the path `p`. Leading periods on the basename are ignored, so `_strip_extension(".bashrc")` returns `".bashrc"`. Args: p: The path to modify. Returns: The path with the extension removed. """ # paths.split_extension() does all of the work. return paths.split_extension(p)[0] # Expands an output path template for the given context and input file. def _expand_out_path_template(ctx, src_file): # Each src_file has a short_path that looks like: # # // # # For some expansions, we want to strip of the source package path, and # only use the rest for output file path in the expansion. # # There is also an option during expansion to just use the or # portion of the input path. # # This means there can be collisions if input files are taken from # `filegroups` defined in different packages, if they happen to use # the same relative path for that package. # # These conflcits are left to the user of this `gensrcs` rule to resolve for # their use case, as at least Bazel will raise an error when they occur. # Try to obtain the path to the package that defines `src_file`. It may or # may not be defined by the same package this `gensrcs` rule is in. # The `owner` label `package` attribute value is the closest we can get # to that path, but it may not be correct in all cases, such as if the # source path is itself for a generated file, where the generated file is # under a build artifact path, and not in the source tree. pkg_dirname = paths.dirname(src_file.short_path) rel_dirname = pkg_dirname if (src_file.is_source and src_file.owner and src_file.short_path.startswith(src_file.owner.package + "/")): rel_dirname = paths.dirname(paths.relativize( src_file.short_path, src_file.owner.package, )) base_inc_ext = src_file.basename base_exc_ext = _remove_extension(base_inc_ext) rel_path_base_inc_ext = paths.join(rel_dirname, base_inc_ext) rel_path_base_exc_ext = paths.join(rel_dirname, base_exc_ext) pkg_path_base_inc_ext = paths.join(pkg_dirname, base_inc_ext) pkg_path_base_exc_ext = paths.join(pkg_dirname, base_exc_ext) # Expand the output template return ctx.attr.output \ .replace("$(SRC:PKG/PATH/BASE.EXT)", pkg_path_base_inc_ext) \ .replace("$(SRC:PKG/PATH/BASE)", pkg_path_base_exc_ext) \ .replace("$(SRC:PATH/BASE.EXT)", rel_path_base_inc_ext) \ .replace("$(SRC:PATH/BASE)", rel_path_base_exc_ext) \ .replace("$(SRC:BASE.EXT)", base_inc_ext) \ .replace("$(SRC:BASE)", base_exc_ext) \ .replace("$(SRC)", rel_path_base_inc_ext) # A rule to generate files based on provided srcs and tools. def _gensrcs_impl(ctx): # The next two assignments can be created by using ctx.resolve_command. # TODO: Switch to using ctx.resolve_command when it is out of # experimental. command = ctx.expand_location(ctx.attr.cmd) tools = [ tool[DefaultInfo].files_to_run for tool in ctx.attr.tools ] # Expand the shell command by substituting $(RULEDIR), which will be # the same for any source file. command = command.replace( "$(RULEDIR)", paths.join( ctx.var["GENDIR"], ctx.label.package, ), ) src_files = ctx.files.srcs out_files = [] for src_file in src_files: # Expand the output path template for this source file. out_file_path = _expand_out_path_template(ctx, src_file) # out_file is at output_file_path that is relative to # /, hence, the fullpath to out_file is # // out_file = ctx.actions.declare_file(out_file_path) # Expand the command template for this source file by performing # substitution for $(SRC) and $(OUT). shell_command = command \ .replace("$(SRC)", src_file.path) \ .replace("$(OUT)", out_file.path) # Run the shell comand to generate the output from the input. ctx.actions.run_shell( tools = tools, outputs = [out_file], inputs = [src_file], command = shell_command, progress_message = "Generating %s from %s" % ( out_file.path, src_file.path, ), ) out_files.append(out_file) return [DefaultInfo( files = depset(out_files), )] gensrcs = rule( implementation = _gensrcs_impl, doc = "This rule generates files, where each of the `srcs` files is " + "passed to `cmd` to generate an `output`.", attrs = { "srcs": attr.label_list( # We allow srcs to directly reference files, instead of only # allowing references to other rules such as filegroups. allow_files = True, # An empty srcs is likely an mistake. allow_empty = False, # srcs must be explicitly specified. mandatory = True, doc = "A list of source files to process", ), "output": attr.string( # By default we generate an output filename based on the input # filename (no extension). default = "$(SRC)", doc = "An output path template which is expanded to generate " + "the output path given an source file. Portions " + "of the source filename can be included in the expansion " + "with one of: $(SRC:BASE), $(SRC:BASE.EXT), " + "$(SRC:PATH/BASE), $(SRC:PATH/BASE), " + "$(SRC:PKG/PATH/BASE), or $(SRC:PKG/PATH/BASE.ext). For " + "example, specifying `output = " + "\"includes/lib/$(SRC:BASE).h\"` would mean the input " + "file `some_path/to/a.txt` generates `includes/lib/a.h`, " + "while instead specifying `output = " + "\"includes/lib/$(SRC:PATH/BASE.EXT).h\"` would expand " + "to `includes/lib/some_path/to/a.txt.h`.", ), "cmd": attr.string( # cmd must be explicitly specified. mandatory = True, doc = "The command to run. Subject to $(location) expansion. " + "$(SRC) represents each input file provided in `srcs` " + "while $(OUT) reprensents corresponding output file " + "generated by the rule. $(RULEDIR) is intepreted the same " + "as it is in genrule.", ), "tools": attr.label_list( # We allow tools to directly reference files, as there could be a local script # used as a tool. allow_files = True, doc = "A list of tool dependencies for this rule. " + "The path of an individual `tools` target //x:y can be " + "obtained using `$(location //x:y)`", cfg = "exec", ), }, )