• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Copyright 2023 The Android Open Source Project
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15"""
16
17load("@bazel_skylib//lib:paths.bzl", "paths")
18
19def _remove_extension(p):
20    """Removes the extension from the path `p`.
21
22    Leading periods on the basename are ignored, so
23    `_strip_extension(".bashrc")` returns `".bashrc"`.
24
25    Args:
26      p: The path to modify.
27
28    Returns:
29      The path with the extension removed.
30    """
31
32    # paths.split_extension() does all of the work.
33    return paths.split_extension(p)[0]
34
35# Expands an output path template for the given context and input file.
36def _expand_out_path_template(ctx, src_file):
37    # Each src_file has a short_path that looks like:
38    #
39    #   <source-package-path>/<source-package-rel-path>/<base.ext>
40    #
41    # For some expansions, we want to strip of the source package path, and
42    # only use the rest for output file path in the expansion.
43    #
44    # There is also an option during expansion to just use the <base> or
45    # <base.ext> portion of the input path.
46    #
47    # This means there can be collisions if input files are taken from
48    # `filegroups` defined in different packages, if they happen to use
49    # the same relative path for that package.
50    #
51    # These conflcits are left to the user of this `gensrcs` rule to resolve for
52    # their use case, as at least Bazel will raise an error when they occur.
53
54    # Try to obtain the path to the package that defines `src_file`. It may or
55    # may not be defined by the same package this `gensrcs` rule is in.
56    # The `owner` label `package` attribute value is the closest we can get
57    # to that path, but it may not be correct in all cases, such as if the
58    # source path is itself for a generated file, where the generated file is
59    # under a build artifact path, and not in the source tree.
60    pkg_dirname = paths.dirname(src_file.short_path)
61    rel_dirname = pkg_dirname
62    if (src_file.is_source and src_file.owner and
63        src_file.short_path.startswith(src_file.owner.package + "/")):
64        rel_dirname = paths.dirname(paths.relativize(
65            src_file.short_path,
66            src_file.owner.package,
67        ))
68
69    base_inc_ext = src_file.basename
70    base_exc_ext = _remove_extension(base_inc_ext)
71    rel_path_base_inc_ext = paths.join(rel_dirname, base_inc_ext)
72    rel_path_base_exc_ext = paths.join(rel_dirname, base_exc_ext)
73    pkg_path_base_inc_ext = paths.join(pkg_dirname, base_inc_ext)
74    pkg_path_base_exc_ext = paths.join(pkg_dirname, base_exc_ext)
75
76    # Expand the output template
77    return ctx.attr.output \
78        .replace("$(SRC:PKG/PATH/BASE.EXT)", pkg_path_base_inc_ext) \
79        .replace("$(SRC:PKG/PATH/BASE)", pkg_path_base_exc_ext) \
80        .replace("$(SRC:PATH/BASE.EXT)", rel_path_base_inc_ext) \
81        .replace("$(SRC:PATH/BASE)", rel_path_base_exc_ext) \
82        .replace("$(SRC:BASE.EXT)", base_inc_ext) \
83        .replace("$(SRC:BASE)", base_exc_ext) \
84        .replace("$(SRC)", rel_path_base_inc_ext)
85
86# A rule to generate files based on provided srcs and tools.
87def _gensrcs_impl(ctx):
88    # The next two assignments can be created by using ctx.resolve_command.
89    # TODO: Switch to using ctx.resolve_command when it is out of
90    # experimental.
91    command = ctx.expand_location(ctx.attr.cmd)
92    tools = [
93        tool[DefaultInfo].files_to_run
94        for tool in ctx.attr.tools
95    ]
96
97    # Expand the shell command by substituting $(RULEDIR), which will be
98    # the same for any source file.
99    command = command.replace(
100        "$(RULEDIR)",
101        paths.join(
102            ctx.var["GENDIR"],
103            ctx.label.package,
104        ),
105    )
106
107    src_files = ctx.files.srcs
108    out_files = []
109    for src_file in src_files:
110        # Expand the output path template for this source file.
111        out_file_path = _expand_out_path_template(ctx, src_file)
112
113        # out_file is at output_file_path that is relative to
114        # <GENDIR>/<gensrc-package-dir>, hence, the fullpath to out_file is
115        # <GENDIR>/<gensrc-package-dir>/<out_file_path>
116        out_file = ctx.actions.declare_file(out_file_path)
117
118        # Expand the command template for this source file by performing
119        # substitution for $(SRC) and $(OUT).
120        shell_command = command \
121            .replace("$(SRC)", src_file.path) \
122            .replace("$(OUT)", out_file.path)
123
124        # Run the shell comand to generate the output from the input.
125        ctx.actions.run_shell(
126            tools = tools,
127            outputs = [out_file],
128            inputs = [src_file],
129            command = shell_command,
130            progress_message = "Generating %s from %s" % (
131                out_file.path,
132                src_file.path,
133            ),
134        )
135        out_files.append(out_file)
136
137    return [DefaultInfo(
138        files = depset(out_files),
139    )]
140
141gensrcs = rule(
142    implementation = _gensrcs_impl,
143    doc = "This rule generates files, where each of the `srcs` files is " +
144          "passed to `cmd` to generate an `output`.",
145    attrs = {
146        "srcs": attr.label_list(
147            # We allow srcs to directly reference files, instead of only
148            # allowing references to other rules such as filegroups.
149            allow_files = True,
150            # An empty srcs is likely an mistake.
151            allow_empty = False,
152            # srcs must be explicitly specified.
153            mandatory = True,
154            doc = "A list of source files to process",
155        ),
156        "output": attr.string(
157            # By default we generate an output filename based on the input
158            # filename (no extension).
159            default = "$(SRC)",
160            doc = "An output path template which is expanded to generate " +
161                  "the output path given an source file. Portions " +
162                  "of the source filename can be included in the expansion " +
163                  "with one of: $(SRC:BASE), $(SRC:BASE.EXT), " +
164                  "$(SRC:PATH/BASE), $(SRC:PATH/BASE), " +
165                  "$(SRC:PKG/PATH/BASE), or $(SRC:PKG/PATH/BASE.ext). For " +
166                  "example, specifying `output = " +
167                  "\"includes/lib/$(SRC:BASE).h\"` would mean the input " +
168                  "file `some_path/to/a.txt` generates `includes/lib/a.h`, " +
169                  "while instead specifying `output = " +
170                  "\"includes/lib/$(SRC:PATH/BASE.EXT).h\"` would expand " +
171                  "to `includes/lib/some_path/to/a.txt.h`.",
172        ),
173        "cmd": attr.string(
174            # cmd must be explicitly specified.
175            mandatory = True,
176            doc = "The command to run. Subject to $(location) expansion. " +
177                  "$(SRC) represents each input file provided in `srcs` " +
178                  "while $(OUT) reprensents corresponding output file " +
179                  "generated by the rule. $(RULEDIR) is intepreted the same " +
180                  "as it is in genrule.",
181        ),
182        "tools": attr.label_list(
183            # We allow tools to directly reference files, as there could be a local script
184            # used as a tool.
185            allow_files = True,
186            doc = "A list of tool dependencies for this rule. " +
187                  "The path of an individual `tools` target //x:y can be " +
188                  "obtained using `$(location //x:y)`",
189            cfg = "exec",
190        ),
191    },
192)
193