// Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package com.twitter.intellij.pants.util;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.CapturingProcessHandler;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.ide.SaveAndSyncHandler;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.ide.plugins.PluginManager;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.externalSystem.model.Key;
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode;
import com.intellij.openapi.externalSystem.service.internal.ExternalSystemExecuteTaskTask;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.ObjectUtils;
import com.intellij.util.PathUtil;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.execution.ParametersListUtil;
import com.twitter.intellij.pants.PantsBundle;
import com.twitter.intellij.pants.PantsException;
import com.twitter.intellij.pants.model.PantsOptions;
import com.twitter.intellij.pants.model.PantsSourceType;
import com.twitter.intellij.pants.model.PantsTargetAddress;
import com.twitter.intellij.pants.model.SimpleExportResult;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.JpsProject;
import org.jetbrains.jps.model.java.JdkVersionDetector;
import org.jetbrains.jps.model.java.JpsJavaSdkType;
import org.jetbrains.jps.model.library.JpsLibrary;
import org.jetbrains.jps.model.library.impl.sdk.JpsSdkImpl;
import org.jetbrains.jps.model.library.sdk.JpsSdkReference;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class PantsUtil {
public static final Gson gson = new Gson();
public static final Type TYPE_LIST_STRING = new TypeToken<List<String>>() {}.getType();
public static final Type TYPE_SET_STRING = new TypeToken<Set<String>>() {}.getType();
public static final Type TYPE_MAP_STRING_INTEGER = new TypeToken<Map<String, Integer>>() {}.getType();
public static final ScheduledExecutorService scheduledThreadPool = Executors.newSingleThreadScheduledExecutor(
new ThreadFactory() {
@Override
public Thread newThread(@NotNull Runnable r) {
return new Thread(r, "Pants-Plugin-Pool");
}
});
private static final Logger LOG = Logger.getInstance(PantsUtil.class);
private static final List<String> PYTHON_PLUGIN_IDS = ContainerUtil.immutableList("PythonCore", "Pythonid");
private static final String PANTS_VERSION_REGEXP = "pants_version: (.+)";
private static final String PEX_RELATIVE_PATH = ".pants.d/bin/pants.pex";
/**
* This aims to prepares for any breakage we might introduce from pants side, in which case we can adjust the version
* of Pants `idea-plugin` goal to be greater than 0.1.0.
*
* @see <a href="https://github.com/pantsbuild/pants/blob/d31ec5b4b1fb4f91e5beb685539ea14278dc62cf/src/python/pants/backend/project_info/tasks/idea_plugin_gen.py#L28">Pants `idea-plugin` goal version</a>
*/
private static final String PANTS_IDEA_PLUGIN_VERESION_MIN = "0.0.1";
private static final String PANTS_IDEA_PLUGIN_VERESION_MAX = "0.1.0";
/**
* @param vFile a virtual file pointing at either a file or a directory
* @return <code>Optional.empty()</code> if `vFile` is not a BUILD file or if it is a directory that
* does not contain one
* @deprecated {@link #findBUILDFiles(VirtualFile)} should be used instead, as this is likely
* a sign that you're missing BUILD files
*/
public static Optional<VirtualFile> findBUILDFile(@Nullable VirtualFile vFile) {
return findBUILDFiles(vFile).stream().findFirst();
}
/**
* @param vFile a virtual file pointing at either a file or a directory
* @return a collection with one item if `vFile` is a valid BUILD file, an empty collection
* if `vFile` is a file but not a valid BUILD file, or if `vFile` is a directory then all
* the valid build files that are in it.
*/
@NotNull
public static Collection<VirtualFile> findBUILDFiles(@NotNull VirtualFile vFile) {
if (vFile.isDirectory()) {
return Stream.of(vFile.getChildren()).filter(f -> isBUILDFileName(f.getName())).collect(Collectors.toList());
}
if (isBUILDFileName(vFile.getName())) {
return Collections.singleton(vFile);
}
return Collections.emptyList();
}
public static boolean isBUILDFilePath(@NotNull String path) {
return isBUILDFileName(PathUtil.getFileName(path));
}
private static boolean isBUILDFile(@NotNull VirtualFile virtualFile) {
return !virtualFile.isDirectory() && isBUILDFileName(virtualFile.getName());
}
public static boolean isBUILDFileName(@NotNull String name) {
return StringUtil.equalsIgnoreCase(PantsConstants.BUILD, FileUtil.getNameWithoutExtension(name));
}
/**
* Checks if it's a BUILD file or folder under a Pants project
*
* @param file - a BUILD file or a directory
*/
public static boolean isPantsProjectFile(VirtualFile file) {
if (file.isDirectory()) {
return findPantsExecutable(file).isPresent();
}
return isBUILDFileName(file.getName());
}
public static Optional<String> findPantsVersion(Optional<VirtualFile> workingDir) {
final Optional<VirtualFile> pantsIniFile = findPantsIniFile(workingDir);
return pantsIniFile.flatMap(PantsUtil::findVersionInFile);
}
public static Optional<VirtualFile> findPantsIniFile(Optional<VirtualFile> workingDir) {
return workingDir.map(file -> file.findChild(PantsConstants.PANTS_INI));
}
private static Optional<String> findVersionInFile(@NotNull VirtualFile file) {
try {
final String fileContent = VfsUtilCore.loadText(file);
final List<String> matches = StringUtil.findMatches(
fileContent, Pattern.compile(PANTS_VERSION_REGEXP)
);
return matches.stream().findFirst();
}
catch (IOException e) {
return Optional.empty();
}
}
public static Optional<VirtualFile> findFolderWithPex() {
return findFolderWithPex(Optional.ofNullable(VfsUtil.getUserHomeDir()));
}
public static Optional<VirtualFile> findFolderWithPex(Optional<VirtualFile> userHomeDir) {
return findFileRelativeToDirectory(PEX_RELATIVE_PATH, userHomeDir);
}
public static Optional<VirtualFile> findPexVersionFile(@NotNull VirtualFile folderWithPex, @NotNull String pantsVersion) {
final String filePrefix = "pants-" + pantsVersion;
return Optional.ofNullable(ContainerUtil.find(
folderWithPex.getChildren(), new Condition<VirtualFile>() {
@Override
public boolean value(VirtualFile virtualFile) {
return "pex".equalsIgnoreCase(virtualFile.getExtension()) && virtualFile.getName().startsWith(filePrefix);
}
}
));
}
public static Optional<File> findBuildRoot(@NotNull File file) {
return findPantsExecutable(file).map(File::getParentFile);
}
public static Optional<VirtualFile> findBuildRoot(@NotNull String filePath) {
return findPantsExecutable(filePath).map(VirtualFile::getParent);
}
public static Optional<VirtualFile> findBuildRoot(@NotNull Project project) {
for (Module module : ModuleManager.getInstance(project).getModules()) {
Optional<VirtualFile> buildRoot = findBuildRoot(module);
if (buildRoot.isPresent()) {
return buildRoot;
}
}
return Optional.empty();
}
public static Optional<VirtualFile> findBuildRoot(@NotNull PsiFile psiFile) {
final VirtualFile virtualFile = psiFile.getOriginalFile().getVirtualFile();
return virtualFile != null ? findBuildRoot(virtualFile) : findBuildRoot(psiFile.getProject());
}
public static Optional<VirtualFile> findBuildRoot(@NotNull Module module) {
final VirtualFile moduleFile = module.getModuleFile();
if (moduleFile != null) {
return findBuildRoot(moduleFile);
}
final ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
for (VirtualFile contentRoot : rootManager.getContentRoots()) {
final Optional<VirtualFile> buildRoot = findBuildRoot(contentRoot);
if (buildRoot.isPresent()) {
return buildRoot;
}
}
for (ContentEntry contentEntry : rootManager.getContentEntries()) {
VirtualFile contentEntryFile = VirtualFileManager.getInstance().refreshAndFindFileByUrl(contentEntry.getUrl());
final Optional<VirtualFile> buildRoot = findBuildRoot(contentEntryFile);
if (buildRoot.isPresent()) {
return buildRoot;
}
}
return Optional.empty();
}
public static Optional<VirtualFile> findBuildRoot(@Nullable VirtualFile file) {
return findPantsExecutable(file).map(VirtualFile::getParent);
}
public static Optional<VirtualFile> findDistExportClasspathDirectory(@NotNull Project project) {
Optional<VirtualFile> buildRoot = findBuildRoot(project);
if (!buildRoot.isPresent()) {
return Optional.empty();
}
return Optional.ofNullable(
VirtualFileManager.getInstance().refreshAndFindFileByUrl("file://" + buildRoot.get().getPath() + "/dist/export-classpath")
);
}
public static Optional<VirtualFile> findProjectManifestJar(@NotNull Project project) {
Optional<VirtualFile> classpathDir = findDistExportClasspathDirectory(project);
return classpathDir.flatMap(
file -> {
String manifestUrl = file.getUrl() + "/manifest.jar";
VirtualFile manifest = VirtualFileManager.getInstance().refreshAndFindFileByUrl(manifestUrl);
return Optional.ofNullable(manifest);
});
}
public static GeneralCommandLine defaultCommandLine(@NotNull Project project) throws PantsException {
Optional<VirtualFile> pantsExecutable = findPantsExecutable(project);
return defaultCommandLine(
pantsExecutable.orElseThrow(
() -> new PantsException("Couldn't find pants executable for: " + project.getProjectFilePath())).getPath()
);
}
public static GeneralCommandLine defaultCommandLine(@NotNull String projectPath) throws PantsException {
final Optional<File> pantsExecutable = findPantsExecutable(new File(projectPath));
return defaultCommandLine(pantsExecutable.orElseThrow(() -> new PantsException("Couldn't find pants executable for: " + projectPath)));
}
@NotNull
public static GeneralCommandLine defaultCommandLine(@NotNull File pantsExecutable) {
final GeneralCommandLine commandLine = new GeneralCommandLine();
final String pantsExecutablePath = StringUtil.notNullize(
System.getProperty("pants.executable.path"),
pantsExecutable.getAbsolutePath()
);
commandLine.setExePath(pantsExecutablePath);
final String workingDir = pantsExecutable.getParentFile().getAbsolutePath();
return commandLine.withWorkDirectory(workingDir);
}
public static Collection<String> listAllTargets(@NotNull String projectPath) throws PantsException {
GeneralCommandLine cmd = PantsUtil.defaultCommandLine(projectPath);
try (TempFile tempFile = TempFile.create("list", ".out")) {
cmd.addParameters(
"list",
Paths.get(projectPath).getParent().toString() + ':',
String.format("%s=%s", PantsConstants.PANTS_CLI_OPTION_LIST_OUTPUT_FILE,
tempFile.getFile().getPath()
)
);
final ProcessOutput processOutput = PantsUtil.getProcessOutput(cmd, null);
if (processOutput.checkSuccess(LOG)) {
String output = FileUtil.loadFile(tempFile.getFile());
return Arrays.asList(output.split("\n"));
}
else {
throw new PantsException("Failed:" + cmd.getCommandLineString());
}
}
catch (IOException | ExecutionException e) {
throw new PantsException("Failed:" + cmd.getCommandLineString());
}
}
public static String removeWhitespace(@NotNull String text) {
return text.replaceAll("\\s", "");
}
public static boolean isGeneratableFile(@NotNull String path) {
// todo(fkorotkov): make it configurable or get it from patns.
// maybe mark target as a target that generates sources and
// we need to refresh the project for any change in the corresponding module
// https://github.com/pantsbuild/intellij-pants-plugin/issues/13
return FileUtilRt.extensionEquals(path, PantsConstants.THRIFT_EXT) ||
FileUtilRt.extensionEquals(path, PantsConstants.ANTLR_EXT) ||
FileUtilRt.extensionEquals(path, PantsConstants.ANTLR_4_EXT) ||
FileUtilRt.extensionEquals(path, PantsConstants.PROTOBUF_EXT);
}
@NotNull
@Nls
public static String getCanonicalModuleName(@NotNull @NonNls String targetName) {
// Do not use ':' because it is used as a separator in a classpath
// while running the app. As well as path separators
return replaceDelimitersInTargetName(targetName, '_');
}
@NotNull
@Nls
public static String getCanonicalTargetId(@NotNull @NonNls String targetName) {
return replaceDelimitersInTargetName(targetName, '.');
}
private static String replaceDelimitersInTargetName(@NotNull @NonNls String targetName, char delimeter) {
return targetName.replace(':', delimeter).replace('/', delimeter).replace('\\', delimeter);
}
@NotNull
public static List<PantsTargetAddress> getTargetAddressesFromModule(@Nullable Module module) {
if (module == null || !isPantsModule(module)) {
return Collections.emptyList();
}
final String targets = module.getOptionValue(PantsConstants.PANTS_TARGET_ADDRESSES_KEY);
if (targets == null) {
return Collections.emptyList();
}
return ContainerUtil.mapNotNull(
hydrateTargetAddresses(targets),
PantsTargetAddress::fromString
);
}
@NotNull
public static List<String> getNonGenTargetAddresses(@Nullable Module module) {
if (module == null) {
return Collections.emptyList();
}
if (!isSourceModule(module)) {
return Collections.emptyList();
}
return getNonGenTargetAddresses(getTargetAddressesFromModule(module));
}
public static boolean isSourceModule(@NotNull Module module) {
// A source module must either contain content root(s),
// or depending on other module(s). Otherwise it can be a gen module
// or 3rdparty module placeholder.
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
return moduleRootManager.getDependencies().length > 0 ||
moduleRootManager.getContentRoots().length > 0 ||
moduleRootManager.getContentEntries().length > 0;
}
@NotNull
public static List<String> getNonGenTargetAddresses(@NotNull List<PantsTargetAddress> targets) {
return targets
.stream()
.map(PantsTargetAddress::toString)
.filter(s -> !PantsUtil.isGenTarget(s))
.collect(Collectors.toList());
}
public static boolean isPantsProject(@NotNull Project project) {
return ContainerUtil.exists(
ModuleManager.getInstance(project).getModules(),
new Condition<Module>() {
@Override
public boolean value(Module module) {
return isPantsModule(module);
}
}
);
}
/**
* Determine whether a project is trigger by Pants `idea-plugin` goal by
* looking at the "pants_idea_plugin_version" property.
*/
public static boolean isSeedPantsProject(@NotNull Project project) {
class SeedPantsProjectKeys {
private static final String PANTS_IDEA_PLUGIN_VERSION = "pants_idea_plugin_version";
}
if (isPantsProject(project)) {
return false;
}
String version = PropertiesComponent.getInstance(project).getValue(SeedPantsProjectKeys.PANTS_IDEA_PLUGIN_VERSION);
if (version == null) {
return false;
}
if (versionCompare(version, PANTS_IDEA_PLUGIN_VERESION_MIN) < 0 ||
versionCompare(version, PANTS_IDEA_PLUGIN_VERESION_MAX) > 0
) {
Messages.showInfoMessage(project, PantsBundle.message("pants.idea.plugin.goal.version.unsupported"), "Version Error");
return false;
}
return true;
}
public static boolean isPantsModule(@NotNull Module module) {
final String systemId = module.getOptionValue(ExternalSystemConstants.EXTERNAL_SYSTEM_ID_KEY);
return StringUtil.equals(systemId, PantsConstants.SYSTEM_ID.getId());
}
@NotNull
public static PantsSourceType getSourceTypeForTargetType(@Nullable String targetType) {
try {
return targetType == null ? PantsSourceType.SOURCE :
PantsSourceType.valueOf(StringUtil.toUpperCase(targetType));
}
catch (IllegalArgumentException e) {
LOG.warn("Got invalid source type " + targetType, e);
return PantsSourceType.SOURCE;
}
}
public static boolean isResource(PantsSourceType sourceType) {
return sourceType == PantsSourceType.RESOURCE || sourceType == PantsSourceType.TEST_RESOURCE;
}
public static Optional<VirtualFile> findBUILDFileForModule(@NotNull Module module) {
final Optional<VirtualFile> virtualFile =
getPathFromAddress(module, ExternalSystemConstants.LINKED_PROJECT_PATH_KEY)
.map(VfsUtil::pathToUrl)
.flatMap(s -> Optional.ofNullable(VirtualFileManager.getInstance().findFileByUrl(s)));
return virtualFile.flatMap(file -> isBUILDFile(file) ? Optional.of(file) : findBUILDFile(virtualFile.orElse(null)));
}
public static <K, V1, V2> Map<K, V2> mapValues(Map<K, V1> map, Function<V1, V2> fun) {
final Map<K, V2> result = new HashMap<K, V2>(map.size());
for (K key : map.keySet()) {
final V1 originalValue = map.get(key);
final V2 newValue = fun.fun(originalValue);
if (newValue != null) {
result.put(key, newValue);
}
}
return result;
}
public static <K, V> Map<K, V> filterByValue(Map<K, V> map, Condition<V> condition) {
final Map<K, V> result = new HashMap<K, V>(map.size());
for (Map.Entry<K, V> entry : map.entrySet()) {
final K key = entry.getKey();
final V value = entry.getValue();
if (condition.value(value)) {
result.put(key, value);
}
}
return result;
}
public static Optional<String> getRelativeProjectPath(@NotNull String projectFile) {
final Optional<File> buildRoot = findBuildRoot(new File(projectFile));
return buildRoot.flatMap(file -> getRelativeProjectPath(file, projectFile));
}
public static Optional<String> getRelativeProjectPath(@NotNull File workDirectory, @NotNull String projectPath) {
final File projectFile = new File(projectPath);
return getRelativeProjectPath(workDirectory, projectFile);
}
public static Optional<String> getRelativeProjectPath(@NotNull File workDirectory, @NotNull File projectFile) {
return Optional
.ofNullable(FileUtil.getRelativePath(workDirectory, projectFile.isDirectory() ? projectFile : projectFile.getParentFile()));
}
public static void refreshAllProjects(@NotNull Project project) {
if (!isPantsProject(project) && !isSeedPantsProject(project)) {
return;
}
ApplicationManager.getApplication().runWriteAction(() -> FileDocumentManager.getInstance().saveAllDocuments());
final ImportSpecBuilder specBuilder = new ImportSpecBuilder(project, PantsConstants.SYSTEM_ID);
ProgressExecutionMode executionMode = ApplicationManager.getApplication().isUnitTestMode() ?
ProgressExecutionMode.MODAL_SYNC : ProgressExecutionMode.IN_BACKGROUND_ASYNC;
specBuilder.use(executionMode);
ExternalSystemUtil.refreshProjects(specBuilder);
}
public static Optional<VirtualFile> findFileByAbsoluteOrRelativePath(
@NotNull String fileOrDirPath,
@NotNull Project project
) {
final VirtualFile absoluteVirtualFile = VirtualFileManager.getInstance().findFileByUrl(VfsUtil.pathToUrl(fileOrDirPath));
if (absoluteVirtualFile != null) {
return Optional.of(absoluteVirtualFile);
}
return findFileRelativeToBuildRoot(project, fileOrDirPath);
}
public static Optional<VirtualFile> findFileRelativeToBuildRoot(@NotNull Project project, @NotNull String fileOrDirPath) {
final Optional<VirtualFile> buildRoot = findBuildRoot(project);
return findFileRelativeToDirectory(fileOrDirPath, buildRoot);
}
public static Optional<VirtualFile> findFileRelativeToBuildRoot(@NotNull PsiFile psiFile, @NotNull String fileOrDirPath) {
final Optional<VirtualFile> buildRoot = findBuildRoot(psiFile);
return findFileRelativeToDirectory(fileOrDirPath, buildRoot);
}
private static Optional<VirtualFile> findFileRelativeToDirectory(@NotNull @Nls String fileOrDirPath, Optional<VirtualFile> directory) {
return directory.flatMap(file -> Optional.ofNullable(file.findFileByRelativePath(fileOrDirPath)));
}
/**
* {@code processor} should return false if we don't want to step into the directory.
*/
public static void traverseDirectoriesRecursively(@NotNull File root, @NotNull Processor<File> processor) {
final LinkedList<File> queue = new LinkedList<File>();
queue.add(root);
while (!queue.isEmpty()) {
final File file = queue.removeFirst();
if (file.isFile()) {
continue;
}
if (!processor.process(file)) {
continue;
}
final File[] children = file.listFiles();
if (children != null) {
ContainerUtil.addAll(queue, children);
}
}
}
public static Optional<String> getPathFromAddress(@NotNull Module module, @NotNull String key) {
final String address = module.getOptionValue(key);
return PantsTargetAddress.extractPath(address);
}
public static void copyDirContent(@NotNull File fromDir, @NotNull File toDir) throws IOException {
final File[] children = ObjectUtils.notNull(fromDir.listFiles(), ArrayUtil.EMPTY_FILE_ARRAY);
for (File child : children) {
final File target = new File(toDir, child.getName());
if (child.isFile()) {
FileUtil.copy(child, target);
}
else {
FileUtil.copyDir(child, target, false);
}
}
}
public static ProcessOutput getCmdOutput(
@NotNull GeneralCommandLine command,
@Nullable ProcessAdapter processAdapter
) throws ExecutionException {
return getOutput(command.createProcess(), processAdapter);
}
public static ProcessOutput getOutput(@NotNull Process process, @Nullable ProcessAdapter processAdapter) {
final CapturingProcessHandler processHandler = new CapturingProcessHandler(process, Charset.defaultCharset(), "PantsUtil command");
if (processAdapter != null) {
processHandler.addProcessListener(processAdapter);
}
return processHandler.runProcess();
}
public static boolean isPythonAvailable() {
for (String pluginId : PYTHON_PLUGIN_IDS) {
final IdeaPluginDescriptor plugin = PluginManager.getPlugin(PluginId.getId(pluginId));
if (plugin != null && plugin.isEnabled()) {
return true;
}
}
return false;
}
@Contract("null -> false")
public static boolean isExecutable(@Nullable String filePath) {
if (filePath == null) {
return false;
}
final File file = new File(filePath);
return file.exists() && file.isFile() && file.canExecute();
}
@NotNull
public static String resolveSymlinks(@NotNull String path) {
try {
return new File(path).getCanonicalPath();
}
catch (IOException e) {
throw new ExternalSystemException("Can't resolve symbolic links for " + path, e);
}
}
@NotNull
public static String fileNameWithoutExtension(@NotNull String name) {
int index = name.lastIndexOf('.');
if (index < 0) return name;
return name.substring(0, index);
}
@NotNull
public static <T> List<T> findChildren(@NotNull DataNode<?> dataNode, @NotNull Key<T> key) {
return ContainerUtil.mapNotNull(
ExternalSystemApiUtil.findAll(dataNode, key),
new Function<DataNode<T>, T>() {
@Override
public T fun(DataNode<T> node) {
return node.getData();
}
}
);
}
public static ProcessOutput getProcessOutput(
@NotNull GeneralCommandLine command,
@Nullable ProcessAdapter processAdapter
) throws ExecutionException {
return getOutput(command.createProcess(), processAdapter);
}
/**
* @param project JpsProject
* @return Path to IDEA Project JDK if exists, else null
*/
@Nullable
public static String getJdkPathFromExternalBuilder(@NotNull JpsProject project) {
JpsSdkReference sdkReference = project.getSdkReferencesTable().getSdkReference(JpsJavaSdkType.INSTANCE);
if (sdkReference != null) {
String sdkName = sdkReference.getSdkName();
JpsLibrary lib = project.getModel().getGlobal().getLibraryCollection().findLibrary(sdkName);
if (lib != null && lib.getProperties() instanceof JpsSdkImpl) {
return ((JpsSdkImpl) lib.getProperties()).getHomePath();
}
}
return null;
}
/**
* @return Path to IDEA Project JDK if exists, else null
*/
@Nullable
public static String getJdkPathFromIntelliJCore() {
// Followed example in com.twitter.intellij.pants.testFramework.PantsIntegrationTestCase.setUpInWriteAction()
final Sdk sdk = JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk();
String javaHome = null;
if (sdk.getHomeDirectory() != null) {
javaHome = sdk.getHomeDirectory().getParent().getPath();
}
return javaHome;
}
/**
* @param jdkPath path to IDEA Project JDK
* @return --jvm-distributions-paths with the parameter if jdkPath is not null,
* otherwise the flag with empty parameter so user can tell there is issue finding the IDEA project JDK.
*/
@NotNull
public static String getJvmDistributionPathParameter(@Nullable final String jdkPath) throws Exception {
if (jdkPath != null) {
HashMap<String, List<String>> distributionFlag = new HashMap<String, List<String>>();
distributionFlag.put(System.getProperty("os.name").toLowerCase(), Collections.singletonList(jdkPath));
return PantsConstants.PANTS_CLI_OPTION_JVM_DISTRIBUTIONS_PATHS + "=" + new Gson().toJson(distributionFlag);
}
else {
throw new Exception("No IDEA Project JDK Found");
}
}
@NotNull
public static Set<String> hydrateTargetAddresses(@NotNull String addresses) {
return gson.fromJson(addresses, TYPE_SET_STRING);
}
@NotNull
public static String dehydrateTargetAddresses(@NotNull Set<String> addresses) {
return gson.toJson(addresses);
}
public static boolean isGenTarget(@NotNull String address) {
return StringUtil.startsWithIgnoreCase(address, ".pants.d") ||
StringUtil.startsWithIgnoreCase(address, PantsConstants.PANTS_PROJECT_MODULE_ID_PREFIX) ||
// Checking "_synthetic_resources" is a temporary fix. It also needs to match the postfix added from pants in
// src.python.pants.backend.python.targets.python_target.PythonTarget#_synthetic_resources_target
// TODO: The long term solution is collect non-synthetic targets at pre-compile stage
// https://github.com/pantsbuild/intellij-pants-plugin/issues/83
address.toLowerCase().endsWith("_synthetic_resources");
}
public static Set<String> filterGenTargets(@NotNull Collection<String> addresses) {
return addresses.stream().filter(s -> !isGenTarget(s)).collect(Collectors.toSet());
}
@Nullable
public static Optional<Sdk> getDefaultJavaSdk(@NotNull final String pantsExecutable) {
// If a JDK belongs to this particular `pantsExecutable`, then its name will contain the path to Pants.
Optional<Sdk> sdkForPants = Arrays.stream(ProjectJdkTable.getInstance().getAllJdks())
.filter(sdk -> sdk.getName().contains(pantsExecutable) && sdk.getSdkType() instanceof JavaSdk)
.findFirst();
if (sdkForPants.isPresent()) {
return sdkForPants;
}
final SimpleExportResult exportResult = SimpleExportResult.getExportResult(pantsExecutable);
if (versionCompare(exportResult.getVersion(), "1.0.7") < 0) {
return Optional.empty();
}
boolean strict = PantsOptions.getPantsOptions(pantsExecutable)
.get(PantsConstants.PANTS_OPTION_TEST_JUNIT_STRICT_JVM_VERSION)
.isPresent();
Optional<String> jdkHome = exportResult.getJdkHome(strict);
if (!jdkHome.isPresent()) {
return Optional.empty();
}
String jdkName = String.format("1.x_from_%s", pantsExecutable);
JdkVersionDetector.JdkVersionInfo jdkInfo = JdkVersionDetector.getInstance().detectJdkVersionInfo(jdkHome.get());
if (jdkInfo != null) {
// Using IJ's framework to detect jdk version. so jdkInfo.getVersion() returns `java version "1.8.0_121"`
for (String version : ContainerUtil.newArrayList("1.6", "1.7", "1.8", "1.9")) {
if (jdkInfo.getVersion().contains(version)) {
jdkName = String.format("%s_from_%s", version, pantsExecutable);
break;
}
}
}
// Finally if we need to create a new JDK, it needs to be registered in the `ProjectJdkTable` on the IDE level
// before it can be used.
Sdk jdk = JavaSdk.getInstance().createJdk(jdkName, jdkHome.get());
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(() -> ProjectJdkTable.getInstance().addJdk(jdk));
}
});
return Optional.of(jdk);
}
/**
* Copied from: http://stackoverflow.com/questions/6701948/efficient-way-to-compare-version-strings-in-java
* Compares two version strings.
* <p/>
* Use this instead of String.compareTo() for a non-lexicographical
* comparison that works for version strings. e.g. "1.10".compareTo("1.6").
*
* @param str1 a string of ordinal numbers separated by decimal points.
* @param str2 a string of ordinal numbers separated by decimal points.
* @return The result is a negative integer if str1 is _numerically_ less than str2.
* The result is a positive integer if str1 is _numerically_ greater than str2.
* The result is zero if the strings are _numerically_ equal.
* @note It does not work if "1.10" is supposed to be equal to "1.10.0".
*/
public static Integer versionCompare(String str1, String str2) {
String[] vals1 = str1.split("\\.");
String[] vals2 = str2.split("\\.");
int i = 0;
// set index to first non-equal ordinal or length of shortest version string
while (i < vals1.length && i < vals2.length && vals1[i].equals(vals2[i])) {
i++;
}
// compare first non-equal ordinal number
if (i < vals1.length && i < vals2.length) {
int diff = Integer.valueOf(vals1[i]).compareTo(Integer.valueOf(vals2[i]));
return Integer.signum(diff);
}
// the strings are equal or one string is a substring of the other
// e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4"
else {
return Integer.signum(vals1.length - vals2.length);
}
}
/**
* Reliable way to find pants executable by a project once it is imported.
* Use project's module in project to find the `buildRoot`,
* then use `buildRoot` to find pantsExecutable.
*/
public static Optional<VirtualFile> findPantsExecutable(@NotNull Project project) {
Module[] modules = ModuleManager.getInstance(project).getModules();
if (modules.length == 0) {
throw new PantsException("No module found in project.");
}
for (Module module : modules) {
Optional<VirtualFile> buildRoot = findBuildRoot(module);
if (buildRoot.isPresent()) {
return findPantsExecutable(buildRoot.get());
}
}
return Optional.empty();
}
public static Optional<VirtualFile> findPantsExecutable(@NotNull String projectPath) {
final VirtualFile buildFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(projectPath);
return findPantsExecutable(buildFile);
}
public static Optional<File> findPantsExecutable(@NotNull File file) {
Optional<VirtualFile> vf = findPantsExecutable(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file));
if (!vf.isPresent()) {
return Optional.empty();
}
return Optional.of(new File(vf.get().getPath()));
}
private static Optional<VirtualFile> findPantsExecutable(@Nullable VirtualFile file) {
if (file == null) return Optional.empty();
if (file.isDirectory()) {
final VirtualFile pantsFile = file.findChild(PantsConstants.PANTS);
if (pantsFile != null && !pantsFile.isDirectory()) {
return Optional.of(pantsFile);
}
}
return findPantsExecutable(file.getParent());
}
public static List<String> convertToTargetSpecs(String importPath, List<String> targetNames) {
File importPathFile = new File(importPath);
final String projectDir =
isBUILDFileName(importPathFile.getName()) ? importPathFile.getParent() : importPathFile.getPath();
final Optional<String> relativeProjectDir = getRelativeProjectPath(projectDir);
// If relativeProjectDir is null, that means the projectDir is already relative.
String relativePath = relativeProjectDir.orElse(projectDir);
if (targetNames.isEmpty()) {
return Collections.singletonList(relativePath + "::");
}
else {
return targetNames.stream().map(targetName -> relativePath + ":" + targetName).collect(Collectors.toList());
}
}
public static void synchronizeFiles() {
/**
* Run in SYNC in unit test mode, and {@link com.twitter.intellij.pants.testFramework.PantsIntegrationTestCase.doImport}
* is required to be wrapped in WriteAction. Otherwise it will run in async mode.
*/
if (ApplicationManager.getApplication().isUnitTestMode() && ApplicationManager.getApplication().isWriteAccessAllowed()) {
ApplicationManager.getApplication().runWriteAction(() -> {
FileDocumentManager.getInstance().saveAllDocuments();
SaveAndSyncHandler.getInstance().refreshOpenFiles();
VirtualFileManager.getInstance().refreshWithoutFileWatcher(false); /** synchronous */
});
}
else {
ApplicationManager.getApplication().invokeLater(() -> {
FileDocumentManager.getInstance().saveAllDocuments();
SaveAndSyncHandler.getInstance().refreshOpenFiles();
VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); /** asynchronous */
});
}
}
public static void invalidatePluginCaches() {
PantsOptions.clearCache();
SimpleExportResult.clearCache();
}
/**
* Copy from {@link ExternalSystemExecuteTaskTask#parseCmdParameters} because it is private.
*/
public static List<String> parseCmdParameters(Optional<String> cmdArgsLine) {
return cmdArgsLine.map(ParametersListUtil::parse).orElse(ContainerUtil.newArrayList());
}
}