blob: 771d31dfa10eebf3e69d28877d8c7fab09c1dd5d [file] [log] [blame]
// Copyright 2023 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.buildtool;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.packages.Attribute.attr;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static com.google.devtools.build.lib.packages.Type.STRING;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.actions.FileWriteActionContext;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.BuildOptionsView;
import com.google.devtools.build.lib.analysis.config.Fragment;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.analysis.config.RequiresOptions;
import com.google.devtools.build.lib.analysis.config.TransitionFactories;
import com.google.devtools.build.lib.analysis.config.transitions.PatchTransition;
import com.google.devtools.build.lib.analysis.config.transitions.TransitionFactory;
import com.google.devtools.build.lib.analysis.util.MockRule;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.buildtool.util.TestRuleModule;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.exec.FileWriteStrategy;
import com.google.devtools.build.lib.exec.ModuleActionContextRegistry;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
import com.google.devtools.build.lib.packages.RuleTransitionData;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Symlinks;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.IOException;
import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(TestParameterInjector.class)
public final class ConvenienceSymlinkTest extends BuildIntegrationTestCase {
/** test options to cause the output directory to change */
public static final class PathTestOptions extends FragmentOptions {
@Option(
name = "output_directory_name",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
defaultValue = "default")
public String outputDirectoryName;
@Option(
name = "useless_option",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "default")
public String uselessOption;
}
/** Test fragment. */
@RequiresOptions(options = {PathTestOptions.class})
public static final class PathTestConfiguration extends Fragment {
private final String outputDirectoryName;
public PathTestConfiguration(BuildOptions buildOptions) {
this.outputDirectoryName = buildOptions.get(PathTestOptions.class).outputDirectoryName;
}
@Override
public void processForOutputPathMnemonic(OutputDirectoriesContext ctx)
throws Fragment.OutputDirectoriesContext.AddToMnemonicException {
ctx.markAsExplicitInOutputPathFor("output_directory_name");
ctx.addToMnemonic(outputDirectoryName);
}
}
private static final class PathTransition implements PatchTransition {
private final String newPath;
PathTransition(String newPath) {
this.newPath = newPath;
}
@Override
public ImmutableSet<Class<? extends FragmentOptions>> requiresOptionFragments() {
return ImmutableSet.of(PathTestOptions.class);
}
@Override
public BuildOptions patch(BuildOptionsView options, EventHandler eventHandler) {
BuildOptionsView clone = options.clone();
clone.get(PathTestOptions.class).outputDirectoryName = newPath;
return clone.underlying();
}
}
private static final class PathTransitionFactory
implements TransitionFactory<RuleTransitionData> {
@Override
public PatchTransition create(RuleTransitionData ruleData) {
return new PathTransition(
NonconfigurableAttributeMapper.of(ruleData.rule()).get("path", STRING));
}
}
private static final class UselessOptionTransition implements PatchTransition {
private final String newValue;
UselessOptionTransition(String newValue) {
this.newValue = newValue;
}
@Override
public ImmutableSet<Class<? extends FragmentOptions>> requiresOptionFragments() {
return ImmutableSet.of(PathTestOptions.class);
}
@Override
public BuildOptions patch(BuildOptionsView options, EventHandler eventHandler) {
BuildOptionsView clone = options.clone();
clone.get(PathTestOptions.class).uselessOption = newValue;
return clone.underlying();
}
}
private static final class UselessOptionTransitionFactory
implements TransitionFactory<RuleTransitionData> {
@Override
public PatchTransition create(RuleTransitionData ruleData) {
return new UselessOptionTransition(
NonconfigurableAttributeMapper.of(ruleData.rule()).get("value", STRING));
}
}
private static final class PathTestRulesModule extends BlazeModule {
@Override
public void registerActionContexts(
ModuleActionContextRegistry.Builder registryBuilder,
CommandEnvironment env,
BuildRequest buildRequest) {
// we need an implementation of FileWriteActionContext to get our file writes to succeed
registryBuilder.register(FileWriteActionContext.class, new FileWriteStrategy());
// we need something to consume FileWriteActionContext or the registration will have no effect
registryBuilder.restrictTo(FileWriteActionContext.class, "local");
}
@Override
public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {
TestRuleModule.getModule().initializeRuleClasses(builder);
MockRule basicRule =
() ->
MockRule.define(
"basic_rule",
(ruleBuilder, env) ->
ruleBuilder
.add(attr("deps", LABEL_LIST).allowedFileTypes())
.setImplicitOutputsFunction(
ImplicitOutputsFunction.fromTemplates("%{name}.bin")));
MockRule incomingTransitionRule =
() ->
MockRule.define(
"incoming_transition_rule",
(ruleBuilder, env) ->
ruleBuilder
.add(
attr("path", STRING)
.mandatory()
.nonconfigurable("used in transition"))
.add(attr("deps", LABEL_LIST).allowedFileTypes())
.setImplicitOutputsFunction(
ImplicitOutputsFunction.fromTemplates("%{name}.bin"))
.cfg(new PathTransitionFactory()));
MockRule incomingUnrelatedTransitionRule =
() ->
MockRule.define(
"incoming_unrelated_transition_rule",
(ruleBuilder, env) ->
ruleBuilder
.add(
attr("value", STRING)
.mandatory()
.nonconfigurable("used in transition"))
.add(attr("deps", LABEL_LIST).allowedFileTypes())
.setImplicitOutputsFunction(
ImplicitOutputsFunction.fromTemplates("%{name}.bin"))
.cfg(new UselessOptionTransitionFactory()));
MockRule outgoingTransitionRule =
() ->
MockRule.define(
"outgoing_transition_rule",
(ruleBuilder, env) ->
ruleBuilder
.add(
attr("deps", LABEL_LIST)
.allowedFileTypes()
.cfg(
TransitionFactories.of(
new PathTransition("set_by_outgoing_transition_rule"))))
.setImplicitOutputsFunction(
ImplicitOutputsFunction.fromTemplates("%{name}.bin")));
builder
.addConfigurationFragment(PathTestConfiguration.class)
.addRuleDefinition(basicRule)
.addRuleDefinition(incomingTransitionRule)
.addRuleDefinition(incomingUnrelatedTransitionRule)
.addRuleDefinition(outgoingTransitionRule);
}
}
@TestParameter boolean mergedAnalysisExecution;
@Override
protected void setupOptions() throws Exception {
super.setupOptions();
addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
}
@Override
protected BlazeModule getRulesModule() {
return new PathTestRulesModule();
}
private Path getExecRoot() {
return getBlazeWorkspace().getDirectories().getExecRoot(TestConstants.WORKSPACE_NAME);
}
private Path getOutputPath() {
return getBlazeWorkspace().getDirectories().getOutputPath(TestConstants.WORKSPACE_NAME);
}
/** Gets a mapping from the workspace-relative paths of symlinks to the paths they point to. */
private ImmutableMap<String, Path> getConvenienceSymlinks() throws IOException {
return getWorkspace().getDirectoryEntries().stream()
.filter(Path::isSymbolicLink)
.collect(
toImmutableMap(
(path) -> path.relativeTo(getWorkspace()).toString(),
(path) -> {
try {
return getWorkspace().getRelative(path.readSymbolicLinkUnchecked());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}));
}
@Test
public void sanityCheckFilesHaveNullConfigurations() throws Exception {
// Other tests in this file expect that files will have a null configuration.
write("files/BUILD", "exports_files(['foo.txt', 'bar.txt'])");
write("files/foo.txt", "This is just a test file to pretend to build.");
write("files/bar.txt", "This is just a test file to pretend to build.");
BuildResult result = buildTarget("//files:foo.txt", "//files:bar.txt");
assertThat(
result.getActualTargets().stream()
.collect(
toImmutableMap(
target -> target.getLabel().toString(),
target -> Optional.ofNullable(target.getConfigurationKey()))))
.containsExactly(
"//files:foo.txt", Optional.empty(),
"//files:bar.txt", Optional.empty());
}
@Test
public void sanityCheckOutputDirectory() throws Exception {
// Other tests in this file expect that changing the output_directory_name flag changes the
// output directory of the configuration to the same value.
// This test relies on hard-coded paths for intermediate artifacts so
// must force output directory naming into legacy behaviors for now.
addOptions(
"--output_directory_name=set_by_flag",
"--compilation_mode=fastbuild",
"--experimental_output_directory_naming_scheme=legacy",
"--experimental_exec_configuration_distinguisher=legacy");
write(
"path/BUILD",
"basic_rule(name='from_flag')",
"incoming_transition_rule(name='from_transition', path='set_by_transition')",
"incoming_unrelated_transition_rule(name='unrelated_transition', value='whatever')",
"outgoing_transition_rule(name='outgoing_transition')");
BuildResult result =
buildTarget(
"//path:from_flag",
"//path:from_transition",
"//path:unrelated_transition",
"//path:outgoing_transition");
assertThat(
result.getActualTargets().stream()
.collect(
toImmutableMap(
(target) -> target.getLabel().toString(),
(target) ->
getConfiguration(target)
.getOutputDirectory(RepositoryName.MAIN)
.getRoot()
.asPath()
.relativeTo(getOutputPath())
.toString())))
.containsExactly(
"//path:from_flag", getTargetConfiguration().getCpu() + "-fastbuild-set_by_flag",
"//path:from_transition",
getTargetConfiguration().getCpu() + "-fastbuild-set_by_transition",
"//path:unrelated_transition",
getTargetConfiguration().getCpu() + "-fastbuild-set_by_flag",
"//path:outgoing_transition",
getTargetConfiguration().getCpu() + "-fastbuild-set_by_flag");
}
@Test
public void buildingNothing_unsetsSymlinks() throws Exception {
addOptions("--symlink_prefix=nothing-", "--incompatible_skip_genfiles_symlink=false");
Path config = getOutputBase().getRelative("some-imaginary-config");
// put symlinks at the convenience symlinks spots to simulate a prior build
Path binLink = getWorkspace().getChild("nothing-bin");
binLink.createSymbolicLink(config.getChild("bin"));
Path genfilesLink = getWorkspace().getChild("nothing-genfiles");
genfilesLink.createSymbolicLink(config.getChild("genfiles"));
Path testlogsLink = getWorkspace().getChild("nothing-testlogs");
testlogsLink.createSymbolicLink(config.getChild("testlogs"));
buildTarget();
// there should be nothing at any of the convenience symlinks which depend on configuration -
// the symlinks put there during the simulated prior build should have been deleted
assertThat(binLink.exists(Symlinks.NOFOLLOW)).isTrue();
assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isTrue();
assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isTrue();
// the execroot and output path symlinks should have been created because they don't depend on
// configuration, but no other symlinks should have been created
assertThat(getConvenienceSymlinks())
.containsExactly(
// notably absent: nothing-bin, nothing-genfiles, nothing-testlogs
// these were also not created under other names
"nothing-bin",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-default/bin"),
"nothing-genfiles",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-default/bin"),
"nothing-testlogs",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-default/testlogs"),
"nothing-" + TestConstants.WORKSPACE_NAME,
getExecRoot(),
"nothing-out",
getOutputPath());
}
@Test
public void buildingOnlyTargetsWithNullConfigurations_unsetsSymlinks() throws Exception {
addOptions("--symlink_prefix=nulled-", "--incompatible_skip_genfiles_symlink=false");
Path config = getOutputBase().getRelative("some-imaginary-config");
// put symlinks at the convenience symlinks spots to simulate a prior build
Path binLink = getWorkspace().getChild("nulled-bin");
binLink.createSymbolicLink(config.getChild("bin"));
Path genfilesLink = getWorkspace().getChild("nulled-genfiles");
genfilesLink.createSymbolicLink(config.getChild("genfiles"));
Path testlogsLink = getWorkspace().getChild("nulled-testlogs");
testlogsLink.createSymbolicLink(config.getChild("testlogs"));
write("files/BUILD", "exports_files(['foo.txt', 'bar.txt'])");
write("files/foo.txt", "This is just a test file to pretend to build.");
write("files/bar.txt", "This is just a test file to pretend to build.");
buildTarget("//files:foo.txt", "//files:bar.txt");
// there should be nothing at any of the convenience symlinks which depend on configuration -
// the symlinks put there during the simulated prior build should have been deleted
assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();
// the execroot and output path symlinks should have been created because they don't depend on
// configuration, but no other symlinks should have been created
assertThat(getConvenienceSymlinks())
.containsExactly(
// notably absent: nulled-bin, nulled-genfiles, nulled-testlogs
// these were also not created under other names
"nulled-" + TestConstants.WORKSPACE_NAME, getExecRoot(), "nulled-out", getOutputPath());
}
@Test
public void buildingTargetsWithDifferentOutputDirectories_unsetsSymlinksIfNoneAreTopLevel()
throws Exception {
addOptions("--symlink_prefix=ambiguous-", "--incompatible_skip_genfiles_symlink=false");
Path config = getOutputPath().getRelative("some-imaginary-config");
// put symlinks at the convenience symlinks spots to simulate a prior build
Path binLink = getWorkspace().getChild("ambiguous-bin");
binLink.createSymbolicLink(config.getChild("bin"));
Path genfilesLink = getWorkspace().getChild("ambiguous-genfiles");
genfilesLink.createSymbolicLink(config.getChild("genfiles"));
Path testlogsLink = getWorkspace().getChild("ambiguous-testlogs");
testlogsLink.createSymbolicLink(config.getChild("testlogs"));
write(
"targets/BUILD",
"incoming_transition_rule(name='config1', path='set_from_config1')",
"incoming_transition_rule(name='config2', path='set_from_config2')");
buildTarget("//targets:config1", "//targets:config2");
// there should be nothing at any of the convenience symlinks which depend on configuration -
// the symlinks put there during the simulated prior build should have been deleted
assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();
// the execroot and output path symlinks should have been created because they don't depend on
// configuration, but no other symlinks should have been created
assertThat(getConvenienceSymlinks())
.containsExactly(
// notably absent: ambiguous-bin, ambiguous-genfiles, ambiguous-testlogs
// these were also not created under other names
"ambiguous-" + TestConstants.WORKSPACE_NAME,
getExecRoot(),
"ambiguous-out",
getOutputPath());
}
@Test
public void buildingTargetsWithDifferentOutputDirectories_setsSymlinksIfAnyAreTopLevel()
throws Exception {
addOptions(
"--symlink_prefix=ambiguous-",
"--incompatible_skip_genfiles_symlink=false",
"--incompatible_merge_genfiles_directory=false",
"--incompatible_skip_genfiles_symlink=false");
Path config = getOutputPath().getRelative("some-imaginary-config");
// put symlinks at the convenience symlinks spots to simulate a prior build
Path binLink = getWorkspace().getChild("ambiguous-bin");
binLink.createSymbolicLink(config.getChild("bin"));
Path genfilesLink = getWorkspace().getChild("ambiguous-genfiles");
genfilesLink.createSymbolicLink(config.getChild("genfiles"));
Path testlogsLink = getWorkspace().getChild("ambiguous-testlogs");
testlogsLink.createSymbolicLink(config.getChild("testlogs"));
write(
"targets/BUILD",
"basic_rule(name='default')",
"incoming_transition_rule(name='config1', path='set_from_config1')");
buildTarget("//targets:default", "//targets:config1");
assertThat(getConvenienceSymlinks())
.containsExactly(
"ambiguous-bin",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-default/bin"),
"ambiguous-genfiles",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-default/genfiles"),
"ambiguous-testlogs",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-default/testlogs"),
"ambiguous-" + TestConstants.WORKSPACE_NAME,
getExecRoot(),
"ambiguous-out",
getOutputPath());
}
@Test
public void buildingTargetsWithSameConfiguration_setsSymlinks() throws Exception {
addOptions(
"--symlink_prefix=same-",
"--compilation_mode=fastbuild",
"--incompatible_merge_genfiles_directory=false",
"--incompatible_skip_genfiles_symlink=false");
write(
"targets/BUILD",
"incoming_transition_rule(name='configged1', path='configured')",
"incoming_transition_rule(name='configged2', path='configured')");
buildTarget("//targets:configged1", "//targets:configged2");
assertThat(getConvenienceSymlinks())
.containsExactly(
"same-bin",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-configured/bin"),
"same-genfiles",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-configured/genfiles"),
"same-testlogs",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-configured/testlogs"),
"same-" + TestConstants.WORKSPACE_NAME,
getExecRoot(),
"same-out",
getOutputPath());
}
@Test
public void buildingSameConfigurationTargetsWithDifferentConfigurationDeps_setsSymlinks()
throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=united-",
"--compilation_mode=fastbuild",
"--incompatible_merge_genfiles_directory=false",
"--incompatible_skip_genfiles_symlink=false");
write(
"targets/BUILD",
"outgoing_transition_rule(name='configged1', deps=[':alternate1'])",
"outgoing_transition_rule(name='configged2', deps=[':alternate2'])",
"basic_rule(name='alternate1')",
"incoming_transition_rule(name='alternate2', path='alternate_transition')");
buildTarget("//targets:configged1", "//targets:configged2");
assertThat(getConvenienceSymlinks())
.containsExactly(
"united-bin",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/bin"),
"united-genfiles",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/genfiles"),
"united-testlogs",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/testlogs"),
"united-" + TestConstants.WORKSPACE_NAME,
getExecRoot(),
"united-out",
getOutputPath());
}
@Test
public void differentConfigurationSameOutputDirectory_setsSymlinks() throws Exception {
// TODO(blaze-configurability-team): Remove when `--experimental_output_directory_naming_scheme`
// is universally set to `diff_from_baseline`
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=unchanged-",
"--compilation_mode=fastbuild",
"--incompatible_merge_genfiles_directory=false",
"--incompatible_skip_genfiles_symlink=false",
"--experimental_output_directory_naming_scheme=legacy");
write(
"targets/BUILD",
"basic_rule(name='from_flag')",
"incoming_unrelated_transition_rule(name='configged1', value='one_transition')",
"incoming_unrelated_transition_rule(name='configged2', value='alternate_transition')");
buildTarget("//targets:from_flag", "//targets:configged1", "//targets:configged2");
assertThat(getConvenienceSymlinks())
.containsExactly(
"unchanged-bin",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/bin"),
"unchanged-genfiles",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/genfiles"),
"unchanged-testlogs",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/testlogs"),
"unchanged-" + TestConstants.WORKSPACE_NAME,
getExecRoot(),
"unchanged-out",
getOutputPath());
}
@Test
public void nullConfigurationWithOtherMatchingOutputDir_setsSymlinks() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=mixed-",
"--compilation_mode=fastbuild",
"--incompatible_merge_genfiles_directory=false",
"--incompatible_skip_genfiles_symlink=false");
write(
"targets/BUILD",
"exports_files(['null'])",
"basic_rule(name='configured1')",
"basic_rule(name='configured2')");
write("targets/null", "This is just a test file to pretend to build.");
buildTarget("//targets:null", "//targets:configured1", "//targets:configured2");
assertThat(getConvenienceSymlinks())
.containsExactly(
"mixed-bin",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/bin"),
"mixed-genfiles",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/genfiles"),
"mixed-testlogs",
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/testlogs"),
"mixed-" + TestConstants.WORKSPACE_NAME,
getExecRoot(),
"mixed-out",
getOutputPath());
}
@Test
public void settingSymlinksReplacesSymlinksAlreadyPresent() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=replaced-",
"--compilation_mode=fastbuild",
"--incompatible_skip_genfiles_symlink=false");
Path binLink = getWorkspace().getChild("replaced-bin");
Path genfilesLink = getWorkspace().getChild("replaced-genfiles");
Path testlogsLink = getWorkspace().getChild("replaced-testlogs");
Path workspaceLink = getWorkspace().getChild("replaced-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("replaced-out");
PathFragment original = getOutputPath().getRelative("original/destination").asFragment();
binLink.createSymbolicLink(original);
genfilesLink.createSymbolicLink(original);
testlogsLink.createSymbolicLink(original);
workspaceLink.createSymbolicLink(original);
outLink.createSymbolicLink(original);
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
// Implicitly test for symlink-ness; readSymbolicLink would throw if they are not symlinks.
assertThat(binLink.readSymbolicLink()).isNotEqualTo(original);
assertThat(genfilesLink.readSymbolicLink()).isNotEqualTo(original);
assertThat(testlogsLink.readSymbolicLink()).isNotEqualTo(original);
assertThat(workspaceLink.readSymbolicLink()).isNotEqualTo(original);
assertThat(outLink.readSymbolicLink()).isNotEqualTo(original);
}
@Test
public void settingSymlinksCreatesSymlinksIfNotAlreadyPresent() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=created-",
"--compilation_mode=fastbuild",
"--incompatible_skip_genfiles_symlink=false");
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
assertThat(getWorkspace().getChild("created-bin").isSymbolicLink()).isTrue();
assertThat(getWorkspace().getChild("created-genfiles").isSymbolicLink()).isTrue();
assertThat(getWorkspace().getChild("created-testlogs").isSymbolicLink()).isTrue();
assertThat(getWorkspace().getChild("created-" + TestConstants.WORKSPACE_NAME).isSymbolicLink())
.isTrue();
assertThat(getWorkspace().getChild("created-out").isSymbolicLink()).isTrue();
}
@Test
public void genfilesLink_omittedWithIncompatibleFlag() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=prefix-",
"--compilation_mode=fastbuild",
"--incompatible_skip_genfiles_symlink=true");
// Simulate leftover symlink from prior build.
Path config = getOutputPath().getRelative("some-imaginary-config");
Path genfilesLink = getWorkspace().getChild("prefix-genfiles");
genfilesLink.createSymbolicLink(config.getChild("genfiles"));
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
assertThat(getWorkspace().getChild("prefix-genfiles").isSymbolicLink()).isFalse();
}
@Test
public void genfilesLink_presentWithoutIncompatibleFlag() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=prefix-",
"--compilation_mode=fastbuild",
"--incompatible_skip_genfiles_symlink=false");
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
assertThat(getWorkspace().getChild("prefix-genfiles").isSymbolicLink()).isTrue();
}
@Test
public void settingSymlinksDoesNotReplaceNormalFilesAlreadyPresent() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=blocked-",
"--compilation_mode=fastbuild");
Path binLink = getWorkspace().getChild("blocked-bin");
Path genfilesLink = getWorkspace().getChild("blocked-genfiles");
Path testlogsLink = getWorkspace().getChild("blocked-testlogs");
Path workspaceLink = getWorkspace().getChild("blocked-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("blocked-out");
FileSystemUtils.writeIsoLatin1(binLink, "this file is not a symlink");
FileSystemUtils.writeIsoLatin1(genfilesLink, "this file is not a symlink");
FileSystemUtils.writeIsoLatin1(testlogsLink, "this file is not a symlink");
FileSystemUtils.writeIsoLatin1(workspaceLink, "this file is not a symlink");
FileSystemUtils.writeIsoLatin1(outLink, "this file is not a symlink");
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
assertThat(binLink.isFile(Symlinks.NOFOLLOW)).isTrue();
assertThat(genfilesLink.isFile(Symlinks.NOFOLLOW)).isTrue();
assertThat(testlogsLink.isFile(Symlinks.NOFOLLOW)).isTrue();
assertThat(workspaceLink.isFile(Symlinks.NOFOLLOW)).isTrue();
assertThat(outLink.isFile(Symlinks.NOFOLLOW)).isTrue();
}
@Test
public void settingSymlinksDoesNotReplaceDirectoriesAlreadyPresent() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=blocked-",
"--compilation_mode=fastbuild");
Path binLink = getWorkspace().getChild("blocked-bin");
Path genfilesLink = getWorkspace().getChild("blocked-genfiles");
Path testlogsLink = getWorkspace().getChild("blocked-testlogs");
Path workspaceLink = getWorkspace().getChild("blocked-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("blocked-out");
binLink.createDirectory();
genfilesLink.createDirectory();
testlogsLink.createDirectory();
workspaceLink.createDirectory();
outLink.createDirectory();
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
assertThat(binLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
assertThat(genfilesLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
assertThat(testlogsLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
assertThat(workspaceLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
assertThat(outLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
}
@Test
public void settingSymlinksReplacesSymlinksEvenIfNotPointingInsideExecRoot() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=replaced-",
"--compilation_mode=fastbuild",
"--incompatible_merge_genfiles_directory=false",
"--incompatible_skip_genfiles_symlink=false");
Path binLink = getWorkspace().getChild("replaced-bin");
Path genfilesLink = getWorkspace().getChild("replaced-genfiles");
Path testlogsLink = getWorkspace().getChild("replaced-testlogs");
Path workspaceLink = getWorkspace().getChild("replaced-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("replaced-out");
Path original = getWorkspace().getRelative("/arbitrary/somewhere/else/in/the/filesystem");
binLink.createSymbolicLink(original);
genfilesLink.createSymbolicLink(original);
testlogsLink.createSymbolicLink(original);
workspaceLink.createSymbolicLink(original);
outLink.createSymbolicLink(original);
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
// Implicitly test for symlink-ness; readSymbolicLink would throw if they are not symlinks.
assertThat(binLink.readSymbolicLink())
.isEqualTo(
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/bin")
.asFragment());
assertThat(genfilesLink.readSymbolicLink())
.isEqualTo(
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/genfiles")
.asFragment());
assertThat(testlogsLink.readSymbolicLink())
.isEqualTo(
getOutputPath()
.getRelative(getTargetConfiguration().getCpu() + "-fastbuild-from_flag/testlogs")
.asFragment());
assertThat(workspaceLink.readSymbolicLink()).isEqualTo(getExecRoot().asFragment());
assertThat(outLink.readSymbolicLink()).isEqualTo(getOutputPath().asFragment());
}
@Test
public void settingSymlinksCreatesDirectoriesIfNeeded() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=created/",
"--compilation_mode=fastbuild",
"--incompatible_skip_genfiles_symlink=false");
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
assertThat(getWorkspace().getChild("created").isDirectory()).isTrue();
assertThat(getWorkspace().getRelative("created/bin").isSymbolicLink()).isTrue();
assertThat(getWorkspace().getRelative("created/genfiles").isSymbolicLink()).isTrue();
assertThat(getWorkspace().getRelative("created/testlogs").isSymbolicLink()).isTrue();
assertThat(
getWorkspace().getRelative("created/" + TestConstants.WORKSPACE_NAME).isSymbolicLink())
.isTrue();
assertThat(getWorkspace().getRelative("created/out").isSymbolicLink()).isTrue();
}
@Test
public void settingSymlinksDoesNothingWhenParentExistsAndIsNotADirectory() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=blocked/",
"--compilation_mode=fastbuild");
Path parentDir = getWorkspace().getChild("blocked");
FileSystemUtils.writeIsoLatin1(parentDir, "this file is not a directory");
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
assertThat(getWorkspace().getChild("blocked").isFile(Symlinks.NOFOLLOW)).isTrue();
}
@Test
public void settingSymlinksUsesExistingOrPopulatedParentDirectoryAsNormal() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=cooperating/",
"--compilation_mode=fastbuild",
"--incompatible_skip_genfiles_symlink=false");
write("target/BUILD", "basic_rule(name='target')");
write("cooperating/original", "this file makes the directory come to life");
buildTarget("//target:target");
assertThat(getWorkspace().getChild("cooperating").isDirectory()).isTrue();
assertThat(getWorkspace().getRelative("cooperating/bin").isSymbolicLink()).isTrue();
assertThat(getWorkspace().getRelative("cooperating/genfiles").isSymbolicLink()).isTrue();
assertThat(getWorkspace().getRelative("cooperating/testlogs").isSymbolicLink()).isTrue();
assertThat(
getWorkspace()
.getRelative("cooperating/" + TestConstants.WORKSPACE_NAME)
.isSymbolicLink())
.isTrue();
assertThat(getWorkspace().getRelative("cooperating/out").isSymbolicLink()).isTrue();
assertThat(getWorkspace().getRelative("cooperating/original").isFile()).isTrue();
}
@Test
public void settingSymlinksIgnoresSymlinksWithDifferentPrefix() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=new-prefix-",
"--compilation_mode=fastbuild");
Path binLink = getWorkspace().getChild("other-prefix-bin");
Path genfilesLink = getWorkspace().getChild("other-prefix-genfiles");
Path testlogsLink = getWorkspace().getChild("other-prefix-testlogs");
Path workspaceLink = getWorkspace().getChild("other-prefix-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("other-prefix-out");
PathFragment original = getOutputPath().getRelative("original/destination").asFragment();
binLink.createSymbolicLink(original);
genfilesLink.createSymbolicLink(original);
testlogsLink.createSymbolicLink(original);
workspaceLink.createSymbolicLink(original);
outLink.createSymbolicLink(original);
write("target/BUILD", "basic_rule(name='target')");
buildTarget("//target:target");
// Implicitly test for symlink-ness; readSymbolicLink would throw if they are not symlinks.
assertThat(binLink.readSymbolicLink()).isEqualTo(original);
assertThat(genfilesLink.readSymbolicLink()).isEqualTo(original);
assertThat(testlogsLink.readSymbolicLink()).isEqualTo(original);
assertThat(workspaceLink.readSymbolicLink()).isEqualTo(original);
assertThat(outLink.readSymbolicLink()).isEqualTo(original);
}
@Test
public void unsettingSymlinksRemovesConfigurationSymlinksIfAlreadyPresent() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=deleted-",
"--compilation_mode=fastbuild",
"--incompatible_skip_genfiles_symlink=false");
Path binLink = getWorkspace().getChild("deleted-bin");
Path genfilesLink = getWorkspace().getChild("deleted-genfiles");
Path testlogsLink = getWorkspace().getChild("deleted-testlogs");
Path workspaceLink = getWorkspace().getChild("deleted-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("deleted-out");
Path config = getOutputPath().getRelative("some-imaginary-config");
// put symlinks at the convenience symlinks spots to simulate a prior build
binLink.createSymbolicLink(config.getChild("bin"));
genfilesLink.createSymbolicLink(config.getChild("genfiles"));
testlogsLink.createSymbolicLink(config.getChild("testlogs"));
write("file/BUILD", "exports_files(['file'])");
write("file/file", "this is just a file to pretend to build");
buildTarget("//file:file");
assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(workspaceLink.isSymbolicLink()).isTrue();
assertThat(outLink.isSymbolicLink()).isTrue();
}
@Test
public void unsettingSymlinksSucceedsIfNotAlreadyPresent() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=already-absent-",
"--compilation_mode=fastbuild");
Path binLink = getWorkspace().getChild("already-absent-bin");
Path genfilesLink = getWorkspace().getChild("already-absent-genfiles");
Path testlogsLink = getWorkspace().getChild("already-absent-testlogs");
Path workspaceLink = getWorkspace().getChild("already-absent-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("already-absent-out");
write("file/BUILD", "exports_files(['file'])");
write("file/file", "this is just a file to pretend to build");
buildTarget("//file:file");
assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(workspaceLink.isSymbolicLink()).isTrue();
assertThat(outLink.isSymbolicLink()).isTrue();
}
@Test
public void unsettingSymlinksDoesNotRemoveNormalFilesAlreadyPresent() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=blocked-",
"--compilation_mode=fastbuild");
Path binLink = getWorkspace().getChild("blocked-bin");
Path genfilesLink = getWorkspace().getChild("blocked-genfiles");
Path testlogsLink = getWorkspace().getChild("blocked-testlogs");
Path workspaceLink = getWorkspace().getChild("blocked-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("blocked-out");
FileSystemUtils.writeIsoLatin1(binLink, "this file is not a symlink");
FileSystemUtils.writeIsoLatin1(genfilesLink, "this file is not a symlink");
FileSystemUtils.writeIsoLatin1(testlogsLink, "this file is not a symlink");
FileSystemUtils.writeIsoLatin1(workspaceLink, "this file is not a symlink");
FileSystemUtils.writeIsoLatin1(outLink, "this file is not a symlink");
write("file/BUILD", "exports_files(['file'])");
write("file/file", "this is just a file to pretend to build");
buildTarget("//file:file");
assertThat(binLink.isFile(Symlinks.NOFOLLOW)).isTrue();
assertThat(genfilesLink.isFile(Symlinks.NOFOLLOW)).isTrue();
assertThat(testlogsLink.isFile(Symlinks.NOFOLLOW)).isTrue();
assertThat(workspaceLink.isFile(Symlinks.NOFOLLOW)).isTrue();
assertThat(outLink.isFile(Symlinks.NOFOLLOW)).isTrue();
}
@Test
public void unsettingSymlinksDoesNotRemoveDirectoriesAlreadyPresent() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=blocked-",
"--compilation_mode=fastbuild");
Path binLink = getWorkspace().getChild("blocked-bin");
Path genfilesLink = getWorkspace().getChild("blocked-genfiles");
Path testlogsLink = getWorkspace().getChild("blocked-testlogs");
Path workspaceLink = getWorkspace().getChild("blocked-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("blocked-out");
binLink.createDirectory();
genfilesLink.createDirectory();
testlogsLink.createDirectory();
workspaceLink.createDirectory();
outLink.createDirectory();
write("file/BUILD", "exports_files(['file'])");
write("file/file", "this is just a file to pretend to build");
buildTarget("//file:file");
assertThat(binLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
assertThat(genfilesLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
assertThat(testlogsLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
assertThat(workspaceLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
assertThat(outLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
}
@Test
public void unsettingSymlinksRemovesSymlinksEvenIfNotPointingInsideExecRoot() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=deleted-",
"--compilation_mode=fastbuild",
"--incompatible_skip_genfiles_symlink=false");
Path binLink = getWorkspace().getChild("deleted-bin");
Path genfilesLink = getWorkspace().getChild("deleted-genfiles");
Path testlogsLink = getWorkspace().getChild("deleted-testlogs");
Path workspaceLink = getWorkspace().getChild("deleted-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("deleted-out");
Path original = getWorkspace().getRelative("/arbitrary/somewhere/else/in/the/filesystem");
binLink.createSymbolicLink(original);
genfilesLink.createSymbolicLink(original);
testlogsLink.createSymbolicLink(original);
workspaceLink.createSymbolicLink(original);
outLink.createSymbolicLink(original);
write("file/BUILD", "exports_files(['file'])");
write("file/file", "this is just a file to pretend to build");
buildTarget("//file:file");
assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();
assertThat(workspaceLink.isSymbolicLink()).isTrue();
assertThat(outLink.isSymbolicLink()).isTrue();
}
@Test
public void unsettingSymlinksIgnoresSymlinksWithDifferentPrefix() throws Exception {
addOptions(
"--output_directory_name=from_flag",
"--symlink_prefix=new-prefix-",
"--compilation_mode=fastbuild");
Path binLink = getWorkspace().getChild("other-prefix-bin");
Path genfilesLink = getWorkspace().getChild("other-prefix-genfiles");
Path testlogsLink = getWorkspace().getChild("other-prefix-testlogs");
Path workspaceLink = getWorkspace().getChild("other-prefix-" + TestConstants.WORKSPACE_NAME);
Path outLink = getWorkspace().getChild("other-prefix-out");
PathFragment original = getOutputPath().getRelative("original/destination").asFragment();
binLink.createSymbolicLink(original);
genfilesLink.createSymbolicLink(original);
testlogsLink.createSymbolicLink(original);
workspaceLink.createSymbolicLink(original);
outLink.createSymbolicLink(original);
write("file/BUILD", "exports_files(['file'])");
write("file/file", "this is just a file to pretend to build");
buildTarget("//file:file");
// Implicitly test for symlink-ness; readSymbolicLink would throw if they are not symlinks.
assertThat(binLink.readSymbolicLink()).isEqualTo(original);
assertThat(genfilesLink.readSymbolicLink()).isEqualTo(original);
assertThat(testlogsLink.readSymbolicLink()).isEqualTo(original);
assertThat(workspaceLink.readSymbolicLink()).isEqualTo(original);
assertThat(outLink.readSymbolicLink()).isEqualTo(original);
}
@Test
public void symlinkPrefix_specialNoCreateValue_doesNotCreateOrDeleteSymlinks() throws Exception {
addOptions("--symlink_prefix=/");
write("foo/BUILD", "exports_files(['bar.txt'])");
write("foo/bar.txt", "This is just a test file to pretend to build.");
// This will be a preexisting symlink and when --symlink_prefix=/ is used, assert that this
// preexisting symlink still exists.
Path binLink = getWorkspace().getChild("blaze-" + TestConstants.WORKSPACE_NAME);
binLink.createSymbolicLink(PathFragment.create("foo/"));
buildTarget("//foo:bar.txt");
ImmutableMap<String, Path> symlinks = getConvenienceSymlinks();
assertThat(symlinks).containsKey("blaze-" + TestConstants.WORKSPACE_NAME);
}
@Test
public void convenienceSymlinks_ignore_leaveSymlinksUntouched() throws Exception {
addOptions("--experimental_convenience_symlinks=ignore");
write("foo/BUILD", "exports_files(['bar.txt'])");
write("foo/bar.txt", "This is just a test file to pretend to build.");
buildTarget("//foo:bar.txt");
// This will be a preexisting symlink that will remain after the build
Path binLink = getWorkspace().getChild("blaze-" + TestConstants.WORKSPACE_NAME);
binLink.createSymbolicLink(PathFragment.create("foo/"));
ImmutableMap<String, Path> symlinks = getConvenienceSymlinks();
assertThat(symlinks).containsKey("blaze-" + TestConstants.WORKSPACE_NAME);
}
@Test
public void convenienceSymlinks_normal_createSymlinks() throws Exception {
addOptions("--symlink_prefix=test-", "--experimental_convenience_symlinks=normal");
write("foo/BUILD", "exports_files(['bar.txt'])");
write("foo/bar.txt", "This is just a test file to pretend to build.");
buildTarget("//foo:bar.txt");
ImmutableMap<String, Path> symlinks = getConvenienceSymlinks();
assertThat(symlinks).containsKey("test-" + TestConstants.WORKSPACE_NAME);
assertThat(symlinks).containsKey("test-out");
}
@Test
public void convenienceSymlinks_clean_deletesAndDoesNotCreateSymlinks() throws Exception {
addOptions("--symlink_prefix=test-", "--experimental_convenience_symlinks=clean");
write("foo/BUILD", "exports_files(['bar.txt'])");
write("foo/bar.txt", "This is just a test file to pretend to build.");
// This will be a preexisting symlink that will be deleted after the build
Path binLink = getWorkspace().getChild("test-" + TestConstants.WORKSPACE_NAME);
binLink.createSymbolicLink(PathFragment.create("foo"));
buildTarget("//foo:bar.txt");
ImmutableMap<String, Path> symlinks = getConvenienceSymlinks();
assertThat(symlinks).doesNotContainKey("test-" + TestConstants.WORKSPACE_NAME);
assertThat(symlinks).doesNotContainKey("test-out");
}
}