| /* |
| * Copyright 2013 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; |
| |
| 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.ArtifactSet; |
| import com.google.gwt.core.ext.linker.EmittedArtifact; |
| import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility; |
| import com.google.gwt.core.linker.SymbolMapsLinker.SourceMapArtifact; |
| import com.google.gwt.dev.Link.LinkOptions; |
| import com.google.gwt.dev.cfg.ResourceLoader; |
| import com.google.gwt.dev.json.JsonArray; |
| import com.google.gwt.dev.json.JsonException; |
| import com.google.gwt.dev.json.JsonObject; |
| import com.google.gwt.dev.util.OutputFileSet; |
| import com.google.gwt.thirdparty.guava.common.collect.Sets; |
| import com.google.gwt.thirdparty.guava.common.io.ByteStreams; |
| import com.google.gwt.thirdparty.guava.common.io.Resources; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.net.URL; |
| import java.util.LinkedHashSet; |
| import java.util.Set; |
| |
| /** |
| * SourceSaver writes source code useful for a debugger. |
| */ |
| class SourceSaver { |
| |
| /** |
| * Copies all source files referenced in at least one sourcemap to the appropriate destination. |
| * |
| * @param artifacts contains the sourcemaps we need source code for |
| * and also generated source code to copy. |
| * @param loader contains the non-generated source code |
| * @param modulePrefix a prefix to add to a source file's path when writing it. |
| */ |
| static void save(TreeLogger logger, ArtifactSet artifacts, ResourceLoader loader, |
| LinkOptions options, String modulePrefix, OutputFileSet extraFileSet) |
| throws IOException, UnableToCompleteException { |
| |
| // First scan artifacts for useful files. |
| |
| Set<EmittedArtifact> genFiles = new LinkedHashSet<EmittedArtifact>(); |
| Set<EmittedArtifact> sourceMaps = new LinkedHashSet<EmittedArtifact>(); |
| for (EmittedArtifact candidate : artifacts.find(EmittedArtifact.class)) { |
| |
| if (candidate.getVisibility() == Visibility.Source) { |
| genFiles.add(candidate); |
| continue; |
| } |
| |
| boolean isSourceMap = |
| SourceMapArtifact.isSourceMapFile.matcher(candidate.getPartialPath()).find(); |
| if (isSourceMap) { |
| sourceMaps.add(candidate); |
| } |
| } |
| |
| if (sourceMaps.isEmpty()) { |
| logger.log(Type.WARN, "Not saving source because sourcemaps weren't generated. " + |
| "Hint: set compiler.useSourceMaps."); |
| return; |
| } |
| |
| boolean saveInExtras = options.getSaveSourceOutput() == null || |
| options.getSaveSourceOutput().equals(options.getExtraDir()); |
| |
| OutputFileSet out; |
| if (saveInExtras) { |
| out = extraFileSet; |
| logger.log(Type.INFO, "Saving source with extras"); |
| } else { |
| out = Link.chooseOutputFileSet(options.getSaveSourceOutput(), modulePrefix); |
| logger.log(Type.INFO, "Saving source to " + options.getSaveSourceOutput()); |
| } |
| |
| try { |
| copySources(logger, sourceMaps, genFiles, loader, out, "src/"); |
| } finally { |
| if (!saveInExtras) { |
| out.close(); |
| } |
| } |
| } |
| |
| private static void copySources(TreeLogger logger, |
| Set<EmittedArtifact> sourceMaps, |
| Set<EmittedArtifact> genFiles, ResourceLoader loader, |
| OutputFileSet dest, String destPrefix) |
| throws UnableToCompleteException { |
| |
| Set<String> filesInSourceMap = getSourcePaths(logger, sourceMaps); |
| |
| // All files in the source map should be either be resources (input source files) or |
| // in the ArtifactSet (generated files). Try both places and log a warning if we |
| // can't find it anywhere. |
| |
| // First, copy input source files.. |
| Set<String> remainingFiles = Sets.newLinkedHashSet(); |
| for (String path : filesInSourceMap) { |
| try { |
| if (!copySourceFile(path, loader, dest, destPrefix)) { |
| remainingFiles.add(path); |
| } |
| } catch (IOException e) { |
| logger.log(Type.ERROR, "Unable to copy source file: " + path, e); |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| // Next, copy generated files. |
| for (EmittedArtifact candidate : genFiles) { |
| if (!remainingFiles.contains(candidate.getPartialPath())) { |
| // This file was generated but not used according to the sourcemap. |
| // Perhaps an interface? Anyway we don't need it for debugging. |
| continue; |
| } |
| copyGeneratedFile(logger, candidate, dest, destPrefix); |
| remainingFiles.remove(candidate.getPartialPath()); |
| } |
| |
| // Nothing should be left. If there is, log a warning. |
| if (!remainingFiles.isEmpty()) { |
| logger.log(Type.WARN, "Unable to find all source code needed by debuggers. " + |
| remainingFiles.size() + " files from sourcemaps weren't found."); |
| if (logger.isLoggable(Type.DEBUG)) { |
| TreeLogger missing = logger.branch(Type.DEBUG, "Missing files:"); |
| int filesPrinted = 0; |
| for (String path : remainingFiles) { |
| if (filesPrinted >= 100) { |
| missing.log(Type.DEBUG, "(truncated)"); |
| break; |
| } |
| missing.log(Type.DEBUG, path); |
| filesPrinted++; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Finds the path of each source file that contributed to at least one sourcemap. |
| */ |
| private static Set<String> getSourcePaths(TreeLogger logger, Set<EmittedArtifact> sourceMaps) |
| throws UnableToCompleteException { |
| Set<String> sourceFiles = new LinkedHashSet<String>(); |
| for (EmittedArtifact map : sourceMaps) { |
| // TODO maybe improve performance by not re-reading the sourcemap files. |
| // (We'd need another way for SourceMapRecorder to pass the list of files here.) |
| JsonObject json = loadSourceMap(logger, map); |
| JsonArray sources = json.get("sources").asArray(); |
| for (int i = 0; i < sources.getLength(); i++) { |
| sourceFiles.add(sources.get(i).asString().getString()); |
| } |
| } |
| return sourceFiles; |
| } |
| |
| /** |
| * Reads a sourcemap as a JSON object. |
| */ |
| private static JsonObject loadSourceMap(TreeLogger logger, EmittedArtifact sourceMap) |
| throws UnableToCompleteException { |
| JsonObject json; |
| try { |
| InputStream bytes = sourceMap.getContents(logger); |
| try { |
| json = JsonObject.parse(new InputStreamReader(bytes)); |
| } finally { |
| bytes.close(); |
| } |
| } catch (JsonException e) { |
| logger.log(Type.ERROR, "Unable to parse sourcemap: " + sourceMap.getPartialPath(), e); |
| throw new UnableToCompleteException(); |
| } catch (IOException e) { |
| logger.log(Type.ERROR, "Unable to read sourcemap: " + sourceMap.getPartialPath(), e); |
| throw new UnableToCompleteException(); |
| } |
| return json; |
| } |
| |
| /** |
| * Copies a source file from the module to a directory or jar. |
| * Returns false if the source file wasn't found. |
| */ |
| private static boolean copySourceFile(String path, ResourceLoader loader, |
| OutputFileSet dest, String destPrefix) throws IOException { |
| |
| URL resource = loader.getResource(path); |
| if (resource == null) { |
| return false; |
| } |
| |
| try (InputStream resourceAsStream = Resources.asByteSource(resource).openStream(); |
| OutputStream out = dest.openForWrite(destPrefix + path);) { |
| ByteStreams.copy(resourceAsStream, out); |
| } |
| |
| return true; |
| } |
| |
| private static void copyGeneratedFile(TreeLogger log, EmittedArtifact src, |
| OutputFileSet dest, String destPrefix) throws UnableToCompleteException { |
| |
| String newPath = destPrefix + src.getPartialPath(); |
| try { |
| OutputStream out = dest.openForWrite(newPath, src.getLastModified()); |
| try { |
| src.writeTo(log, out); |
| } finally { |
| out.close(); |
| } |
| } catch (IOException e) { |
| log.log(TreeLogger.WARN, "Error emitting artifact: " + newPath, e); |
| } |
| } |
| } |