| // Copyright 2025 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.devtools.build.lib.analysis; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.assertThrows; |
| |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil; |
| import com.google.devtools.build.lib.analysis.util.AnalysisTestCase; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| import com.google.devtools.build.lib.testutil.TestConstants; |
| import java.util.List; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| @RunWith(JUnit4.class) |
| public final class MaterializerRulesRealDepsTest extends AnalysisTestCase { |
| |
| @Before |
| public void enableDormantDeps() throws Exception { |
| useConfiguration( |
| "--experimental_dormant_deps", "--incompatible_package_group_has_public_syntax"); |
| } |
| |
| @Before |
| public void writeMaterializerRulesAllowlist() throws Exception { |
| writeMaterializerRulesAllowlist(true, true); |
| } |
| |
| public void writeMaterializerRulesAllowlist( |
| boolean materializerRuleAllowed, boolean allowRealDeps) throws Exception { |
| |
| scratch.overwriteFile( |
| TestConstants.TOOLS_REPOSITORY_SCRATCH |
| + "tools/allowlists/materializer_rule_allowlist/BUILD", |
| """ |
| package_group( |
| name = 'materializer_rule_allowlist', |
| packages = [%s], |
| ) |
| |
| package_group( |
| name = 'materializer_rule_real_deps_allowlist', |
| packages = [%s], |
| ) |
| """ |
| .formatted( |
| materializerRuleAllowed ? "\"public\"" : "", allowRealDeps ? "\"public\"" : "")); |
| } |
| |
| private void writeBasicMaterializerRule() throws Exception { |
| scratch.file( |
| "defs.bzl", |
| """ |
| # Component ###################################### |
| |
| ComponentInfo = provider(fields = ["output", "info"]) |
| |
| def _component_impl(ctx): |
| f = ctx.actions.declare_file(ctx.label.name + ".txt") |
| ctx.actions.write(f, ctx.label.name) |
| return ComponentInfo(output = f, info = ctx.attr.info) |
| |
| component = rule( |
| implementation = _component_impl, |
| provides = [ComponentInfo], |
| attrs = { |
| "info": attr.string(), |
| } |
| ) |
| |
| # Component selector ############################# |
| |
| def _component_selector_impl(ctx): |
| selected = [] |
| for c in ctx.attr.all_components: |
| if "yes" in c[ComponentInfo].info: |
| selected.append(c) |
| return MaterializedDepsInfo(deps = selected) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "all_components": attr.label_list(), |
| }, |
| ) |
| |
| # Binary ######################################### |
| |
| def _binary_impl(ctx): |
| files = [dep[ComponentInfo].output for dep in ctx.attr.deps] |
| return DefaultInfo(files = depset(direct = files)) |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo]), |
| }, |
| ) |
| """); |
| } |
| |
| /** Tests materializing real deps through materializer rules. */ |
| @Test |
| public void basicMaterializerRuleRealDeps_works() throws Exception { |
| writeBasicMaterializerRule(); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [ |
| ":aaa", |
| ":component_selector", |
| ":zzz", |
| ], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", |
| ":c", |
| ":d", |
| ], |
| ) |
| |
| component(name = "aaa", info = "yes") |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "no") |
| component(name = "d", info = "no") |
| component(name = "zzz", info = "yes") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild)) |
| .containsExactly("aaa.txt", "a.txt", "b.txt", "zzz.txt"); |
| } |
| |
| /** Tests that multiple materializer rules in an attribute works. */ |
| @Test |
| public void multipleMaterializerRules_works() throws Exception { |
| writeBasicMaterializerRule(); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [ |
| ":aaa", |
| ":component_selector", |
| ":component_selector_2", |
| ":zzz", |
| ], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", |
| ":c", |
| ":d", |
| ], |
| ) |
| |
| component_selector( |
| name = "component_selector_2", |
| all_components = [ |
| ":e", |
| ":f", |
| ":g", |
| ":h", |
| ], |
| ) |
| |
| component(name = "aaa", info = "yes") |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "no") |
| component(name = "d", info = "no") |
| component(name = "e", info = "yes") |
| component(name = "f", info = "yes") |
| component(name = "g", info = "no") |
| component(name = "h", info = "no") |
| component(name = "zzz", info = "yes") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild)) |
| .containsExactly("aaa.txt", "a.txt", "b.txt", "e.txt", "f.txt", "zzz.txt"); |
| } |
| |
| @Test |
| public void multipleMaterializersReturnSameTarget_works() throws Exception { |
| writeBasicMaterializerRule(); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [ |
| ":aaa", |
| ":component_selector", |
| ":component_selector_2", |
| ":zzz", |
| ], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", # <- overlap |
| ":c", # <- overlap |
| ":d", |
| ":e", |
| ], |
| ) |
| |
| component_selector( |
| name = "component_selector_2", |
| all_components = [ |
| ":b", # <- overlap |
| ":c", # <- overlap |
| ":f", |
| ":g", |
| ":h", |
| ], |
| ) |
| |
| component(name = "aaa", info = "yes") |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "yes") |
| component(name = "d", info = "no") |
| component(name = "e", info = "no") |
| component(name = "f", info = "yes") |
| component(name = "g", info = "no") |
| component(name = "h", info = "no") |
| component(name = "zzz", info = "yes") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild)) |
| .containsExactly("aaa.txt", "a.txt", "b.txt", "c.txt", "f.txt", "zzz.txt"); |
| } |
| |
| @Test |
| public void materializerWithRealDepsNotInAllowlist_throwsError() throws Exception { |
| |
| writeMaterializerRulesAllowlist(true, false); |
| |
| writeBasicMaterializerRule(); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [":component_selector"], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [":a"], |
| ) |
| |
| component(name = "a", info = "yes") |
| """); |
| |
| this.reporter.removeHandler(failFastHandler); |
| assertThrows(ViewCreationFailedException.class, () -> update("//:bin")); |
| assertContainsEvent( |
| "in component_selector rule //:component_selector: Non-allowlisted use of real deps in " |
| + "materializer target"); |
| } |
| |
| @Test |
| public void aspectsThroughMaterializerRules_works() throws Exception { |
| scratch.file( |
| "defs.bzl", |
| """ |
| # Component ###################################### |
| |
| ComponentInfo = provider(fields = ["info"]) |
| |
| def _component_impl(ctx): |
| return ComponentInfo(info = ctx.attr.info) |
| |
| component = rule( |
| implementation = _component_impl, |
| provides = [ComponentInfo], |
| attrs = { |
| "info": attr.string(), |
| } |
| ) |
| |
| # Component selector ############################# |
| |
| def _component_selector_impl(ctx): |
| selected = [] |
| for c in ctx.attr.all_components: |
| if "yes" in c[ComponentInfo].info: |
| selected.append(c) |
| return MaterializedDepsInfo(deps = selected) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "all_components": attr.label_list(), |
| }, |
| ) |
| |
| # Aspect ######################################### |
| |
| AspectInfo = provider(fields = ["info_artifact"]) |
| |
| def _mt_aspect_impl(target, ctx): |
| if ctx.rule.kind != "component": |
| return [] |
| artifact = ctx.actions.declare_file(target.label.name + ".info") |
| ctx.actions.write(artifact, str(target.label)) |
| return AspectInfo(info_artifact = artifact) |
| |
| mt_aspect = aspect( |
| implementation = _mt_aspect_impl, |
| attrs = { |
| # Context creation code is shared between aspects and rules, so having this aspect |
| # dependency in the test ensure that materializer dependency validation is only performed |
| # for rules and not for aspects. |
| "_tool": attr.label( |
| default = Label("//:aspect_tool"), |
| cfg = "exec", |
| ), |
| } |
| ) |
| |
| # Binary ######################################### |
| |
| def _binary_impl(ctx): |
| for dep in ctx.attr.deps: |
| print(dep[AspectInfo]) |
| return DefaultInfo(files = depset(direct = [dep[AspectInfo].info_artifact for dep in ctx.attr.deps])) |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo], aspects = [mt_aspect]), |
| }, |
| ) |
| """); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [ |
| ":aaa", |
| ":component_selector", |
| ":zzz", |
| ], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", |
| ":c", |
| ":d", |
| ], |
| ) |
| |
| component(name = "aaa", info = "yes") |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "no") |
| component(name = "d", info = "no") |
| component(name = "zzz", info = "yes") |
| |
| genrule( |
| name = "aspect_tool", |
| outs = ["tool"], |
| executable = True, |
| cmd = "echo 'touch $$1' > $@", |
| ) |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| // The .info files come from the aspect, and only the files from the selected deps |
| // should be returned. |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild)) |
| .containsExactly("aaa.info", "a.info", "b.info", "zzz.info"); |
| } |
| |
| @Test |
| public void aspectsOriginatingFromMaterializerRules_works() throws Exception { |
| scratch.file( |
| "defs.bzl", |
| """ |
| # Component ###################################### |
| |
| ComponentInfo = provider(fields = ["output", "deps_outputs", "include"]) |
| |
| def _component_impl(ctx): |
| f = ctx.actions.declare_file(ctx.label.name + ".txt") |
| ctx.actions.write(f, ctx.label.name) |
| return ComponentInfo( |
| output = f, |
| deps_outputs = depset( |
| direct = [f], |
| transitive = [dep[ComponentInfo].deps_outputs for dep in ctx.attr.deps], |
| ), |
| include = ctx.attr.include, |
| ) |
| |
| component = rule( |
| implementation = _component_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo]), |
| "include": attr.bool(), |
| }, |
| provides = [ComponentInfo], |
| ) |
| |
| # Aspect ######################################### |
| |
| AspectInfo = provider(fields = ["include"]) |
| |
| def _mt_aspect_impl(target, ctx): |
| include = False |
| for dep in ctx.rule.attr.deps: |
| if dep[AspectInfo].include or dep[ComponentInfo].include: |
| include = True |
| break |
| return AspectInfo(include = include) |
| |
| mt_aspect = aspect( |
| implementation = _mt_aspect_impl, |
| attr_aspects = ["deps"], |
| ) |
| |
| # Component selector ############################# |
| |
| def _component_selector_impl(ctx): |
| |
| components = [] |
| for component in ctx.attr.components: |
| if AspectInfo in component: |
| if component[AspectInfo].include: |
| components.append(component) |
| |
| return MaterializedDepsInfo(deps = components) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "components": attr.label_list(aspects = [mt_aspect]), |
| }, |
| ) |
| |
| # Binary ######################################### |
| |
| def _binary_impl(ctx): |
| files = [impl[ComponentInfo].deps_outputs for impl in ctx.attr.deps] |
| return DefaultInfo(files = depset(transitive = files)) |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo]), |
| }, |
| ) |
| """); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "binary", "component_selector", "component") |
| |
| binary( |
| name = "bin", |
| deps = [":component_selector"], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| components = [":a", ":d", ":g"], |
| ) |
| |
| component(name = "a", deps = [":b", ":c"]) |
| component(name = "b") |
| component(name = "c") |
| |
| component(name = "d", deps = [":e", ":f"]) |
| component(name = "e") |
| component(name = "f", include = True) |
| |
| component(name = "g", deps = [":h", ":i"]) |
| component(name = "h") |
| component(name = "i") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| |
| // Only the artifacts from branches that have "include = True" somewhere in the DAG as collected |
| // by the aspect are included. |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild)) |
| .containsExactly("d.txt", "e.txt", "f.txt"); |
| } |
| |
| /** |
| * Materializers-to-materializers aren't implemented to with dormant deps because they would need |
| * to be recursively resolved within a single ConfiguredTarget function (and cycles would need to |
| * be detected), but with "real deps" they're properly resolved at each level. |
| */ |
| @Test |
| public void materializerToMaterializer_works() throws Exception { |
| |
| scratch.file( |
| "defs.bzl", |
| """ |
| ################################################# |
| # Component |
| |
| ComponentInfo = provider(fields = ["output", "info"]) |
| |
| def _component_impl(ctx): |
| f = ctx.actions.declare_file(ctx.label.name + ".txt") |
| ctx.actions.write(f, ctx.label.name) |
| return ComponentInfo(output = f, info = ctx.attr.info) |
| |
| component = rule( |
| implementation = _component_impl, |
| provides = [ComponentInfo], |
| attrs = { |
| "info": attr.string(), |
| } |
| ) |
| |
| ################################################# |
| # Component selector |
| |
| def _component_selector_impl(ctx): |
| selected = [] |
| for c in ctx.attr.all_components: |
| if "yes" in c[ComponentInfo].info: |
| selected.append(c) |
| return MaterializedDepsInfo(deps = selected) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "all_components": attr.label_list(), |
| }, |
| ) |
| |
| ################################################# |
| # Binary |
| |
| def _binary_impl(ctx): |
| files = [dep[ComponentInfo].output for dep in ctx.attr.deps] |
| return DefaultInfo(files = depset(direct = files)) |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo]), |
| }, |
| ) |
| """); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [":component_selector"], |
| ) |
| |
| # Because the materializer handler code codes doesn't account for materializers materializing |
| # other materializers, component_selector2 would materialize "b" and "c" and "a" would be |
| # overridden here. So we throw an error. |
| component_selector( |
| name = "component_selector", |
| all_components = [":component_selector2", ":a"], |
| ) |
| |
| component_selector( |
| name = "component_selector2", |
| all_components = [":b", ":c", ":d"], |
| ) |
| |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "yes") |
| component(name = "d", info = "yes") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild)) |
| .containsExactly("a.txt", "b.txt", "c.txt", "d.txt"); |
| } |
| |
| /** |
| * Tests that an alias can point to a materializer rule (i.e. a materializer rule can go through |
| * an alias). |
| */ |
| @Test |
| public void aliasToMaterializerRule_works() throws Exception { |
| |
| scratch.file( |
| "defs.bzl", |
| """ |
| # Component ###################################### |
| |
| ComponentInfo = provider(fields = ["info"]) |
| |
| def _component_impl(ctx): |
| return ComponentInfo(info = ctx.attr.info) |
| |
| component = rule( |
| implementation = _component_impl, |
| provides = [ComponentInfo], |
| attrs = { |
| "info": attr.string(), |
| } |
| ) |
| |
| # Component selector ############################# |
| |
| def _component_selector_impl(ctx): |
| selected = [] |
| for c in ctx.attr.all_components: |
| if "yes" in c[ComponentInfo].info: |
| selected.append(c) |
| return MaterializedDepsInfo(deps = selected) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "all_components": attr.label_list(), |
| }, |
| ) |
| |
| # Aspect ######################################### |
| |
| AspectInfo = provider(fields = ["info_artifact"]) |
| |
| def _mt_aspect_impl(target, ctx): |
| if ctx.rule.kind != "component": |
| return [] |
| print("aspect visiting target: " + str(target.label)) |
| artifact = ctx.actions.declare_file(target.label.name + ".info") |
| ctx.actions.write(artifact, str(target.label)) |
| return AspectInfo(info_artifact = artifact) |
| |
| mt_aspect = aspect( |
| implementation = _mt_aspect_impl, |
| ) |
| |
| # Binary ######################################### |
| |
| def _binary_impl(ctx): |
| return DefaultInfo(files = depset(direct = [dep[AspectInfo].info_artifact for dep in ctx.attr.deps])) |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo], aspects = [mt_aspect]), |
| }, |
| ) |
| """); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| # Real deps through single alias #################################### |
| |
| binary( |
| name = "bin", |
| deps = [ |
| ":AAA", |
| ":component_selector_alias", |
| ":ZZZ", |
| ], |
| ) |
| |
| alias( |
| name = "component_selector_alias", |
| actual = ":component_selector", |
| ) |
| |
| # Real deps through alias chain #################################### |
| |
| binary( |
| name = "bin_alias_chain", |
| deps = [ |
| ":AAA", |
| ":component_selector_alias_alias", |
| ":ZZZ", |
| ], |
| ) |
| |
| alias( |
| name = "component_selector_alias_alias", |
| actual = ":component_selector_alias", |
| ) |
| |
| # Materializer rules ############################# |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", |
| ":c", |
| ":d", |
| ], |
| ) |
| |
| # capitalized because the "a" component will also match the "aaa" component in the assertions below |
| component(name = "AAA", info = "yes") |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "no") |
| component(name = "d", info = "no") |
| component(name = "ZZZ", info = "yes") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild)) |
| .containsExactly("AAA.info", "a.info", "b.info", "ZZZ.info"); |
| eventCollector.clear(); |
| |
| cleanSkyframe(); |
| update("//:bin_alias_chain"); |
| ConfiguredTarget targetAliasChain = getConfiguredTarget("//:bin_alias_chain"); |
| NestedSet<Artifact> filesToBuildAliasChain = |
| targetAliasChain.getProvider(FileProvider.class).getFilesToBuild(); |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuildAliasChain)) |
| .containsExactly("AAA.info", "a.info", "b.info", "ZZZ.info"); |
| |
| // Especially when going through an alias chain, an aspect should still visit the nodes once. |
| assertContainsEventWithFrequency("aspect visiting target: @@//:AAA", 1); |
| assertContainsEventWithFrequency("aspect visiting target: @@//:ZZZ", 1); |
| assertContainsEventWithFrequency("aspect visiting target: @@//:b", 1); |
| assertContainsEventWithFrequency("aspect visiting target: @@//:a", 1); |
| assertDoesNotContainEvent("aspect visiting target: @@//:c"); |
| assertDoesNotContainEvent("aspect visiting target: @@//:d"); |
| } |
| |
| /** Tests that a materializer can point to an alias and the final target is materialized. */ |
| @Test |
| public void materializerToAlias_works() throws Exception { |
| |
| writeBasicMaterializerRule(); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [ |
| ":aaa", |
| ":component_selector", |
| ":zzz", |
| ], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a_alias", |
| ":b_alias", |
| ":c", |
| ":d", |
| ":d_alias", |
| ], |
| ) |
| |
| alias(name = "a_alias", actual = "a") |
| alias(name = "b_alias", actual = "b") |
| alias(name = "d_alias", actual = "d") |
| |
| component(name = "aaa", info = "yes") |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "no") |
| component(name = "d", info = "no") |
| component(name = "zzz", info = "yes") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild)) |
| .containsExactly("aaa.txt", "a.txt", "b.txt", "zzz.txt"); |
| } |
| |
| /** Tests alias -> materializer -> alias -> materializer -> alias works with real deps. */ |
| @Test |
| public void aliasToMaterializerToAliasToMaterializerToAlias_works() throws Exception { |
| scratch.file( |
| "defs.bzl", |
| """ |
| # Component ###################################### |
| |
| ComponentInfo = provider(fields = ["output", "info"]) |
| |
| def _component_impl(ctx): |
| f = ctx.actions.declare_file(ctx.label.name + ".txt") |
| ctx.actions.write(f, ctx.label.name) |
| return ComponentInfo(output = f, info = ctx.attr.info) |
| |
| component = rule( |
| implementation = _component_impl, |
| provides = [ComponentInfo], |
| attrs = { |
| "info": attr.string(), |
| } |
| ) |
| |
| # Component selector ############################# |
| |
| def _component_selector_impl(ctx): |
| return MaterializedDepsInfo(deps = ctx.attr.components) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "components": attr.label_list(), |
| }, |
| ) |
| |
| # Binary ######################################### |
| |
| def _binary_impl(ctx): |
| files = [dep[ComponentInfo].output for dep in ctx.attr.deps] |
| return DefaultInfo(files = depset(direct = files)) |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo]), |
| }, |
| ) |
| """); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [ |
| ":aaa", |
| ":alias_to_materializer_to_alias_to_materializer_to_alias", |
| ":zzz", |
| ], |
| ) |
| |
| alias( |
| name = "alias_to_materializer_to_alias_to_materializer_to_alias", |
| actual = ":materializer_to_alias_to_materializer_to_alias", |
| ) |
| |
| component_selector( |
| name = "materializer_to_alias_to_materializer_to_alias", |
| components = [":alias_to_materializer_to_alias"], |
| ) |
| |
| alias( |
| name = "alias_to_materializer_to_alias", |
| actual = ":materializer_to_alias", |
| ) |
| |
| component_selector( |
| name = "materializer_to_alias", |
| components = [":a_alias"], |
| ) |
| |
| alias( |
| name = "a_alias", |
| actual = ":a", |
| ) |
| |
| component(name = "aaa", info = "yes") |
| component(name = "a", info = "yes") |
| component(name = "zzz", info = "yes") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| List<String> artifactNames = ActionsTestUtil.baseArtifactNames(filesToBuild); |
| assertThat(artifactNames).containsExactly("aaa.txt", "a.txt", "zzz.txt"); |
| } |
| |
| private void writeMaterializerSplitTransitionBzlFile() throws Exception { |
| scratch.file( |
| "defs.bzl", |
| """ |
| # Component selector setting ########################################### |
| |
| ComponentSelectorProvider = provider(fields = ["selector"]) |
| |
| def _component_selector_setting_impl(ctx): |
| return ComponentSelectorProvider(selector = ctx.build_setting_value) |
| |
| component_selector_setting = rule( |
| implementation = _component_selector_setting_impl, |
| build_setting = config.string() |
| ) |
| |
| # Component transition |
| |
| def _component_selector_setting_transition_impl(settings, attr): |
| return [ |
| {"//:component_selector_setting" : "foo"}, |
| {"//:component_selector_setting" : "bar"}, |
| ] |
| |
| component_transition = transition( |
| implementation = _component_selector_setting_transition_impl, |
| inputs = [], |
| outputs = ["//:component_selector_setting"], |
| ) |
| |
| # Component ############################################################ |
| |
| ComponentInfo = provider(fields = ["output", "info"]) |
| |
| def _component_impl(ctx): |
| f = ctx.actions.declare_file(ctx.label.name + ".txt") |
| ctx.actions.write(f, ctx.label.name) |
| return ComponentInfo(output = f, info = ctx.attr.info) |
| |
| component = rule( |
| implementation = _component_impl, |
| provides = [ComponentInfo], |
| attrs = { |
| "info": attr.string(), |
| } |
| ) |
| |
| # Component selector ################################################### |
| |
| def _component_selector_impl(ctx): |
| selected = [] |
| for c in ctx.attr.all_components: |
| if "yes" in c[ComponentInfo].info: |
| selected.append(c) |
| return MaterializedDepsInfo(deps = selected) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "all_components": attr.label_list(), |
| }, |
| ) |
| |
| # Binary ############################################################### |
| |
| def _binary_impl(ctx): |
| files = [dep[ComponentInfo].output for dep in ctx.attr.deps] |
| return DefaultInfo(files = depset(direct = files)) |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo], cfg = component_transition), |
| }, |
| ) |
| """); |
| } |
| |
| /** Tests a materializer rule under a split transition. */ |
| @Test |
| public void materializerRulesUnderSplitTransition_works() throws Exception { |
| |
| writeMaterializerSplitTransitionBzlFile(); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary", "component_selector_setting") |
| |
| component_selector_setting( |
| name = "component_selector_setting", |
| build_setting_default = "foo", |
| ) |
| |
| binary( |
| name = "bin", |
| # has a split transition! |
| deps = [":component_selector"], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", |
| ":c", |
| ":d", |
| ], |
| ) |
| |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "no") |
| component(name = "d", info = "no") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| List<String> artifactNames = ActionsTestUtil.baseArtifactNames(filesToBuild); |
| assertThat(artifactNames).containsAtLeast("a.txt", "b.txt"); |
| assertThat(artifactNames).containsNoneOf("c.txt", "d.txt"); |
| } |
| |
| /** |
| * Tests a materializer rule under a split transition with a select() input to the materializer. |
| */ |
| @Test |
| public void materializerRulesUnderSplitTransitionAndSelect_works() throws Exception { |
| |
| writeMaterializerSplitTransitionBzlFile(); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary", "component_selector_setting") |
| |
| component_selector_setting( |
| name = "component_selector_setting", |
| build_setting_default = "foo", |
| ) |
| |
| config_setting( |
| name = "config_setting_foo", |
| flag_values = {"//:component_selector_setting": "foo"}, |
| ) |
| |
| config_setting( |
| name = "config_setting_bar", |
| flag_values = {"//:component_selector_setting": "bar"}, |
| ) |
| |
| binary( |
| name = "bin", |
| # has a split transition! |
| deps = [":component_selector"], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = select({ |
| ":config_setting_foo": [ |
| ":a", |
| ":b", |
| ":f", |
| ":g", |
| ], |
| ":config_setting_bar": [ |
| ":c", |
| ":d", |
| ":e", |
| ":f", |
| ":g", |
| ], |
| }), |
| ) |
| |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "yes") |
| component(name = "d", info = "yes") |
| component(name = "e", info = "yes") |
| component(name = "f", info = "no") |
| component(name = "g", info = "no") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild(); |
| List<String> artifactNames = ActionsTestUtil.baseArtifactNames(filesToBuild); |
| assertThat(artifactNames).containsAtLeast("a.txt", "b.txt", "c.txt", "d.txt", "e.txt"); |
| assertThat(artifactNames).containsNoneOf("f.txt", "g.txt"); |
| } |
| |
| @Test |
| public void materializerRulesPropagatesValidationActions_works() throws Exception { |
| scratch.file( |
| "defs.bzl", |
| """ |
| # Component ###################################### |
| |
| ComponentInfo = provider(fields = ["output", "info"]) |
| |
| def _component_impl(ctx): |
| f = ctx.actions.declare_file(ctx.label.name + ".txt") |
| ctx.actions.write(f, ctx.label.name) |
| |
| validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation") |
| ctx.actions.run_shell( |
| inputs = [f], |
| outputs = [validation_output], |
| command = "touch $1", |
| arguments = [validation_output.path], |
| ) |
| |
| return [ |
| ComponentInfo(output = f, info = ctx.attr.info), |
| OutputGroupInfo(_validation = depset([validation_output])), |
| ] |
| |
| component = rule( |
| implementation = _component_impl, |
| provides = [ComponentInfo], |
| attrs = { |
| "info": attr.string(), |
| } |
| ) |
| |
| # Component selector ############################# |
| |
| def _component_selector_impl(ctx): |
| selected = [] |
| for c in ctx.attr.all_components: |
| if "yes" in c[ComponentInfo].info: |
| selected.append(c) |
| return MaterializedDepsInfo(deps = selected) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "all_components": attr.label_list(), |
| }, |
| ) |
| |
| # Binary ######################################### |
| |
| def _binary_impl(ctx): |
| files = [dep[ComponentInfo].output for dep in ctx.attr.deps] |
| return DefaultInfo(files = depset(direct = files)) |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo]), |
| }, |
| ) |
| """); |
| |
| scratch.file( |
| "BUILD", |
| """ |
| load(":defs.bzl", "component", "component_selector", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [ |
| ":aaa", |
| ":component_selector", |
| ":zzz", |
| ], |
| ) |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", |
| ":c", |
| ":d", |
| ], |
| ) |
| |
| component(name = "aaa", info = "yes") |
| component(name = "a", info = "yes") |
| component(name = "b", info = "yes") |
| component(name = "c", info = "no") |
| component(name = "d", info = "no") |
| component(name = "zzz", info = "yes") |
| """); |
| |
| update("//:bin"); |
| ConfiguredTarget target = getConfiguredTarget("//:bin"); |
| OutputGroupInfo outputGroupInfo = target.get(OutputGroupInfo.STARLARK_CONSTRUCTOR); |
| NestedSet<Artifact> validationOutputs = |
| outputGroupInfo.getOutputGroup(OutputGroupInfo.VALIDATION); |
| List<String> artifactNames = ActionsTestUtil.baseArtifactNames(validationOutputs); |
| assertThat(artifactNames) |
| .containsExactly("aaa.validation", "a.validation", "b.validation", "zzz.validation"); |
| } |
| |
| private void writeVisibilityDefsBzlFile() throws Exception { |
| scratch.file( |
| "defs.bzl", |
| """ |
| # Component ###################################### |
| |
| ComponentInfo = provider(fields = ["info"]) |
| |
| def _component_impl(ctx): |
| return ComponentInfo(info = ctx.attr.info) |
| |
| component = rule( |
| implementation = _component_impl, |
| provides = [ComponentInfo], |
| attrs = { |
| "info": attr.string(), |
| } |
| ) |
| |
| # Component selector ############################# |
| |
| def _component_selector_impl(ctx): |
| selected = [] |
| for c in ctx.attr.all_components: |
| if "yes" in c[ComponentInfo].info: |
| selected.append(c) |
| return MaterializedDepsInfo(deps = selected) |
| |
| component_selector = materializer_rule( |
| implementation = _component_selector_impl, |
| allow_real_deps = True, |
| attrs = { |
| "all_components": attr.label_list(), |
| }, |
| ) |
| |
| # Binary ######################################### |
| |
| def _binary_impl(ctx): |
| return DefaultInfo() |
| |
| binary = rule( |
| implementation = _binary_impl, |
| attrs = { |
| "deps": attr.label_list(providers = [ComponentInfo]), |
| }, |
| ) |
| """); |
| } |
| |
| @Test |
| public void materializerRuleVisibilityViolation_throwsError() throws Exception { |
| writeVisibilityDefsBzlFile(); |
| scratch.file("BUILD", ""); |
| |
| scratch.file( |
| "binary1/BUILD", |
| """ |
| load("//:defs.bzl", "binary") |
| |
| binary( |
| name = "bin1", |
| deps = [ |
| "//components:component_selector", |
| ], |
| ) |
| """); |
| |
| scratch.file( |
| "binary2/BUILD", |
| """ |
| load("//:defs.bzl", "binary") |
| |
| binary( |
| name = "bin2", |
| deps = [ |
| "//components:component_selector", |
| ], |
| ) |
| """); |
| |
| scratch.file( |
| "components/BUILD", |
| """ |
| load("//:defs.bzl", "component", "component_selector", "binary") |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", |
| ":c", |
| ":d", |
| ], |
| visibility = ["//binary1:__pkg__"], |
| ) |
| |
| component(name = "a", info = "yes", visibility = ["//:__subpackages__"]) |
| component(name = "b", info = "yes", visibility = ["//:__subpackages__"]) |
| component(name = "c", info = "no", visibility = ["//:__subpackages__"]) |
| component(name = "d", info = "no", visibility = ["//:__subpackages__"]) |
| """); |
| |
| // The materializer target is visible to bin1. |
| update("//binary1:bin1"); |
| |
| // The materializer target is not visible to bin2. |
| reporter.removeHandler(failFastHandler); |
| assertThrows(ViewCreationFailedException.class, () -> update("//binary2:bin2")); |
| assertContainsEvent( |
| """ |
| ERROR /workspace/binary2/BUILD:3:7: in binary rule //binary2:bin2: Visibility error: |
| target '//components:component_selector' is not visible from |
| target '//binary2:bin2' |
| """); |
| } |
| |
| @Test |
| public void materializerRuleMaterializedTargetVisibilityViolation_throwsError() throws Exception { |
| scratch.file("BUILD", ""); |
| |
| writeVisibilityDefsBzlFile(); |
| |
| scratch.file( |
| "binary/BUILD", |
| """ |
| load("//:defs.bzl", "binary") |
| |
| binary( |
| name = "bin", |
| deps = [ |
| "//components:component_selector", |
| ], |
| ) |
| """); |
| |
| scratch.file( |
| "components/BUILD", |
| """ |
| load("//:defs.bzl", "component", "component_selector", "binary") |
| |
| component_selector( |
| name = "component_selector", |
| all_components = [ |
| ":a", |
| ":b", |
| ":c", |
| ":d", |
| ], |
| visibility = ["//binary:__pkg__"], |
| ) |
| |
| component(name = "a", info = "yes", visibility = ["//visibility:private"]) |
| component(name = "b", info = "yes", visibility = ["//visibility:private"]) |
| component(name = "c", info = "no", visibility = ["//visibility:private"]) |
| component(name = "d", info = "no", visibility = ["//visibility:private"]) |
| """); |
| |
| // The materializer target is visible to bin, but the materialized targets are not. |
| reporter.removeHandler(failFastHandler); |
| assertThrows(ViewCreationFailedException.class, () -> update("//binary:bin")); |
| assertContainsEvent( |
| """ |
| ERROR /workspace/binary/BUILD:3:7: in binary rule //binary:bin: Visibility error: |
| target '//components:a' is not visible from |
| target '//binary:bin' |
| """); |
| assertContainsEvent( |
| """ |
| ERROR /workspace/binary/BUILD:3:7: in binary rule //binary:bin: Visibility error: |
| target '//components:b' is not visible from |
| target '//binary:bin' |
| """); |
| } |
| } |