blob: 3dcf3cd629d2e46481e5d716daa0cc46985181a4 [file] [log] [blame]
/*
* Copyright 2012-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.cli;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.parser.BuildTargetParser;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.parser.ParseContext;
import com.facebook.buck.rules.ArtifactCache;
import com.facebook.buck.rules.BuildDependencies;
import com.facebook.buck.rules.CassandraArtifactCache;
import com.facebook.buck.rules.DirArtifactCache;
import com.facebook.buck.rules.NoopArtifactCache;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.ProjectFilesystem;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.netflix.astyanax.connectionpool.exceptions.ConnectionException;
import org.ini4j.Ini;
import org.ini4j.Profile.Section;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* Structured representation of data read from a {@code .buckconfig} file.
*/
@Immutable
class BuckConfig {
private static final String ALIAS_SECTION_HEADER = "alias";
/**
* This pattern is designed so that a fully-qualified build target cannot be a valid alias name
* and vice-versa.
*/
private static final Pattern ALIAS_PATTERN = Pattern.compile("[a-zA-Z_-][a-zA-Z0-9_-]*");
private static final BuckConfig EMPTY_INSTANCE = new BuckConfig(
ImmutableMap.<String, Map<String, String>>of(), null /* buildTargetParser */);
private static final String DEFAULT_CACHE_DIR = "buck-cache";
private static final String DEFAULT_CASSANDRA_PORT = "9160";
private final ImmutableMap<String, ImmutableMap<String, String>> sectionsToEntries;
private final ImmutableMap<String, BuildTarget> aliasToBuildTargetMap;
@Nullable
private final BuildTargetParser buildTargetParser;
private enum ArtifactCacheNames {
noop,
dir,
cassandra
}
private final ArtifactCache artifactCache;
@VisibleForTesting
BuckConfig(Map<String, Map<String, String>> sectionsToEntries,
BuildTargetParser buildTargetParser) {
Preconditions.checkNotNull(sectionsToEntries);
this.buildTargetParser = buildTargetParser;
ImmutableMap.Builder<String, ImmutableMap<String, String>> sectionsToEntriesBuilder =
ImmutableMap.builder();
for (Map.Entry<String, Map<String, String>> entry : sectionsToEntries.entrySet()) {
sectionsToEntriesBuilder.put(entry.getKey(), ImmutableMap.copyOf(entry.getValue()));
}
this.sectionsToEntries = sectionsToEntriesBuilder.build();
// We could create this Map on demand; however, in practice, it is almost always needed when
// BuckConfig is needed because CommandLineBuildTargetNormalizer needs it.
this.aliasToBuildTargetMap = createAliasToBuildTargetMap(
this.getEntriesForSection(ALIAS_SECTION_HEADER),
buildTargetParser);
this.artifactCache = initArtifactCache();
}
public static BuckConfig emptyConfig() {
return EMPTY_INSTANCE;
}
public static BuckConfig createFromFile(File file) throws IOException {
Preconditions.checkNotNull(file);
// It is necessary to get the absolute file before getting the parent file because the file
// is most likely `new File(".buckproject")`, whose parent file is null, according to Java.
File projectRoot = file.getAbsoluteFile().getParentFile();
ProjectFilesystem projectFilesystem = new ProjectFilesystem(projectRoot);
BuildTargetParser buildTargetParser = new BuildTargetParser(projectFilesystem);
BufferedReader reader = Files.newReader(file, Charsets.UTF_8);
return createFromReader(reader, buildTargetParser);
}
/**
* @return whether {@code aliasName} conforms to the pattern for a valid alias name. This does not
* indicate whether it is an alias that maps to a build target in a BuckConfig.
*/
private static boolean isValidAliasName(String aliasName) {
return aliasName != null && ALIAS_PATTERN.matcher(aliasName).matches();
}
public static void validateAliasName(String aliasName) throws HumanReadableException {
validateAgainstAlias(aliasName, "Alias");
}
public static void validateLabelName(String aliasName) throws HumanReadableException {
validateAgainstAlias(aliasName, "Label");
}
private static void validateAgainstAlias(String aliasName, String fieldName) {
if (isValidAliasName(aliasName)) {
return;
}
if (aliasName == null) {
throw new HumanReadableException("%s cannot be null.", fieldName);
}
if (aliasName.isEmpty()) {
throw new HumanReadableException("%s cannot be the empty string.", fieldName);
}
throw new HumanReadableException("Not a valid %s: %s.", fieldName.toLowerCase(), aliasName);
}
@VisibleForTesting
static Map<String, Map<String, String>> createFromReader(Reader reader) throws IOException {
Preconditions.checkNotNull(reader);
Ini ini = new Ini();
ini.load(reader);
Map<String, Map<String, String>> sectionsToEntries = Maps.newHashMap();
for (String sectionName : ini.keySet()) {
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
Section section = ini.get(sectionName);
for (String propertyName : section.keySet()) {
// Verify that a section does not have the same key specified more than once.
if (section.getAll(propertyName).size() > 1) {
throw new HumanReadableException("Duplicate definition for %s in [%s].",
propertyName,
sectionName);
}
String propertyValue = section.get(propertyName);
builder.put(propertyName, propertyValue);
}
ImmutableMap<String, String> sectionToEntries = builder.build();
sectionsToEntries.put(sectionName, sectionToEntries);
}
return sectionsToEntries;
}
@VisibleForTesting
static BuckConfig createFromReader(Reader reader, BuildTargetParser buildTargetParser)
throws IOException {
Map<String, Map<String, String>> sectionsToEntries = createFromReader(reader);
return new BuckConfig(sectionsToEntries, buildTargetParser);
}
public ImmutableMap<String, String> getEntriesForSection(String section) {
Preconditions.checkNotNull(section);
ImmutableMap<String, String> entries = sectionsToEntries.get(section);
if (entries != null) {
return entries;
} else {
return ImmutableMap.of();
}
}
/**
* A [possibly empty] sequence of paths to files that should be included by default when
* evaluating a build file.
*/
public Iterable<String> getDefaultIncludes() {
ImmutableMap<String, String> entries = getEntriesForSection("buildfile");
String includes = Strings.nullToEmpty(entries.get("includes"));
return Splitter.on(' ').trimResults().omitEmptyStrings().split(includes);
}
@Nullable
public String getBuildTargetForAlias(String alias) {
Preconditions.checkNotNull(alias);
BuildTarget buildTarget = aliasToBuildTargetMap.get(alias);
if (buildTarget != null) {
return buildTarget.getFullyQualifiedName();
} else {
return null;
}
}
public BuildTarget getBuildTargetForFullyQualifiedTarget(String target)
throws NoSuchBuildTargetException {
return buildTargetParser.parse(target, ParseContext.fullyQualified());
}
/**
* In a {@link BuckConfig}, an alias can either refer to a fully-qualified build target, or an
* alias defined earlier in the {@code alias} section. The mapping produced by this method
* reflects the result of resolving all aliases as values in the {@code alias} section.
*/
private static ImmutableMap<String, BuildTarget> createAliasToBuildTargetMap(
ImmutableMap<String, String> rawAliasMap, BuildTargetParser buildTargetParser) {
// We use a LinkedHashMap rather than an ImmutableMap.Builder because we want both (1) order to
// be preserved, and (2) the ability to inspect the Map while building it up.
LinkedHashMap<String, BuildTarget> aliasToBuildTarget = Maps.newLinkedHashMap();
for (Map.Entry<String, String> aliasEntry : rawAliasMap.entrySet()) {
String alias = aliasEntry.getKey();
validateAliasName(alias);
// Determine whether the mapping is to a build target or to an alias.
String value = aliasEntry.getValue();
BuildTarget buildTarget;
if (isValidAliasName(value)) {
buildTarget = aliasToBuildTarget.get(value);
if (buildTarget == null) {
throw new HumanReadableException("No alias for: %s.", value);
}
} else {
// Here we parse the alias values with a BuildTargetParser to be strict. We could be looser
// and just grab everything between "//" and ":" and assume it's a valid base path.
try {
buildTarget = buildTargetParser.parse(value, ParseContext.fullyQualified());
} catch (NoSuchBuildTargetException e) {
throw new HumanReadableException(e);
}
}
aliasToBuildTarget.put(alias, buildTarget);
}
return ImmutableMap.copyOf(aliasToBuildTarget);
}
/**
* Create a map of {@link BuildTarget} base paths to aliases. Note that there may be more than
* one alias to a base path, so the first one listed in the .buckconfig will be chosen.
*/
public ImmutableMap<String, String> getBasePathToAliasMap() {
ImmutableMap<String, String> aliases = sectionsToEntries.get(ALIAS_SECTION_HEADER);
if (aliases == null) {
return ImmutableMap.of();
}
Preconditions.checkNotNull(buildTargetParser,
"buildTargetParser should be set for all instances of BuckConfig except EMPTY_INSTANCE");
// Build up the Map with an ordinary HashMap because we need to be able to check whether the Map
// already contains the key before inserting.
Map<String, String> basePathToAlias = Maps.newHashMap();
for (Map.Entry<String, BuildTarget> entry : aliasToBuildTargetMap.entrySet()) {
String alias = entry.getKey();
BuildTarget buildTarget = entry.getValue();
String basePath = buildTarget.getBasePath();
if (!basePathToAlias.containsKey(basePath)) {
basePathToAlias.put(basePath, alias);
}
}
return ImmutableMap.copyOf(basePathToAlias);
}
@VisibleForTesting
DefaultJavaPackageFinder createDefaultJavaPackageFinder() {
Optional<String> srcRootsOptional = getValue("java", "src_roots");
ImmutableSet<String> paths;
if (srcRootsOptional.isPresent()) {
String srcRoots = srcRootsOptional.get();
Splitter splitter = Splitter.on(',').omitEmptyStrings().trimResults();
paths = ImmutableSet.copyOf(splitter.split(srcRoots));
} else {
paths = ImmutableSet.of();
}
return DefaultJavaPackageFinder.createDefaultJavaPackageFinder(paths);
}
ImmutableSet<String> getDefaultExcludedLabels() {
Optional<String> excludedRulesOptional = getValue("test", "excluded_labels");
if (excludedRulesOptional.isPresent()) {
String excludedRules = excludedRulesOptional.get();
Splitter splitter = Splitter.on(',').omitEmptyStrings().trimResults();
ImmutableSet<String> result = ImmutableSet.copyOf(splitter.split(excludedRules));
// Validate that all specified labels are valid.
for (String label : result) {
validateLabelName(label);
}
return result;
} else {
return ImmutableSet.of();
}
}
Optional<String> getPathToDefaultAndroidManifest() {
return getValue("project", "default_android_manifest");
}
@Beta
Optional<BuildDependencies> getBuildDependencies() {
Optional<String> buildDependenciesOptional = getValue("build", "build_dependencies");
if (buildDependenciesOptional.isPresent()) {
try {
return Optional.of(BuildDependencies.valueOf(buildDependenciesOptional.get()));
} catch (IllegalArgumentException e) {
throw new HumanReadableException(
"%s is not a valid value for build_dependencies. Must be one of: %s",
buildDependenciesOptional.get(),
Joiner.on(", ").join(BuildDependencies.values()));
}
} else {
return Optional.absent();
}
}
List<String> getInitialTargets() {
Optional<String> initialTargets = getValue("project", "initial_targets");
return initialTargets.isPresent()
? Lists.newArrayList(Splitter.on(' ').trimResults().split(initialTargets.get()))
: ImmutableList.<String>of();
}
private ArtifactCache initNoopArtifactCache() {
return new NoopArtifactCache();
}
private ArtifactCache initDirArtifactCache() {
String cacheDir = getValue("cache", "dir").or(DEFAULT_CACHE_DIR);
File dir = new File(cacheDir);
try {
return new DirArtifactCache(dir);
} catch (IOException e) {
throw new HumanReadableException(String.format(
"Failure initializing artifact cache directory: %s",
dir));
}
}
private ArtifactCache initCassandraArtifactCache() {
// cache.hosts
String cacheHosts = getValue("cache", "hosts").or("");
// cache.port
int port = Integer.parseInt(getValue("cache", "port").or(DEFAULT_CASSANDRA_PORT));
try {
return new CassandraArtifactCache(cacheHosts, port);
} catch (ConnectionException e) {
throw new HumanReadableException("Cassandra cache connection failure");
}
}
private ArtifactCache initArtifactCache() {
String cacheMode = getValue("cache", "mode").or("noop");
try {
switch (ArtifactCacheNames.valueOf(cacheMode)) {
case noop:
return initNoopArtifactCache();
case dir:
return initDirArtifactCache();
case cassandra:
return initCassandraArtifactCache();
}
} catch (IllegalArgumentException e) {
throw new HumanReadableException(String.format("Unusable cache.mode: '%s'", cacheMode));
}
throw new HumanReadableException(String.format("Unusable cache.mode: '%s'", cacheMode));
}
public ArtifactCache getArtifactCache() {
return artifactCache;
}
private Optional<String> getValue(String sectionName, String propertyName) {
ImmutableMap<String, String> properties = this.getEntriesForSection(sectionName);
String value = properties.get(propertyName);
if (value != null) {
return Optional.of(value);
} else {
return Optional.absent();
}
}
}