blob: 5a819cd2f7b9d52c5c066163753d6230c3468457 [file] [log] [blame]
/*
* 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.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.impl.PropertiesUtil;
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.CompilerContext;
import com.google.gwt.dev.CompilerOptions;
import com.google.gwt.dev.MinimalRebuildCache;
import com.google.gwt.dev.MinimalRebuildCacheManager;
import com.google.gwt.dev.NullRebuildCache;
import com.google.gwt.dev.cfg.BindingProperty;
import com.google.gwt.dev.cfg.ConfigurationProperties;
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.PropertyCombinations;
import com.google.gwt.dev.cfg.PropertyCombinations.PermutationDescription;
import com.google.gwt.dev.cfg.ResourceLoader;
import com.google.gwt.dev.cfg.ResourceLoaders;
import com.google.gwt.dev.codeserver.Job.Result;
import com.google.gwt.dev.codeserver.JobEvent.CompileStrategy;
import com.google.gwt.dev.javac.UnitCache;
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 com.google.gwt.thirdparty.guava.common.base.Charsets;
import com.google.gwt.thirdparty.guava.common.base.Joiner;
import com.google.gwt.thirdparty.guava.common.base.Objects;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.io.Files;
import com.google.gwt.thirdparty.guava.common.io.Resources;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* Recompiles a GWT module on demand.
*/
public class Recompiler {
private final OutboxDir outboxDir;
private final LauncherDir launcherDir;
private final MinimalRebuildCacheManager minimalRebuildCacheManager;
private final String inputModuleName;
private String serverPrefix;
private int compilesDone = 0;
// after renaming
private AtomicReference<String> outputModuleName = new AtomicReference<String>(null);
private final AtomicReference<CompileDir> lastBuild = new AtomicReference<CompileDir>();
private InputSummary previousInputSummary;
private CompileDir publishedCompileDir;
private final AtomicReference<ResourceLoader> resourceLoader =
new AtomicReference<ResourceLoader>();
private final CompilerContext.Builder compilerContextBuilder = new CompilerContext.Builder();
private CompilerContext compilerContext;
private final Options options;
private final UnitCache unitCache;
Recompiler(OutboxDir outboxDir, LauncherDir launcherDir,
String inputModuleName, Options options,
UnitCache unitCache, MinimalRebuildCacheManager minimalRebuildCacheManager) {
this.outboxDir = outboxDir;
this.launcherDir = launcherDir;
this.inputModuleName = inputModuleName;
this.options = options;
this.unitCache = unitCache;
this.minimalRebuildCacheManager = minimalRebuildCacheManager;
this.serverPrefix = options.getPreferredHost() + ":" + options.getPort();
compilerContext = compilerContextBuilder.build();
}
/**
* Forces the next recompile even if no input files have changed.
*/
void forceNextRecompile() {
previousInputSummary = null;
}
/**
* Compiles the first time, while Super Dev Mode is starting up.
* Either this method or {@link #initWithoutPrecompile} should be called first.
*/
synchronized Job.Result precompile(Job job) throws UnableToCompleteException {
Result result = compile(job);
job.onFinished(result);
assert result.isOk();
return result;
}
/**
* Recompiles the module.
*
* <p>Prerequisite: either {@link #precompile} or {@link #initWithoutPrecompile} should have been
* called first.
*
* <p>Sets the job's result and returns normally whether the compile succeeds or not.
*
* @param job should already be in the "in progress" state.
*/
synchronized Job.Result recompile(Job job) {
Job.Result result;
try {
result = compile(job);
} catch (UnableToCompleteException e) {
// No point in logging a stack trace for this exception
job.getLogger().log(TreeLogger.Type.WARN, "recompile failed");
result = new Result(null, null, e);
} catch (Throwable error) {
job.getLogger().log(TreeLogger.Type.WARN, "recompile failed", error);
result = new Result(null, null, error);
}
job.onFinished(result);
return result;
}
/**
* Calls the GWT compiler with the appropriate settings.
* Side-effect: a MinimalRebuildCache for the current binding properties will be found or created.
*
* @param job used for reporting progress. (Its result will not be set.)
* @return a non-error Job.Result if successful.
* @throws UnableToCompleteException for compile failures.
*/
private Job.Result compile(Job job) throws UnableToCompleteException {
assert job.wasSubmitted();
if (compilesDone == 0) {
System.setProperty("java.awt.headless", "true");
if (System.getProperty("gwt.speedtracerlog") == null) {
System.setProperty("gwt.speedtracerlog",
outboxDir.getSpeedTracerLogFile().getAbsolutePath());
}
compilerContext = compilerContextBuilder.unitCache(unitCache).build();
}
long startTime = System.currentTimeMillis();
int compileId = ++compilesDone;
CompileDir compileDir = outboxDir.makeCompileDir(job.getLogger());
TreeLogger compileLogger = makeCompileLogger(compileDir, job.getLogger());
try {
job.onStarted(compileId, compileDir);
boolean success = doCompile(compileLogger, compileDir, job);
if (!success) {
compileLogger.log(TreeLogger.Type.ERROR, "Compiler returned false");
throw new UnableToCompleteException();
}
} finally {
// Make the error log available no matter what happens
lastBuild.set(compileDir);
}
long elapsedTime = System.currentTimeMillis() - startTime;
compileLogger.log(TreeLogger.Type.INFO,
String.format("%.3fs total -- Compile completed", elapsedTime / 1000d));
return new Result(publishedCompileDir, outputModuleName.get(), null);
}
/**
* Creates a dummy output directory without compiling the module.
* Either this method or {@link #precompile} should be called first.
*/
synchronized Job.Result initWithoutPrecompile(TreeLogger logger)
throws UnableToCompleteException {
long startTime = System.currentTimeMillis();
CompileDir compileDir = outboxDir.makeCompileDir(logger);
TreeLogger compileLogger = makeCompileLogger(compileDir, logger);
ModuleDef module;
try {
module = loadModule(compileLogger);
logger.log(TreeLogger.INFO, "Loading Java files in " + inputModuleName + ".");
CompilerOptions loadOptions = new CompilerOptionsImpl(compileDir, inputModuleName, options);
compilerContext = compilerContextBuilder.options(loadOptions).unitCache(unitCache).build();
// Loads and parses all the Java files in the GWT application using the JDT.
// (This is warmup to make compiling faster later; we stop at this point to avoid
// needing to know the binding properties.)
module.getCompilationState(compileLogger, compilerContext);
setUpCompileDir(compileDir, module, compileLogger);
if (launcherDir != null) {
launcherDir.update(module, compileDir, compileLogger);
}
outputModuleName.set(module.getName());
} finally {
// Make the compile log available no matter what happens.
lastBuild.set(compileDir);
}
long elapsedTime = System.currentTimeMillis() - startTime;
compileLogger.log(TreeLogger.Type.INFO, "Module setup completed in " + elapsedTime + " ms");
return new Result(compileDir, module.getName(), null);
}
/**
* Prepares a stub compile directory.
* It will include all "public" resources and a nocache.js file that invokes the compiler.
*/
private void setUpCompileDir(CompileDir compileDir, ModuleDef module,
TreeLogger compileLogger) throws UnableToCompleteException {
try {
String currentModuleName = module.getName();
// Create the directory.
File outputDir = new File(
compileDir.getWarDir().getCanonicalPath() + "/" + currentModuleName);
if (!outputDir.exists()) {
if (!outputDir.mkdir()) {
compileLogger.log(Type.WARN, "cannot create directory: " + outputDir);
}
}
LauncherDir.writePublicResources(outputDir, module, compileLogger);
// write no cache that will inject recompile.nocache.js
String stub = LauncherDir.generateStubNocacheJs(module.getName(), options);
File noCacheJs = new File(outputDir.getCanonicalPath(), module.getName() + ".nocache.js");
Files.write(stub, noCacheJs, Charsets.UTF_8);
// Create a "module_name.recompile.nocache.js" that calculates the permutation
// and forces a recompile.
String recompileNoCache = generateModuleRecompileJs(module, compileLogger);
writeRecompileNoCacheJs(outputDir, currentModuleName, recompileNoCache, compileLogger);
} catch (IOException e) {
compileLogger.log(Type.ERROR, "Error creating stub compile directory.", e);
UnableToCompleteException wrapped = new UnableToCompleteException();
wrapped.initCause(e);
throw wrapped;
}
}
/**
* Generates the nocache.js file to use when precompile is not on.
*/
private static String generateModuleRecompileJs(ModuleDef module, TreeLogger compileLogger)
throws UnableToCompleteException {
String outputModuleName = module.getName();
try {
String templateJs = Resources.toString(
Resources.getResource(Recompiler.class, "recompile_template.js"), Charsets.UTF_8);
String propertyProviders = PropertiesUtil.generatePropertiesSnippet(module, compileLogger);
String libJs = Resources.toString(
Resources.getResource(Recompiler.class, "recompile_lib.js"), Charsets.UTF_8);
String recompileJs = Resources.toString(
Resources.getResource(Recompiler.class, "recompile_main.js"), Charsets.UTF_8);
templateJs = templateJs.replace("__MODULE_NAME__", "'" + outputModuleName + "'");
templateJs = templateJs.replace("__PROPERTY_PROVIDERS__", propertyProviders);
templateJs = templateJs.replace("__LIB_JS__", libJs);
templateJs = templateJs.replace("__MAIN__", recompileJs);
return templateJs;
} catch (IOException e) {
compileLogger.log(Type.ERROR, "Can not generate + " + outputModuleName + " recompile js", e);
throw new UnableToCompleteException();
}
}
synchronized String getRecompileJs(TreeLogger logger) throws UnableToCompleteException {
ModuleDef loadModule = loadModule(logger);
return generateModuleRecompileJs(loadModule, logger);
}
private boolean doCompile(TreeLogger compileLogger, CompileDir compileDir, Job job)
throws UnableToCompleteException {
job.onProgress("Loading modules");
CompilerOptions loadOptions = new CompilerOptionsImpl(compileDir, inputModuleName, options);
compilerContext = compilerContextBuilder.options(loadOptions).build();
ModuleDef module = loadModule(compileLogger);
// We need to generate the stub before restricting permutations
String recompileJs = generateModuleRecompileJs(module, compileLogger);
Map<String, String> bindingProperties =
restrictPermutations(compileLogger, module, job.getBindingProperties());
// Propagates module rename.
String newModuleName = module.getName();
outputModuleName.set(newModuleName);
// Check if the module definition + job specific binding property restrictions expanded to more
// than permutation.
PropertyCombinations propertyCombinations =
new PropertyCombinations(module.getProperties(), module.getActiveLinkerNames());
List<PropertyCombinations> permutationPropertySets = propertyCombinations.collapseProperties();
if (options.isIncrementalCompileEnabled() && permutationPropertySets.size() > 1) {
compileLogger.log(Type.INFO,
"Current binding properties are expanding to more than one permutation "
+ "but incremental compilation requires that each compile operate on only "
+ "one permutation.");
job.setCompileStrategy(CompileStrategy.SKIPPED);
return true;
}
PropertyCombinations permutationPropertySet = permutationPropertySets.get(0);
PermutationDescription permutationDescription =
permutationPropertySet.getPermutationDescription(0);
// Check if we can skip the compile altogether.
InputSummary inputSummary = new InputSummary(bindingProperties, module);
if (inputSummary.equals(previousInputSummary)) {
compileLogger.log(Type.INFO, "skipped compile because no input files have changed");
job.setCompileStrategy(CompileStrategy.SKIPPED);
return true;
}
// Force a recompile if we don't succeed.
forceNextRecompile();
job.onProgress("Compiling");
CompilerOptions runOptions = new CompilerOptionsImpl(compileDir, newModuleName, options);
compilerContext = compilerContextBuilder.options(runOptions).build();
MinimalRebuildCache minimalRebuildCache = new NullRebuildCache();
if (options.isIncrementalCompileEnabled()) {
// Returns a copy of the intended cache, which is safe to modify in this compile.
minimalRebuildCache =
minimalRebuildCacheManager.getCache(inputModuleName, permutationDescription);
}
job.setCompileStrategy(minimalRebuildCache.isPopulated() ? CompileStrategy.INCREMENTAL
: CompileStrategy.FULL);
boolean success = Compiler.compile(compileLogger, runOptions, minimalRebuildCache, module);
if (success) {
publishedCompileDir = compileDir;
previousInputSummary = inputSummary;
if (options.isIncrementalCompileEnabled()) {
minimalRebuildCacheManager.putCache(inputModuleName, permutationDescription,
minimalRebuildCache);
}
String moduleName = outputModuleName.get();
writeRecompileNoCacheJs(new File(publishedCompileDir.getWarDir(), moduleName), moduleName,
recompileJs, compileLogger);
if (launcherDir != null) {
launcherDir.update(module, compileDir, compileLogger);
}
}
return success;
}
private static void writeRecompileNoCacheJs(File outputDir, String moduleName, String content,
TreeLogger compileLogger) throws UnableToCompleteException {
try {
Files.write(content,
new File(outputDir.getCanonicalPath() + "/" + moduleName + ".recompile.nocache.js"),
Charsets.UTF_8);
} catch (IOException e) {
compileLogger.log(Type.ERROR, "Can not write recompile.nocache.js", e);
throw new UnableToCompleteException();
}
}
/**
* Returns the log from the last compile. (It may be a failed build.)
*/
File getLastLog() {
return lastBuild.get().getLogFile();
}
/**
* The module name that the recompiler passes as input to the GWT compiler (before renaming).
*/
public String getInputModuleName() {
return inputModuleName;
}
/**
* The module name that the GWT compiler uses in compiled output (after renaming).
*/
String getOutputModuleName() {
return outputModuleName.get();
}
ResourceLoader getResourceLoader() {
return resourceLoader.get();
}
private TreeLogger makeCompileLogger(CompileDir compileDir, TreeLogger parent)
throws UnableToCompleteException {
try {
PrintWriterTreeLogger fileLogger =
new PrintWriterTreeLogger(compileDir.getLogFile());
fileLogger.setMaxDetail(options.getLogLevel());
return new CompositeTreeLogger(parent, fileLogger);
} catch (IOException e) {
parent.log(TreeLogger.ERROR, "unable to open log file: " + compileDir.getLogFile(), e);
throw new UnableToCompleteException();
}
}
/**
* Loads the module and configures it for SuperDevMode. (Does not restrict permutations.)
*/
private ModuleDef loadModule(TreeLogger logger) throws UnableToCompleteException {
// make sure we get the latest version of any modified jar
ZipFileClassPathEntry.clearCache();
ResourceOracleImpl.clearCache();
ResourceLoader resources = ResourceLoaders.fromContextClassLoader();
resources = ResourceLoaders.forPathAndFallback(options.getSourcePath(), resources);
this.resourceLoader.set(resources);
// ModuleDefLoader.loadFromResources() checks for modified .gwt.xml files.
ModuleDef moduleDef = ModuleDefLoader.loadFromResources(
logger, inputModuleName, resources, true);
compilerContext = compilerContextBuilder.module(moduleDef).build();
// Undo all permutation restriction customizations from previous compiles.
for (BindingProperty bindingProperty : moduleDef.getProperties().getBindingProperties()) {
bindingProperty.resetGeneratedValues();
}
// A snapshot of the module's configuration before we modified it.
ConfigurationProperties config = new ConfigurationProperties(moduleDef);
// 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();
}
// Deactivate precompress linker.
if (moduleDef.deactivateLinker("precompress")) {
logger.log(TreeLogger.WARN, "Deactivated PrecompressLinker");
}
// Print a nice error if the superdevmode hook isn't present
if (config.getStrings("devModeRedirectEnabled").isEmpty()) {
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");
// Turn off "installCode" if it's on because it makes debugging harder.
// (If it's already off, don't change anything.)
if (config.getBoolean("installCode", true)) {
overrideConfig(moduleDef, "installCode", "false");
// Make sure installScriptJs is set to the default for compiling without installCode.
overrideConfig(moduleDef, "installScriptJs",
"com/google/gwt/core/ext/linker/impl/installScriptDirect.js");
}
// 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 +
SourceHandler.sourceMapLocationTemplate(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");
if (options.isIncrementalCompileEnabled()) {
// CSSResourceGenerator needs to produce stable, unique naming for its input.
// Currently on default settings CssResourceGenerator's obfuscation depends on
// whole world knowledge and thus will produce collision in obfuscated mode, since in
// incremental compiles that information is not available.
//
// TODO(dankurka): Once we do proper stable hashing of classes in CssResourceGenerator, we
// can probably replace / remove this.
maybeOverrideConfig(moduleDef, "CssResource.style", "stable");
}
overrideBinding(moduleDef, "compiler.useSourceMaps", "true");
overrideBinding(moduleDef, "compiler.useSymbolMaps", "false");
overrideBinding(moduleDef, "superdevmode", "on");
return moduleDef;
}
/**
* Restricts the compiled permutations by applying the given binding properties, if possible.
* In some cases, a different binding may be chosen instead.
*/
private Map<String, String> restrictPermutations(TreeLogger logger, ModuleDef moduleDef,
Map<String, String> bindingProperties) {
Map<String, String> chosenProps = Maps.newHashMap();
for (Map.Entry<String, String> entry : bindingProperties.entrySet()) {
String propName = entry.getKey();
String propValue = entry.getValue();
String actual = maybeSetBinding(logger, moduleDef, propName, propValue);
if (actual != null) {
chosenProps.put(propName, actual);
}
}
return chosenProps;
}
/**
* Attempts to set a binding property to the given value.
* If the value is not allowed, see if we can find a value that will work.
* There is a special case for "locale".
* @return the value actually set, or null if unable to set the property
*/
private static String maybeSetBinding(TreeLogger logger, ModuleDef module, String propName,
String newValue) {
logger = logger.branch(TreeLogger.Type.INFO, "binding: " + propName + "=" + newValue);
BindingProperty binding = module.getProperties().findBindingProp(propName);
if (binding == null) {
logger.log(TreeLogger.Type.WARN, "undefined property: '" + propName + "'");
return null;
}
if (!binding.isAllowedValue(newValue)) {
String[] allowedValues = binding.getAllowedValues(binding.getRootCondition());
logger.log(TreeLogger.Type.WARN, "property '" + propName +
"' cannot be set to '" + newValue + "'");
logger.log(TreeLogger.Type.INFO, "allowed values: " +
Joiner.on(", ").join(allowedValues));
// See if we can fall back on a reasonable default.
if (allowedValues.length == 1) {
// There is only one possibility, so use it.
newValue = allowedValues[0];
} else if (binding.getName().equals("locale")) {
// TODO: come up with a more general solution. Perhaps fail
// the compile and give the user a way to override the property?
newValue = chooseDefault(binding, "default", "en", "en_US");
} else {
// There is more than one. Continue and possibly compile multiple permutations.
logger.log(TreeLogger.Type.INFO, "continuing without " + propName +
". Sourcemaps may not work.");
return null;
}
logger.log(TreeLogger.Type.INFO, "recovered with " + propName + "=" + newValue);
}
binding.setRootGeneratedValues(newValue);
return newValue;
}
private static String chooseDefault(BindingProperty property, String... candidates) {
for (String candidate : candidates) {
if (property.isAllowedValue(candidate)) {
return candidate;
}
}
return property.getFirstAllowedValue();
}
/**
* Sets a binding even if it's set to a different value in the GWT application.
*/
private static void overrideBinding(ModuleDef module, String propName, String newValue) {
BindingProperty binding = module.getProperties().findBindingProp(propName);
if (binding != null) {
// This sets both allowed and generated values, which is needed since the module
// might have explicitly disallowed the value.
// It persists over multiple compiles but that's okay since we set it the same way
// every time.
binding.setValues(binding.getRootCondition(), newValue);
}
}
private static boolean maybeOverrideConfig(ModuleDef module, String propName, String newValue) {
ConfigurationProperty config = module.getProperties().findConfigProp(propName);
if (config != null) {
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);
}
}
/**
* Summarizes the inputs to a GWT compile. (Immutable.)
* Two summaries should be equal if the compiler's inputs are equal (with high probability).
*/
private static class InputSummary {
private final ImmutableMap<String, String> bindingProperties;
private final long moduleLastModified;
private final long resourcesLastModified;
private final long filenameHash;
InputSummary(Map<String, String> bindingProperties, ModuleDef module) {
this.bindingProperties = ImmutableMap.copyOf(bindingProperties);
this.moduleLastModified = module.lastModified();
this.resourcesLastModified = module.getResourceLastModified();
this.filenameHash = module.getInputFilenameHash();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof InputSummary) {
InputSummary other = (InputSummary) obj;
return bindingProperties.equals(other.bindingProperties) &&
moduleLastModified == other.moduleLastModified &&
resourcesLastModified == other.resourcesLastModified &&
filenameHash == other.filenameHash;
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(bindingProperties, moduleLastModified, resourcesLastModified,
filenameHash);
}
}
}