package ee.edio.garmin.jps.builder; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.BaseOSProcessHandler; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.projectRoots.JdkUtil; import com.intellij.openapi.util.io.FileUtil; import com.intellij.util.SystemProperties; import com.intellij.util.containers.ContainerUtil; import ee.edio.garmin.jps.model.JpsMonkeyModuleProperties; import ee.edio.garmin.jps.model.JpsMonkeyModuleType; import ee.edio.garmin.jps.model.JpsMonkeySdkType; import org.jetbrains.annotations.NotNull; import org.jetbrains.jps.builders.BuildOutputConsumer; import org.jetbrains.jps.builders.DirtyFilesHolder; import org.jetbrains.jps.incremental.CompileContext; import org.jetbrains.jps.incremental.ProjectBuildException; import org.jetbrains.jps.incremental.TargetBuilder; import org.jetbrains.jps.incremental.messages.BuildMessage; import org.jetbrains.jps.incremental.messages.CompilerMessage; import org.jetbrains.jps.incremental.resources.ResourcesBuilder; import org.jetbrains.jps.incremental.resources.StandardResourceBuilderEnabler; import org.jetbrains.jps.model.JpsDummyElement; import org.jetbrains.jps.model.JpsElement; import org.jetbrains.jps.model.JpsSimpleElement; import org.jetbrains.jps.model.java.JpsJavaExtensionService; import org.jetbrains.jps.model.library.sdk.JpsSdk; import org.jetbrains.jps.model.module.JpsModule; import org.jetbrains.jps.model.module.JpsTypedModule; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.charset.Charset; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; public class MonkeyBuilder extends TargetBuilder<MonkeySourceRootDescriptor, MonkeyBuildTarget> { public static final String NAME = "Monkey C"; private final static Logger LOG = Logger.getInstance(MonkeyBuilder.class); public static final String MONKEYBRAINS_FQN = "com.garmin.monkeybrains.Monkeybrains"; public static final String MONKEYBRAINS_JAR_FILENAME = "monkeybrains.jar"; public MonkeyBuilder() { super(Arrays.asList(MonkeyBuildTargetType.PRODUCTION, MonkeyBuildTargetType.TESTS)); ResourcesBuilder.registerEnabler(new StandardResourceBuilderEnabler() { @Override public boolean isResourceProcessingEnabled(@NotNull JpsModule module) { return module.getModuleType() != JpsMonkeyModuleType.INSTANCE; } }); } @Override public void build(@NotNull MonkeyBuildTarget target, @NotNull DirtyFilesHolder<MonkeySourceRootDescriptor, MonkeyBuildTarget> holder, @NotNull BuildOutputConsumer outputConsumer, @NotNull CompileContext context) throws ProjectBuildException, IOException { LOG.debug(target.getPresentableName()); if (!holder.hasDirtyFiles() && !holder.hasRemovedFiles()) return; JpsModule jpsModule = target.getModule(); if (jpsModule.getModuleType() != JpsMonkeyModuleType.INSTANCE) return; //JpsTypedModule<JpsSimpleElement<JpsGoModuleProperties>> module = JpsTypedModule<JpsSimpleElement<JpsMonkeyModuleProperties>> module = jpsModule.asTyped(JpsMonkeyModuleType.INSTANCE); assert module != null; final JpsElement propertiesUntyped = target.getModule().getProperties(); if (!(propertiesUntyped instanceof JpsSimpleElement)) { throw new ProjectBuildException("module properties has wrong type"); } @SuppressWarnings("unchecked") JpsSimpleElement<JpsMonkeyModuleProperties> properties = (JpsSimpleElement<JpsMonkeyModuleProperties>) propertiesUntyped; JpsMonkeyModuleProperties moduleProperties = properties.getData(); final String targetDeviceId = moduleProperties.TARGET_DEVICE_ID; JpsSdk<JpsDummyElement> sdk = getSdk(context, module); File outputDirectory = getBuildOutputDirectory(jpsModule, target.isTests(), context); for (String contentRootUrl : jpsModule.getContentRootsList().getUrls()) { String contentRootPath = new URL(contentRootUrl).getPath(); final String projectName = context.getProjectDescriptor().getProject().getName(); final GeneralCommandLine buildCmd = createBuildCmd(projectName, contentRootPath, outputDirectory, sdk.getHomePath(), targetDeviceId); runBuildProcess(context, buildCmd, contentRootPath); } } private static void runBuildProcess(@NotNull CompileContext context, @NotNull GeneralCommandLine commandLine, @NotNull String path) throws ProjectBuildException { try { final Process process = commandLine.createProcess(); BaseOSProcessHandler handler = new BaseOSProcessHandler(process, commandLine.getCommandLineString(), Charset.defaultCharset()); handler.startNotify(); handler.waitFor(); } catch (ExecutionException e) { throw new ProjectBuildException(e.getMessage()); } } // TODO: paths that contain spaces should be quoted? public GeneralCommandLine createBuildCmd(String projectName, String projectRootPath, File outputDirectory, String sdkHomePath, String targetDeviceId) { final File projectRoot = new File(FileUtil.toSystemIndependentName(projectRootPath)); // TODO: Use module sources functionality instead Pattern sourcePattern = Pattern.compile(".*\\.mc"); final List<File> mcFiles = FileUtil.findFilesByMask(sourcePattern, projectRoot); final ImmutableList<String> sourceFilePaths = FluentIterable.from(mcFiles) .transform(new Function<File, String>() { @Override public String apply(File file) { return file.getAbsolutePath(); } }).toList(); // TODO: Use module resources functionality instead Pattern resourcePattern = Pattern.compile(".*\\.xml"); final List<File> xmlFiles = FileUtil.findFilesByMask(resourcePattern, projectRoot); final ImmutableList<String> resourceFilePaths = FluentIterable.from(xmlFiles) .filter(new Predicate<File>() { @Override public boolean apply(File file) { return file != null && file.getParentFile().getAbsolutePath().contains("resource"); } }) .transform(new Function<File, String>() { @Override public String apply(File file) { return file.getAbsolutePath(); } }).toList(); String sdkPath = sdkHomePath + File.separator; String sdkBinPath = sdkPath + "bin" + File.separator; String outputName = projectName + ".prg"; String outputDir = outputDirectory.getAbsolutePath() + File.separator; ImmutableList.Builder<String> parameters = ImmutableList.<String>builder() .add("-a", sdkBinPath + "api.db") .add("-i", sdkBinPath + "api.debug.xml") .add("-o", outputDir + outputName); // .add("-w") // Show compilation warnings in the Console // .add("-g") // Print debug output (-g) if (!resourceFilePaths.isEmpty()) { // in format: -z C:\xyz\resources\layouts\layout.xml;C:\xyz\resources\menus\menu.xml;C:\xyz\resources\resources.xml StringBuilder builder = new StringBuilder(); for (String resourceFilePath : resourceFilePaths) { // if not first if (builder.length() != 0) { builder.append(File.pathSeparator); } builder.append(resourceFilePath); } parameters.add("-z", builder.toString()); } String manifestXmlPath = projectRootPath + File.separator + "manifest.xml"; String devicesXmlPath = sdkBinPath + "devices.xml"; String projectInfoXmlPath = sdkBinPath + "projectInfo.xml"; // todo: is this file optional? parameters.add("-m", manifestXmlPath) .add("-u", devicesXmlPath) .add("-p", projectInfoXmlPath); // optional file? // in format: C:\xyz\source\aaApp.mc C:\xyz\source\aaMenuDelegate.mc C:\xyz\source\aaView.mc parameters.addAll(sourceFilePaths); final String deviceId = targetDeviceId != null ? targetDeviceId : "round_watch"; final String deviceSim = deviceId + "_sim"; // parameters.add("-r"); // if release build parameters.add("-d", deviceSim); //final String javaHome = SystemProperties.getJavaHome(); //String javaPath = javaHome + File.separator + "bin" + File.separator + "java"; final String jdkHome = findRealJdkHome() + File.separator; String javaPath = jdkHome + "bin" + File.separator + "java"; String toolsJarPath = jdkHome + "lib" + File.separator + "tools.jar"; String monkeybrainsJarPath = sdkBinPath + MONKEYBRAINS_JAR_FILENAME; GeneralCommandLine commandLine = new GeneralCommandLine(); commandLine.setExePath(javaPath); commandLine.addParameters("-Dfile.encoding=UTF-8", "-Dapple.awt.UIElement=true"); String classPath = toolsJarPath + ";" + monkeybrainsJarPath + ";"; commandLine.addParameters("-classpath", classPath); commandLine.addParameters(MONKEYBRAINS_FQN); commandLine.addParameters(parameters.build()); return commandLine; } // searches some common directories and misconfigurations public static String findRealJdkHome() { String javaHome = SystemProperties.getJavaHome(); List<String> paths = ContainerUtil.packNullables(javaHome, new File(javaHome).getParent(), System.getenv("JDK_16_x64"), System.getenv("JDK_16")); for (String path : paths) { if (JdkUtil.checkForJdk(new File(path))) { return path; } } throw new RuntimeException("could not find JDK"); } @NotNull private static JpsSdk<JpsDummyElement> getSdk(@NotNull CompileContext context, @NotNull JpsModule module) throws ProjectBuildException { JpsSdk<JpsDummyElement> sdk = module.getSdk(JpsMonkeySdkType.INSTANCE); if (sdk == null) { String errorMessage = "No SDK for module " + module.getName(); context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.ERROR, errorMessage)); throw new ProjectBuildException(errorMessage); } return sdk; } @NotNull private static File getBuildOutputDirectory(@NotNull JpsModule module, boolean forTests, @NotNull CompileContext context) throws ProjectBuildException { JpsJavaExtensionService instance = JpsJavaExtensionService.getInstance(); File outputDirectory = instance.getOutputDirectory(module, forTests); if (outputDirectory == null) { String errorMessage = "No output dir for module " + module.getName(); context.processMessage(new CompilerMessage(NAME, BuildMessage.Kind.ERROR, errorMessage)); throw new ProjectBuildException(errorMessage); } if (!outputDirectory.exists()) { FileUtil.createDirectory(outputDirectory); } return outputDirectory; } @NotNull @Override public String getPresentableName() { return NAME; } }