| /* |
| * 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); |
| } |
| } |
| } |