diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-03-08 04:22:46 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-03-08 04:22:46 +0000 |
commit | 326005d3816cb37dfe418fc5be89d433e529c0c1 (patch) | |
tree | 3f1f16ec6e8cf77a12f3fec5a2f5812422d63fce | |
parent | 5bc39608281f02f968b0583a460814319e43ba8f (diff) | |
parent | 7872cbd89f2620c7188cec6b53009c5726d3dfdf (diff) | |
download | wayland-protocols-android14-release.tar.gz |
Snap for 9705326 from 7872cbd89f2620c7188cec6b53009c5726d3dfdf to udc-releaseandroid-vts-14.0_r4android-vts-14.0_r3android-vts-14.0_r2android-vts-14.0_r1android-security-14.0.0_r8android-security-14.0.0_r7android-security-14.0.0_r6android-security-14.0.0_r5android-security-14.0.0_r4android-security-14.0.0_r3android-security-14.0.0_r2android-security-14.0.0_r1android-platform-14.0.0_r7android-platform-14.0.0_r6android-platform-14.0.0_r5android-platform-14.0.0_r4android-platform-14.0.0_r3android-platform-14.0.0_r2android-platform-14.0.0_r1android-cts-14.0_r4android-cts-14.0_r3android-cts-14.0_r2android-cts-14.0_r1android-14.0.0_r28android-14.0.0_r2android-14.0.0_r15android-14.0.0_r14android-14.0.0_r13android-14.0.0_r1android14-tests-releaseandroid14-security-releaseandroid14-s2-releaseandroid14-s1-releaseandroid14-releaseandroid14-platform-release
Change-Id: Ieb53e4435f0b611097ceab0d1bbf57bda0c72895
-rw-r--r-- | Android.bp | 49 | ||||
-rw-r--r-- | bazel/BUILD.bazel | 20 | ||||
-rw-r--r-- | bazel/gensrcs.bzl | 192 | ||||
-rw-r--r-- | bazel/gensrcs_test.bzl | 244 | ||||
-rw-r--r-- | go.mod | 17 | ||||
-rw-r--r-- | locations.go | 112 | ||||
-rw-r--r-- | wayland_protocol_codegen.go | 1144 | ||||
-rw-r--r-- | wayland_protocol_codegen_test.go | 488 |
8 files changed, 2021 insertions, 245 deletions
@@ -54,8 +54,10 @@ bootstrap_go_package { "soong-genrule", ], srcs: [ + "locations.go", "wayland_protocol_codegen.go", ], + testSrcs: ["wayland_protocol_codegen_test.go"], pluginFor: ["soong_build"], } @@ -120,31 +122,60 @@ filegroup { ], } +// Common settings for processing these protocols +wayland_protocol_codegen_defaults { + name: "wayland_extension_protocol_defaults", + + // All the protocol files to generate code for. + srcs: [":wayland_extension_protocols"], + + // Use "wayland_scanner" out of external/wayland. + tools: ["wayland_scanner"], +} + // Generate protocol source files used by both client and server wayland_protocol_codegen { name: "wayland_extension_protocol_sources", + defaults: ["wayland_extension_protocol_defaults"], + + // Specifies the command to run to generate each output file for each input file. cmd: "$(location wayland_scanner) private-code < $(in) > $(out)", - suffix: ".c", - srcs: [":wayland_extension_protocols"], - tools: ["wayland_scanner"], + + // There is a 1:1 correspondence between each generated output file and each source file. + // The output filename should use the base filename of the protocol file (no extension), and + // add a ".c" suffix. For example, "freedesktop.org/stable/xdg-shell/xdg-shell.xml" generates + // "xdg-shell.c". + output: "$(in).c", } // Generate protocol header files used by the client wayland_protocol_codegen { name: "wayland_extension_client_protocol_headers", + defaults: ["wayland_extension_protocol_defaults"], + + // Specifies the command to run to generate each output file for each input file. cmd: "$(location wayland_scanner) client-header < $(in) > $(out)", - suffix: "-client-protocol.h", - srcs: [":wayland_extension_protocols"], - tools: ["wayland_scanner"], + + // There is a 1:1 correspondence between each generated output file and each source file. + // The output filename should use the base filename of the protocol file (no extension), and + // add a "-client-protocol.h" suffix. For example, + // "freedesktop.org/stable/xdg-shell/xdg-shell.xml" generates "xdg-shell-client-protocol.h". + output: "$(in)-client-protocol.h", } // Generate protocol header files used by the server wayland_protocol_codegen { name: "wayland_extension_server_protocol_headers", + defaults: ["wayland_extension_protocol_defaults"], + + // Specifies the command to run to generate each output file for each input file. cmd: "$(location wayland_scanner) server-header < $(in) > $(out)", - suffix: "-server-protocol.h", - srcs: [":wayland_extension_protocols"], - tools: ["wayland_scanner"], + + // There is a 1:1 correspondence between each generated output file and each source file. + // The output filename should use the base filename of the protocol file (no extension), and + // add a "-server-protocol.h" suffix. For example, + // "freedesktop.org/stable/xdg-shell/xdg-shell.xml" generates "xdg-shell-server-protocol.h". + output: "$(in)-server-protocol.h", } // Generate a library with the protocol files, configured to export the client diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel new file mode 100644 index 0000000..1b7969d --- /dev/null +++ b/bazel/BUILD.bazel @@ -0,0 +1,20 @@ +""" +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(":gensrcs.bzl", "gensrcs") +load(":gensrcs_test.bzl", "gensrcs_test_suite") + +gensrcs_test_suite(name = "gensrcs_tests") diff --git a/bazel/gensrcs.bzl b/bazel/gensrcs.bzl new file mode 100644 index 0000000..730922e --- /dev/null +++ b/bazel/gensrcs.bzl @@ -0,0 +1,192 @@ +""" +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: + # + # <source-package-path>/<source-package-rel-path>/<base.ext> + # + # 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 <base> or + # <base.ext> 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 + # <GENDIR>/<gensrc-package-dir>, hence, the fullpath to out_file is + # <GENDIR>/<gensrc-package-dir>/<out_file_path> + 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", + ), + }, +) diff --git a/bazel/gensrcs_test.bzl b/bazel/gensrcs_test.bzl new file mode 100644 index 0000000..7c85c61 --- /dev/null +++ b/bazel/gensrcs_test.bzl @@ -0,0 +1,244 @@ +""" +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:new_sets.bzl", "sets") +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("@bazel_skylib//lib:paths.bzl", "paths") +load(":gensrcs.bzl", "gensrcs") + +SRCS = [ + "texts/src1.txt", + "texts/src2.txt", + "src3.txt", +] + +# ==== Check the output paths created by gensrcs ==== + +def _test_output_path_expansion_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + actions = analysistest.target_actions(env) + + # Expect an action for each input/output file pair. + asserts.equals( + env, + expected = len(ctx.attr.expected_outputs), + actual = len(actions), + ) + + # Expect the correct set of output files. + asserts.set_equals( + env, + expected = sets.make([ + paths.join( + ctx.genfiles_dir.path, + paths.dirname(ctx.build_file_path), + out, + ) + for out in ctx.attr.expected_outputs + ]), + actual = sets.make( + [file.path for file in target.files.to_list()], + ), + ) + + return analysistest.end(env) + +output_path_expansion_test = analysistest.make( + _test_output_path_expansion_impl, + attrs = { + "expected_outputs": attr.string_list( + doc = "The expected list of output files", + ), + }, +) + +def _test_output_expansion_base(): + name = "gensrcs_output_expansion_base" + test_name = name + "_test" + + gensrcs( + name = name, + cmd = "cat $(SRC) > $(OUT)", + srcs = SRCS, + output = "prefix_$(SRC:BASE)_suffix", + tags = ["manual"], # make sure it's not built using `:all` + ) + + output_path_expansion_test( + name = test_name, + target_under_test = name, + expected_outputs = [ + "prefix_src1_suffix", + "prefix_src2_suffix", + "prefix_src3_suffix", + ], + ) + return test_name + +def _test_output_expansion_base_ext(): + name = "gensrcs_output_expansion_base_ext" + test_name = name + "_test" + + gensrcs( + name = name, + cmd = "cat $(SRC) > $(OUT)", + srcs = SRCS, + output = "prefix_$(SRC:BASE.EXT)_suffix", + tags = ["manual"], # make sure it's not built using `:all` + ) + + output_path_expansion_test( + name = test_name, + target_under_test = name, + expected_outputs = [ + "prefix_src1.txt_suffix", + "prefix_src2.txt_suffix", + "prefix_src3.txt_suffix", + ], + ) + return test_name + +def _test_output_expansion_path_base(): + name = "gensrcs_output_expansion_path_base" + test_name = name + "_test" + + gensrcs( + name = name, + cmd = "cat $(SRC) > $(OUT)", + srcs = SRCS, + output = "prefix_$(SRC:PATH/BASE)_suffix", + tags = ["manual"], # make sure it's not built using `:all` + ) + + output_path_expansion_test( + name = test_name, + target_under_test = name, + expected_outputs = [ + "prefix_texts/src1_suffix", + "prefix_texts/src2_suffix", + "prefix_src3_suffix", + ], + ) + return test_name + +def _test_output_expansion_path_base_ext(): + name = "gensrcs_output_expansion_path_base_ext" + test_name = name + "_test" + + gensrcs( + name = name, + cmd = "cat $(SRC) > $(OUT)", + srcs = SRCS, + output = "prefix_$(SRC:PATH/BASE.EXT)_suffix", + tags = ["manual"], # make sure it's not built using `:all` + ) + + output_path_expansion_test( + name = test_name, + target_under_test = name, + expected_outputs = [ + "prefix_texts/src1.txt_suffix", + "prefix_texts/src2.txt_suffix", + "prefix_src3.txt_suffix", + ], + ) + return test_name + +def _test_output_expansion_pkg_path_base(): + name = "gensrcs_output_expansion_pkg_path_base" + test_name = name + "_test" + + gensrcs( + name = name, + cmd = "cat $(SRC) > $(OUT)", + srcs = SRCS, + output = "prefix_$(SRC:PKG/PATH/BASE)_suffix", + tags = ["manual"], # make sure it's not built using `:all` + ) + + output_path_expansion_test( + name = test_name, + target_under_test = name, + expected_outputs = [ + "prefix_external/wayland-protocols/bazel/texts/src1_suffix", + "prefix_external/wayland-protocols/bazel/texts/src2_suffix", + "prefix_external/wayland-protocols/bazel/src3_suffix", + ], + ) + return test_name + +def _test_output_expansion_pkg_path_base_ext(): + name = "gensrcs_output_expansion_pkg_path_base_ext" + test_name = name + "_test" + + gensrcs( + name = name, + cmd = "cat $(SRC) > $(OUT)", + srcs = SRCS, + output = "prefix_$(SRC:PKG/PATH/BASE.EXT)_suffix", + tags = ["manual"], # make sure it's not built using `:all` + ) + + output_path_expansion_test( + name = test_name, + target_under_test = name, + expected_outputs = [ + "prefix_external/wayland-protocols/bazel/texts/src1.txt_suffix", + "prefix_external/wayland-protocols/bazel/texts/src2.txt_suffix", + "prefix_external/wayland-protocols/bazel/src3.txt_suffix", + ], + ) + return test_name + +def _test_output_expansion_default(): + name = "gensrcs_output_expansion_default" + test_name = name + "_test" + + gensrcs( + name = name, + cmd = "cat $(SRC) > $(OUT)", + srcs = SRCS, + tags = ["manual"], # make sure it's not built using `:all` + ) + + output_path_expansion_test( + name = test_name, + target_under_test = name, + expected_outputs = [ + "texts/src1.txt", + "texts/src2.txt", + "src3.txt", + ], + ) + return test_name + +# ==== test suite ==== + +def gensrcs_test_suite(name): + """Creates test targets for gensrcs.bzl""" + native.test_suite( + name = name, + tests = [ + _test_output_expansion_base(), + _test_output_expansion_base_ext(), + _test_output_expansion_path_base(), + _test_output_expansion_path_base_ext(), + _test_output_expansion_pkg_path_base(), + _test_output_expansion_pkg_path_base_ext(), + _test_output_expansion_default(), + ], + ) @@ -0,0 +1,17 @@ +// 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. + +module android.googlesource.com/platform/external/wayland_protocols + +go 1.19 diff --git a/locations.go b/locations.go new file mode 100644 index 0000000..a2d06d9 --- /dev/null +++ b/locations.go @@ -0,0 +1,112 @@ +// 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. + +package soong_wayland_protocol_codegen + +import ( + "strings" + + "android/soong/android" +) + +// Note: the types defined here are identical to the types defined in the +// locations.go file included as "android/soong/genrule" package. +// Unfortunately all the types defined there are not public, which means we +// can't reference them from that package. + +// location is used to service $(location) and $(locations) entries in +// wayland_protocol_codegen commands. +type location interface { + Paths(cmd *android.RuleBuilderCommand) []string + String() string +} + +// inputLocation is a $(location) result for an entry in the srcs property. +type inputLocation struct { + paths android.Paths +} + +func (l inputLocation) String() string { + return strings.Join(l.paths.Strings(), " ") +} + +func (l inputLocation) Paths(cmd *android.RuleBuilderCommand) []string { + return cmd.PathsForInputs(l.paths) +} + +var _ location = inputLocation{} + +// outputLocation is a $(location) result for an entry in the out property. +type outputLocation struct { + path android.WritablePath +} + +func (l outputLocation) String() string { + return l.path.String() +} + +func (l outputLocation) Paths(cmd *android.RuleBuilderCommand) []string { + return []string{cmd.PathForOutput(l.path)} +} + +var _ location = outputLocation{} + +// toolLocation is a $(location) result for an entry in the tools or +// tool_files property. +type toolLocation struct { + paths android.Paths +} + +func (l toolLocation) String() string { + return strings.Join(l.paths.Strings(), " ") +} + +func (l toolLocation) Paths(cmd *android.RuleBuilderCommand) []string { + return cmd.PathsForTools(l.paths) +} + +var _ location = toolLocation{} + +// packagedToolLocation is a $(location) result for an entry in the tools or +// tool_files property that has PackagingSpecs. +type packagedToolLocation struct { + spec android.PackagingSpec +} + +func (l packagedToolLocation) String() string { + return l.spec.FileName() +} + +func (l packagedToolLocation) Paths(cmd *android.RuleBuilderCommand) []string { + return []string{cmd.PathForPackagedTool(l.spec)} +} + +var _ location = packagedToolLocation{} + +// errorLocation is a placeholder for a $(location) result that returns +// garbage to break the command when error reporting is delayed by +// ALLOW_MISSING_DEPENDENCIES=true. +type errorLocation struct { + err string +} + +func (l errorLocation) String() string { + return l.err +} + +func (l errorLocation) Paths(cmd *android.RuleBuilderCommand) []string { + return []string{l.err} +} + +var _ location = errorLocation{} diff --git a/wayland_protocol_codegen.go b/wayland_protocol_codegen.go index f9171af..bf7a9af 100644 --- a/wayland_protocol_codegen.go +++ b/wayland_protocol_codegen.go @@ -12,354 +12,1026 @@ // See the License for the specific language governing permissions and // limitations under the License. -// --------------------------------------------------------------------------- +/* +Package wayland_protocol defines an plugin module for the Soong build system, +which makes it easier to generate code from a list of Wayland protocol files. -// Package wayland_protcool defines extension modules for the Soong build system -// to make it easier to generate code from a list of Wayland protocol files. -// -// The primary extension module is "wayland_protocol_codegen", which applies a -// code generation tool to a list of source protocol files. -// -// Note that the code generation done here is similar to what is done by the -// base Soong "gensrcs" module, but there are two functional differences: -// -// 1) The output filenames are computed from the input filenames, rather -// than needing to be specified explicitly. An optional prefix as well -// as a suffix can be added to the protocol filename (without extension). -// -// 2) Code generation is done for each file independently by emitting -// multiple Ninja build commands, rather than one build command which -// does it all. -package wayland_protocol +The primary build module is "wayland_protocol_codegen", which takes a list of +protocol files, and runs a configurable code-generation tool to generate +source code for each one. There is also a "wayland_protocol_codegen_defaults" +for setting common properties. + +This package is substantially similar to the base "android/soong/genrule" +package, which was originally used for inspiration for this one, and has +been recently restructured so that it can be kept in sync with a tool like +"vimdiff" to keep things in sync as needed. + +However this build system plugin will not be needed in the future, as +evidenced by some of the other files that have been added as part of the +last big sync-up. In the future, Android will be built with Bazel, and it +is much simpler there to define our own local version of "gensrcs" that +implements the functionality we need See bazel/gensrcs.bzl for it. + +Notable differences: + + - This package implements a more powerful template mechanism for specifying + what output path/filename should be used for each source filename. The + genrule package only allows the extension on each source filename to be + replaced. + + - This package drops support for depfiles, after observing comments that + they are problematic in the genrule package sources. + + - This package drops "Extra" and "CmdModifier" from the public Module + structure, as this module is not expected to be extended. + + - This package drops "rule" from the public Module structure, as it was + unused but present in genrule. + +# Usage + + wayland_protocol_codegen { + // A standard target name. + name: "wayland_extension_protocol_sources", + + // A simple template for generating output filenames. + output: "$(in).c" + + // The command line template. See "Cmd". + cmd: "$(location wayland_scanner) code < $(in) > $(out)", + + // Protocol source files for the expansion. + srcs: [":wayland_extension_protocols"], + + // Any buildable binaries to use as tools + tools: ["wayland_scanner"], + + // Any source files to be used (scripts, template files) + tools_files: [], + } +*/ +package soong_wayland_protocol_codegen import ( "fmt" + "strconv" "strings" "github.com/google/blueprint" + "github.com/google/blueprint/bootstrap" "github.com/google/blueprint/proptools" "android/soong/android" + "android/soong/bazel" + "android/soong/bazel/cquery" "android/soong/genrule" + "path/filepath" ) func init() { - // Register out extension module type name with Soong. - android.RegisterModuleType( - "wayland_protocol_codegen", waylandCodegenModuleFactory) + registerCodeGenBuildComponents(android.InitRegistrationContext) +} + +func registerCodeGenBuildComponents(ctx android.RegistrationContext) { + ctx.RegisterModuleType("wayland_protocol_codegen_defaults", defaultsFactory) + + ctx.RegisterModuleType("wayland_protocol_codegen", codegenFactory) + + ctx.FinalDepsMutators(func(ctx android.RegisterMutatorsContext) { + ctx.BottomUp("wayland_protocol_codegen_tool_deps", toolDepsMutator).Parallel() + }) } var ( - // Create a context for build rule output from this package - pctx = android.NewPackageContext("android/soong/external/wayland-protocol") + pctx = android.NewPackageContext("android/soong/external/wayland_protocol_codegen") + + // Used by wayland_protocol_codegen when there is more than 1 shard to merge the outputs + // of each shard into a zip file. + gensrcsMerge = pctx.AndroidStaticRule("wayland_protocol_codegenMerge", blueprint.RuleParams{ + Command: "${soongZip} -o ${tmpZip} @${tmpZip}.rsp && ${zipSync} -d ${genDir} ${tmpZip}", + CommandDeps: []string{"${soongZip}", "${zipSync}"}, + Rspfile: "${tmpZip}.rsp", + RspfileContent: "${zipArgs}", + }, "tmpZip", "genDir", "zipArgs") ) +func init() { + pctx.Import("android/soong/android") + + pctx.HostBinToolVariable("soongZip", "soong_zip") + pctx.HostBinToolVariable("zipSync", "zipsync") +} + type hostToolDependencyTag struct { blueprint.BaseDependencyTag + android.LicenseAnnotationToolchainDependencyTag + label string +} + +func (t hostToolDependencyTag) AllowDisabledModuleDependency(target android.Module) bool { + // Allow depending on a disabled module if it's replaced by a prebuilt + // counterpart. We get the prebuilt through android.PrebuiltGetPreferred in + // GenerateAndroidBuildActions. + return target.IsReplacedByPrebuilt() } -var hostToolDepTag hostToolDependencyTag +var _ android.AllowDisabledModuleDependency = (*hostToolDependencyTag)(nil) -// waylandCodegenProperties defines the properties that will be read in from the -// Android.bp file for each instantiation of the module. -type waylandCodegenProperties struct { - // This string gives the command line template to run on each protocol file - // to wayland_protocol_codegen. +type generatorProperties struct { + // The command to run on one or more input files. Cmd supports + // substitution of a few variables (the actual substitution is implemented + // in GenerateAndroidBuildActions below) + // + // Available variables for substitution: // - // The string can contain one or more "$" prefixed variable names for - // values that can vary. At a minimum you need to use ${location}, ${out} - // and ${in} + // - $(location) + // the path to the first entry in tools or tool_files + // - $(location <label>) + // the path to the tool, tool_file, input or output with name <label>. Use + // $(location) if <label> refers to a rule that outputs exactly one file. + // - $(locations <label>) + // the paths to the tools, tool_files, inputs or outputs with name + // <label>. Use $(locations) if <label> refers to a rule that outputs two + // or more files. + // - $(in) + // one or more input files + // - $(out) + // a single output file + // - $(genDir) + // the sandbox directory for this tool; contains $(out) + // - $$ + // a literal '$' // - // $(location): the path to the first entry in tools or tool_files - // $(location <label>): the path to the tool or tool_file with name <label> - // $(in): A protocol file from srcs - // $(out): The constructed output filename from the protocol filename. - // $$: a literal $ + // All files used must be declared as inputs (to ensure proper up-to-date + // checks). Use "$(in)" directly in Cmd to ensure that all inputs used are + // declared. Cmd *string - // The string to prepend to every protcol filename to generate the - // corresponding output filename. The empty string by default. - Prefix *string + // name of the modules (if any) that produces the host executable. Leave + // empty for prebuilts or scripts that do not need a module to build them. + Tools []string - // The suffix to append to every protocol filename to generate the - // corresponding output filename. The empty string by default. - Suffix *string + // Local source files that are used as scripts or other input files needed + // by a tool. + Tool_files []string `android:"path"` - // The list of protocol files to process. - Srcs []string `android:"path"` + // List of directories to export generated headers from. + Export_include_dirs []string - // The names of any built host executables to use for code generation. Can - // be left empty if a local script is used instead (specified in - // tool_files). - Tools []string + // List of input files. + Srcs []string `android:"path,arch_variant"` - // Local files that are used for code generation. Can be scripts to run, but - // should also include any other files that the code generation step should - // depend on that might be used by the code gen tool. - Tool_files []string + // Input files to exclude. + Exclude_srcs []string `android:"path,arch_variant"` } -// waylandGenModule defines the Soong module for each instance. -type waylandGenModule struct { +type Module struct { android.ModuleBase + android.DefaultableModuleBase + android.BazelModuleBase + android.ApexModuleBase + + android.ImageInterface - // Store a copy of the parsed properties for easy reference. - properties waylandCodegenProperties + properties generatorProperties - // Each module emits its own blueprint (Ninja) rule. Store a reference - // to the one created for this instance. - rule blueprint.Rule + taskGenerator taskFunc + + rawCommands []string - // Each module exports one or more include directories. Store the paths here - // here for easy retrieval. exportedIncludeDirs android.Paths - // Each module has a list of files it outputs, that can be used by other - // modules. Store the list of paths here for easy reference. outputFiles android.Paths + outputDeps android.Paths + + subName string + subDir string + + // Collect the module directory for IDE info in java/jdeps.go. + modulePaths []string } -// For the uninitiated, this is an idiom to check that a given object implements -// an interface. In this case we want to be sure that waylandGenModule -// implements genrule.SourceFileGenerator -var _ genrule.SourceFileGenerator = (*waylandGenModule)(nil) +// Ensure Module implements the android.MixedBuildBuildable interface. +var _ android.MixedBuildBuildable = (*Module)(nil) + +type taskFunc func(ctx android.ModuleContext, rawCommand string, srcFiles android.Paths) []generateTask -// Check that we implement android.SourceFileProducer -var _ android.SourceFileProducer = (*waylandGenModule)(nil) +type generateTask struct { + in android.Paths + out android.WritablePaths + copyTo android.WritablePaths + genDir android.WritablePath + cmd string -// GeneratedSourceFiles implements the genrule.SourceFileGenerator -// GeneratedSourceFiles method to return the list of generated source files. -func (g *waylandGenModule) GeneratedSourceFiles() android.Paths { + shard int + shards int +} + +// Part of genrule.SourceFileGenerator. +// Returns the list of generated source files. +func (g *Module) GeneratedSourceFiles() android.Paths { return g.outputFiles } -// GeneratedHeaderDirs implements the genrule.SourceFileGenerator -// GeneratedHeaderDirs method to return the list of exported include -// directories. -func (g *waylandGenModule) GeneratedHeaderDirs() android.Paths { - return g.exportedIncludeDirs +// Part of genrule.SourceFileGenerator. +// Returns the list of input source files. +func (g *Module) Srcs() android.Paths { + return append(android.Paths{}, g.outputFiles...) } -// GeneratedDeps implements the genrule.SourceFileGenerator GeneratedDeps -// method to return the list of files to be used as dependencies when using -// GeneratedHeaderDirs. -func (g *waylandGenModule) GeneratedDeps() android.Paths { - return g.outputFiles +// Part of genrule.SourceFileGenerator. +// Returns the list of the list of exported include paths. +func (g *Module) GeneratedHeaderDirs() android.Paths { + return g.exportedIncludeDirs } -// Srcs implements the android.SourceFileProducer Srcs method to return the list -// of source files. -func (g *waylandGenModule) Srcs() android.Paths { - return g.outputFiles +// Part of genrule.SourceFileGenerator. +// Returns the list of files to be used as dependencies when using +// GeneratedHeaderDirs +func (g *Module) GeneratedDeps() android.Paths { + return g.outputDeps } -// DepsMutator implements the android.Module DepsMutator method to apply a -// mutator context to the build graph. -func (g *waylandGenModule) DepsMutator(ctx android.BottomUpMutatorContext) { - // This implementation duplicates the one from genrule.go, where gensrcs is - // defined. - if g, ok := ctx.Module().(*waylandGenModule); ok { - if len(g.properties.Tools) > 0 { - ctx.AddFarVariationDependencies(ctx.Config().BuildOSTarget.Variations(), - hostToolDepTag, g.properties.Tools...) +// Part of android.OutputFileProducer. +// Returns the list of output files matching a tag, or an error if there is no +// match. +func (g *Module) OutputFiles(tag string) (android.Paths, error) { + if tag == "" { + return append(android.Paths{}, g.outputFiles...), nil + } + // otherwise, tag should match one of outputs + for _, outputFile := range g.outputFiles { + if outputFile.Rel() == tag { + return android.Paths{outputFile}, nil } } + return nil, fmt.Errorf("unsupported module reference tag %q", tag) } -// GenerateAndroidBuildActions implements the android.Module -// GenerateAndroidBuildActions method, which generates all the rules and builds -// commands used by this module instance. -func (g *waylandGenModule) GenerateAndroidBuildActions(ctx android.ModuleContext) { - if len(g.properties.Tools) == 0 && len(g.properties.Tool_files) == 0 { - ctx.ModuleErrorf("at least one `tools` or `tool_files` is required") - return - } +// Ensure Module implements the genrule.SourceFileGenerator interface. +var _ genrule.SourceFileGenerator = (*Module)(nil) - // Prepare the list of tools that were defined for codegen purposes. - tools, implicitDeps := g.prepareTools(ctx) +// Ensure Module implements the android.SourceFileProducer interface. +var _ android.SourceFileProducer = (*Module)(nil) - if ctx.Failed() { - return +// Ensure Module implements the android.OutputFileProducer interface. +var _ android.OutputFileProducer = (*Module)(nil) + +func toolDepsMutator(ctx android.BottomUpMutatorContext) { + if g, ok := ctx.Module().(*Module); ok { + for _, tool := range g.properties.Tools { + tag := hostToolDependencyTag{label: tool} + if m := android.SrcIsModule(tool); m != "" { + tool = m + } + ctx.AddFarVariationDependencies(ctx.Config().BuildOSTarget.Variations(), tag, tool) + } } +} - // Emit the rule for generating for processing each source file - g.emitRule(ctx, tools) +// Part of android.MixedBuildBuildable. +// Allow this module to be a bridge between Bazel and Soong. Fills in Soong +// properties for the module from the Bazel cquery, so that other Soong +// modules can depend on the module when it was actually built by Bazel. +func (g *Module) ProcessBazelQueryResponse(ctx android.ModuleContext) { + g.generateCommonBuildActions(ctx) - if ctx.Failed() { + // Get the list of output files that Bazel generates for the target. + label := g.GetBazelLabel(ctx, g) + bazelCtx := ctx.Config().BazelContext + filePaths, err := bazelCtx.GetOutputFiles(label, android.GetConfigKey(ctx)) + if err != nil { + ctx.ModuleErrorf(err.Error()) return } - generatedFilenamePrefix := proptools.String(g.properties.Prefix) - generatedFilenameSuffix := proptools.String(g.properties.Suffix) - for _, src := range android.PathsForModuleSrc(ctx, g.properties.Srcs) { - out := g.generateOutputPath(ctx, src, generatedFilenamePrefix, generatedFilenameSuffix) - if out == nil { - continue - } - - g.emitBuild(ctx, src, out, implicitDeps) - g.outputFiles = append(g.outputFiles, out) + // Convert to android.Paths, and also form the set of include directories + // that might be needed for those paths. + var bazelOutputFiles android.Paths + exportIncludeDirs := map[string]bool{} + for _, bazelOutputFile := range filePaths { + bazelOutputFiles = append(bazelOutputFiles, + android.PathForBazelOutRelative(ctx, ctx.ModuleDir(), bazelOutputFile)) + exportIncludeDirs[filepath.Dir(bazelOutputFile)] = true } - g.exportedIncludeDirs = append(g.exportedIncludeDirs, android.PathForModuleGen(ctx)) -} - -// genrateOutputPath takes an source path, a prefix, and a suffix, and use them -// to generate and return corresponding an output file path. -func (g *waylandGenModule) generateOutputPath(ctx android.ModuleContext, src android.Path, prefix string, suffix string) android.WritablePath { - // Construct a new filename by adding the requested prefix and suffix for this - // code generator instance. If the input file name is "wayland.xml", and the - // properties specify a prefix of "test-" and a suffix of "-client.cpp", we - // will end up with a fulename of "test-wayland-client.cpp" - protocolFilename, protocolExt := splitExt(src.Base()) - if protocolExt != ".xml" { - ctx.ModuleErrorf("Source file %q does not end with .xml", src) - return nil + // Set the Soong module properties to refer to the Bazel files + g.outputFiles = bazelOutputFiles + g.outputDeps = bazelOutputFiles + for includePath, _ := range exportIncludeDirs { + g.exportedIncludeDirs = append(g.exportedIncludeDirs, + android.PathForBazelOut(ctx, includePath)) } - return android.PathForModuleGen(ctx, prefix+protocolFilename+suffix) } -// emitRule is an internal function to emit each Ninja rule. -func (g *waylandGenModule) emitRule(ctx android.ModuleContext, tools map[string]android.Path) { - // Get the command to run to process each protocol file. Since everything - // should be templated, we generate a Ninja rule that uses the command, - // and invoke it from each Ninja build command we emit. - g.rule = ctx.Rule(pctx, "generator", blueprint.RuleParams{ - Command: g.expandCmd(ctx, tools), - }) -} +// Part of android.Module. +// Generates all the rules and builds commands used by this module instance. +func (g *Module) generateCommonBuildActions(ctx android.ModuleContext) { + g.subName = ctx.ModuleSubDir() -// emitBuild is an internal function to emit each Build command. -func (g *waylandGenModule) emitBuild(ctx android.ModuleContext, src android.Path, out android.WritablePath, implicitDeps android.Paths) android.Path { - ctx.Build(pctx, android.BuildParams{ - Rule: g.rule, - Description: "generate " + out.Base(), - Output: out, - Inputs: android.Paths{src}, - Implicits: implicitDeps, - }) + // Collect the module directory for IDE info in java/jdeps.go. + g.modulePaths = append(g.modulePaths, ctx.ModuleDir()) - return out -} + if len(g.properties.Export_include_dirs) > 0 { + for _, dir := range g.properties.Export_include_dirs { + g.exportedIncludeDirs = append(g.exportedIncludeDirs, + android.PathForModuleGen(ctx, g.subDir, ctx.ModuleDir(), dir)) + } + } else { + g.exportedIncludeDirs = append(g.exportedIncludeDirs, android.PathForModuleGen(ctx, g.subDir)) + } -// prepareTools is an internal function to prepare a list of tools. -func (g *waylandGenModule) prepareTools(ctx android.ModuleContext) (tools map[string]android.Path, implicitDeps android.Paths) { - tools = map[string]android.Path{} + locationLabels := map[string]location{} + firstLabel := "" - // This was extracted and slightly simplifed from equivalent code in - // genrule.go. + addLocationLabel := func(label string, loc location) { + if firstLabel == "" { + firstLabel = label + } + if _, exists := locationLabels[label]; !exists { + locationLabels[label] = loc + } else { + ctx.ModuleErrorf("multiple locations for label %q: %q and %q (do you have duplicate srcs entries?)", + label, locationLabels[label], loc) + } + } - // For each entry in "tool", walk the dependency graph to get more - // information about it. + var tools android.Paths + var packagedTools []android.PackagingSpec if len(g.properties.Tools) > 0 { + seenTools := make(map[string]bool) + ctx.VisitDirectDepsBlueprint(func(module blueprint.Module) { - tag := ctx.OtherModuleDependencyTag(module) - if android.IsSourceDepTagWithOutputTag(tag, "") { - // Nothing to do - return - } - switch tag { - case hostToolDepTag: + switch tag := ctx.OtherModuleDependencyTag(module).(type) { + case hostToolDependencyTag: tool := ctx.OtherModuleName(module) - var path android.OptionalPath + if m, ok := module.(android.Module); ok { + // Necessary to retrieve any prebuilt replacement for the tool, since + // toolDepsMutator runs too late for the prebuilt mutators to have + // replaced the dependency. + module = android.PrebuiltGetPreferred(ctx, m) + } - if t, ok := module.(genrule.HostToolProvider); ok { + switch t := module.(type) { + case android.HostToolProvider: + // A HostToolProvider provides the path to a tool, which will be copied + // into the sandbox. if !t.(android.Module).Enabled() { if ctx.Config().AllowMissingDependencies() { ctx.AddMissingDependencies([]string{tool}) } else { ctx.ModuleErrorf("depends on disabled module %q", tool) } - break + return } - path = t.HostToolPath() - } else { + path := t.HostToolPath() + if !path.Valid() { + ctx.ModuleErrorf("host tool %q missing output file", tool) + return + } + if specs := t.TransitivePackagingSpecs(); specs != nil { + // If the HostToolProvider has PackgingSpecs, which are definitions of the + // required relative locations of the tool and its dependencies, use those + // instead. They will be copied to those relative locations in the sbox + // sandbox. + packagedTools = append(packagedTools, specs...) + // Assume that the first PackagingSpec of the module is the tool. + addLocationLabel(tag.label, packagedToolLocation{specs[0]}) + } else { + tools = append(tools, path.Path()) + addLocationLabel(tag.label, toolLocation{android.Paths{path.Path()}}) + } + case bootstrap.GoBinaryTool: + // A GoBinaryTool provides the install path to a tool, which will be copied. + p := android.PathForGoBinary(ctx, t) + tools = append(tools, p) + addLocationLabel(tag.label, toolLocation{android.Paths{p}}) + default: ctx.ModuleErrorf("%q is not a host tool provider", tool) - break + return + } + + seenTools[tag.label] = true + } + }) + + // If AllowMissingDependencies is enabled, the build will not have stopped when + // AddFarVariationDependencies was called on a missing tool, which will result in nonsensical + // "cmd: unknown location label ..." errors later. Add a placeholder file to the local label. + // The command that uses this placeholder file will never be executed because the rule will be + // replaced with an android.Error rule reporting the missing dependencies. + if ctx.Config().AllowMissingDependencies() { + for _, tool := range g.properties.Tools { + if !seenTools[tool] { + addLocationLabel(tool, errorLocation{"***missing tool " + tool + "***"}) } + } + } + } + + if ctx.Failed() { + return + } - if path.Valid() { - implicitDeps = append(implicitDeps, path.Path()) - if _, exists := tools[tool]; !exists { - tools[tool] = path.Path() + for _, toolFile := range g.properties.Tool_files { + paths := android.PathsForModuleSrc(ctx, []string{toolFile}) + tools = append(tools, paths...) + addLocationLabel(toolFile, toolLocation{paths}) + } + + includeDirInPaths := ctx.DeviceConfig().BuildBrokenInputDir(g.Name()) + var srcFiles android.Paths + for _, in := range g.properties.Srcs { + paths, missingDeps := android.PathsAndMissingDepsRelativeToModuleSourceDir(android.SourceInput{ + Context: ctx, Paths: []string{in}, ExcludePaths: g.properties.Exclude_srcs, IncludeDirs: includeDirInPaths, + }) + if len(missingDeps) > 0 { + if !ctx.Config().AllowMissingDependencies() { + panic(fmt.Errorf("should never get here, the missing dependencies %q should have been reported in DepsMutator", + missingDeps)) + } + + // If AllowMissingDependencies is enabled, the build will not have stopped when + // the dependency was added on a missing SourceFileProducer module, which will result in nonsensical + // "cmd: label ":..." has no files" errors later. Add a placeholder file to the local label. + // The command that uses this placeholder file will never be executed because the rule will be + // replaced with an android.Error rule reporting the missing dependencies. + ctx.AddMissingDependencies(missingDeps) + addLocationLabel(in, errorLocation{"***missing srcs " + in + "***"}) + } else { + srcFiles = append(srcFiles, paths...) + addLocationLabel(in, inputLocation{paths}) + } + } + + var copyFrom android.Paths + var outputFiles android.WritablePaths + var zipArgs strings.Builder + + cmd := proptools.String(g.properties.Cmd) + + tasks := g.taskGenerator(ctx, cmd, srcFiles) + if ctx.Failed() { + return + } + + for _, task := range tasks { + if len(task.out) == 0 { + ctx.ModuleErrorf("must have at least one output file") + return + } + + // Pick a unique path outside the task.genDir for the sbox manifest textproto, + // a unique rule name, and the user-visible description. + manifestName := "wayland_protocol_codegen.sbox.textproto" + desc := "generate" + name := "generator" + if task.shards > 0 { + manifestName = "wayland_protocol_codegen_" + strconv.Itoa(task.shard) + ".sbox.textproto" + desc += " " + strconv.Itoa(task.shard) + name += strconv.Itoa(task.shard) + } else if len(task.out) == 1 { + desc += " " + task.out[0].Base() + } + + manifestPath := android.PathForModuleOut(ctx, manifestName) + + // Use a RuleBuilder to create a rule that runs the command inside an sbox sandbox. + rule := android.NewRuleBuilder(pctx, ctx).Sbox(task.genDir, manifestPath).SandboxTools() + cmd := rule.Command() + + for _, out := range task.out { + addLocationLabel(out.Rel(), outputLocation{out}) + } + + rawCommand, err := android.Expand(task.cmd, func(name string) (string, error) { + // Report the error directly without returning an error to android.Expand to catch multiple errors in a + // single run + reportError := func(fmt string, args ...interface{}) (string, error) { + ctx.PropertyErrorf("cmd", fmt, args...) + return "SOONG_ERROR", nil + } + + // Apply shell escape to each cases to prevent source file paths containing $ from being evaluated in shell + switch name { + case "location": + if len(g.properties.Tools) == 0 && len(g.properties.Tool_files) == 0 { + return reportError("at least one `tools` or `tool_files` is required if $(location) is used") + } + loc := locationLabels[firstLabel] + paths := loc.Paths(cmd) + if len(paths) == 0 { + return reportError("default label %q has no files", firstLabel) + } else if len(paths) > 1 { + return reportError("default label %q has multiple files, use $(locations %s) to reference it", + firstLabel, firstLabel) + } + return proptools.ShellEscape(paths[0]), nil + case "in": + return strings.Join(proptools.ShellEscapeList(cmd.PathsForInputs(srcFiles)), " "), nil + case "out": + var sandboxOuts []string + for _, out := range task.out { + sandboxOuts = append(sandboxOuts, cmd.PathForOutput(out)) + } + return strings.Join(proptools.ShellEscapeList(sandboxOuts), " "), nil + case "genDir": + return proptools.ShellEscape(cmd.PathForOutput(task.genDir)), nil + default: + if strings.HasPrefix(name, "location ") { + label := strings.TrimSpace(strings.TrimPrefix(name, "location ")) + if loc, ok := locationLabels[label]; ok { + paths := loc.Paths(cmd) + if len(paths) == 0 { + return reportError("label %q has no files", label) + } else if len(paths) > 1 { + return reportError("label %q has multiple files, use $(locations %s) to reference it", + label, label) + } + return proptools.ShellEscape(paths[0]), nil } else { - ctx.ModuleErrorf("multiple tools for %q, %q and %q", tool, tools[tool], path.Path().String()) + return reportError("unknown location label %q is not in srcs, out, tools or tool_files.", label) + } + } else if strings.HasPrefix(name, "locations ") { + label := strings.TrimSpace(strings.TrimPrefix(name, "locations ")) + if loc, ok := locationLabels[label]; ok { + paths := loc.Paths(cmd) + if len(paths) == 0 { + return reportError("label %q has no files", label) + } + return proptools.ShellEscape(strings.Join(paths, " ")), nil + } else { + return reportError("unknown locations label %q is not in srcs, out, tools or tool_files.", label) } } else { - ctx.ModuleErrorf("host tool %q missing output file", tool) + return reportError("unknown variable '$(%s)'", name) } } }) - } - // Get more information about each entry in "tool_files". - for _, tool := range g.properties.Tool_files { - toolPath := android.PathForModuleSrc(ctx, tool) - implicitDeps = append(implicitDeps, toolPath) - if _, exists := tools[tool]; !exists { - tools[tool] = toolPath + if err != nil { + ctx.PropertyErrorf("cmd", "%s", err.Error()) + return + } + + g.rawCommands = append(g.rawCommands, rawCommand) + + cmd.Text(rawCommand) + cmd.ImplicitOutputs(task.out) + cmd.Implicits(task.in) + cmd.ImplicitTools(tools) + cmd.ImplicitPackagedTools(packagedTools) + + // Create the rule to run the genrule command inside sbox. + rule.Build(name, desc) + + if len(task.copyTo) > 0 { + // If copyTo is set, multiple shards need to be copied into a single directory. + // task.out contains the per-shard paths, and copyTo contains the corresponding + // final path. The files need to be copied into the final directory by a + // single rule so it can remove the directory before it starts to ensure no + // old files remain. zipsync already does this, so build up zipArgs that + // zip all the per-shard directories into a single zip. + outputFiles = append(outputFiles, task.copyTo...) + copyFrom = append(copyFrom, task.out.Paths()...) + zipArgs.WriteString(" -C " + task.genDir.String()) + zipArgs.WriteString(android.JoinWithPrefix(task.out.Strings(), " -f ")) } else { - ctx.ModuleErrorf("multiple tools for %q, %q and %q", tool, tools[tool], toolPath.String()) + outputFiles = append(outputFiles, task.out...) } } - return + + if len(copyFrom) > 0 { + // Create a rule that zips all the per-shard directories into a single zip and then + // uses zipsync to unzip it into the final directory. + ctx.Build(pctx, android.BuildParams{ + Rule: gensrcsMerge, + Implicits: copyFrom, + Outputs: outputFiles, + Description: "merge shards", + Args: map[string]string{ + "zipArgs": zipArgs.String(), + "tmpZip": android.PathForModuleGen(ctx, g.subDir+".zip").String(), + "genDir": android.PathForModuleGen(ctx, g.subDir).String(), + }, + }) + } + + g.outputFiles = outputFiles.Paths() } -// expandCmd is an internal function to do some expansion and any additional -// wrapping of the generator command line. Returns the command line to use and -// an error value. -func (g *waylandGenModule) expandCmd(ctx android.ModuleContext, tools map[string]android.Path) (cmd string) { - cmd, err := android.Expand(proptools.String(g.properties.Cmd), func(name string) (string, error) { - switch name { - case "in": - return "$in", nil - case "out": - // We need to use the sandbox out path instead - //return "$sandboxOut", nil - return "$out", nil - case "location": - if len(g.properties.Tools) > 0 { - return tools[g.properties.Tools[0]].String(), nil - } else { - return tools[g.properties.Tool_files[0]].String(), nil +func (g *Module) GenerateAndroidBuildActions(ctx android.ModuleContext) { + g.generateCommonBuildActions(ctx) + + // When there are less than six outputs, we directly give those as the + // output dependency for this module. However, if there are more outputs, + // we inject a phony target. This potentially saves space in the generated + // ninja file, as well as simplifying any visualizations of the dependency + // graph. + if len(g.outputFiles) <= 6 { + g.outputDeps = g.outputFiles + } else { + phonyFile := android.PathForModuleGen(ctx, "genrule-phony") + ctx.Build(pctx, android.BuildParams{ + Rule: blueprint.Phony, + Output: phonyFile, + Inputs: g.outputFiles, + }) + g.outputDeps = android.Paths{phonyFile} + } +} + +// Part of android.MixedBuildBuildable. +// Queues up Bazel cquery requests related to this module. +func (g *Module) QueueBazelCall(ctx android.BaseModuleContext) { + bazelCtx := ctx.Config().BazelContext + bazelCtx.QueueBazelRequest(g.GetBazelLabel(ctx, g), cquery.GetOutputFiles, + android.GetConfigKey(ctx)) +} + +// Part of android.MixedBuildBuildable. +// Returns true if Bazel can and should build this module in a mixed build. +func (g *Module) IsMixedBuildSupported(ctx android.BaseModuleContext) bool { + return true +} + +// Part of android.IDEInfo. +// Collect information for opening IDE project files in java/jdeps.go. +func (g *Module) IDEInfo(dpInfo *android.IdeInfo) { + dpInfo.Srcs = append(dpInfo.Srcs, g.Srcs().Strings()...) + for _, src := range g.properties.Srcs { + if strings.HasPrefix(src, ":") { + src = strings.Trim(src, ":") + dpInfo.Deps = append(dpInfo.Deps, src) + } + } + dpInfo.Paths = append(dpInfo.Paths, g.modulePaths...) +} + +// Ensure Module implements android.ApexModule +// Note: gensrcs implements it but it's possible we do not actually need to. +var _ android.ApexModule = (*Module)(nil) + +// Part of android.ApexModule. +func (g *Module) ShouldSupportSdkVersion(ctx android.BaseModuleContext, + sdkVersion android.ApiLevel) error { + // Because generated outputs are checked by client modules(e.g. cc_library, ...) + // we can safely ignore the check here. + return nil +} + +func generatorFactory(taskGenerator taskFunc, props ...interface{}) *Module { + module := &Module{ + taskGenerator: taskGenerator, + } + + module.AddProperties(props...) + module.AddProperties(&module.properties) + + module.ImageInterface = noopImageInterface{} + + return module +} + +type noopImageInterface struct{} + +func (x noopImageInterface) ImageMutatorBegin(android.BaseModuleContext) {} +func (x noopImageInterface) CoreVariantNeeded(android.BaseModuleContext) bool { return false } +func (x noopImageInterface) RamdiskVariantNeeded(android.BaseModuleContext) bool { return false } +func (x noopImageInterface) VendorRamdiskVariantNeeded(android.BaseModuleContext) bool { return false } +func (x noopImageInterface) DebugRamdiskVariantNeeded(android.BaseModuleContext) bool { return false } +func (x noopImageInterface) RecoveryVariantNeeded(android.BaseModuleContext) bool { return false } +func (x noopImageInterface) ExtraImageVariations(ctx android.BaseModuleContext) []string { return nil } +func (x noopImageInterface) SetImageVariation(ctx android.BaseModuleContext, variation string, module android.Module) { +} + +// Constructs a Module for handling the code generation. +func newCodegen() *Module { + properties := &codegenProperties{} + + // finalSubDir is the name of the subdirectory that output files will be generated into. + // It is used so that per-shard directories can be placed alongside it an then finally + // merged into it. + const finalSubDir = "wayland_protocol_codegen" + + // Code generation commands are sharded so that up to this many files + // are generated as part of one sandbox process. + const defaultShardSize = 100 + + taskGenerator := func(ctx android.ModuleContext, rawCommand string, srcFiles android.Paths) []generateTask { + shardSize := defaultShardSize + + if len(srcFiles) == 0 { + ctx.ModuleErrorf("must have at least one source file") + return []generateTask{} + } + + // wayland_protocol_codegen rules can easily hit command line limits by + // repeating the command for every input file. Shard the input files into + // groups. + shards := android.ShardPaths(srcFiles, shardSize) + var generateTasks []generateTask + + distinctOutputs := make(map[string]android.Path) + + for i, shard := range shards { + var commands []string + var outFiles android.WritablePaths + var copyTo android.WritablePaths + + // When sharding is enabled (i.e. len(shards) > 1), the sbox rules for each + // shard will be write to their own directories and then be merged together + // into finalSubDir. If sharding is not enabled (i.e. len(shards) == 1), + // the sbox rule will write directly to finalSubDir. + genSubDir := finalSubDir + if len(shards) > 1 { + genSubDir = strconv.Itoa(i) } - default: - if strings.HasPrefix(name, "location ") { - label := strings.TrimSpace(strings.TrimPrefix(name, "location ")) - if tool, ok := tools[label]; ok { - return tool.String(), nil - } else { - return "", fmt.Errorf("unknown location label %q", label) + + genDir := android.PathForModuleGen(ctx, genSubDir) + // NOTE: This TODO is copied from gensrcs, as applies here too. + // TODO(ccross): this RuleBuilder is a hack to be able to call + // rule.Command().PathForOutput. Replace this with passing the rule into the + // generator. + rule := android.NewRuleBuilder(pctx, ctx).Sbox(genDir, nil).SandboxTools() + + for _, in := range shard { + outFileRaw := expandOutputPath(ctx, *properties, in) + + if conflictWith, hasKey := distinctOutputs[outFileRaw]; hasKey { + ctx.ModuleErrorf("generation conflict: both '%v' and '%v' generate '%v'", + conflictWith.String(), in.String(), outFileRaw) } + + distinctOutputs[outFileRaw] = in + + outFile := android.PathForModuleGen(ctx, finalSubDir, outFileRaw) + + // If sharding is enabled, then outFile is the path to the output file in + // the shard directory, and copyTo is the path to the output file in the + // final directory. + if len(shards) > 1 { + shardFile := android.PathForModuleGen(ctx, genSubDir, outFileRaw) + copyTo = append(copyTo, outFile) + outFile = shardFile + } + + outFiles = append(outFiles, outFile) + + // pre-expand the command line to replace $in and $out with references to + // a single input and output file. + command, err := android.Expand(rawCommand, func(name string) (string, error) { + switch name { + case "in": + return in.String(), nil + case "out": + return rule.Command().PathForOutput(outFile), nil + default: + return "$(" + name + ")", nil + } + }) + if err != nil { + ctx.PropertyErrorf("cmd", err.Error()) + } + + // escape the command in case for example it contains '#', an odd number of '"', etc + command = fmt.Sprintf("bash -c %v", proptools.ShellEscape(command)) + commands = append(commands, command) } - return "", fmt.Errorf("unknown variable '$(%s)'", name) + fullCommand := strings.Join(commands, " && ") + + generateTasks = append(generateTasks, generateTask{ + in: shard, + out: outFiles, + copyTo: copyTo, + genDir: genDir, + cmd: fullCommand, + shard: i, + shards: len(shards), + }) } - }) - if err != nil { - ctx.PropertyErrorf("cmd", "%s", err.Error()) + + return generateTasks } - return + + g := generatorFactory(taskGenerator, properties) + g.subDir = finalSubDir + return g } -// waylandCodegenModuleFactory creates an extension module instance. -func waylandCodegenModuleFactory() android.Module { - m := &waylandGenModule{} - m.AddProperties(&m.properties) +// Factory for code generation modules +func codegenFactory() android.Module { + m := newCodegen() android.InitAndroidModule(m) + android.InitBazelModule(m) + android.InitDefaultableModule(m) return m } -// splitExt splits a base filename into (filename, ext) components, such that -// input == filename + ext -func splitExt(input string) (filename string, ext string) { - // There is no filepath.SplitExt() or equivalent. - dot := strings.LastIndex(input, ".") - if dot != -1 { - ext = input[dot:] - filename = input[:dot] - } else { - ext = "" - filename = input +// The custom properties specific to this code generation module. +type codegenProperties struct { + // The string to prepend to every protocol filename to generate the + // corresponding output filename. The empty string by default. + // Deprecated. Prefer "Output" instead. + Prefix *string + + // The suffix to append to every protocol filename to generate the + // corresponding output filename. The empty string by default. + // Deprecated. Prefer "Output" instead. + Suffix *string + + // The output filename template. + // + // This template string allows the output file name to be generated for + // each source file, using some limited properties of the source file. + // + // $(in:base): The base filename, no path or extension + // $(in:base.ext): The filename, no path + // $(in:path/base): The filename with path but no extension + // $(in:path/base.ext): The full source filename + // $(in): An alias for $(in:base) for the base filename, no extension + // + // Note that the path that is maintained is the relative path used when + // including the source in an Android.bp file. + // + // The template allows arbitrary prefixes and suffixes to be added to the + // output filename. For example, "a_$(in).d" would take an source filename + // of "b.c" and turn it into "a_b.d". + // + // The output template does not have to generate a unique filename, + // however the implementation will raise an error if the same output file + // is generated by more than one source file. + Output *string +} + +// Expands the output path pattern to form the output path for the given +// input path. +func expandOutputPath(ctx android.ModuleContext, properties codegenProperties, in android.Path) string { + template := proptools.String(properties.Output) + if len(template) == 0 { + prefix := proptools.String(properties.Prefix) + suffix := proptools.String(properties.Suffix) + return prefix + removeExtension(in.Base()) + suffix + } + + outPath, _ := android.Expand(template, func(name string) (string, error) { + // Report the error directly without returning an error to + // android.Expand to catch multiple errors in a single run. + reportError := func(fmt string, args ...interface{}) (string, error) { + ctx.PropertyErrorf("output", fmt, args...) + return "EXPANSION_ERROR", nil + } + + switch name { + case "in": + return removeExtension(in.Base()), nil + case "in:base": + return removeExtension(in.Base()), nil + case "in:base.ext": + return in.Base(), nil + case "in:path/base": + return removeExtension(in.Rel()), nil + case "in:path/base.ext": + return in.Rel(), nil + default: + return reportError("unknown variable '$(%s)'", name) + } + }) + + return outPath +} + +// Removes any extension from the final component of a path. +func removeExtension(path string) string { + // Note: This implementation does not handle files like ".bashrc" correctly. + if dot := strings.LastIndex(path, "."); dot != -1 { + return path[:dot] + } + return path +} + +// The attributes for the custom local ./bazel/gensrcs.bzl. See the .bzl file +// for attribute documentation. +type bazelGensrcsAttributes struct { + Srcs bazel.LabelListAttribute + Output string + Tools bazel.LabelListAttribute + Cmd string +} + +// ConvertWithBp2build converts a Soong module -> Bazel target. +func (m *Module) ConvertWithBp2build(ctx android.TopDownMutatorContext) { + // Bazel only has the "tools" attribute. + tools_prop := android.BazelLabelForModuleDeps(ctx, m.properties.Tools) + tool_files_prop := android.BazelLabelForModuleSrc(ctx, m.properties.Tool_files) + tools_prop.Append(tool_files_prop) + + tools := bazel.MakeLabelListAttribute(tools_prop) + srcs := bazel.LabelListAttribute{} + srcs_labels := bazel.LabelList{} + srcs_labels = android.BazelLabelForModuleSrcExcludes( + ctx, m.properties.Srcs, m.properties.Exclude_srcs) + srcs = bazel.MakeLabelListAttribute(srcs_labels) + + var allReplacements bazel.LabelList + allReplacements.Append(tools.Value) + allReplacements.Append(bazel.FirstUniqueBazelLabelList(srcs_labels)) + + // Convert the command line template. + var cmd string + if m.properties.Cmd != nil { + // $(in) becomes $(SRC) in our custom gensrcs.bzl + cmd = strings.ReplaceAll(*m.properties.Cmd, "$(in)", "$(SRC)") + // $(out) becomes $(OUT) in our custom gensrcs.bzl + cmd = strings.ReplaceAll(cmd, "$(out)", "$(OUT)") + // $(gendir) becomes $(RULEDIR) in our custom gensrcs.bzl + cmd = strings.Replace(cmd, "$(genDir)", "$(RULEDIR)", -1) + + // $(location) or $(locations) becomes the more explicit + // $(location <default-tool-label>) in Bazel. + if len(tools.Value.Includes) > 0 { + cmd = strings.Replace(cmd, "$(location)", + fmt.Sprintf("$(location %s)", tools.Value.Includes[0].Label), -1) + cmd = strings.Replace(cmd, "$(locations)", + fmt.Sprintf("$(locations %s)", tools.Value.Includes[0].Label), -1) + } + + // Translate all the other $(location <name>) and $(locations <name>) + // expansion placeholders. + for _, l := range allReplacements.Includes { + bpLoc := fmt.Sprintf("$(location %s)", l.OriginalModuleName) + bpLocs := fmt.Sprintf("$(locations %s)", l.OriginalModuleName) + bazelLoc := fmt.Sprintf("$(location %s)", l.Label) + bazelLocs := fmt.Sprintf("$(locations %s)", l.Label) + cmd = strings.Replace(cmd, bpLoc, bazelLoc, -1) + cmd = strings.Replace(cmd, bpLocs, bazelLocs, -1) + } } - return + + tags := android.ApexAvailableTags(m) + + // The Output_extension prop is not in an immediately accessible field + // in the Module struct, so use GetProperties and cast it + // to the known struct prop. + var outputFileTemplate string + for _, propIntf := range m.GetProperties() { + if props, ok := propIntf.(*codegenProperties); ok { + // Convert the output path template. + outputFileTemplate = proptools.String(props.Output) + if len(outputFileTemplate) > 0 { + outputFileTemplate = strings.Replace( + outputFileTemplate, "$(in)", "$(SRC:BASE)", -1) + outputFileTemplate = strings.Replace( + outputFileTemplate, "$(in:path/base.ext)", "$(SRC:PATH/BASE.EXT)", -1) + outputFileTemplate = strings.Replace( + outputFileTemplate, "$(in:path/base)", "$(SRC:PATH/BASE)", -1) + outputFileTemplate = strings.Replace( + outputFileTemplate, "$(in:base.ext)", "$(SRC:BASE.EXT)", -1) + outputFileTemplate = strings.Replace( + outputFileTemplate, "$(in:base)", "$(SRC:BASE)", -1) + } else { + outputFileTemplate = proptools.String(props.Prefix) + "$(SRC:BASE)" + + proptools.String(props.Suffix) + } + break + } + } + props := bazel.BazelTargetModuleProperties{ + Rule_class: "gensrcs", + Bzl_load_location: "//external/wayland-protocols/bazel:gensrcs.bzl", + } + attrs := &bazelGensrcsAttributes{ + Srcs: srcs, + Output: outputFileTemplate, + Cmd: cmd, + Tools: tools, + } + ctx.CreateBazelTargetModule(props, android.CommonAttributes{ + Name: m.Name(), + Tags: tags, + }, attrs) +} + +// Defaults module. +type Defaults struct { + android.ModuleBase + android.DefaultsModuleBase +} + +func defaultsFactory() android.Module { + return DefaultsFactory() +} + +func DefaultsFactory(props ...interface{}) android.Module { + module := &Defaults{} + + module.AddProperties(props...) + module.AddProperties( + &generatorProperties{}, + &codegenProperties{}, + ) + + android.InitDefaultsModule(module) + + return module } diff --git a/wayland_protocol_codegen_test.go b/wayland_protocol_codegen_test.go new file mode 100644 index 0000000..90f1200 --- /dev/null +++ b/wayland_protocol_codegen_test.go @@ -0,0 +1,488 @@ +// 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. + +package soong_wayland_protocol_codegen + +import ( + "os" + "regexp" + "testing" + + "android/soong/android" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +var prepareForCodeGenTest = android.GroupFixturePreparers( + android.PrepareForTestWithArchMutator, + android.PrepareForTestWithDefaults, + android.PrepareForTestWithFilegroup, + android.GroupFixturePreparers( + android.FixtureRegisterWithContext(registerCodeGenBuildComponents), + ), + android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) { + android.RegisterPrebuiltMutators(ctx) + ctx.RegisterModuleType("fake_android_host_tool", fakeAndroidHostToolFactory) + }), + android.FixtureMergeMockFs(android.MockFS{ + "android_host_tool": nil, + "tool_src_file": nil, + "tool_src_file_1": nil, + "tool_src_file_2": nil, + "src_file": nil, + "src_file_1": nil, + "src_file_2": nil, + }), +) + +func testCodeGenBp() string { + return ` + fake_android_host_tool { + name: "host_tool", + } + + filegroup { + name: "tool_single_source_file_filegroup", + srcs: [ + "tool_src_file", + ], + } + + filegroup { + name: "tool_multi_source_files_filegroup", + srcs: [ + "tool_src_file_1", + "tool_src_file_2", + ], + } + + filegroup { + name: "single_source_filegroup", + srcs: [ + "src_file", + ], + } + + filegroup { + name: "multi_source_filegroup", + srcs: [ + "src_file_1", + "src_file_2", + ], + } + + filegroup { + name: "empty_filegroup", + } + ` +} + +func TestWaylandCodeGen(t *testing.T) { + testcases := []struct { + name string + prop string + + err string + cmds []string + files []string + }{ + { + name: "single_source_with_host_tool", + prop: ` + tools: ["host_tool"], + srcs: ["src_file"], + output: "prefix_$(in)_suffix", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < src_file > __SBOX_SANDBOX_DIR__/out/prefix_src_file_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_suffix", + }, + }, + { + name: "multi_source_with_host_tool", + prop: ` + tools: ["host_tool"], + srcs: ["src_file_1", "src_file_2"], + output: "prefix_$(in)_suffix", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < src_file_1 > __SBOX_SANDBOX_DIR__/out/prefix_src_file_1_suffix' && bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < src_file_2 > __SBOX_SANDBOX_DIR__/out/prefix_src_file_2_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_1_suffix", + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_2_suffix", + }, + }, + { + name: "single_source_filegroup_with_host_tool", + prop: ` + tools: ["host_tool"], + srcs: [":single_source_filegroup"], + output: "prefix_$(in)_suffix", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < src_file > __SBOX_SANDBOX_DIR__/out/prefix_src_file_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_suffix", + }, + }, + { + name: "multi_source_filegroup_with_host_tool", + prop: ` + tools: ["host_tool"], + srcs: [":multi_source_filegroup"], + output: "prefix_$(in)_suffix", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < src_file_1 > __SBOX_SANDBOX_DIR__/out/prefix_src_file_1_suffix' && bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < src_file_2 > __SBOX_SANDBOX_DIR__/out/prefix_src_file_2_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_1_suffix", + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_2_suffix", + }, + }, + { + name: "single_source_with_single_tool_file", + prop: ` + tool_files: ["tool_src_file"], + srcs: ["src_file"], + output: "prefix_$(in)_suffix", + cmd: "$(location tool_src_file) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/tool_src_file gen < src_file > __SBOX_SANDBOX_DIR__/out/prefix_src_file_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_suffix", + }, + }, + { + name: "multi_source_with_single_tool_file", + prop: ` + tool_files: ["tool_src_file"], + srcs: ["src_file_1", "src_file_2"], + output: "prefix_$(in)_suffix", + cmd: "$(location tool_src_file) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/tool_src_file gen < src_file_1 > __SBOX_SANDBOX_DIR__/out/prefix_src_file_1_suffix' && bash -c '__SBOX_SANDBOX_DIR__/tools/src/tool_src_file gen < src_file_2 > __SBOX_SANDBOX_DIR__/out/prefix_src_file_2_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_1_suffix", + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_2_suffix", + }, + }, + { + name: "single_source_filegroup_with_single_tool_file", + prop: ` + tool_files: ["tool_src_file"], + srcs: [":single_source_filegroup"], + output: "prefix_$(in)_suffix", + cmd: "$(location tool_src_file) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/tool_src_file gen < src_file > __SBOX_SANDBOX_DIR__/out/prefix_src_file_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_suffix", + }, + }, + { + name: "multi_source_filegroup_with_single_tool_file", + prop: ` + tool_files: ["tool_src_file"], + srcs: [":multi_source_filegroup"], + output: "prefix_$(in)_suffix", + cmd: "$(location tool_src_file) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/tool_src_file gen < src_file_1 > __SBOX_SANDBOX_DIR__/out/prefix_src_file_1_suffix' && bash -c '__SBOX_SANDBOX_DIR__/tools/src/tool_src_file gen < src_file_2 > __SBOX_SANDBOX_DIR__/out/prefix_src_file_2_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_1_suffix", + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_2_suffix", + }, + }, + { + name: "multiple_tool_files", + prop: ` + tool_files: ["tool_src_file_1", "tool_src_file_2"], + srcs: ["src_file"], + output: "prefix_$(in)_suffix", + cmd: "$(location tool_src_file_1) $(location tool_src_file_2) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/tool_src_file_1 __SBOX_SANDBOX_DIR__/tools/src/tool_src_file_2 gen < src_file > __SBOX_SANDBOX_DIR__/out/prefix_src_file_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/prefix_src_file_suffix", + }, + }, + { + name: "output_template_explicit_base_only", + prop: ` + tools: ["host_tool"], + srcs: ["txt/a/file.txt"], + output: "$(in:base)", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < txt/a/file.txt > __SBOX_SANDBOX_DIR__/out/file'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/file", + }, + }, + { + name: "output_template_explicit_base_and_ext", + prop: ` + tools: ["host_tool"], + srcs: ["txt/a/file.txt"], + output: "$(in:base.ext)", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < txt/a/file.txt > __SBOX_SANDBOX_DIR__/out/file.txt'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/file.txt", + }, + }, + { + name: "output_template_explicit_path_and_base", + prop: ` + tools: ["host_tool"], + srcs: ["txt/a/file.txt"], + output: "$(in:path/base)", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < txt/a/file.txt > __SBOX_SANDBOX_DIR__/out/txt/a/file'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/txt/a/file", + }, + }, + { + name: "output_template_explicit_path_and_base_and_ext", + prop: ` + tools: ["host_tool"], + srcs: ["txt/a/file.txt"], + output: "$(in:path/base.ext)", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < txt/a/file.txt > __SBOX_SANDBOX_DIR__/out/txt/a/file.txt'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/txt/a/file.txt", + }, + }, + { + name: "single_source_file_does_not_need_distinct_outputs", + prop: ` + tools: ["host_tool"], + srcs: ["src_file"], + output: "output", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < src_file > __SBOX_SANDBOX_DIR__/out/output'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/output", + }, + }, + { + name: "legacy_prefix_suffix", + prop: ` + tools: ["host_tool"], + srcs: ["src_file"], + prefix: "legacy_prefix_", + suffix: "_legacy_suffix", + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + cmds: []string{ + "bash -c '__SBOX_SANDBOX_DIR__/tools/src/out/host_tool gen < src_file > __SBOX_SANDBOX_DIR__/out/legacy_prefix_src_file_legacy_suffix'", + }, + files: []string{ + "out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/legacy_prefix_src_file_legacy_suffix", + }, + }, + { + name: "error_if_no_sources", + prop: ` + tools: ["host_tool"], + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + err: "must have at least one source file", + }, + { + name: "error_if_no_filegroup_sources", + prop: ` + tools: ["host_tool"], + srcs: [":empty_filegroup"], + cmd: "$(location host_tool) gen < $(in) > $(out)", + `, + err: "must have at least one source file", + }, + { + name: "error_if_in_outputs_are_not_distinct", + prop: ` + tools: ["host_tool"], + tool_files: ["tool_src_file"], + srcs: ["src_file_1", "src_file_2"], + output: "not_unique", + cmd: "$(location)" + `, + err: "Android.bp:39:2: module \"codegen\": generation conflict: both 'src_file_1' and 'src_file_2' generate 'not_unique'", + }, + { + name: "error_if_output_expansion_fails", + prop: ` + tools: ["host_tool"], + tool_files: ["tool_src_file"], + srcs: ["src_file"], + output: "prefix_$(bad)_suffix", + cmd: "$(location)" + `, + err: "Android.bp:45:11: module \"codegen\": output: unknown variable '$(bad)'", + }, + { + name: "error_if_cmd_expansion_fails", + prop: ` + tools: ["host_tool"], + tool_files: ["tool_src_file"], + srcs: ["src_file"], + output: "prefix_$(in)_suffix", + cmd: "$(location bad_name)" + `, + err: "Android.bp:46:8: module \"codegen\": cmd: unknown location label \"bad_name\"", + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + bp := "wayland_protocol_codegen {\n" + bp += `name: "codegen",` + "\n" + bp += test.prop + bp += "}\n" + + var expectedErrors []string + if test.err != "" { + expectedErrors = append(expectedErrors, regexp.QuoteMeta(test.err)) + } + + result := prepareForCodeGenTest. + ExtendWithErrorHandler(android.FixtureExpectsAllErrorsToMatchAPattern(expectedErrors)). + RunTestWithBp(t, testCodeGenBp()+bp) + + if expectedErrors != nil { + return + } + + gen := result.Module("codegen", "").(*Module) + android.AssertDeepEquals(t, "cmd", test.cmds, gen.rawCommands) + android.AssertPathsRelativeToTopEquals(t, "files", test.files, gen.outputFiles) + }) + } +} + +func TestGenruleWithBazel(t *testing.T) { + bp := ` + wayland_protocol_codegen { + name: "mixed_codegen", + srcs: ["src_file"], + bazel_module: { label: "//example:bazel_codegen" }, + } + ` + + result := android.GroupFixturePreparers( + prepareForCodeGenTest, android.FixtureModifyConfig(func(config android.Config) { + config.BazelContext = android.MockBazelContext{ + OutputBaseDir: "outputbase", + LabelToOutputFiles: map[string][]string{ + "//example:bazel_codegen": {"bazelone.txt", "bazeltwo.txt"}}} + })).RunTestWithBp(t, testCodeGenBp()+bp) + + gen := result.Module("mixed_codegen", "").(*Module) + + expectedOutputFiles := []string{"outputbase/execroot/__main__/bazelone.txt", + "outputbase/execroot/__main__/bazeltwo.txt"} + android.AssertDeepEquals(t, "output files", expectedOutputFiles, gen.outputFiles.Strings()) + android.AssertDeepEquals(t, "output deps", expectedOutputFiles, gen.outputDeps.Strings()) +} + +func TestDefaults(t *testing.T) { + bp := ` + wayland_protocol_codegen_defaults { + name: "gen_defaults1", + cmd: "cp $(in) $(out)", + output: "$(in).h", + } + + wayland_protocol_codegen_defaults { + name: "gen_defaults2", + srcs: ["in1"], + } + + wayland_protocol_codegen { + name: "codegen", + defaults: ["gen_defaults1", "gen_defaults2"], + } + ` + + result := prepareForCodeGenTest.RunTestWithBp(t, testCodeGenBp()+bp) + + gen := result.Module("codegen", "").(*Module) + + expectedCmd := "bash -c cp in1 __SBOX_SANDBOX_DIR__/out/in1.h" + android.AssertStringEquals(t, "cmd", expectedCmd, gen.rawCommands[0]) + + expectedSrcs := []string{"in1"} + android.AssertDeepEquals(t, "srcs", expectedSrcs, gen.properties.Srcs) + + expectedFiles := []string{"out/soong/.intermediates/codegen/gen/wayland_protocol_codegen/in1.h"} + android.AssertPathsRelativeToTopEquals(t, "files", expectedFiles, gen.outputFiles) +} + +type fakeAndroidHostTool struct { + android.ModuleBase + outputFile android.Path +} + +func fakeAndroidHostToolFactory() android.Module { + module := &fakeAndroidHostTool{} + android.InitAndroidArchModule(module, android.HostSupported, android.MultilibFirst) + return module +} + +func (t *fakeAndroidHostTool) GenerateAndroidBuildActions(ctx android.ModuleContext) { + t.outputFile = android.PathForTesting("out", ctx.ModuleName()) +} + +func (t *fakeAndroidHostTool) HostToolPath() android.OptionalPath { + return android.OptionalPathForPath(t.outputFile) +} + +var _ android.HostToolProvider = (*fakeAndroidHostTool)(nil) |