/* * Copyright 2011 Google Inc. * * 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 com.google.gwt.dev.codeserver; import com.google.gwt.core.ext.Linker; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.linker.CrossSiteIframeLinker; import com.google.gwt.core.linker.IFrameLinker; import com.google.gwt.dev.Compiler; import com.google.gwt.dev.CompilerOptions; import com.google.gwt.dev.cfg.BindingProperty; import com.google.gwt.dev.cfg.ConfigurationProperty; import com.google.gwt.dev.cfg.ModuleDef; import com.google.gwt.dev.cfg.ModuleDefLoader; import com.google.gwt.dev.cfg.Property; import com.google.gwt.dev.cfg.ResourceLoader; import com.google.gwt.dev.cfg.ResourceLoaders; import com.google.gwt.dev.javac.CompilationStateBuilder; import com.google.gwt.dev.resource.impl.ResourceOracleImpl; import com.google.gwt.dev.resource.impl.ZipFileClassPathEntry; import com.google.gwt.dev.util.log.CompositeTreeLogger; import com.google.gwt.dev.util.log.PrintWriterTreeLogger; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; /** * Recompiles a GWT module on demand. */ class Recompiler { private final AppSpace appSpace; private final String originalModuleName; private final List<File> sourcePath; private final RecompileListener listener; private final boolean failIfListenerFails; private final TreeLogger logger; private String serverPrefix; private int compilesDone = 0; // after renaming private AtomicReference<String> moduleName = new AtomicReference<String>(null); private final AtomicReference<CompileDir> lastBuild = new AtomicReference<CompileDir>(); private final AtomicReference<ResourceLoader> resourceLoader = new AtomicReference<ResourceLoader>(); Recompiler(AppSpace appSpace, String moduleName, List<File> sourcePath, String serverPrefix, RecompileListener listener, boolean failIfListenerFails, TreeLogger logger) { this.appSpace = appSpace; this.originalModuleName = moduleName; this.sourcePath = sourcePath; this.listener = listener; this.failIfListenerFails = failIfListenerFails; this.logger = logger; this.serverPrefix = serverPrefix; } synchronized CompileDir compile(Map<String, String> bindingProperties) throws UnableToCompleteException { if (compilesDone == 0) { System.setProperty("java.awt.headless", "true"); if (System.getProperty("gwt.speedtracerlog") == null) { System.setProperty("gwt.speedtracerlog", appSpace.getSpeedTracerLogFile().getAbsolutePath()); } CompilationStateBuilder.init(logger, appSpace.getUnitCacheDir()); } long startTime = System.currentTimeMillis(); int compileId = ++compilesDone; CompileDir compileDir = makeCompileDir(compileId); TreeLogger compileLogger = makeCompileLogger(compileDir); boolean listenerFailed = false; try { listener.startedCompile(originalModuleName, compileId, compileDir); } catch (Exception e) { compileLogger.log(TreeLogger.Type.WARN, "listener threw exception", e); listenerFailed = true; } boolean success = false; try { ModuleDef module = loadModule(compileLogger, bindingProperties); String newModuleName = module.getName(); // includes any rename moduleName.set(newModuleName); CompilerOptions options = new CompilerOptionsImpl(compileDir, newModuleName); success = new Compiler(options).run(compileLogger, module); lastBuild.set(compileDir); // makes compile log available over HTTP } finally { try { listener.finishedCompile(originalModuleName, compileId, success); } catch (Exception e) { compileLogger.log(TreeLogger.Type.WARN, "listener threw exception", e); listenerFailed = true; } } if (!success) { compileLogger.log(TreeLogger.Type.ERROR, "Compiler returned " + success); throw new UnableToCompleteException(); } long elapsedTime = System.currentTimeMillis() - startTime; compileLogger.log(TreeLogger.Type.INFO, "Compile completed in " + elapsedTime + " ms"); if (failIfListenerFails && listenerFailed) { throw new UnableToCompleteException(); } return compileDir; } synchronized CompileDir noCompile() throws UnableToCompleteException { long startTime = System.currentTimeMillis(); CompileDir compileDir = makeCompileDir(++compilesDone); TreeLogger compileLogger = makeCompileLogger(compileDir); ModuleDef module = loadModule(compileLogger, new HashMap<String, String>()); String newModuleName = module.getName(); // includes any rename. moduleName.set(newModuleName); lastBuild.set(compileDir); try { // Prepare directory. File outputDir = new File( compileDir.getWarDir().getCanonicalPath() + "/" + getModuleName()); if (!outputDir.exists()) { outputDir.mkdir(); } // Creates a "module_name.nocache.js" that just forces a recompile. String moduleScript = PageUtil.loadResource(Recompiler.class, "nomodule.nocache.js"); moduleScript = moduleScript.replace("__MODULE_NAME__", getModuleName()); PageUtil.writeFile(outputDir.getCanonicalPath() + "/" + getModuleName() + ".nocache.js", moduleScript); } catch (IOException e) { compileLogger.log(TreeLogger.Type.ERROR, "Error creating uncompiled module.", e); } long elapsedTime = System.currentTimeMillis() - startTime; compileLogger.log(TreeLogger.Type.INFO, "Module setup completed in " + elapsedTime + " ms"); return compileDir; } /** * Returns the log from the last compile. (It may be a failed build.) */ File getLastLog() { return lastBuild.get().getLogFile(); } String getModuleName() { return moduleName.get(); } ResourceLoader getResourceLoader() { return resourceLoader.get(); } private TreeLogger makeCompileLogger(CompileDir compileDir) throws UnableToCompleteException { try { PrintWriterTreeLogger fileLogger = new PrintWriterTreeLogger(compileDir.getLogFile()); fileLogger.setMaxDetail(TreeLogger.Type.INFO); return new CompositeTreeLogger(logger, fileLogger); } catch (IOException e) { logger.log(TreeLogger.ERROR, "unable to open log file: " + compileDir.getLogFile(), e); throw new UnableToCompleteException(); } } private ModuleDef loadModule(TreeLogger logger, Map<String, String> bindingProperties) throws UnableToCompleteException { // make sure we get the latest version of any modified jar ZipFileClassPathEntry.clearCache(); ResourceOracleImpl.clearCache(); ModuleDefLoader.clearModuleCache(); ResourceLoader resources = ResourceLoaders.forClassLoader(Thread.currentThread()); resources = ResourceLoaders.forPathAndFallback(sourcePath, resources); this.resourceLoader.set(resources); ModuleDef moduleDef = ModuleDefLoader.loadFromResources(logger, originalModuleName, resources, true); // We need a cross-site linker. Automatically replace the default linker. if (IFrameLinker.class.isAssignableFrom(moduleDef.getActivePrimaryLinker())) { moduleDef.addLinker("xsiframe"); } // Check that we have a compatible linker. Class<? extends Linker> linker = moduleDef.getActivePrimaryLinker(); if (! CrossSiteIframeLinker.class.isAssignableFrom(linker)) { logger.log(TreeLogger.ERROR, "linkers other than CrossSiteIFrameLinker aren't supported. Found: " + linker.getName()); throw new UnableToCompleteException(); } // Print a nice error if the superdevmode hook isn't present if (moduleDef.getProperties().find("devModeRedirectEnabled") == null) { throw new RuntimeException("devModeRedirectEnabled isn't set for module: " + moduleDef.getName()); } // Disable the redirect hook here to make sure we don't have an infinite loop. // (There is another check in the JavaScript, but just in case.) overrideConfig(moduleDef, "devModeRedirectEnabled", "false"); // Normally the GWT bootstrap script installs GWT code by calling eval() with a string of // JavaScript, so that it can control the scope that the code runs in. Sourcemaps don't seem // to be working in Chrome when we do this, so turn it off for now. // TODO(cromwellian) remove when Chrome is fixed. overrideConfig(moduleDef, "installScriptJs", "com/google/gwt/core/ext/linker/impl/installScriptDirect.js"); overrideConfig(moduleDef, "installCode", "false"); // override computeScriptBase.js to enable the "Compile" button overrideConfig(moduleDef, "computeScriptBaseJs", "com/google/gwt/dev/codeserver/computeScriptBase.js"); // Fix bug with SDM and Chrome 24+ where //@ sourceURL directives cause X-SourceMap header to be ignored // Frustratingly, Chrome won't canonicalize a relative URL overrideConfig(moduleDef, "includeSourceMapUrl", "http://" + serverPrefix + WebServer.sourceMapLocationForModule(moduleDef.getName())); // If present, set some config properties back to defaults. // (Needed for Google's server-side linker.) maybeOverrideConfig(moduleDef, "includeBootstrapInPrimaryFragment", "false"); maybeOverrideConfig(moduleDef, "permutationsJs", "com/google/gwt/core/ext/linker/impl/permutations.js"); maybeOverrideConfig(moduleDef, "propertiesJs", "com/google/gwt/core/ext/linker/impl/properties.js"); for (Map.Entry<String, String> entry : bindingProperties.entrySet()) { String propName = entry.getKey(); String propValue = entry.getValue(); logger.log(TreeLogger.Type.INFO, "binding: " + propName + "=" + propValue); overrideBinding(moduleDef, propName, propValue); } overrideBinding(moduleDef, "compiler.useSourceMaps", "true"); return moduleDef; } private static void overrideBinding(ModuleDef module, String propName, String newValue) { Property prop = module.getProperties().find(propName); if (prop instanceof BindingProperty) { BindingProperty binding = (BindingProperty) prop; if (binding.isAllowedValue(newValue) || "compiler.useSourceMaps".equals(propName)) { binding.setAllowedValues(binding.getRootCondition(), newValue); } } } private static boolean maybeOverrideConfig(ModuleDef module, String propName, String newValue) { Property prop = module.getProperties().find(propName); if (prop instanceof ConfigurationProperty) { ConfigurationProperty config = (ConfigurationProperty) prop; config.setValue(newValue); return true; } return false; } private static void overrideConfig(ModuleDef module, String propName, String newValue) { if (!maybeOverrideConfig(module, propName, newValue)) { throw new RuntimeException("not found: " + propName); } } private CompileDir makeCompileDir(int compileId) throws UnableToCompleteException { return CompileDir.create(appSpace.getCompileDir(compileId), logger); } }