blob: baed34248f469d0dffdce301c630bef17e382c2b [file] [log] [blame]
/*
* 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);
}
}
}