/* * 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.project_generator.XCodeProjectCommandHelper; import com.facebook.buck.event.ProjectGenerationEvent; import com.facebook.buck.ide.intellij.IjProjectBuckConfig; import com.facebook.buck.ide.intellij.IjProjectCommandHelper; import com.facebook.buck.ide.intellij.aggregation.AggregationMode; import com.facebook.buck.ide.intellij.model.IjProjectConfig; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.step.ExecutorPool; import com.facebook.buck.util.ForwardingProcessListener; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.ListeningProcessExecutor; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.ProcessExecutorParams; import com.google.common.base.Ascii; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.nio.channels.Channels; import java.nio.file.Paths; import java.util.Optional; import javax.annotation.Nullable; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; import org.kohsuke.args4j.OptionDef; import org.kohsuke.args4j.spi.OptionHandler; import org.kohsuke.args4j.spi.Parameters; import org.kohsuke.args4j.spi.Setter; public class ProjectCommand extends BuildCommand { public enum Ide { INTELLIJ, XCODE; public static Ide fromString(String string) { switch (Ascii.toLowerCase(string)) { case "intellij": return Ide.INTELLIJ; case "xcode": return Ide.XCODE; default: throw new HumanReadableException("Invalid ide value %s.", string); } } } private static final boolean DEFAULT_READ_ONLY_VALUE = false; @Option( name = "--combined-project", usage = "Generate an xcode project of a target and its dependencies." ) private boolean combinedProject; @Option( name = "--build-with-buck", usage = "Use Buck to build the generated project instead of delegating the build to the IDE." ) boolean buildWithBuck; @Option(name = "--process-annotations", usage = "Enable annotation processing") private boolean processAnnotations; @Option( name = "--without-tests", usage = "When generating a project slice, exclude tests that test the code in that slice" ) private boolean withoutTests = false; @Option( name = "--with-tests", usage = "When generating a project slice, generate with all the tests" ) private boolean withTests = false; @Option( name = "--without-dependencies-tests", usage = "When generating a project slice, includes tests that test code in main target, " + "but exclude tests that test dependencies" ) private boolean withoutDependenciesTests = false; @Option( name = "--ide", usage = "The type of IDE for which to generate a project. You may specify it in the " + ".buckconfig file. Please refer to https://buckbuild.com/concept/buckconfig.html#project" ) @Nullable private Ide ide = null; @Option( name = "--read-only", usage = "If true, generate project files read-only. Defaults to '" + DEFAULT_READ_ONLY_VALUE + "' if not specified in .buckconfig. (Only " + "applies to generated Xcode projects.)" ) private boolean readOnly = DEFAULT_READ_ONLY_VALUE; @Option( name = "--dry-run", usage = "Instead of actually generating the project, only print out the targets that " + "would be included." ) private boolean dryRun = false; @Option( name = "--intellij-aggregation-mode", handler = AggregationModeOptionHandler.class, usage = "Changes how modules are aggregated. Valid options are 'none' (no aggregation), " + "'shallow' (Minimum of 3 directory levels deep), 'auto' (based on project size), or an " + "integer to specify the minimum directory depth modules should be aggregated to (e.g." + "specifying 3 would aggrgate modules to a/b/c from lower levels where possible). " + "Defaults to 'auto' if not specified in .buckconfig." ) @Nullable private AggregationMode intellijAggregationMode = null; @Option( name = "--run-ij-cleaner", usage = "After generating an IntelliJ project using --experimental-ij-generation, start a " + "cleaner which removes any .iml files which weren't generated as part of the project." ) private boolean runIjCleaner = false; @Option( name = "--remove-unused-ij-libraries", usage = "After generating an IntelliJ project remove all IntelliJ libraries that are not " + "used in the project." ) private boolean removeUnusedLibraries = false; @Option( name = "--exclude-artifacts", usage = "Don't include references to the artifacts created by compiling a target in" + "the module representing that target." ) private boolean excludeArtifacts = false; @Option( name = "--skip-build", usage = "Don't try to build any of the targets for the generated project." ) private boolean skipBuild = false; @Option(name = "--build", usage = "Also build all the targets in the project.") private boolean build = true; @Option( name = "--focus", usage = "Space separated list of build target full qualified names that should be part of " + "focused project. " + "For example, //Libs/CommonLibs:BaseLib //Libs/ImportantLib:ImportantLib" ) @Nullable private String modulesToFocusOn = null; @Option( name = "--file-with-list-of-generated-files", usage = "If present, forces command to save the list of generated file names to a provided" + " file" ) @Nullable private String generatedFilesListFilename = null; @Option( name = "--view", usage = "Experimental command to build a 'project view', which is a directory outside the " + "repo, containing symlinks in to the repo. This directory looks a lot like a standard " + "IntelliJ project with all resources under /res, but what's really important is that it " + "generates a single IntelliJ module, so that editing is much faster than when you use " + "`buck project`." ) @Nullable private String projectView = null; private Optional<String> getPathToPreProcessScript(BuckConfig buckConfig) { return buckConfig.getValue("project", "pre_process"); } private Optional<Ide> getIdeFromBuckConfig(BuckConfig buckConfig) { return buckConfig.getValue("project", "ide").map(Ide::fromString); } private boolean getReadOnly(BuckConfig buckConfig) { if (readOnly) { return readOnly; } return buckConfig.getBooleanValue("project", "read_only", DEFAULT_READ_ONLY_VALUE); } @Override public int runWithoutHelp(CommandRunnerParams params) throws IOException, InterruptedException { int rc = runPreprocessScriptIfNeeded(params); if (rc != 0) { return rc; } try (CommandThreadManager pool = new CommandThreadManager("Project", getConcurrencyLimit(params.getBuckConfig()))) { final Ide projectIde = (ide == null) ? getIdeFromBuckConfig(params.getBuckConfig()).orElse(null) : ide; if (projectIde == null) { params .getConsole() .getStdErr() .println("\nCannot build a project: project IDE is not specified."); return 1; } ListeningExecutorService executor = pool.getExecutor(); params.getBuckEventBus().post(ProjectGenerationEvent.started()); int result; try { switch (projectIde) { case INTELLIJ: IjProjectConfig projectConfig = IjProjectBuckConfig.create( params.getBuckConfig(), intellijAggregationMode, generatedFilesListFilename, runIjCleaner, removeUnusedLibraries, excludeArtifacts, skipBuild || !build); IjProjectCommandHelper projectCommandHelper = new IjProjectCommandHelper( params.getBuckEventBus(), params.getConsole(), executor, params.getParser(), params.getBuckConfig(), params.getActionGraphCache(), params.getCell(), projectConfig, processAnnotations, getEnableParserProfiling(), projectView, dryRun, withTests, withoutTests, withoutDependenciesTests, (buildTargets, disableCaching) -> runBuild(params, buildTargets, disableCaching), arguments -> parseArgumentsAsTargetNodeSpecs(params.getBuckConfig(), arguments)); result = projectCommandHelper.parseTargetsAndRunProjectGenerator(getArguments()); break; case XCODE: XCodeProjectCommandHelper xcodeProjectCommandHelper = new XCodeProjectCommandHelper( params.getBuckEventBus(), params.getParser(), params.getBuckConfig(), params.getCell(), params.getConsole(), params.getProcessManager(), params.getEnvironment(), params.getExecutors().get(ExecutorPool.PROJECT), getArguments(), getEnableParserProfiling(), withTests, withoutTests, withoutDependenciesTests, modulesToFocusOn, combinedProject, dryRun, getReadOnly(params.getBuckConfig()), arguments -> parseArgumentsAsTargetNodeSpecs(params.getBuckConfig(), arguments), arguments -> { try { return runBuild(params, arguments); } catch (IOException | InterruptedException e) { throw new RuntimeException("Cannot run a build", e); } }); result = xcodeProjectCommandHelper.parseTargetsAndRunXCodeGenerator(executor); break; default: // unreachable throw new IllegalStateException("'ide' should always be of type 'INTELLIJ' or 'XCODE'"); } } finally { params.getBuckEventBus().post(ProjectGenerationEvent.finished()); } return result; } } @Override public boolean isReadOnly() { return false; } private int runBuild(CommandRunnerParams params, ImmutableList<String> arguments) throws IOException, InterruptedException { BuildCommand buildCommand = new BuildCommand(arguments); return buildCommand.run(params); } private int runBuild( CommandRunnerParams params, ImmutableSet<BuildTarget> targets, boolean disableCaching) throws IOException, InterruptedException { BuildCommand buildCommand = new BuildCommand( targets.stream().map(Object::toString).collect(MoreCollectors.toImmutableList())); buildCommand.setKeepGoing(true); buildCommand.setArtifactCacheDisabled(disableCaching); return buildCommand.run(params); } private int runPreprocessScriptIfNeeded(CommandRunnerParams params) throws IOException, InterruptedException { Optional<String> pathToPreProcessScript = getPathToPreProcessScript(params.getBuckConfig()); if (!pathToPreProcessScript.isPresent()) { return 0; } String pathToScript = pathToPreProcessScript.get(); if (!Paths.get(pathToScript).isAbsolute()) { pathToScript = params .getCell() .getFilesystem() .getPathForRelativePath(pathToScript) .toAbsolutePath() .toString(); } ListeningProcessExecutor processExecutor = new ListeningProcessExecutor(); ProcessExecutorParams processExecutorParams = ProcessExecutorParams.builder() .addCommand(pathToScript) .setEnvironment( ImmutableMap.<String, String>builder() .putAll(params.getEnvironment()) .put("BUCK_PROJECT_TARGETS", Joiner.on(" ").join(getArguments())) .build()) .setDirectory(params.getCell().getFilesystem().getRootPath()) .build(); ForwardingProcessListener processListener = new ForwardingProcessListener( // Using rawStream to avoid shutting down SuperConsole. This is safe to do // because this process finishes before we start parsing process. Channels.newChannel(params.getConsole().getStdOut().getRawStream()), Channels.newChannel(params.getConsole().getStdErr().getRawStream())); ListeningProcessExecutor.LaunchedProcess process = processExecutor.launchProcess(processExecutorParams, processListener); try { return processExecutor.waitForProcess(process); } finally { processExecutor.destroyProcess(process, /* force */ false); processExecutor.waitForProcess(process); } } @Override public String getShortDescription() { return "generates project configuration files for an IDE"; } public static class AggregationModeOptionHandler extends OptionHandler<AggregationMode> { public AggregationModeOptionHandler( CmdLineParser parser, OptionDef option, Setter<? super AggregationMode> setter) { super(parser, option, setter); } @Override public int parseArguments(Parameters params) throws CmdLineException { String param = params.getParameter(0); setter.addValue(AggregationMode.fromString(param)); return 1; } @Override @Nullable public String getDefaultMetaVariable() { return null; } } }