/* * 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.parser; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.FlavorParser; import com.facebook.buck.model.InternalFlavor; import com.facebook.buck.model.UnflavoredBuildTarget; import com.facebook.buck.rules.CellPathResolver; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Interner; import com.google.common.collect.Interners; import java.nio.file.Path; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; public class BuildTargetParser { /** The BuildTargetParser is stateless, so this single instance can be shared. */ public static final BuildTargetParser INSTANCE = new BuildTargetParser(); private static final String BUILD_RULE_PREFIX = "//"; private static final String BUILD_RULE_SEPARATOR = ":"; private static final Splitter BUILD_RULE_SEPARATOR_SPLITTER = Splitter.on(BUILD_RULE_SEPARATOR); private static final Set<String> INVALID_BASE_NAME_PARTS = ImmutableSet.of(".", ".."); private final Interner<BuildTarget> flavoredTargetCache = Interners.newWeakInterner(); private final FlavorParser flavorParser = new FlavorParser(); private BuildTargetParser() { // this is stateless. There's no need to do anything other than grab the instance needed. } /** * @param buildTargetName either a fully-qualified name or relative to the {@link * BuildTargetPatternParser}. For example, inside {@code first-party/orca/orcaapp/BUCK}, which * can be obtained by calling {@code ParseContext.forBaseName("first-party/orca/orcaapp")}, * {@code //first-party/orca/orcaapp:assets} and {@code :assets} refer to the same target. * However, from the command line the context is obtained by calling {@link * BuildTargetPatternParser#fullyQualified()} and relative names are not recognized. * @param buildTargetPatternParser how targets should be interpreted, such in the context of a * specific build file or only as fully-qualified names (as is the case for targets from the * command line). */ public BuildTarget parse( String buildTargetName, BuildTargetPatternParser<?> buildTargetPatternParser, CellPathResolver cellNames) { if (buildTargetName.endsWith(BUILD_RULE_SEPARATOR) && !buildTargetPatternParser.isWildCardAllowed()) { throw new BuildTargetParseException( String.format("%s cannot end with a colon", buildTargetName)); } Optional<String> givenCellName = Optional.empty(); String targetAfterCell = buildTargetName; if (buildTargetName.contains(BUILD_RULE_PREFIX) && !buildTargetName.startsWith(BUILD_RULE_PREFIX)) { int slashIndex = buildTargetName.indexOf(BUILD_RULE_PREFIX); givenCellName = Optional.of(buildTargetName.substring(0, slashIndex)); targetAfterCell = buildTargetName.substring(slashIndex); } if (givenCellName.isPresent() && givenCellName.get().isEmpty()) { throw new BuildTargetParseException("Cell name must not be empty."); } List<String> parts = BUILD_RULE_SEPARATOR_SPLITTER.splitToList(targetAfterCell); if (parts.size() != 2) { throw new BuildTargetParseException( String.format( "%s must contain exactly one colon (found %d)", buildTargetName, parts.size() - 1)); } String baseName = parts.get(0).isEmpty() ? buildTargetPatternParser.getBaseName() : parts.get(0); String shortName = parts.get(1); Iterable<String> flavorNames = new HashSet<>(); int hashIndex = shortName.indexOf('#'); if (hashIndex != -1 && hashIndex < shortName.length()) { flavorNames = flavorParser.parseFlavorString(shortName.substring(hashIndex + 1)); shortName = shortName.substring(0, hashIndex); } Preconditions.checkNotNull(baseName); // On Windows, baseName may contain backslashes, which are not permitted by BuildTarget. baseName = baseName.replace('\\', '/'); checkBaseName(baseName, buildTargetName); Path cellPath = cellNames.getCellPath(givenCellName); UnflavoredBuildTarget.Builder unflavoredBuilder = UnflavoredBuildTarget.builder() .setBaseName(baseName) .setShortName(shortName) // Set the cell path correctly. Because the cellNames comes from the owning cell we can // be sure that if this doesn't throw an exception the target cell is visible to the // owning cell. .setCellPath(cellPath) // We are setting the cell name so we can print it later .setCell(cellNames.getCanonicalCellName(cellPath)); UnflavoredBuildTarget unflavoredBuildTarget = unflavoredBuilder.build(); BuildTarget.Builder builder = BuildTarget.builder(unflavoredBuildTarget); for (String flavor : flavorNames) { builder.addFlavors(InternalFlavor.of(flavor)); } return flavoredTargetCache.intern(builder.build()); } protected static void checkBaseName(String baseName, String buildTargetName) { if (baseName.equals(BUILD_RULE_PREFIX)) { return; } if (!baseName.startsWith(BUILD_RULE_PREFIX)) { throw new BuildTargetParseException( String.format("Path in %s must start with %s", buildTargetName, BUILD_RULE_PREFIX)); } String baseNamePath = baseName.substring(BUILD_RULE_PREFIX.length()); if (baseNamePath.startsWith("/")) { throw new BuildTargetParseException( String.format( "Build target path should start with an optional cell name, then // and then a " + "relative directory name, not an absolute directory path (found %s)", buildTargetName)); } for (String baseNamePart : Splitter.on('/').split(baseNamePath)) { if ("".equals(baseNamePart)) { throw new BuildTargetParseException( String.format( "Build target path cannot contain // other than at the start " + "(or after a cell name) (found %s)", buildTargetName)); } if (INVALID_BASE_NAME_PARTS.contains(baseNamePart)) { throw new BuildTargetParseException( String.format("Build target path cannot contain . or .. (found %s)", buildTargetName)); } } } }