/*
* Copyright (C) 2015 RoboVM AB
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
*/
package org.robovm.idea;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;
import org.robovm.compiler.Version;
import org.robovm.compiler.config.Arch;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.config.Resource;
import org.robovm.compiler.log.Logger;
import org.robovm.compiler.util.InfoPList;
import org.robovm.idea.compilation.RoboVmCompileTask;
import org.robovm.idea.config.RoboVmGlobalConfig;
import org.robovm.idea.interfacebuilder.RoboVmFileEditorManagerListener;
import org.robovm.idea.sdk.RoboVmSdkType;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.compiler.CompileScope;
import com.intellij.openapi.compiler.CompileTask;
import com.intellij.openapi.compiler.CompilerManager;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.OrderEnumerator;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowAnchor;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.util.ui.UIUtil;
/**
* Provides util for the other components of the plugin such
* as logging.
*/
public class RoboVmPlugin {
public enum OS {
MacOsX,
Windows,
Linux
}
static {
if(System.getProperty("os.name").contains("Mac")) {
os = OS.MacOsX;
} else if(System.getProperty("os.name").contains("Windows")) {
os = OS.Windows;
} else if(System.getProperty("os.name").contains("Linux")) {
os = OS.Linux;
}
}
private static final String ROBOVM_TOOLWINDOW_ID = "RoboVM";
private static OS os;
static volatile Map<Project, ConsoleView> consoleViews = new ConcurrentHashMap<>();
static volatile Map<Project, ToolWindow> toolWindows = new ConcurrentHashMap<>();
static volatile Map<Project, VirtualFileListener> fileListeners = new ConcurrentHashMap<>();
static final List<UnprintedMessage> unprintedMessages = new ArrayList<UnprintedMessage>();
public static OS getOs() {
return os;
}
public static String getBestAndroidSdkVersion() {
int androidSdkVersion = 0;
File androidSdkDir = getBestAndroidSdkDir();
if(androidSdkDir == null) {
return "23";
}
File platformsDir = new File(androidSdkDir, "platforms");
for(File file: platformsDir.listFiles()) {
String[] tokens = file.getName().split("-");
if(tokens.length == 2) {
try {
int version = Integer.parseInt(tokens[1]);
if(version > androidSdkVersion) {
androidSdkVersion = version;
}
} catch(NumberFormatException e) {
// nothing we can do
}
}
}
if(androidSdkVersion == 0) {
return "23";
} else {
return Integer.toString(androidSdkVersion);
}
}
public static String getBestAndroidBuildToolsVersion() {
int androidBuildToolsVersion = 0;
String androidBuildToolsVersionString = "";
File androidSdkDir = getBestAndroidSdkDir();
if(androidSdkDir == null) {
return "23.0.1";
}
File platformsDir = new File(androidSdkDir, "build-tools");
for(File file: platformsDir.listFiles()) {
String[] tokens = file.getName().split("\\.");
if(tokens.length == 3) {
try {
int version = Integer.parseInt(tokens[0]) * 1000 * 1000 +
Integer.parseInt(tokens[1]) * 1000 +
Integer.parseInt(tokens[2]);
if(version > androidBuildToolsVersion) {
androidBuildToolsVersion = version;
androidBuildToolsVersionString = file.getName();
}
} catch(NumberFormatException e) {
// nothing we can do
}
}
}
if(androidBuildToolsVersion == 0) {
return "23.0.1";
} else {
return androidBuildToolsVersionString;
}
}
public static File getBestAndroidSdkDir() {
Sdk bestSdk = null;
for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
if (sdk.getSdkType().getName().equals("Android SDK")) {
if(sdk.getHomePath().contains("/Library/RoboVM/")) {
return new File(sdk.getHomePath());
} else {
bestSdk = sdk;
}
}
}
return new File(bestSdk.getHomePath());
}
public static boolean isAndroidSdkInstalled(String sdkDir) {
File sdk = new File(sdkDir, os == OS.Windows? "tools/android.bat": "tools/android");
return sdk.exists();
}
public static boolean isAndroidSdkSetup() {
for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
if (sdk.getSdkType().getName().equals("Android SDK")) {
return true;
}
}
return false;
}
public static boolean areAndroidComponentsInstalled(String sdkDir) {
return new File(sdkDir, "platforms").list().length > 0;
}
static class UnprintedMessage {
final String string;
final ConsoleViewContentType type;
public UnprintedMessage(String string, ConsoleViewContentType type) {
this.string = string;
this.type = type;
}
}
public static void logBalloon(final Project project, final MessageType messageType, final String message) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
if (project != null) {
// this may throw an exception, see #88. It appears to be a timing
// issue
try {
ToolWindowManager.getInstance(project).notifyByBalloon(ROBOVM_TOOLWINDOW_ID, MessageType.ERROR, message);
} catch (Throwable t) {
logError(project, message, t);
}
}
}
});
}
public static void logInfo(Project project, String format, Object... args) {
log(project, ConsoleViewContentType.SYSTEM_OUTPUT, "[INFO] " + format, args);
}
public static void logError(Project project, String format, Object... args) {
log(project, ConsoleViewContentType.ERROR_OUTPUT, "[ERROR] " + format, args);
}
public static void logErrorThrowable(Project project, String s, Throwable t, boolean showBalloon) {
StringWriter stringWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stringWriter);
t.printStackTrace(writer);
log(project, ConsoleViewContentType.ERROR_OUTPUT, "[ERROR] %s\n%s", s, stringWriter.toString());
logBalloon(project, MessageType.ERROR, s);
}
public static void logWarn(Project project, String format, Object... args) {
log(project, ConsoleViewContentType.ERROR_OUTPUT, "[WARNING] " + format, args);
}
public static void logDebug(Project project, String format, Object... args) {
log(project, ConsoleViewContentType.NORMAL_OUTPUT, "[DEBUG] " + format, args);
}
private static void log(final Project project, final ConsoleViewContentType type, String format, Object... args) {
final String s = String.format(format, args) + "\n";
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
ConsoleView consoleView = project == null ? null : consoleViews.get(project);
if (consoleView != null) {
for (UnprintedMessage unprinted : unprintedMessages) {
consoleView.print(unprinted.string, unprinted.type);
}
unprintedMessages.clear();
consoleView.print(s, type);
} else {
unprintedMessages.add(new UnprintedMessage(s, type));
if (type == ConsoleViewContentType.ERROR_OUTPUT) {
System.err.print(s);
} else {
System.out.print(s);
}
}
}
});
}
public static Logger getLogger(final Project project) {
return new Logger() {
@Override
public void debug(String s, Object... objects) {
logDebug(project, s, objects);
}
@Override
public void info(String s, Object... objects) {
logInfo(project, s, objects);
}
@Override
public void warn(String s, Object... objects) {
logWarn(project, s, objects);
}
@Override
public void error(String s, Object... objects) {
logError(project, s, objects);
}
};
}
public static void initializeProject(final Project project) {
// setup a compile task if there isn't one yet
boolean found = false;
for (CompileTask task : CompilerManager.getInstance(project).getAfterTasks()) {
if (task instanceof RoboVmCompileTask) {
found = true;
break;
}
}
if (!found) {
CompilerManager.getInstance(project).addAfterTask(new RoboVmCompileTask());
}
// hook ito the message bus so we get to know if a storyboard/xib
// file is opened
project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new RoboVmFileEditorManagerListener(project));
// initialize our tool window to which we
// log all messages
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
if (project.isDisposed()) {
return;
}
ToolWindow toolWindow = ToolWindowManager.getInstance(project).registerToolWindow(ROBOVM_TOOLWINDOW_ID, false, ToolWindowAnchor.BOTTOM, project, true);
ConsoleView consoleView = TextConsoleBuilderFactory.getInstance().createBuilder(project).getConsole();
Content content = toolWindow.getContentManager().getFactory().createContent(consoleView.getComponent(), "Console", true);
toolWindow.getContentManager().addContent(content);
toolWindow.setIcon(RoboVmIcons.ROBOVM_SMALL);
consoleViews.put(project, consoleView);
toolWindows.put(project, toolWindow);
logInfo(project, "RoboVM plugin initialized");
}
});
// initialize virtual file change listener so we can
// trigger recompiles on file saves
VirtualFileListener listener = new VirtualFileAdapter() {
@Override
public void contentsChanged(@NotNull VirtualFileEvent event) {
compileIfChanged(event, project);
}
};
VirtualFileManager.getInstance().addVirtualFileListener(listener);
fileListeners.put(project, listener);
}
private static void compileIfChanged(VirtualFileEvent event, final Project project) {
if(!RoboVmGlobalConfig.isCompileOnSave()) {
return;
}
VirtualFile file = event.getFile();
Module module = null;
for(Module m: ModuleManager.getInstance(project).getModules()) {
if(ModuleRootManager.getInstance(m).getFileIndex().isInContent(file)) {
module = m;
break;
}
}
if(module != null) {
if(isRoboVmModule(module)) {
final Module foundModule = module;
OrderEntry orderEntry = ModuleRootManager.getInstance(module).getFileIndex().getOrderEntryForFile(file);
if(orderEntry != null && orderEntry.getFiles(OrderRootType.SOURCES).length != 0) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
if(!CompilerManager.getInstance(project).isCompilationActive()) {
CompileScope scope = CompilerManager.getInstance(project).createModuleCompileScope(foundModule, true);
CompilerManager.getInstance(project).compile(scope, null);
}
}
});
}
}
}
}
public static void unregisterProject(Project project) {
ConsoleView consoleView = consoleViews.remove(project);
if (consoleView != null) {
consoleView.dispose();
}
toolWindows.remove(project);
ToolWindowManager.getInstance(project).unregisterToolWindow(ROBOVM_TOOLWINDOW_ID);
VirtualFileManager.getInstance().removeVirtualFileListener(fileListeners.remove(project));
}
public static void extractSdk() {
File sdkHome = getSdkHomeBase();
if (!sdkHome.exists()) {
if (!sdkHome.mkdirs()) {
logError(null, "Couldn't create sdk dir in %s", sdkHome.getAbsolutePath());
throw new RuntimeException("Couldn't create sdk dir in " + sdkHome.getAbsolutePath());
}
}
extractArchive("robovm-dist", sdkHome);
// create an SDK if it doesn't exist yet
RoboVmSdkType.createSdkIfNotExists();
}
public static File getSdkHome() {
File sdkHome = new File(getSdkHomeBase(), "robovm-" + Version.getVersion());
return sdkHome;
}
public static File getSdkHomeBase() {
return new File(System.getProperty("user.home"), ".robovm-sdks");
}
public static Sdk getSdk() {
RoboVmSdkType sdkType = new RoboVmSdkType();
for(Sdk sdk: ProjectJdkTable.getInstance().getAllJdks()) {
if(sdkType.suggestSdkName(null, null).equals(sdk.getName())) {
return sdk;
}
}
return null;
}
private static void extractArchive(String archive, File dest) {
archive = "/" + archive;
TarArchiveInputStream in = null;
boolean isSnapshot = Version.getVersion().toLowerCase().contains("snapshot");
try {
in = new TarArchiveInputStream(new GZIPInputStream(RoboVmPlugin.class.getResourceAsStream(archive)));
ArchiveEntry entry = null;
while ((entry = in.getNextEntry()) != null) {
File f = new File(dest, entry.getName());
if (entry.isDirectory()) {
f.mkdirs();
} else {
if(!isSnapshot && f.exists()) {
continue;
}
f.getParentFile().mkdirs();
OutputStream out = null;
try {
out = new FileOutputStream(f);
IOUtils.copy(in, out);
} finally {
IOUtils.closeQuietly(out);
}
}
}
logInfo(null, "Installed RoboVM SDK %s to %s", Version.getVersion(), dest.getAbsolutePath());
// make all files in bin executable
for (File file : new File(getSdkHome(), "bin").listFiles()) {
file.setExecutable(true);
}
} catch (Throwable t) {
logError(null, "Couldn't extract SDK to %s", dest.getAbsolutePath());
throw new RuntimeException("Couldn't extract SDK to " + dest.getAbsolutePath(), t);
} finally {
IOUtils.closeQuietly(in);
}
}
/**
* @return all sdk runtime libraries and their source jars
*/
public static List<File> getSdkLibraries() {
List<File> libs = new ArrayList<File>();
File libsDir = new File(getSdkHome(), "lib");
for (File file : libsDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
})) {
libs.add(file);
}
return libs;
}
/**
* @return the source jars of all runtime libraries
*/
public static List<File> getSdkLibrariesWithoutSources() {
List<File> libs = getSdkLibraries();
Iterator<File> iter = libs.iterator();
while(iter.hasNext()) {
File file = iter.next();
if(file.getName().endsWith("-sources.jar")) {
iter.remove();
}
}
return libs;
}
/**
* @return the source jars of all runtime libraries
*/
public static List<File> getSdkLibrarySources() {
List<File> libs = getSdkLibraries();
Iterator<File> iter = libs.iterator();
while(iter.hasNext()) {
File file = iter.next();
if(!file.getName().endsWith("-sources.jar")) {
iter.remove();
}
}
return libs;
}
public static Config.Home getRoboVmHome() {
try {
return Config.Home.find();
} catch(Throwable t) {
return new Config.Home(getSdkHome());
}
}
public static List<Module> getRoboVmModules(Project project) {
List<Module> validModules = new ArrayList<Module>();
for (Module module : ModuleManager.getInstance(project).getModules()) {
if (isRoboVmModule(module)) {
validModules.add(module);
}
}
return validModules;
}
public static boolean isRoboVmModule(Module module) {
// HACK! to identify if the module uses a robovm sdk
if (ModuleRootManager.getInstance(module).getSdk() != null) {
if (ModuleRootManager.getInstance(module).getSdk().getSdkType().getName().toLowerCase().contains("robovm")) {
return true;
}
}
// check if there's any RoboVM RT libs in the classpath
OrderEnumerator classes = ModuleRootManager.getInstance(module).orderEntries().recursively().withoutSdk().compileOnly();
for (String path : classes.getPathsList().getPathList()) {
if (isSdkLibrary(path)) {
return true;
}
}
// check if there's a robovm.xml file in the root of the module
for(VirtualFile file: ModuleRootManager.getInstance(module).getContentRoots()) {
if(file.findChild("robovm.xml") != null) {
return true;
}
}
return false;
}
public static void focusToolWindow(final Project project) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
ToolWindow toolWindow = toolWindows.get(project);
if(toolWindow != null) {
toolWindow.show(new Runnable() {
@Override
public void run() {
}
});
}
}
});
}
public static File getModuleLogDir(Module module) {
File logDir = new File(getModuleBaseDir(module), "robovm-build/logs/");
if (!logDir.exists()) {
if (!logDir.mkdirs()) {
throw new RuntimeException("Couldn't create log dir '" + logDir.getAbsolutePath() + "'");
}
}
return logDir;
}
public static File getModuleXcodeDir(Module module) {
File buildDir = new File(getModuleBaseDir(module), "robovm-build/xcode/");
if (!buildDir.exists()) {
if (!buildDir.mkdirs()) {
throw new RuntimeException("Couldn't create build dir '" + buildDir.getAbsolutePath() + "'");
}
}
return buildDir;
}
public static File getModuleBuildDir(Module module, String runConfigName, org.robovm.compiler.config.OS os, Arch arch) {
File buildDir = new File(getModuleBaseDir(module), "robovm-build/tmp/" + runConfigName + "/" + os + "/" + arch);
if (!buildDir.exists()) {
if (!buildDir.mkdirs()) {
throw new RuntimeException("Couldn't create build dir '" + buildDir.getAbsolutePath() + "'");
}
}
return buildDir;
}
public static File getModuleClassesDir(String moduleBaseDir) {
File classesDir = new File(moduleBaseDir, "robovm-build/classes/");
if(!classesDir.exists()) {
if (!classesDir.mkdirs()) {
throw new RuntimeException("Couldn't create classes dir '" + classesDir.getAbsolutePath() + "'");
}
}
return classesDir;
}
public static File getModuleBaseDir(Module module) {
return new File(ModuleRootManager.getInstance(module).getContentRoots()[0].getPath());
}
public static Set<File> getModuleResourcePaths(Module module) {
try {
File moduleBaseDir = new File(ModuleRootManager.getInstance(module).getContentRoots()[0].getPath());
Config.Builder configBuilder = new Config.Builder();
configBuilder.home(RoboVmPlugin.getRoboVmHome());
configBuilder.addClasspathEntry(new File(".")); // Fake a classpath to make Config happy
configBuilder.skipLinking(true);
RoboVmCompileTask.loadConfig(module.getProject(), configBuilder, moduleBaseDir, false);
Config config = configBuilder.build();
Set<File> paths = new HashSet<>();
for (Resource r : config.getResources()) {
if (r.getPath() != null) {
if (r.getPath().exists() && r.getPath().isDirectory()) {
paths.add(r.getPath());
}
} else if (r.getDirectory() != null) {
if (r.getDirectory().exists() && r.getDirectory().isDirectory()) {
paths.add(r.getDirectory());
}
}
}
return paths;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Module isRoboVmModuleResourcePath(Project project, VirtualFile file) {
try {
// using reflection here as building the config takes an
// immense amount of time
Field field = Config.Builder.class.getDeclaredField("config");
field.setAccessible(true);
for (Module module : ModuleManager.getInstance(project).getModules()) {
File moduleBaseDir = new File(ModuleRootManager.getInstance(module).getContentRoots()[0].getPath());
Config.Builder builder = new Config.Builder();
builder.home(RoboVmPlugin.getRoboVmHome());
builder.addClasspathEntry(new File(".")); // Fake a classpath to make Config happy
builder.skipLinking(true);
builder.readProjectProperties(moduleBaseDir, false);
builder.readProjectConfig(moduleBaseDir, false);
Config config = (Config)field.get(builder);
for(Resource res: config.getResources()) {
if(new File(file.getCanonicalPath()).getAbsolutePath().startsWith(res.getDirectory().getAbsolutePath())) {
return module;
}
}
}
return null;
} catch(Throwable t) {
return null;
}
}
public static File getModuleInfoPlist(Module module) {
try {
File projectRoot = getModuleBaseDir(module);
Config.Builder configBuilder = new Config.Builder();
configBuilder.home(RoboVmPlugin.getRoboVmHome());
// Fake a classpath to make Config happy
configBuilder.addClasspathEntry(new File("."));
configBuilder.skipLinking(true);
RoboVmCompileTask.loadConfig(module.getProject(), configBuilder, projectRoot, false);
Config config = configBuilder.build();
InfoPList iosInfoPList = config.getIosInfoPList();
if(iosInfoPList != null) return iosInfoPList.getFile();
else return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static boolean isSdkLibrary(String path) {
String name = new File(path).getName();
return name.startsWith("robovm-rt") ||
name.startsWith("robovm-objc") ||
name.startsWith("robovm-cocoatouch") ||
name.startsWith("robovm-cacerts");
}
public static boolean isBootClasspathLibrary(File path) {
return path.getName().startsWith("robovm-rt");
}
}