// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package com.twitter.intellij.pants.service;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.util.Consumer;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
import com.twitter.intellij.pants.PantsBundle;
import com.twitter.intellij.pants.PantsExecutionException;
import com.twitter.intellij.pants.metrics.PantsMetrics;
import com.twitter.intellij.pants.model.PantsCompileOptions;
import com.twitter.intellij.pants.model.PantsExecutionOptions;
import com.twitter.intellij.pants.settings.PantsExecutionSettings;
import com.twitter.intellij.pants.util.PantsUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class PantsCompileOptionsExecutor {
protected static final Logger LOG = Logger.getInstance(PantsCompileOptionsExecutor.class);
public static final int PROJECT_NAME_LIMIT = 200;
private final List<Process> myProcesses = ContainerUtil.createConcurrentList();
private final PantsCompileOptions myOptions;
private final File myBuildRoot;
private final boolean myResolveSourcesAndDocsForJars;
@NotNull
public static PantsCompileOptionsExecutor create(
@NotNull String projectRootPath,
@Nullable PantsExecutionSettings executionOptions
) throws ExternalSystemException {
if (executionOptions == null) {
throw new ExternalSystemException("No execution options for " + projectRootPath);
}
PantsCompileOptions options = new MyPantsCompileOptions(projectRootPath, executionOptions);
Optional<File> buildRoot = PantsUtil.findBuildRoot(new File(options.getExternalProjectPath()));
if (!buildRoot.isPresent() || !buildRoot.get().exists()) {
throw new ExternalSystemException(PantsBundle.message("pants.error.no.pants.executable.by.path", options.getExternalProjectPath()));
}
return new PantsCompileOptionsExecutor(
buildRoot.get(),
options,
executionOptions.isLibsWithSourcesAndDocs()
);
}
@NotNull
@TestOnly
public static PantsCompileOptionsExecutor createMock() {
return new PantsCompileOptionsExecutor(
new File("/"),
new MyPantsCompileOptions("", PantsExecutionSettings.createDefault()),
true
) {
};
}
private PantsCompileOptionsExecutor(
@NotNull File buildRoot,
@NotNull PantsCompileOptions compilerOptions,
boolean resolveSourcesAndDocsForJars
) {
myBuildRoot = buildRoot;
myOptions = compilerOptions;
myResolveSourcesAndDocsForJars = resolveSourcesAndDocsForJars;
}
public String getProjectRelativePath() {
return PantsUtil.getRelativeProjectPath(getBuildRoot(), getProjectPath()).get();
}
@NotNull
public File getBuildRoot() {
return myBuildRoot;
}
public String getProjectPath() {
return myOptions.getExternalProjectPath();
}
@NotNull
public String getProjectDir() {
final File projectFile = new File(getProjectPath());
final File projectDir = projectFile.isDirectory() ? projectFile : FileUtil.getParentFile(projectFile);
return projectDir != null ? projectDir.getAbsolutePath() : projectFile.getAbsolutePath();
}
@NotNull
@Nls
public String getProjectName() {
final String buildRootName = getBuildRoot().getName();
List<String> buildRootPrefixedSpecs = myOptions.getTargetSpecs().stream()
.map(s -> buildRootName + File.separator + s)
.collect(Collectors.toList());
String candidateName = String.join("__", buildRootPrefixedSpecs).replaceAll(File.separator, ".");
return candidateName.substring(0, Math.min(PROJECT_NAME_LIMIT, candidateName.length()));
}
@NotNull
@Nls
public String getRootModuleName() {
if (PantsUtil.isExecutable(myOptions.getExternalProjectPath())) {
//noinspection ConstantConditions
return PantsUtil.fileNameWithoutExtension(VfsUtil.extractFileName(myOptions.getExternalProjectPath()));
}
return getProjectRelativePath();
}
@NotNull
public PantsCompileOptions getOptions() {
return myOptions;
}
@NotNull
public String loadProjectStructure(
@NotNull Consumer<String> statusConsumer,
@Nullable ProcessAdapter processAdapter
) throws IOException, ExecutionException {
if (PantsUtil.isExecutable(getProjectPath())) {
return loadProjectStructureFromScript(getProjectPath(), statusConsumer, processAdapter);
}
else {
return loadProjectStructureFromTargets(statusConsumer, processAdapter);
}
}
@NotNull
private static String loadProjectStructureFromScript(
@NotNull String scriptPath,
@NotNull Consumer<String> statusConsumer,
@Nullable ProcessAdapter processAdapter
) throws IOException, ExecutionException {
final GeneralCommandLine commandLine = PantsUtil.defaultCommandLine(scriptPath);
commandLine.setExePath(scriptPath);
statusConsumer.consume("Executing " + PathUtil.getFileName(scriptPath));
final ProcessOutput processOutput = PantsUtil.getCmdOutput(commandLine, processAdapter);
if (processOutput.checkSuccess(LOG)) {
return processOutput.getStdout();
}
else {
throw new PantsExecutionException("Failed to update the project!", scriptPath, processOutput);
}
}
@NotNull
private String loadProjectStructureFromTargets(
@NotNull Consumer<String> statusConsumer,
@Nullable ProcessAdapter processAdapter
) throws IOException, ExecutionException {
final File outputFile = FileUtil.createTempFile("pants_depmap_run", ".out");
final GeneralCommandLine command = getCommand(outputFile, statusConsumer);
statusConsumer.consume("Resolving dependencies...");
PantsMetrics.markExportStart();
final ProcessOutput processOutput = getProcessOutput(command, processAdapter);
PantsMetrics.markExportEnd();
if (processOutput.getStdout().contains("no such option")) {
throw new ExternalSystemException("Pants doesn't have necessary APIs. Please upgrade you pants!");
}
if (processOutput.checkSuccess(LOG)) {
return FileUtil.loadFile(outputFile);
}
else {
throw new PantsExecutionException("Failed to update the project!", command.getCommandLineString("pants"), processOutput);
}
}
private ProcessOutput getProcessOutput(
@NotNull GeneralCommandLine command,
@Nullable ProcessAdapter processAdapter
) throws ExecutionException {
final Process process = command.createProcess();
myProcesses.add(process);
final ProcessOutput processOutput = PantsUtil.getOutput(process, processAdapter);
myProcesses.remove(process);
return processOutput;
}
@NotNull
private GeneralCommandLine getCommand(final File outputFile, @NotNull Consumer<String> statusConsumer)
throws IOException, ExecutionException {
final GeneralCommandLine commandLine = PantsUtil.defaultCommandLine(getProjectPath());
commandLine.addParameter("export");
commandLine.addParameter("--formatted"); // json outputs in a compact format
if (myResolveSourcesAndDocsForJars) {
commandLine.addParameter("--export-libraries-sources");
commandLine.addParameter("--export-libraries-javadocs");
}
commandLine.addParameters(getTargetSpecs());
System.out.println(getTargetSpecs());
commandLine.addParameter("--export-output-file=" + outputFile.getPath());
LOG.debug(commandLine.toString());
return commandLine;
}
@NotNull
private List<String> getTargetSpecs() {
// If project is opened via pants cli, the targets are in specs.
return Collections.unmodifiableList(getOptions().getTargetSpecs());
}
/**
* @return if successfully canceled all running processes. false if failed ot there were no processes to cancel.
*/
public boolean cancelAllProcesses() {
if (myProcesses.isEmpty()) {
return false;
}
for (Process process : myProcesses) {
process.destroy();
}
return true;
}
public String getAbsolutePathFromWorkingDir(@NotNull String relativePath) {
return new File(getBuildRoot(), relativePath).getPath();
}
private static class MyPantsCompileOptions implements PantsCompileOptions {
private final String myExternalProjectPath;
private final PantsExecutionOptions myExecutionOptions;
private MyPantsCompileOptions(@NotNull String externalProjectPath, @NotNull PantsExecutionOptions executionOptions) {
myExternalProjectPath = PantsUtil.resolveSymlinks(externalProjectPath);
myExecutionOptions = executionOptions;
}
@NotNull
@Override
public String getExternalProjectPath() {
return myExternalProjectPath;
}
@NotNull
public List<String> getTargetSpecs() {
return myExecutionOptions.getTargetSpecs();
}
public boolean isEnableIncrementalImport() {
return myExecutionOptions.isEnableIncrementalImport();
}
}
}