/*
* Copyright 2012-2014 Sergey Ignatov
*
* 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 org.intellij.erlang.sdk;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.*;
import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl;
import com.intellij.openapi.roots.JavadocOrderRootType;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiElement;
import com.intellij.util.containers.WeakHashMap;
import org.intellij.erlang.icons.ErlangIcons;
import org.intellij.erlang.jps.model.JpsErlangModelSerializerExtension;
import org.intellij.erlang.jps.model.JpsErlangSdkType;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
public class ErlangSdkType extends SdkType {
private static final String OTP_RELEASE_PREFIX_LINE = "ErlangSdkType_OTP_RELEASE:";
private static final String ERTS_VERSION_PREFIX_LINE = "ErlangSdkType_ERTS_VERSION:";
private static final String PRINT_VERSION_INFO_EXPRESSION =
"io:format(\"~n~s~n~s~n~s~n~s~n\",[" +
"\"" + OTP_RELEASE_PREFIX_LINE + "\"," +
"erlang:system_info(otp_release)," +
"\"" + ERTS_VERSION_PREFIX_LINE + "\"," +
"erlang:system_info(version)" +
"]),erlang:halt().";
private static final Logger LOG = Logger.getInstance(ErlangSdkType.class);
private final Map<String, ErlangSdkRelease> mySdkHomeToReleaseCache = ApplicationManager.getApplication().isUnitTestMode() ?
new HashMap<>() : new WeakHashMap<>();
@NotNull
public static ErlangSdkType getInstance() {
return SdkType.findInstance(ErlangSdkType.class);
}
private ErlangSdkType() {
super(JpsErlangModelSerializerExtension.ERLANG_SDK_TYPE_ID);
}
@NotNull
@Override
public Icon getIcon() {
return ErlangIcons.FILE;
}
@NotNull
@Override
public Icon getIconForAddAction() {
return getIcon();
}
@Nullable
@Override
public String suggestHomePath() {
if (SystemInfo.isWindows) {
return "C:\\cygwin\\bin";
}
else if (SystemInfo.isMac) {
String macPorts = "/opt/local/lib/erlang";
if (new File(macPorts).exists()) return macPorts;
// For home brew we trying to find something like /usr/local/Cellar/erlang/*/lib/erlang as SDK root
for (String version : new String[]{"", "-r14", "-r15", "-r16"}) {
File brewRoot = new File("/usr/local/Cellar/erlang" + version);
if (brewRoot.exists()) {
final Ref<String> ref = Ref.create();
FileUtil.processFilesRecursively(brewRoot, file -> {
if (!ref.isNull()) return false;
if (!file.isDirectory()) return true;
if ("erlang".equals(file.getName()) && file.getParent().endsWith("lib")) {
ref.set(file.getAbsolutePath());
return false;
}
return true;
});
if (!ref.isNull()) return ref.get();
}
}
return null;
}
else if (SystemInfo.isLinux) {
return "/usr/lib/erlang";
}
return null;
}
@Override
public boolean isValidSdkHome(@NotNull String path) {
File erl = JpsErlangSdkType.getByteCodeInterpreterExecutable(path);
File erlc = JpsErlangSdkType.getByteCodeCompilerExecutable(path);
return erl.canExecute() && erlc.canExecute();
}
@NotNull
@Override
public String suggestSdkName(@Nullable String currentSdkName, @NotNull String sdkHome) {
return getDefaultSdkName(sdkHome, detectSdkVersion(sdkHome));
}
@Nullable
@Override
public String getVersionString(@NotNull String sdkHome) {
return getVersionString(detectSdkVersion(sdkHome));
}
@Nullable
@Override
public String getDefaultDocumentationUrl(@NotNull Sdk sdk) {
return getDefaultDocumentationUrl(getRelease(sdk));
}
@Nullable
@Override
public AdditionalDataConfigurable createAdditionalDataConfigurable(@NotNull SdkModel sdkModel, @NotNull SdkModificator sdkModificator) {
return null;
}
@Override
public void saveAdditionalData(@NotNull SdkAdditionalData additionalData, @NotNull Element additional) {
}
@NotNull
@NonNls
@Override
public String getPresentableName() {
return "Erlang SDK";
}
@Override
public void setupSdkPaths(@NotNull Sdk sdk) {
configureSdkPaths(sdk);
}
@Nullable
public static String getSdkPath(@NotNull final Project project) {
if (ErlangSystemUtil.isSmallIde()) {
return ErlangSdkForSmallIdes.getSdkHome(project);
}
Sdk sdk = ProjectRootManager.getInstance(project).getProjectSdk();
return sdk != null && sdk.getSdkType() == getInstance() ? sdk.getHomePath() : null;
}
@Nullable
public static ErlangSdkRelease getRelease(@NotNull PsiElement element) {
if (ErlangSystemUtil.isSmallIde()) {
return getReleaseForSmallIde(element.getProject());
}
Module module = ModuleUtilCore.findModuleForPsiElement(element);
ErlangSdkRelease byModuleSdk = getRelease(module != null ? ModuleRootManager.getInstance(module).getSdk() : null);
return byModuleSdk != null ? byModuleSdk : getRelease(element.getProject());
}
@Nullable
public static ErlangSdkRelease getRelease(@NotNull Project project) {
if (ErlangSystemUtil.isSmallIde()) {
return getReleaseForSmallIde(project);
}
return getRelease(ProjectRootManager.getInstance(project).getProjectSdk());
}
@TestOnly
@NotNull
public static Sdk createMockSdk(@NotNull String sdkHome, @NotNull ErlangSdkRelease version) {
getInstance().mySdkHomeToReleaseCache.put(getVersionCacheKey(sdkHome), version); // we'll not try to detect sdk version in tests environment
Sdk sdk = new ProjectJdkImpl(getDefaultSdkName(sdkHome, version), getInstance());
SdkModificator sdkModificator = sdk.getSdkModificator();
sdkModificator.setHomePath(sdkHome);
sdkModificator.setVersionString(getVersionString(version)); // must be set after home path, otherwise setting home path clears the version string
sdkModificator.commitChanges();
configureSdkPaths(sdk);
return sdk;
}
@Nullable
private ErlangSdkRelease detectSdkVersion(@NotNull String sdkHome) {
ErlangSdkRelease cachedRelease = mySdkHomeToReleaseCache.get(getVersionCacheKey(sdkHome));
if (cachedRelease != null) {
return ensureReleaseDetected(cachedRelease);
}
File erl = JpsErlangSdkType.getByteCodeInterpreterExecutable(sdkHome);
if (!erl.canExecute()) {
String reason = erl.getPath() + (erl.exists() ? " is not executable." : " is missing.");
LOG.warn("Can't detect Erlang version: " + reason);
return ensureReleaseDetected(null);
}
try {
ProcessOutput output = ErlangSystemUtil.getProcessOutput(sdkHome, erl.getAbsolutePath(), "-noshell",
"-eval", PRINT_VERSION_INFO_EXPRESSION);
ErlangSdkRelease release = output.getExitCode() != 0 || output.isCancelled() || output.isTimeout() ? null :
parseSdkVersion(output.getStdoutLines());
if (release != null) {
mySdkHomeToReleaseCache.put(getVersionCacheKey(sdkHome), release);
}
else {
LOG.warn("Failed to detect Erlang version.\n" +
"StdOut: " + output.getStdout() + "\n" +
"StdErr: " + output.getStderr());
}
return ensureReleaseDetected(release);
} catch (ExecutionException e) {
LOG.warn(e);
}
return ensureReleaseDetected(null);
}
@Nullable
private static ErlangSdkRelease ensureReleaseDetected(@Nullable ErlangSdkRelease release) {
if (ApplicationManager.getApplication().isUnitTestMode() && release == null) {
throw new AssertionError("SDK version detection failed. If you're using a mock SDK, make sure you have your SDK version pre-cached");
}
return release;
}
@Nullable
private static ErlangSdkRelease parseSdkVersion(@NotNull List<String> printVersionInfoOutput) {
String otpRelease = null;
String ertsVersion = null;
ListIterator<String> iterator = printVersionInfoOutput.listIterator();
while (iterator.hasNext()) {
String line = iterator.next();
if (OTP_RELEASE_PREFIX_LINE.equals(line) && iterator.hasNext()) {
otpRelease = iterator.next();
}
else if (ERTS_VERSION_PREFIX_LINE.equals(line) && iterator.hasNext()) {
ertsVersion = iterator.next();
}
}
return otpRelease != null && ertsVersion != null ? new ErlangSdkRelease(otpRelease, ertsVersion) : null;
}
private static void configureSdkPaths(@NotNull Sdk sdk) {
SdkModificator sdkModificator = sdk.getSdkModificator();
setupLocalSdkPaths(sdkModificator);
String externalDocUrl = getDefaultDocumentationUrl(getRelease(sdk));
if (externalDocUrl != null) {
VirtualFile fileByUrl = VirtualFileManager.getInstance().findFileByUrl(externalDocUrl);
sdkModificator.addRoot(fileByUrl, JavadocOrderRootType.getInstance());
}
sdkModificator.commitChanges();
}
@Nullable
private static String getDefaultDocumentationUrl(@Nullable ErlangSdkRelease version) {
return version == null ? null : "http://www.erlang.org/documentation/doc-" + version.getErtsVersion();
}
private static void setupLocalSdkPaths(@NotNull SdkModificator sdkModificator) {
String sdkHome = sdkModificator.getHomePath();
{
File stdLibDir = new File(new File(sdkHome), "lib");
if (tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir)) return;
}
assert !ApplicationManager.getApplication().isUnitTestMode() : "Failed to setup a mock SDK!";
try {
String exePath = JpsErlangSdkType.getByteCodeCompilerExecutable(sdkHome).getAbsolutePath();
ProcessOutput processOutput = ErlangSystemUtil.getProcessOutput(sdkHome, exePath, "-where");
if (processOutput.getExitCode() == 0) {
String stdout = processOutput.getStdout().trim();
if (!stdout.isEmpty()) {
if (SystemInfo.isWindows && stdout.startsWith("/")) {
for (File root : File.listRoots()) {
File stdLibDir = new File(root, stdout);
if (tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir)) return;
}
}
else {
File stdLibDir = new File(stdout);
if (tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir)) return;
}
}
}
} catch (ExecutionException ignore) {
}
File stdLibDir = new File("/usr/lib/erlang");
tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir);
}
private static boolean tryToProcessAsStandardLibraryDir(@NotNull SdkModificator sdkModificator, @NotNull File stdLibDir) {
if (!isStandardLibraryDir(stdLibDir)) return false;
VirtualFile dir = LocalFileSystem.getInstance().findFileByIoFile(stdLibDir);
if (dir != null) {
sdkModificator.addRoot(dir, OrderRootType.SOURCES);
sdkModificator.addRoot(dir, OrderRootType.CLASSES);
}
return true;
}
private static boolean isStandardLibraryDir(@NotNull File dir) {
return dir.isDirectory();
}
@NotNull
private static String getDefaultSdkName(@NotNull String sdkHome, @Nullable ErlangSdkRelease version) {
return version != null ? "Erlang " + version.getOtpRelease() : "Unknown Erlang version at " + sdkHome;
}
@Nullable
private static String getVersionString(@Nullable ErlangSdkRelease version) {
return version != null ? version.toString() : null;
}
@Nullable
private static ErlangSdkRelease getRelease(@Nullable Sdk sdk) {
if (sdk != null && sdk.getSdkType() == getInstance()) {
ErlangSdkRelease fromVersionString = ErlangSdkRelease.fromString(sdk.getVersionString());
return fromVersionString != null ? fromVersionString :
getInstance().detectSdkVersion(StringUtil.notNullize(sdk.getHomePath()));
}
return null;
}
@Nullable
private static ErlangSdkRelease getReleaseForSmallIde(@NotNull Project project) {
String sdkPath = getSdkPath(project);
return StringUtil.isEmpty(sdkPath) ? null : getInstance().detectSdkVersion(sdkPath);
}
@Nullable
private static String getVersionCacheKey(@Nullable String sdkHome) {
return sdkHome != null ? new File(sdkHome).getAbsolutePath() : null;
}
}