/*
* 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.compilation;
import com.intellij.compiler.options.CompileStepBeforeRun;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.compiler.CompileContext;
import com.intellij.openapi.compiler.CompileTask;
import com.intellij.openapi.compiler.CompilerMessageCategory;
import com.intellij.openapi.compiler.CompilerPaths;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEnumerator;
import com.intellij.openapi.roots.OrderRootsEnumerator;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.util.Computable;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.io.FileUtils;
import org.robovm.compiler.AppCompiler;
import org.robovm.compiler.config.Arch;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.config.OS;
import org.robovm.compiler.plugin.PluginArgument;
import org.robovm.compiler.target.ConsoleTarget;
import org.robovm.compiler.target.ios.IOSTarget;
import org.robovm.compiler.target.ios.ProvisioningProfile;
import org.robovm.compiler.target.ios.SigningIdentity;
import org.robovm.idea.RoboVmPlugin;
import org.robovm.idea.actions.CreateIpaAction;
import org.robovm.idea.running.RoboVmRunConfiguration;
import org.robovm.idea.running.RoboVmIOSRunConfigurationSettingsEditor;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.*;
/**
* Registered by {@link org.robovm.idea.RoboVmPlugin} on startup. Responsible
* for compiling an app in case there's a run configuration in the {@link com.intellij.openapi.compiler.CompileContext}
* or if we perform an ad-hoc/IPA build from the RoboVM menu.
*/
public class RoboVmCompileTask implements CompileTask {
@Override
public boolean execute(CompileContext context) {
if(context.getMessageCount(CompilerMessageCategory.ERROR) > 0) {
RoboVmPlugin.logError(context.getProject(), "Can't compile application due to previous compilation errors");
return false;
}
RunConfiguration c = context.getCompileScope().getUserData(CompileStepBeforeRun.RUN_CONFIGURATION);
if(c == null || !(c instanceof RoboVmRunConfiguration)) {
CreateIpaAction.IpaConfig ipaConfig = context.getCompileScope().getUserData(CreateIpaAction.IPA_CONFIG_KEY);
if(ipaConfig != null) {
return compileForIpa(context, ipaConfig);
} else {
return true;
}
} else {
return compileForRunConfiguration(context, (RoboVmRunConfiguration)c);
}
}
private boolean compileForIpa(CompileContext context, final CreateIpaAction.IpaConfig ipaConfig) {
try {
ProgressIndicator progress = context.getProgressIndicator();
context.getProgressIndicator().pushState();
RoboVmPlugin.focusToolWindow(context.getProject());
progress.setText("Creating IPA");
RoboVmPlugin.logInfo(context.getProject(), "Creating package in " + ipaConfig.getDestinationDir().getAbsolutePath() + " ...");
Config.Builder builder = new Config.Builder();
builder.logger(RoboVmPlugin.getLogger(context.getProject()));
File moduleBaseDir = new File(ModuleRootManager.getInstance(ipaConfig.getModule()).getContentRoots()[0].getPath());
// load the robovm.xml file
loadConfig(context.getProject(), builder, moduleBaseDir, false);
builder.os(OS.ios);
builder.archs(ipaConfig.getArchs());
builder.installDir(ipaConfig.getDestinationDir());
builder.iosSignIdentity(SigningIdentity.find(SigningIdentity.list(), ipaConfig.getSigningIdentity()));
if (ipaConfig.getProvisioningProfile() != null) {
builder.iosProvisioningProfile(ProvisioningProfile.find(ProvisioningProfile.list(), ipaConfig.getProvisioningProfile()));
}
configureClassAndSourcepaths(context, builder, ipaConfig.getModule());
builder.home(RoboVmPlugin.getRoboVmHome());
Config config = builder.build();
progress.setFraction(0.5);
AppCompiler compiler = new AppCompiler(config);
RoboVmCompilerThread thread = new RoboVmCompilerThread(compiler, progress) {
protected void doCompile() throws Exception {
compiler.build();
compiler.archive();
}
};
thread.compile();
if(progress.isCanceled()) {
RoboVmPlugin.logInfo(context.getProject(), "Build canceled");
return false;
}
progress.setFraction(1);
RoboVmPlugin.logInfo(context.getProject(), "Package successfully created in " + ipaConfig.getDestinationDir().getAbsolutePath());
} catch(Throwable t) {
RoboVmPlugin.logErrorThrowable(context.getProject(), "Couldn't create IPA", t, false);
return false;
} finally {
context.getProgressIndicator().popState();
}
return true;
}
private boolean compileForRunConfiguration(CompileContext context, final RoboVmRunConfiguration runConfig) {
try {
ProgressIndicator progress = context.getProgressIndicator();
context.getProgressIndicator().pushState();
RoboVmPlugin.focusToolWindow(context.getProject());
progress.setText("Compiling RoboVM app");
Config.Builder builder = new Config.Builder();
builder.logger(RoboVmPlugin.getLogger(context.getProject()));
// get the module we are about to compile
ModuleManager moduleManager = ModuleManager.getInstance(runConfig.getProject());
Module module = ApplicationManager.getApplication().runReadAction(new Computable<Module>() {
@Override
public Module compute() {
return ModuleManager.getInstance(runConfig.getProject()).findModuleByName(runConfig.getModuleName());
}
});
if(module == null) {
RoboVmPlugin.logBalloon(context.getProject(), MessageType.ERROR, "Couldn't find Module '" + runConfig.getModuleName() + "'");
return false;
}
File moduleBaseDir = new File(ModuleRootManager.getInstance(module).getContentRoots()[0].getPath());
// load the robovm.xml file
loadConfig(context.getProject(), builder, moduleBaseDir, false);
// set OS and arch
OS os = null;
Arch arch = null;
if(runConfig.getTargetType() == RoboVmRunConfiguration.TargetType.Device) {
os = OS.ios;
arch = runConfig.getDeviceArch();
} else if(runConfig.getTargetType() == RoboVmRunConfiguration.TargetType.Simulator) {
os = OS.ios;
arch = runConfig.getSimArch();
} else {
os = OS.getDefaultOS();
arch = Arch.getDefaultArch();
}
builder.os(os);
builder.arch(arch);
// set the plugin args
List<String> args = splitArgs(runConfig.getArguments());
applyPluginArguments(args, builder);
// set build dir and install dir, pattern
// module-basedir/robovm-build/tmp/module-name/runconfig-name/os/arch.
// module-basedir/robovm-build/app/module-name/runconfig-name/os/arch.
File buildDir = RoboVmPlugin.getModuleBuildDir(module, runConfig.getName(), os, arch);
builder.tmpDir(buildDir);
builder.skipInstall(true);
RoboVmPlugin.logInfo(context.getProject(), "Building executable in %s", buildDir.getAbsolutePath());
RoboVmPlugin.logInfo(context.getProject(), "Installation of app in %s", buildDir.getAbsolutePath());
// setup classpath entries, debug build parameters and target
// parameters, e.g. signing identity etc.
configureClassAndSourcepaths(context, builder, module);
configureDebugging(builder, runConfig, module);
configureTarget(builder, runConfig);
// clean build dir
RoboVmPlugin.logInfo(context.getProject(), "Cleaning output dir " + buildDir.getAbsolutePath());
FileUtils.deleteDirectory(buildDir);
buildDir.mkdirs();
// Set the Home to be used, create the Config and AppCompiler
Config.Home home = RoboVmPlugin.getRoboVmHome();
if(home.isDev()) {
builder.useDebugLibs(true);
builder.dumpIntermediates(true);
builder.addPluginArgument("debug:logconsole=true");
}
builder.home(home);
Config config = builder.build();
AppCompiler compiler = new AppCompiler(config);
if(progress.isCanceled()) {
RoboVmPlugin.logInfo(context.getProject(), "Build canceled");
return false;
}
progress.setFraction(0.5);
// Start the build in a separate thread, check if
// user canceled it.
RoboVmCompilerThread thread = new RoboVmCompilerThread(compiler, progress);
thread.compile();
if(progress.isCanceled()) {
RoboVmPlugin.logInfo(context.getProject(), "Build canceled");
return false;
}
RoboVmPlugin.logInfo(context.getProject(), "Build done");
// set the config and compiler on the run configuration so
// it knows where to find things.
runConfig.setConfig(config);
runConfig.setCompiler(compiler);
runConfig.setProgramArguments(args);
} catch(Throwable t) {
RoboVmPlugin.logErrorThrowable(context.getProject(), "Couldn't compile app", t, false);
return false;
} finally {
context.getProgressIndicator().popState();
}
return true;
}
private void configureClassAndSourcepaths(CompileContext context, Config.Builder builder, Module module) {
// gather the boot and user classpaths. RoboVM RT libs may be
// specified in a Maven/Gradle build file, in which case they'll
// turn up as order entries. We filter them out here.
// FIXME junit needs to include test classes
OrderEnumerator classes = ModuleRootManager.getInstance(module).orderEntries().recursively().withoutSdk().compileOnly().productionOnly();
Set<File> classPaths = new HashSet<File>();
Set<File> bootClassPaths = new HashSet<File>();
for(String path: classes.getPathsList().getPathList()) {
if(!RoboVmPlugin.isSdkLibrary(path)) {
classPaths.add(new File(path));
}
}
// add the output dirs of all affected modules to the
// classpath. IDEA will make the output directory
// of a module an order entry after the first compile
// so we add the path twice. Fixed by using a set.
// FIXME junit needs to include test output directories
for(Module mod: context.getCompileScope().getAffectedModules()) {
String path = CompilerPaths.getModuleOutputPath(mod, false);
if(path != null && !path.isEmpty()) {
classPaths.add(new File(path));
} else {
RoboVmPlugin.logWarn(context.getProject(), "Output path of module %s not defined", mod.getName());
}
}
// set the user classpath entries
for(File path: classPaths) {
RoboVmPlugin.logInfo(context.getProject(), "classpath entry: %s", path.getAbsolutePath());
builder.addClasspathEntry(path);
}
// Use the RT from the SDK
RoboVmPlugin.logInfo(context.getProject(), "Using SDK boot classpath");
for(File path: RoboVmPlugin.getSdkLibrariesWithoutSources()) {
if(RoboVmPlugin.isBootClasspathLibrary(path)) {
builder.addBootClasspathEntry(path);
} else {
builder.addClasspathEntry(path);
}
}
}
private void configureDebugging(Config.Builder builder, RoboVmRunConfiguration runConfig, Module module) {
// setup debug configuration if necessary
if(runConfig.isDebug()) {
Set<String> sourcesPaths = new HashSet<String>();
// source paths of dependencies and modules
OrderRootsEnumerator sources = ModuleRootManager.getInstance(module).orderEntries().recursively().withoutSdk().sources();
for (String path : sources.getPathsList().getPathList()) {
RoboVmPlugin.logInfo(module.getProject(), "source path entry: %s", path);
sourcesPaths.add(path);
}
StringBuilder b = new StringBuilder();
// SDK sourcepaths
for(File path: RoboVmPlugin.getSdkLibrarySources()) {
b.append(path.getAbsolutePath());
b.append(":");
}
for(String path: sourcesPaths) {
b.append(path);
b.append(":");
}
// set arguments for debug plugin
runConfig.setDebugPort(findFreePort());
builder.debug(true);
builder.addPluginArgument("debug:sourcepath=" + b.toString());
builder.addPluginArgument("debug:jdwpport=" + runConfig.getDebugPort());
builder.addPluginArgument("debug:clientmode=true");
builder.addPluginArgument("debug:logdir=" + RoboVmPlugin.getModuleLogDir(module).getAbsolutePath());
}
}
private void configureTarget(Config.Builder builder, RoboVmRunConfiguration runConfig) {
if(runConfig.getTargetType() == RoboVmRunConfiguration.TargetType.Device) {
// configure device build
builder.targetType(IOSTarget.TYPE);
String signingId = runConfig.getSigningIdentity();
String profile = runConfig.getProvisioningProfile();
if (RoboVmIOSRunConfigurationSettingsEditor.SKIP_SIGNING.equals(signingId)) {
builder.iosSkipSigning(true);
} else {
if (signingId != null && !RoboVmIOSRunConfigurationSettingsEditor.AUTO_SIGNING_IDENTITY.equals(signingId)) {
builder.iosSignIdentity(SigningIdentity.find(SigningIdentity.list(), signingId));
}
if (profile != null && !RoboVmIOSRunConfigurationSettingsEditor.AUTO_PROVISIONING_PROFILE.equals(profile)) {
builder.iosProvisioningProfile(ProvisioningProfile.find(ProvisioningProfile.list(), profile));
}
}
} else if(runConfig.getTargetType() == RoboVmRunConfiguration.TargetType.Simulator) {
builder.targetType(IOSTarget.TYPE);
} else if(runConfig.getTargetType() == RoboVmRunConfiguration.TargetType.Console) {
builder.targetType(ConsoleTarget.TYPE);
} else {
throw new RuntimeException("Unsupported target type: " + runConfig.getTargetType());
}
}
public static Config.Builder loadConfig(Project project, Config.Builder configBuilder, File projectRoot, boolean isTest) {
try {
configBuilder.readProjectProperties(projectRoot, isTest);
configBuilder.readProjectConfig(projectRoot, isTest);
} catch (IOException e) {
RoboVmPlugin.logErrorThrowable(project, "Couldn't load robovm.xml", e, true);
throw new RuntimeException(e);
}
// Ignore classpath entries in config XML file.
configBuilder.clearBootClasspathEntries();
configBuilder.clearClasspathEntries();
return configBuilder;
}
public int findFreePort()
{
ServerSocket socket = null;
try {
socket = new ServerSocket(0);
return socket.getLocalPort();
} catch (IOException localIOException2) {
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException localIOException4) {
}
}
}
return -1;
}
private static String unquoteArg(String arg) {
if (arg.startsWith("\"") && arg.endsWith("\"")) {
return arg.substring(1, arg.length() - 1);
}
return arg;
}
public static List<String> splitArgs(String args) {
if (args == null || args.trim().length() == 0) {
return new ArrayList<String>();
}
String[] parts = CommandLine.parse("foo " + args).toStrings();
if (parts.length <= 1) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>(parts.length - 1);
for (int i = 1; i < parts.length; i++) {
result.add(unquoteArg(parts[i]));
}
return result;
}
/**
* Filters any plugin arguments and sets them on the provided builder
* @param args
* @param configBuilder builder or null to filter the args list
*/
public static void applyPluginArguments(List<String> args, Config.Builder configBuilder) {
Map<String, PluginArgument> pluginArguments = configBuilder.fetchPluginArguments();
Iterator<String> iter = args.iterator();
while (iter.hasNext()) {
String arg = iter.next();
if (!arg.startsWith("-rvm") && arg.startsWith("-")) {
String argName = arg.substring(1);
if (argName.contains("=")) {
argName = argName.substring(0, argName.indexOf('='));
}
PluginArgument pluginArg = pluginArguments.get(argName);
if (pluginArg != null) {
if(configBuilder != null) {
configBuilder.addPluginArgument(arg.substring(1));
iter.remove();
}
}
}
}
}
}