blob: b649b10f5a561089a3400ee5f3de2ef56d86afd7 [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.apple.AppleBuildRules;
import com.facebook.buck.apple.AppleTestDescription;
import com.facebook.buck.apple.ProjectGenerator;
import com.facebook.buck.apple.WorkspaceAndProjectGenerator;
import com.facebook.buck.apple.XcodeWorkspaceConfigDescription;
import com.facebook.buck.java.JavaLibraryDescription;
import com.facebook.buck.java.intellij.Project;
import com.facebook.buck.json.BuildFileParseException;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetException;
import com.facebook.buck.model.FilesystemBackedBuildFileTree;
import com.facebook.buck.model.HasBuildTarget;
import com.facebook.buck.parser.BuildTargetSpec;
import com.facebook.buck.parser.ParserConfig;
import com.facebook.buck.parser.TargetNodePredicateSpec;
import com.facebook.buck.parser.TargetNodeSpec;
import com.facebook.buck.python.PythonBuckConfig;
import com.facebook.buck.rules.ActionGraph;
import com.facebook.buck.rules.AssociatedTargetNodePredicate;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.ProjectConfig;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetGraphAndTargets;
import com.facebook.buck.rules.TargetGraphToActionGraph;
import com.facebook.buck.rules.TargetGraphTransformer;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.ProcessManager;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Functions;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class ProjectCommand extends AbstractCommandRunner<ProjectCommandOptions> {
private static final Logger LOG = Logger.get(ProjectCommand.class);
/**
* Include java library targets (and android library targets) that use annotation
* processing. The sources generated by these annotation processors is needed by
* IntelliJ.
*/
private static final Predicate<TargetNode<?>> ANNOTATION_PREDICATE =
new Predicate<TargetNode<?>>() {
@Override
public boolean apply(TargetNode<?> input) {
if (input.getType() != JavaLibraryDescription.TYPE) {
return false;
}
JavaLibraryDescription.Arg arg = ((JavaLibraryDescription.Arg) input.getConstructorArg());
return !arg.annotationProcessors.get().isEmpty();
}
};
private static final String XCODE_PROCESS_NAME = "Xcode";
private final TargetGraphTransformer<ActionGraph> targetGraphTransformer;
public ProjectCommand(CommandRunnerParams params) {
super(params);
this.targetGraphTransformer = new TargetGraphToActionGraph(
params.getBuckEventBus(),
new BuildTargetNodeToBuildRuleTransformer());
}
@Override
ProjectCommandOptions createOptions(BuckConfig buckConfig) {
return new ProjectCommandOptions(buckConfig);
}
@Override
int runCommandWithOptionsInternal(ProjectCommandOptions options)
throws IOException, InterruptedException {
if (options.getIde() == ProjectCommandOptions.Ide.XCODE) {
checkForAndKillXcodeIfRunning(options.getIdePrompt());
}
ImmutableSet<BuildTarget> passedInTargetsSet =
getBuildTargets(options.getArgumentsFormattedAsBuildTargets());
ProjectGraphParser projectGraphParser = ProjectGraphParsers.createProjectGraphParser(
getParser(),
new ParserConfig(options.getBuckConfig()),
getBuckEventBus(),
console,
environment,
options.getEnableProfiling());
TargetGraph projectGraph = projectGraphParser.buildTargetGraphForTargetNodeSpecs(
getTargetNodeSpecsForIde(
options.getIde(),
passedInTargetsSet,
getProjectFilesystem().getIgnorePaths()));
ProjectPredicates projectPredicates = ProjectPredicates.forIde(options.getIde());
ImmutableSet<BuildTarget> graphRoots;
if (!passedInTargetsSet.isEmpty()) {
graphRoots = passedInTargetsSet;
} else {
graphRoots = getRootsFromPredicate(
projectGraph,
projectPredicates.getProjectRootsPredicate());
}
TargetGraphAndTargets targetGraphAndTargets = createTargetGraph(
projectGraph,
graphRoots,
projectGraphParser,
projectPredicates.getAssociatedProjectPredicate(),
options.isWithTests(),
options.getIde(),
getProjectFilesystem().getIgnorePaths());
if (options.getDryRun()) {
for (TargetNode<?> targetNode : targetGraphAndTargets.getTargetGraph().getNodes()) {
console.getStdOut().println(targetNode.toString());
}
return 0;
}
switch (options.getIde()) {
case INTELLIJ:
return runIntellijProjectGenerator(
projectGraph,
targetGraphAndTargets,
passedInTargetsSet,
options);
case XCODE:
return runXcodeProjectGenerator(
targetGraphAndTargets,
passedInTargetsSet,
options);
default:
// unreachable
throw new IllegalStateException("'ide' should always be of type 'INTELLIJ' or 'XCODE'");
}
}
/**
* Run intellij specific project generation actions.
*/
int runIntellijProjectGenerator(
TargetGraph projectGraph,
TargetGraphAndTargets targetGraphAndTargets,
ImmutableSet<BuildTarget> passedInTargetsSet,
ProjectCommandOptions options)
throws IOException, InterruptedException {
// Create an ActionGraph that only contains targets that can be represented as IDE
// configuration files.
ActionGraph actionGraph = targetGraphTransformer.apply(targetGraphAndTargets.getTargetGraph());
try (ExecutionContext executionContext = createExecutionContext()) {
Project project = new Project(
new SourcePathResolver(new BuildRuleResolver(actionGraph.getNodes())),
FluentIterable
.from(actionGraph.getNodes())
.filter(ProjectConfig.class)
.toSet(),
actionGraph,
options.getBasePathToAliasMap(),
options.getJavaPackageFinder(),
executionContext,
new FilesystemBackedBuildFileTree(
getProjectFilesystem(),
new ParserConfig(options.getBuckConfig()).getBuildFileName()),
getProjectFilesystem(),
options.getPathToDefaultAndroidManifest(),
options.getPathToPostProcessScript(),
new PythonBuckConfig(options.getBuckConfig()).getPythonInterpreter(),
getObjectMapper(),
options.isAndroidAutoGenerateEnabled());
File tempDir = Files.createTempDir();
File tempFile = new File(tempDir, "project.json");
int exitCode;
try {
exitCode = project.createIntellijProject(
tempFile,
executionContext.getProcessExecutor(),
!passedInTargetsSet.isEmpty(),
console.getStdOut(),
console.getStdErr());
if (exitCode != 0) {
return exitCode;
}
List<String> additionalInitialTargets = ImmutableList.of();
if (options.shouldProcessAnnotations()) {
try {
additionalInitialTargets = getAnnotationProcessingTargets(
projectGraph,
passedInTargetsSet);
} catch (BuildTargetException | BuildFileParseException e) {
throw new HumanReadableException(e);
}
}
// Build initial targets.
if (options.hasInitialTargets() || !additionalInitialTargets.isEmpty()) {
BuildCommand buildCommand = new BuildCommand(getCommandRunnerParams());
BuildCommandOptions buildOptions =
options.createBuildCommandOptionsWithInitialTargets(additionalInitialTargets);
exitCode = buildCommand.runCommandWithOptions(buildOptions);
if (exitCode != 0) {
return exitCode;
}
}
} finally {
// Either leave project.json around for debugging or delete it on exit.
if (console.getVerbosity().shouldPrintOutput()) {
getStdErr().printf("project.json was written to %s", tempFile.getAbsolutePath());
} else {
tempFile.delete();
tempDir.delete();
}
}
if (passedInTargetsSet.isEmpty()) {
String greenStar = console.getAnsi().asHighlightedSuccessText(" * ");
getStdErr().printf(
console.getAnsi().asHighlightedSuccessText("=== Did you know ===") + "\n" +
greenStar + "You can run `buck project <target>` to generate a minimal project " +
"just for that target.\n" +
greenStar + "This will make your IDE faster when working on large projects.\n" +
greenStar + "See buck project --help for more info.\n" +
console.getAnsi().asHighlightedSuccessText(
"--=* Knowing is half the battle!") + "\n");
}
return 0;
}
}
ImmutableList<String> getAnnotationProcessingTargets(
TargetGraph projectGraph,
ImmutableSet<BuildTarget> passedInTargetsSet)
throws BuildTargetException, BuildFileParseException, IOException, InterruptedException {
ImmutableSet<BuildTarget> buildTargets;
if (!passedInTargetsSet.isEmpty()) {
buildTargets = passedInTargetsSet;
} else {
buildTargets = getRootsFromPredicate(
projectGraph,
ANNOTATION_PREDICATE);
}
return FluentIterable
.from(buildTargets)
.transform(Functions.toStringFunction())
.toList();
}
/**
* Run xcode specific project generation actions.
*/
int runXcodeProjectGenerator(
TargetGraphAndTargets targetGraphAndTargets,
ImmutableSet<BuildTarget> passedInTargetsSet,
ProjectCommandOptions options)
throws IOException, InterruptedException {
ImmutableSet.Builder<ProjectGenerator.Option> optionsBuilder = ImmutableSet.builder();
if (options.getReadOnly()) {
optionsBuilder.add(ProjectGenerator.Option.GENERATE_READ_ONLY_FILES);
}
if (options.isWithTests()) {
optionsBuilder.add(ProjectGenerator.Option.INCLUDE_TESTS);
}
boolean combinedProject = options.getCombinedProject();
ImmutableSet<BuildTarget> targets;
if (passedInTargetsSet.isEmpty()) {
targets = FluentIterable
.from(targetGraphAndTargets.getProjectRoots())
.transform(HasBuildTarget.TO_TARGET)
.toSet();
} else {
targets = passedInTargetsSet;
}
if (combinedProject) {
optionsBuilder.addAll(ProjectGenerator.COMBINED_PROJECT_OPTIONS);
} else {
optionsBuilder.addAll(ProjectGenerator.SEPARATED_PROJECT_OPTIONS);
}
LOG.debug("Generating workspace for config targets %s", targets);
Map<Path, ProjectGenerator> projectGenerators = new HashMap<>();
ImmutableSet<TargetNode<?>> testTargetNodes = targetGraphAndTargets.getAssociatedTests();
ImmutableSet<TargetNode<AppleTestDescription.Arg>> groupableTests =
options.getCombineTestBundles()
? AppleBuildRules.filterGroupableTests(testTargetNodes)
: ImmutableSet.<TargetNode<AppleTestDescription.Arg>>of();
for (BuildTarget workspaceTarget : targets) {
TargetNode<?> workspaceNode = Preconditions.checkNotNull(
targetGraphAndTargets.getTargetGraph().get(workspaceTarget));
if (workspaceNode.getType() != XcodeWorkspaceConfigDescription.TYPE) {
throw new HumanReadableException(
"%s must be a xcode_workspace_config",
workspaceTarget);
}
WorkspaceAndProjectGenerator generator = new WorkspaceAndProjectGenerator(
getProjectFilesystem(),
targetGraphAndTargets.getTargetGraph(),
castToXcodeWorkspaceTargetNode(workspaceNode),
optionsBuilder.build(),
combinedProject,
new ParserConfig(options.getBuckConfig()).getBuildFileName());
generator.setGroupableTests(groupableTests);
generator.generateWorkspaceAndDependentProjects(projectGenerators);
}
return 0;
}
@SuppressWarnings(value = "unchecked")
private static TargetNode<XcodeWorkspaceConfigDescription.Arg> castToXcodeWorkspaceTargetNode(
TargetNode<?> targetNode) {
Preconditions.checkArgument(targetNode.getType() == XcodeWorkspaceConfigDescription.TYPE);
return (TargetNode<XcodeWorkspaceConfigDescription.Arg>) targetNode;
}
private void checkForAndKillXcodeIfRunning(boolean enablePrompt)
throws InterruptedException, IOException {
Optional<ProcessManager> processManager = getProcessManager();
if (!processManager.isPresent()) {
LOG.warn("Could not check if Xcode is running (no process manager)");
return;
}
if (!processManager.get().isProcessRunning(XCODE_PROCESS_NAME)) {
LOG.debug("Xcode is not running.");
return;
}
if (enablePrompt && canPrompt()) {
if (prompt(
"Xcode is currently running. Buck will modify files Xcode currently has " +
"open, which can cause it to become unstable.\n\n" +
"Kill Xcode and continue?")) {
processManager.get().killProcess(XCODE_PROCESS_NAME);
} else {
console.getStdOut().println(
console.getAnsi().asWarningText(
"Xcode is running. Generated projects might be lost or corrupted if Xcode " +
"currently has them open."));
}
console.getStdOut().format(
"To disable this prompt in the future, add the following to %s: \n\n" +
"[project]\n" +
" ide_prompt = false\n\n",
getProjectFilesystem()
.getRootPath()
.resolve(BuckConfig.DEFAULT_BUCK_CONFIG_OVERRIDE_FILE_NAME)
.toAbsolutePath());
} else {
LOG.debug(
"Xcode is running, but cannot prompt to kill it (enabled %s, can prompt %s)",
enablePrompt, canPrompt());
}
}
private boolean canPrompt() {
return System.console() != null;
}
private boolean prompt(String prompt) throws IOException {
Preconditions.checkState(canPrompt());
LOG.debug("Displaying prompt %s..", prompt);
console.getStdOut().print(console.getAnsi().asWarningText(prompt + " [Y/n] "));
Optional<String> result;
try (InputStreamReader stdinReader = new InputStreamReader(System.in, Charsets.UTF_8);
BufferedReader bufferedStdinReader = new BufferedReader(stdinReader)) {
result = Optional.fromNullable(bufferedStdinReader.readLine());
}
LOG.debug("Result of prompt: [%s]", result);
return result.isPresent() &&
(result.get().isEmpty() || result.get().toLowerCase(Locale.US).startsWith("y"));
}
@VisibleForTesting
static ImmutableSet<BuildTarget> getRootsFromPredicate(
TargetGraph projectGraph,
Predicate<TargetNode<?>> rootsPredicate) {
return FluentIterable
.from(projectGraph.getNodes())
.filter(rootsPredicate)
.transform(HasBuildTarget.TO_TARGET)
.toSet();
}
private static Iterable<? extends TargetNodeSpec> getTargetNodeSpecsForIde(
ProjectCommandOptions.Ide ide,
Collection<BuildTarget> passedInBuildTargets,
ImmutableSet<Path> ignoreDirs
) {
if (ide == ProjectCommandOptions.Ide.XCODE &&
!passedInBuildTargets.isEmpty()) {
return Iterables.transform(
passedInBuildTargets,
BuildTargetSpec.TO_BUILD_TARGET_SPEC);
} else {
return ImmutableList.of(
new TargetNodePredicateSpec(
Predicates.<TargetNode<?>>alwaysTrue(),
ignoreDirs));
}
}
private static TargetGraphAndTargets createTargetGraph(
TargetGraph projectGraph,
ImmutableSet<BuildTarget> graphRoots,
ProjectGraphParser projectGraphParser,
AssociatedTargetNodePredicate associatedProjectPredicate,
boolean isWithTests,
ProjectCommandOptions.Ide ide,
ImmutableSet<Path> ignoreDirs
)
throws IOException, InterruptedException {
TargetGraph resultProjectGraph;
ImmutableSet<BuildTarget> explicitTestTargets;
if (isWithTests) {
explicitTestTargets = TargetGraphAndTargets.getExplicitTestTargets(
graphRoots,
projectGraph);
resultProjectGraph =
projectGraphParser.buildTargetGraphForTargetNodeSpecs(
getTargetNodeSpecsForIde(
ide,
Sets.union(graphRoots, explicitTestTargets),
ignoreDirs));
} else {
resultProjectGraph = projectGraph;
explicitTestTargets = ImmutableSet.of();
}
return TargetGraphAndTargets.create(
graphRoots,
resultProjectGraph,
associatedProjectPredicate,
isWithTests,
explicitTestTargets);
}
@Override
String getUsageIntro() {
return "generates project configuration files for an IDE";
}
}