/*
* Copyright 2014-present Facebook, Inc.
*
* 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.facebook.buck.rules;
import com.facebook.buck.event.ConsoleEvent;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.keys.SupportsInputBasedRuleKey;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepExecutionResult;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.SymlinkTreeStep;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Multiset;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Stream;
public class SymlinkTree implements BuildRule, HasRuntimeDeps, SupportsInputBasedRuleKey {
private final Path root;
private final ImmutableSortedMap<Path, SourcePath> links;
private final BuildTarget target;
private final ProjectFilesystem filesystem;
private final SourcePathRuleFinder ruleFinder;
public SymlinkTree(
BuildTarget target,
ProjectFilesystem filesystem,
Path root,
final ImmutableMap<Path, SourcePath> links,
SourcePathRuleFinder ruleFinder) {
this.target = target;
this.filesystem = filesystem;
this.ruleFinder = ruleFinder;
Preconditions.checkState(
!root.isAbsolute(), "Expected symlink tree root to be relative: %s", root);
this.root = root;
this.links = ImmutableSortedMap.copyOf(links);
}
@Override
public void appendToRuleKey(RuleKeyObjectSink sink) {
sink.setReflectively("links", getLinksForRuleKey());
}
/**
* Because of cross-cell, multiple {@link SourcePath}s can resolve to the same relative path,
* despite having distinct absolute paths. This presents a challenge for rules that require
* gathering all of the inputs in one directory.
*
* @param sourcePaths set of SourcePaths to process
* @param resolver resolver
* @return a map that assigns a unique relative path to each of the SourcePaths.
*/
public static ImmutableBiMap<SourcePath, Path> resolveDuplicateRelativePaths(
ImmutableSortedSet<SourcePath> sourcePaths, SourcePathResolver resolver) {
// This serves a dual purpose - it keeps track of whether a particular relative path had been
// assigned to a SourcePath and how many times a particular relative path had been seen.
Multiset<Path> assignedPaths = HashMultiset.create();
ImmutableBiMap.Builder<SourcePath, Path> builder = ImmutableBiMap.builder();
List<SourcePath> conflicts = new ArrayList<>();
for (SourcePath sourcePath : sourcePaths) {
Path relativePath = resolver.getRelativePath(sourcePath);
if (!assignedPaths.contains(relativePath)) {
builder.put(sourcePath, relativePath);
assignedPaths.add(relativePath);
} else {
conflicts.add(sourcePath);
}
}
for (SourcePath conflict : conflicts) {
Path relativePath = resolver.getRelativePath(conflict);
Path parent = MorePaths.getParentOrEmpty(relativePath);
String extension = MorePaths.getFileExtension(relativePath);
String name = MorePaths.getNameWithoutExtension(relativePath);
while (true) {
StringBuilder candidateName = new StringBuilder(name);
candidateName.append('-');
int suffix = assignedPaths.count(relativePath);
candidateName.append(suffix);
if (!extension.isEmpty()) {
candidateName.append('.');
candidateName.append(extension);
}
Path candidate = parent.resolve(candidateName.toString());
if (!assignedPaths.contains(candidate)) {
assignedPaths.add(candidate);
builder.put(conflict, candidate);
break;
} else {
assignedPaths.add(relativePath);
}
}
}
return builder.build();
}
@Override
public BuildTarget getBuildTarget() {
return target;
}
@Override
public String getType() {
return "symlink_tree";
}
@Override
public BuildableProperties getProperties() {
return BuildableProperties.NONE;
}
/**
* SymlinkTree never has any compile-time deps, only runtime deps.
*
* <p>All rules which consume SymlinkTrees are themselves required to have dependencies anything
* which may alter the SymlinkTree contents.
*
* <p>This is to avoid removing and re-creating the same symlinks every build.
*/
@Override
public ImmutableSortedSet<BuildRule> getBuildDeps() {
return ImmutableSortedSet.of();
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
return new ImmutableList.Builder<Step>()
.add(getVerifyStep())
.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), root))
.add(
new SymlinkTreeStep(
getProjectFilesystem(),
root,
context.getSourcePathResolver().getMappedPaths(links)))
.build();
}
// Put the link map into the rule key, as if it changes at all, we need to
// re-run it.
private ImmutableSortedMap<String, NonHashableSourcePathContainer> getLinksForRuleKey() {
ImmutableSortedMap.Builder<String, NonHashableSourcePathContainer> linksForRuleKeyBuilder =
ImmutableSortedMap.naturalOrder();
for (Map.Entry<Path, SourcePath> entry : links.entrySet()) {
linksForRuleKeyBuilder.put(
entry.getKey().toString(), new NonHashableSourcePathContainer(entry.getValue()));
}
return linksForRuleKeyBuilder.build();
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), root);
}
@Override
public ProjectFilesystem getProjectFilesystem() {
return filesystem;
}
@VisibleForTesting
protected Step getVerifyStep() {
return new AbstractExecutionStep("verify_symlink_tree") {
@Override
public StepExecutionResult execute(ExecutionContext context) throws IOException {
for (ImmutableMap.Entry<Path, SourcePath> entry : getLinks().entrySet()) {
for (Path pathPart : entry.getKey()) {
if (pathPart.toString().equals("..")) {
context
.getBuckEventBus()
.post(
ConsoleEvent.create(
Level.SEVERE,
String.format(
"Path '%s' should not contain '%s'.", entry.getKey(), pathPart)));
return StepExecutionResult.ERROR;
}
}
}
return StepExecutionResult.SUCCESS;
}
};
}
// We don't cache symlinks because:
// 1) We don't currently support caching symlinks.
// 2) It's almost certainly always more expensive to cache them rather than just re-create them.
// 3) The symlinks are absolute.
@Override
public boolean isCacheable() {
return false;
}
@Override
public Stream<BuildTarget> getRuntimeDeps() {
return links
.values()
.stream()
.map(ruleFinder::filterBuildRuleInputs)
.flatMap(ImmutableSet::stream)
.map(BuildRule::getBuildTarget);
}
public Path getRoot() {
return getProjectFilesystem().resolve(root);
}
public ImmutableSortedMap<Path, SourcePath> getLinks() {
return links;
}
@Override
public String toString() {
return getFullyQualifiedName();
}
}