blob: af4b954343748d8da0133edc9278f8f841cb9152 [file] [log] [blame]
// 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'
""");
}
}