/*
* Copyright 2015-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.cxx;
import static com.facebook.buck.cxx.CxxDescriptionEnhancer.normalizeModuleName;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AddToRuleKey;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.ExplicitBuildTargetSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.step.fs.WriteFileStep;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
import java.nio.file.Path;
import java.util.Optional;
import org.stringtemplate.v4.ST;
public final class HeaderSymlinkTreeWithHeaderMap extends HeaderSymlinkTree {
private static final Logger LOG = Logger.get(HeaderSymlinkTreeWithHeaderMap.class);
private static final String MODULE_MAP = "buck.modulemap";
private static final String MODULEMAP_TEMPLATE_PATH = getTemplate("modulemap.st");
private static String getTemplate(String template) {
try {
return Resources.toString(
Resources.getResource(HeaderSymlinkTreeWithHeaderMap.class, template), Charsets.UTF_8);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@AddToRuleKey(stringify = true)
private final Path headerMapPath;
@AddToRuleKey private final boolean shouldCreateModule;
private HeaderSymlinkTreeWithHeaderMap(
BuildTarget target,
ProjectFilesystem filesystem,
Path root,
ImmutableMap<Path, SourcePath> links,
SourcePathRuleFinder ruleFinder,
Path headerMapPath,
boolean shouldCreateModule) {
super(target, filesystem, root, links, ruleFinder);
this.headerMapPath = headerMapPath;
this.shouldCreateModule = shouldCreateModule;
}
public static HeaderSymlinkTreeWithHeaderMap create(
BuildTarget target,
ProjectFilesystem filesystem,
Path root,
ImmutableMap<Path, SourcePath> links,
SourcePathRuleFinder ruleFinder) {
Path headerMapPath = getPath(filesystem, target);
boolean shouldCreateModule =
target.getFlavors().contains(CxxDescriptionEnhancer.EXPORTED_HEADER_SYMLINK_TREE_FLAVOR);
return new HeaderSymlinkTreeWithHeaderMap(
target, filesystem, root, links, ruleFinder, headerMapPath, shouldCreateModule);
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), headerMapPath);
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
LOG.debug("Generating post-build steps to write header map to %s", headerMapPath);
Path buckOut =
getProjectFilesystem().resolve(getProjectFilesystem().getBuckPaths().getBuckOut());
ImmutableMap.Builder<Path, Path> headerMapEntries = ImmutableMap.builder();
for (Path key : getLinks().keySet()) {
// The key is the path that will be referred to in headers. It can be anything. However, the
// value given in the headerMapEntries is the path of that entry in the generated symlink
// tree. Because "reasons", we don't want to cache that value, so we need to relativize the
// path to the output directory of this current rule. We then rely on magic and the stars
// aligning in order to get this to work. May we find peace in another life.
headerMapEntries.put(key, buckOut.relativize(getRoot().resolve(key)));
}
ImmutableList.Builder<Step> builder =
ImmutableList.<Step>builder()
.addAll(super.getBuildSteps(context, buildableContext))
.add(
new HeaderMapStep(getProjectFilesystem(), headerMapPath, headerMapEntries.build()));
if (shouldCreateModule) {
Optional<String> umbrellaHeader = getUmbrellaHeader(getBuildTarget().getShortName());
String moduleName = normalizeModuleName(getBuildTarget().getShortName());
builder.add(MkdirStep.of(getProjectFilesystem(), getRoot().resolve(moduleName)));
builder.add(createCreateModuleStep(moduleName, umbrellaHeader));
}
return builder.build();
}
/**
* If any header file name matches the name of the target, it will be implicitly used as the
* umbrella header of the module of that target.
*/
private Optional<String> getUmbrellaHeader(String moduleName) {
return getLinks()
.keySet()
.stream()
.filter(input -> moduleName.equals(MorePaths.getNameWithoutExtension(input)))
.map(Path::toString)
.findFirst();
}
private Step createCreateModuleStep(String moduleName, Optional<String> umbrellaHeader) {
ST st =
new ST(MODULEMAP_TEMPLATE_PATH)
.add("module_name", moduleName)
.add("use_umbrella_header", umbrellaHeader.isPresent());
if (umbrellaHeader.isPresent()) {
st.add("umbrella_header_name", umbrellaHeader.get());
} else {
st.add("umbrella_directory", moduleName);
}
return new WriteFileStep(
getProjectFilesystem(),
st.render(),
getProjectFilesystem().relativize(getRoot().resolve(MODULE_MAP)),
false);
}
@Override
public Path getIncludePath() {
return getProjectFilesystem().resolve(getProjectFilesystem().getBuckPaths().getBuckOut());
}
@Override
public Optional<Path> getHeaderMap() {
return Optional.of(getProjectFilesystem().resolve(headerMapPath));
}
@VisibleForTesting
static Path getPath(ProjectFilesystem filesystem, BuildTarget target) {
return BuildTargets.getGenPath(filesystem, target, "%s.hmap");
}
}