package com.intellij.jps.flex.build;
import com.intellij.flex.FlexCommonBundle;
import com.intellij.flex.FlexCommonUtils;
import com.intellij.flex.build.CompilerConfigGeneratorRt;
import com.intellij.flex.build.FlexBuildTarget;
import com.intellij.flex.build.FlexBuildTargetType;
import com.intellij.flex.model.JpsFlexCompilerProjectExtension;
import com.intellij.flex.model.JpsFlexProjectLevelCompilerOptionsExtension;
import com.intellij.flex.model.bc.JpsFlexBuildConfiguration;
import com.intellij.flex.model.bc.JpsFlexCompilerOptions;
import com.intellij.flex.model.bc.OutputType;
import com.intellij.flex.model.bc.TargetPlatform;
import com.intellij.flex.model.sdk.JpsFlexSdkType;
import com.intellij.flex.model.sdk.JpsFlexmojosSdkType;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.PathUtilRt;
import com.intellij.util.concurrency.Semaphore;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.builders.BuildOutputConsumer;
import org.jetbrains.jps.builders.BuildRootDescriptor;
import org.jetbrains.jps.builders.DirtyFilesHolder;
import org.jetbrains.jps.builders.FileProcessor;
import org.jetbrains.jps.cmdline.ProjectDescriptor;
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.ProjectBuildException;
import org.jetbrains.jps.incremental.StopBuildException;
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.messages.ProgressMessage;
import org.jetbrains.jps.model.JpsProject;
import org.jetbrains.jps.model.library.sdk.JpsSdk;
import org.jetbrains.jps.model.module.JpsModule;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class FlexBuilder extends TargetBuilder<BuildRootDescriptor, FlexBuildTarget> {
private static Logger LOG = Logger.getInstance(FlexBuilder.class.getName());
private JpsBuiltInFlexCompilerHandler myBuiltInCompilerHandler;
private enum Status {Ok, Failed, Cancelled}
protected FlexBuilder() {
super(Collections.singletonList(FlexBuildTargetType.INSTANCE));
}
@Override
@NotNull
public String getPresentableName() {
return "Flash Compiler";
}
@Override
public void buildStarted(final CompileContext context) {
super.buildStarted(context);
myBuiltInCompilerHandler = new JpsBuiltInFlexCompilerHandler(context.getProjectDescriptor().getProject());
}
@Override
public void buildFinished(final CompileContext context) {
LOG.assertTrue(myBuiltInCompilerHandler.getActiveCompilationsNumber() == 0,
myBuiltInCompilerHandler.getActiveCompilationsNumber() + " Flex compilation(s) are not finished!");
myBuiltInCompilerHandler.stopCompilerProcess();
myBuiltInCompilerHandler = null;
FlexCommonUtils.deleteTempFlexConfigFiles(context.getProjectDescriptor().getProject().getName());
super.buildFinished(context);
}
@Override
public void build(@NotNull final FlexBuildTarget buildTarget,
@NotNull final DirtyFilesHolder<BuildRootDescriptor, FlexBuildTarget> holder,
@NotNull final BuildOutputConsumer outputConsumer,
@NotNull final CompileContext context) throws ProjectBuildException, IOException {
final Collection<String> dirtyFilePaths = new ArrayList<>();
holder.processDirtyFiles(new FileProcessor<BuildRootDescriptor, FlexBuildTarget>() {
@Override
public boolean apply(final FlexBuildTarget target, final File file, final BuildRootDescriptor root) throws IOException {
assert target == buildTarget;
dirtyFilePaths.add(file.getPath());
return true;
}
});
if (LOG.isDebugEnabled()) {
final StringBuilder b = new StringBuilder();
b.append(buildTarget.getId()).append(", ").append("dirty files: ").append(dirtyFilePaths.size());
if (dirtyFilePaths.size() < 10) {
for (String path : dirtyFilePaths) {
b.append('\n').append(path);
}
}
LOG.debug(b.toString());
}
final JpsFlexBuildConfiguration mainBC = buildTarget.getBC();
final List<JpsFlexBuildConfiguration> bcsToCompile = getAllBCsToCompile(mainBC);
if (!FlexCommonUtils.isFlexUnitBC(mainBC) && !isFlexmojosBCWithUpdatedConfigFile(mainBC)) {
if (dirtyFilePaths.isEmpty()) {
boolean outputFilesExist = true;
for (JpsFlexBuildConfiguration bc : bcsToCompile) {
if (!new File(bc.getActualOutputFilePath()).isFile()) {
outputFilesExist = false;
LOG.debug("recompile because output file doesn't exist: " + bc.getActualOutputFilePath());
break;
}
}
if (outputFilesExist) {
return;
}
}
else if (mainBC.getNature().isApp() && isOnlyWrapperFilesDirty(mainBC, dirtyFilePaths)) {
LOG.debug("only wrapper files dirty");
FlexBuilderUtils.performPostCompileActions(context, mainBC, dirtyFilePaths, outputConsumer);
return;
}
}
for (JpsFlexBuildConfiguration bc : bcsToCompile) {
final Status status = compileBuildConfiguration(context, bc, myBuiltInCompilerHandler);
switch (status) {
case Ok:
outputConsumer.registerOutputFile(new File(mainBC.getActualOutputFilePath()), dirtyFilePaths);
FlexBuilderUtils.performPostCompileActions(context, bc, dirtyFilePaths, outputConsumer);
context.processMessage(
new CompilerMessage(FlexBuilderUtils.getCompilerName(bc), BuildMessage.Kind.INFO,
FlexCommonBundle.message("compilation.successful")));
break;
case Failed:
final String message = bc.getOutputType() == OutputType.Application
? FlexCommonBundle.message("compilation.failed")
: FlexCommonBundle.message("compilation.failed.dependent.will.be.skipped");
context.processMessage(new CompilerMessage(FlexBuilderUtils.getCompilerName(bc), BuildMessage.Kind.INFO, message));
throw new StopBuildException();
case Cancelled:
context.processMessage(
new CompilerMessage(FlexBuilderUtils.getCompilerName(bc), BuildMessage.Kind.INFO,
FlexCommonBundle.message("compilation.cancelled")));
return;
}
}
}
/**
* This is a hacky workaround, needed because IDEA doesn't report files changed under .idea folder as dirty
*/
private static boolean isFlexmojosBCWithUpdatedConfigFile(final JpsFlexBuildConfiguration bc) {
final String configFilePath = bc.getCompilerOptions().getAdditionalConfigFilePath();
if (configFilePath.isEmpty() || !configFilePath.contains("/.idea/flexmojos/")) {
return false;
}
final File configFile = new File(configFilePath);
final File outputFile = new File(bc.getActualOutputFilePath());
return configFile.lastModified() > outputFile.lastModified();
}
private static boolean isOnlyWrapperFilesDirty(final JpsFlexBuildConfiguration bc, final Collection<String> dirtyFilePaths) {
if (bc.getTargetPlatform() == TargetPlatform.Web && bc.isUseHtmlWrapper() && !bc.getWrapperTemplatePath().isEmpty()) {
for (String dirtyFilePath : dirtyFilePaths) {
if (FileUtil.isAncestor(bc.getWrapperTemplatePath(), dirtyFilePath, true)) {
continue;
}
return false;
}
return true;
}
return false;
}
private static List<JpsFlexBuildConfiguration> getAllBCsToCompile(final JpsFlexBuildConfiguration bc) {
final List<JpsFlexBuildConfiguration> result =
new ArrayList<>(1 + bc.getRLMs().size() + bc.getCssFilesToCompile().size());
result.add(bc);
if (FlexCommonUtils.canHaveRLMsAndRuntimeStylesheets(bc)) {
for (JpsFlexBuildConfiguration.RLMInfo rlm : bc.getRLMs()) {
result.add(createRlmBC(bc, rlm));
}
for (String cssPath : bc.getCssFilesToCompile()) {
if (new File(cssPath).isFile()) {
result.add(createCssBC(bc, cssPath));
}
}
}
return result;
}
private static JpsFlexBuildConfiguration createRlmBC(final JpsFlexBuildConfiguration mainBC,
final JpsFlexBuildConfiguration.RLMInfo rlm) {
final JpsFlexBuildConfiguration rlmBC = mainBC.getModule().getProperties().createTemporaryCopyForCompilation(mainBC);
rlmBC.setOutputType(OutputType.RuntimeLoadedModule);
rlmBC.setOptimizeFor(rlm.OPTIMIZE ? mainBC.getName() : ""); // any not empty string means that need to optimize
final String subdir = PathUtilRt.getParentPath(rlm.OUTPUT_FILE);
final String outputFileName = PathUtilRt.getFileName(rlm.OUTPUT_FILE);
rlmBC.setMainClass(rlm.MAIN_CLASS);
rlmBC.setOutputFileName(outputFileName);
if (!subdir.isEmpty()) {
final String outputFolder = PathUtilRt.getParentPath(mainBC.getActualOutputFilePath());
rlmBC.setOutputFolder(outputFolder + "/" + subdir);
}
rlmBC.setUseHtmlWrapper(false);
rlmBC.setRLMs(Collections.emptyList());
rlmBC.setCssFilesToCompile(Collections.emptyList());
final JpsFlexCompilerOptions compilerOptions = rlmBC.getCompilerOptions();
compilerOptions.setResourceFilesMode(JpsFlexCompilerOptions.ResourceFilesMode.None);
String additionalOptions = compilerOptions.getAdditionalOptions();
additionalOptions = FlexCommonUtils.removeOptions(additionalOptions, "link-report");
additionalOptions = FlexCommonUtils.fixSizeReportOption(additionalOptions, StringUtil.getShortName(rlmBC.getMainClass()));
compilerOptions.setAdditionalOptions(additionalOptions);
return rlmBC;
}
private static JpsFlexBuildConfiguration createCssBC(final JpsFlexBuildConfiguration mainBC, final String cssPath) {
final JpsFlexBuildConfiguration cssBC = mainBC.getModule().getProperties().createTemporaryCopyForCompilation(mainBC);
cssBC.setOutputType(OutputType.Application);
cssBC.setMainClass(cssPath);
cssBC.setOutputFileName(FileUtil.getNameWithoutExtension(PathUtilRt.getFileName(cssPath)) + ".swf");
final String cssDirPath = PathUtilRt.getParentPath(cssPath);
String relativeToRoot = FlexCommonUtils.getPathRelativeToSourceRoot(mainBC.getModule(), cssDirPath);
if (relativeToRoot == null) {
relativeToRoot = FlexCommonUtils.getPathRelativeToContentRoot(mainBC.getModule(), cssDirPath);
}
if (!StringUtil.isEmpty(relativeToRoot)) {
final String outputFolder = PathUtilRt.getParentPath(mainBC.getActualOutputFilePath());
cssBC.setOutputFolder(outputFolder + "/" + relativeToRoot);
}
cssBC.setUseHtmlWrapper(false);
cssBC.setRLMs(Collections.emptyList());
cssBC.setCssFilesToCompile(Collections.emptyList());
final JpsFlexCompilerOptions compilerOptions = cssBC.getCompilerOptions();
compilerOptions.setResourceFilesMode(JpsFlexCompilerOptions.ResourceFilesMode.None);
String additionalOptions = compilerOptions.getAdditionalOptions();
additionalOptions = FlexCommonUtils.removeOptions(additionalOptions, "link-report");
additionalOptions =
FlexCommonUtils.fixSizeReportOption(additionalOptions, FileUtil.getNameWithoutExtension(PathUtilRt.getFileName(cssPath)));
compilerOptions.setAdditionalOptions(additionalOptions);
return cssBC;
}
private static Status compileBuildConfiguration(final CompileContext context,
final JpsFlexBuildConfiguration bc,
final JpsBuiltInFlexCompilerHandler builtInCompilerHandler) {
setProgressMessage(context, bc);
final String compilerName = FlexBuilderUtils.getCompilerName(bc);
try {
final List<File> configFiles = createConfigFiles(bc, context.getProjectDescriptor());
final String outputFilePath = bc.getActualOutputFilePath();
if (!ensureCanCreateFile(new File(outputFilePath))) {
context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.ERROR,
FlexCommonBundle.message("failed.to.create.file", bc.getActualOutputFilePath())));
return Status.Failed;
}
return doCompile(context, bc, configFiles, compilerName, builtInCompilerHandler);
}
catch (IOException e) {
context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.ERROR, e.getMessage()));
return Status.Failed;
}
}
private static boolean ensureCanCreateFile(@NotNull File file) {
final int maxAttempts = 3; // FileUtil.ensureCanCreateFile() may return false because of race conditions
for (int i = 0; i < maxAttempts; i++) {
if (FileUtil.ensureCanCreateFile(file)) return true;
try {
//noinspection BusyWait
Thread.sleep(10);
}
catch (InterruptedException ignore) {/**/}
}
return false;
}
private static void setProgressMessage(final CompileContext context, final JpsFlexBuildConfiguration bc) {
String postfix = bc.isTempBCForCompilation() ? " - " + FlexCommonUtils.getBCSpecifier(bc) : "";
if (!bc.getName().equals(bc.getModule().getName())) postfix += " (module " + bc.getModule().getName() + ")";
context.processMessage(new ProgressMessage(FlexCommonBundle.message("compiling", bc.getName() + postfix)));
}
private static List<File> createConfigFiles(final JpsFlexBuildConfiguration bc,
final ProjectDescriptor projectDescriptor) throws IOException {
final ArrayList<File> configFiles = new ArrayList<>(2);
configFiles.add(CompilerConfigGeneratorRt.getOrCreateConfigFile(bc, projectDescriptor));
final String additionalConfigFilePath = bc.getCompilerOptions().getAdditionalConfigFilePath();
if (!bc.isTempBCForCompilation() && !additionalConfigFilePath.isEmpty()) {
final File additionalConfigFile = new File(additionalConfigFilePath);
if (!additionalConfigFile.isFile()) {
throw new IOException(
FlexCommonBundle.message("additional.config.file.not.found.for.bc.0.of.module.1", additionalConfigFilePath, bc.getName(),
bc.getModule().getName()));
}
configFiles.add(additionalConfigFile);
}
return configFiles;
}
private static Status doCompile(final CompileContext context,
final JpsFlexBuildConfiguration bc,
final List<File> configFiles,
final String compilerName,
final JpsBuiltInFlexCompilerHandler builtInCompilerHandler) {
final boolean app = bc.getOutputType() != OutputType.Library;
final JpsSdk<?> sdk = bc.getSdk();
assert sdk != null;
final boolean asc20 = bc.isPureAs() &&
FlexCommonUtils.containsASC20(sdk.getHomePath()) &&
(JpsFlexCompilerProjectExtension.getInstance(bc.getModule().getProject()).PREFER_ASC_20 ||
FlexCommonUtils.isAirSdkWithoutFlex(sdk));
final boolean builtIn = !asc20 &&
JpsFlexCompilerProjectExtension.getInstance(bc.getModule().getProject()).USE_BUILT_IN_COMPILER &&
builtInCompilerHandler.canBeUsedForSdk(sdk.getHomePath());
if (builtIn) {
return doCompileWithBuiltInCompiler(context, bc, configFiles, compilerName, builtInCompilerHandler);
}
final List<String> compilerCommand = asc20 ? getASC20Command(bc.getModule().getProject(), sdk, app)
: getMxmlcCompcCommand(bc.getModule().getProject(), sdk, app);
final List<String> command = buildCommand(compilerCommand, configFiles, bc);
final ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
processBuilder.directory(new File(FlexCommonUtils.getFlexCompilerWorkDirPath(bc.getModule().getProject())));
try {
final Process process = processBuilder.start();
final FlexCompilerProcessHandler processHandler =
new FlexCompilerProcessHandler(context, process, asc20, compilerName, StringUtil.join(command, " "));
processHandler.startNotify();
processHandler.waitFor();
return processHandler.isCancelled() ? Status.Cancelled
: processHandler.isCompilationFailed()
? Status.Failed
: Status.Ok;
}
catch (IOException e) {
context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.ERROR, e.getMessage()));
return Status.Failed;
}
}
private static Status doCompileWithBuiltInCompiler(final CompileContext context,
final JpsFlexBuildConfiguration bc,
final List<File> configFiles,
final String compilerName,
final JpsBuiltInFlexCompilerHandler builtInCompilerHandler) {
try {
builtInCompilerHandler.startCompilerIfNeeded(bc.getSdk(), context, compilerName);
}
catch (IOException e) {
context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.ERROR, e.toString()));
return Status.Failed;
}
final List<String> mxmlcOrCompc = Collections.singletonList(bc.getOutputType() == OutputType.Library ? "compc" : "mxmlc");
final List<String> command = buildCommand(mxmlcOrCompc, configFiles, bc);
final String plainCommand = StringUtil.join(command,
s -> s.indexOf(' ') >= 0 && !(s.startsWith("\"") && s.endsWith("\"")) ? '\"' + s + '\"' : s, " ");
final Semaphore semaphore = new Semaphore();
semaphore.down();
context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.INFO, plainCommand));
final BuiltInCompilerListener listener = new BuiltInCompilerListener(context, compilerName, () -> semaphore.up());
builtInCompilerHandler.sendCompilationCommand(plainCommand, listener);
semaphore.waitFor();
builtInCompilerHandler.removeListener(listener);
return listener.isCompilationCancelled() ? Status.Cancelled
: listener.isCompilationFailed()
? Status.Failed
: Status.Ok;
}
private static List<String> getASC20Command(final JpsProject project, final JpsSdk<?> flexSdk, final boolean isApp) {
final String mainClass = isApp ? "com.adobe.flash.compiler.clients.MXMLC" : "com.adobe.flash.compiler.clients.COMPC";
final String additionalClasspath = flexSdk.getSdkType() == JpsFlexmojosSdkType.INSTANCE
? null
: FileUtil.toSystemDependentName(flexSdk.getHomePath() + "/lib/compiler.jar");
return FlexCommonUtils.getCommandLineForSdkTool(project, flexSdk, additionalClasspath, mainClass);
}
private static List<String> getMxmlcCompcCommand(final JpsProject project, final JpsSdk<?> flexSdk, final boolean isApp) {
final String mainClass = isApp ? StringUtil.compareVersionNumbers(flexSdk.getVersionString(), "4") >= 0 ? "flex2.tools.Mxmlc"
: "flex2.tools.Compiler"
: "flex2.tools.Compc";
String additionalClasspath = FileUtil.toSystemDependentName(FlexCommonUtils.getPathToBundledJar("idea-flex-compiler-fix.jar"));
if (flexSdk.getSdkType() == JpsFlexSdkType.INSTANCE) {
additionalClasspath += File.pathSeparator + FileUtil.toSystemDependentName(flexSdk.getHomePath() + "/lib/compc.jar");
}
return FlexCommonUtils.getCommandLineForSdkTool(project, flexSdk, additionalClasspath, mainClass);
}
private static List<String> buildCommand(final List<String> compilerCommand,
final List<File> configFiles,
final JpsFlexBuildConfiguration bc) {
final List<String> command = new ArrayList<>(compilerCommand);
for (File configFile : configFiles) {
command.add("-load-config=" + configFile.getPath());
}
final JpsSdk<?> sdk = bc.getSdk();
assert sdk != null;
addAdditionalOptions(command, bc.getModule(), sdk.getHomePath(),
JpsFlexProjectLevelCompilerOptionsExtension
.getProjectLevelCompilerOptions(bc.getModule().getProject()).getAdditionalOptions());
addAdditionalOptions(command, bc.getModule(), sdk.getHomePath(),
bc.getModule().getProperties().getModuleLevelCompilerOptions().getAdditionalOptions());
addAdditionalOptions(command, bc.getModule(), sdk.getHomePath(), bc.getCompilerOptions().getAdditionalOptions());
return command;
}
private static void addAdditionalOptions(List<String> command, JpsModule module, String sdkHome, String additionalOptions) {
if (!StringUtil.isEmpty(additionalOptions)) {
// TODO handle -option="path with spaces"
for (final String s : StringUtil.split(additionalOptions, " ")) {
command.add(FlexCommonUtils.replacePathMacros(s, module, sdkHome));
}
}
}
private static class BuiltInCompilerListener extends CompilerMessageHandlerBase implements JpsBuiltInFlexCompilerHandler.Listener {
private final Runnable myOnCompilationFinishedRunnable;
public BuiltInCompilerListener(final CompileContext context, final String compilerName, final Runnable onCompilationFinishedRunnable) {
super(context, false, compilerName);
myOnCompilationFinishedRunnable = onCompilationFinishedRunnable;
}
@Override
public void textAvailable(final String text) {
handleText(text);
}
@Override
public void compilationFinished() {
registerCompilationFinished();
myOnCompilationFinishedRunnable.run();
}
@Override
protected void onCancelled() {
compilationFinished();
}
}
}