blob: c5ce16567be490d24eb7fa7951c5edc76b080e9d [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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 org.jetbrains.jps.incremental.artifacts.impl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileSystemUtil;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.graph.CachingSemiGraph;
import com.intellij.util.graph.DFSTBuilder;
import com.intellij.util.graph.GraphGenerator;
import com.intellij.util.io.ZipUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.builders.BuildOutputConsumer;
import org.jetbrains.jps.builders.logging.ProjectBuilderLogger;
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.ProjectBuildException;
import org.jetbrains.jps.incremental.artifacts.ArtifactOutputToSourceMapping;
import org.jetbrains.jps.incremental.artifacts.IncArtifactBuilder;
import org.jetbrains.jps.incremental.artifacts.instructions.*;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.incremental.messages.ProgressMessage;
import java.io.*;
import java.util.*;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @author nik
*/
public class JarsBuilder {
private static final Logger LOG = Logger.getInstance("#com.intellij.compiler.impl.packagingCompiler.JarsBuilder");
private final Set<JarInfo> myJarsToBuild;
private final CompileContext myContext;
private Map<JarInfo, File> myBuiltJars;
private final BuildOutputConsumer myOutputConsumer;
private final ArtifactOutputToSourceMapping myOutSrcMapping;
public JarsBuilder(Set<JarInfo> jarsToBuild, CompileContext context, BuildOutputConsumer outputConsumer,
ArtifactOutputToSourceMapping outSrcMapping) {
myOutputConsumer = outputConsumer;
myOutSrcMapping = outSrcMapping;
DependentJarsEvaluator evaluator = new DependentJarsEvaluator();
for (JarInfo jarInfo : jarsToBuild) {
evaluator.addJarWithDependencies(jarInfo);
}
myJarsToBuild = evaluator.getJars();
myContext = context;
}
public boolean buildJars() throws IOException, ProjectBuildException {
myContext.processMessage(new ProgressMessage("Building archives..."));
final JarInfo[] sortedJars = sortJars();
if (sortedJars == null) {
return false;
}
myBuiltJars = new HashMap<JarInfo, File>();
try {
for (JarInfo jar : sortedJars) {
myContext.checkCanceled();
buildJar(jar);
}
myContext.processMessage(new ProgressMessage("Copying archives..."));
copyJars();
}
finally {
deleteTemporaryJars();
}
return true;
}
private void deleteTemporaryJars() {
for (File file : myBuiltJars.values()) {
FileUtil.delete(file);
}
}
private void copyJars() throws IOException {
for (Map.Entry<JarInfo, File> entry : myBuiltJars.entrySet()) {
File fromFile = entry.getValue();
final JarInfo jarInfo = entry.getKey();
DestinationInfo destination = jarInfo.getDestination();
if (destination instanceof ExplodedDestinationInfo) {
File toFile = new File(FileUtil.toSystemDependentName(destination.getOutputPath()));
FileUtil.rename(fromFile, toFile);
}
}
}
@Nullable
private JarInfo[] sortJars() {
final DFSTBuilder<JarInfo> builder = new DFSTBuilder<JarInfo>(GraphGenerator.create(CachingSemiGraph.create(new JarsGraph())));
if (!builder.isAcyclic()) {
final Pair<JarInfo, JarInfo> dependency = builder.getCircularDependency();
String message = "Cannot build: circular dependency found between '" + dependency.getFirst().getPresentableDestination() +
"' and '" + dependency.getSecond().getPresentableDestination() + "'";
myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.ERROR, message));
return null;
}
JarInfo[] jars = myJarsToBuild.toArray(new JarInfo[myJarsToBuild.size()]);
Arrays.sort(jars, builder.comparator());
jars = ArrayUtil.reverseArray(jars);
return jars;
}
private void buildJar(final JarInfo jar) throws IOException {
final String emptyArchiveMessage = "Archive '" + jar.getPresentableDestination() + "' doesn't contain files so it won't be created";
if (jar.getContent().isEmpty()) {
myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.WARNING, emptyArchiveMessage));
return;
}
myContext.processMessage(new ProgressMessage("Building " + jar.getPresentableDestination() + "..."));
File jarFile = FileUtil.createTempFile("artifactCompiler", "tmp");
myBuiltJars.put(jar, jarFile);
FileUtil.createParentDirs(jarFile);
final String targetJarPath = jar.getDestination().getOutputFilePath();
List<String> packedFilePaths = new ArrayList<String>();
Manifest manifest = loadManifest(jar, packedFilePaths);
final JarOutputStream jarOutputStream = createJarOutputStream(jarFile, manifest);
final THashSet<String> writtenPaths = new THashSet<String>();
try {
if (manifest != null) {
writtenPaths.add(JarFile.MANIFEST_NAME);
}
for (Pair<String, Object> pair : jar.getContent()) {
final String relativePath = pair.getFirst();
if (pair.getSecond() instanceof ArtifactRootDescriptor) {
final ArtifactRootDescriptor descriptor = (ArtifactRootDescriptor)pair.getSecond();
final int rootIndex = descriptor.getRootIndex();
if (descriptor instanceof FileBasedArtifactRootDescriptor) {
addFileToJar(jarOutputStream, jarFile, descriptor.getRootFile(), descriptor.getFilter(), relativePath, targetJarPath, writtenPaths,
packedFilePaths, rootIndex);
}
else {
final String filePath = FileUtil.toSystemIndependentName(descriptor.getRootFile().getAbsolutePath());
packedFilePaths.add(filePath);
myOutSrcMapping.appendData(targetJarPath, rootIndex, filePath);
extractFileAndAddToJar(jarOutputStream, (JarBasedArtifactRootDescriptor)descriptor, relativePath, writtenPaths);
}
}
else {
JarInfo nestedJar = (JarInfo)pair.getSecond();
File nestedJarFile = myBuiltJars.get(nestedJar);
if (nestedJarFile != null) {
addFileToJar(jarOutputStream, jarFile, nestedJarFile, SourceFileFilter.ALL, relativePath, targetJarPath, writtenPaths,
packedFilePaths, -1);
}
else {
LOG.debug("nested JAR file " + relativePath + " for " + jar.getPresentableDestination() + " not found");
}
}
}
if (writtenPaths.isEmpty()) {
myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.WARNING, emptyArchiveMessage));
return;
}
final ProjectBuilderLogger logger = myContext.getLoggingManager().getProjectBuilderLogger();
if (logger.isEnabled()) {
logger.logCompiledPaths(packedFilePaths, IncArtifactBuilder.BUILDER_NAME, "Packing files:");
}
myOutputConsumer.registerOutputFile(new File(targetJarPath), packedFilePaths);
}
finally {
if (writtenPaths.isEmpty()) {
try {
jarOutputStream.close();
}
catch (IOException ignored) {
}
FileUtil.delete(jarFile);
myBuiltJars.remove(jar);
}
else {
jarOutputStream.close();
}
}
}
private static JarOutputStream createJarOutputStream(File jarFile, @Nullable Manifest manifest) throws IOException {
final BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(jarFile));
if (manifest != null) {
return new JarOutputStream(outputStream, manifest);
}
return new JarOutputStream(outputStream);
}
@Nullable
private Manifest loadManifest(JarInfo jar, List<String> packedFilePaths) throws IOException {
for (Pair<String, Object> pair : jar.getContent()) {
if (pair.getSecond() instanceof ArtifactRootDescriptor) {
final String rootPath = pair.getFirst();
if (!JarFile.MANIFEST_NAME.startsWith(rootPath)) {
continue;
}
final String manifestPath = JpsArtifactPathUtil.trimForwardSlashes(JarFile.MANIFEST_NAME.substring(rootPath.length()));
final ArtifactRootDescriptor descriptor = (ArtifactRootDescriptor)pair.getSecond();
if (descriptor instanceof FileBasedArtifactRootDescriptor) {
final File manifestFile = new File(descriptor.getRootFile(), manifestPath);
if (manifestFile.exists()) {
final String fullManifestPath = FileUtil.toSystemIndependentName(manifestFile.getAbsolutePath());
packedFilePaths.add(fullManifestPath);
//noinspection IOResourceOpenedButNotSafelyClosed
return createManifest(new FileInputStream(manifestFile), manifestFile);
}
}
else {
final Ref<Manifest> manifestRef = Ref.create(null);
((JarBasedArtifactRootDescriptor)descriptor).processEntries(new JarBasedArtifactRootDescriptor.EntryProcessor() {
@Override
public void process(@Nullable InputStream inputStream, @NotNull String relativePath, ZipEntry entry) throws IOException {
if (manifestRef.isNull() && relativePath.equals(manifestPath) && inputStream != null) {
manifestRef.set(createManifest(inputStream, descriptor.getRootFile()));
}
}
});
if (!manifestRef.isNull()) {
return manifestRef.get();
}
}
}
}
return null;
}
@Nullable
private Manifest createManifest(InputStream manifestStream, File manifestFile) {
try {
try {
return new Manifest(manifestStream);
}
finally {
manifestStream.close();
}
}
catch (IOException e) {
myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.ERROR,
"Cannot create MANIFEST.MF from " + manifestFile.getAbsolutePath() + ":" + e.getMessage()));
LOG.debug(e);
return null;
}
}
private static void extractFileAndAddToJar(final JarOutputStream jarOutputStream, final JarBasedArtifactRootDescriptor root,
final String relativeOutputPath, final Set<String> writtenPaths)
throws IOException {
final long timestamp = FileSystemUtil.lastModified(root.getRootFile());
root.processEntries(new JarBasedArtifactRootDescriptor.EntryProcessor() {
@Override
public void process(@Nullable InputStream inputStream, @NotNull String relativePath, ZipEntry entry) throws IOException {
String pathInJar = addParentDirectories(jarOutputStream, writtenPaths, JpsArtifactPathUtil
.appendToPath(relativeOutputPath, relativePath));
if (inputStream == null) {
addDirectoryEntry(jarOutputStream, pathInJar + "/", writtenPaths);
}
else if (writtenPaths.add(pathInJar)) {
ZipEntry newEntry = new ZipEntry(pathInJar);
newEntry.setTime(timestamp);
if (entry.getMethod() == ZipEntry.STORED) {
newEntry.setMethod(ZipEntry.STORED);
newEntry.setSize(entry.getSize());
newEntry.setCrc(entry.getCrc());
}
jarOutputStream.putNextEntry(newEntry);
FileUtil.copy(inputStream, jarOutputStream);
jarOutputStream.closeEntry();
}
}
});
}
private void addFileToJar(final @NotNull JarOutputStream jarOutputStream, final @NotNull File jarFile, @NotNull File file,
SourceFileFilter filter, @NotNull String relativePath, String targetJarPath,
final @NotNull Set<String> writtenPaths, List<String> packedFilePaths, final int rootIndex) throws IOException {
if (!file.exists() || FileUtil.isAncestor(file, jarFile, false)) {
return;
}
relativePath = addParentDirectories(jarOutputStream, writtenPaths, relativePath);
addFileOrDirRecursively(jarOutputStream, file, filter, relativePath, targetJarPath, writtenPaths, packedFilePaths, rootIndex);
}
private void addFileOrDirRecursively(@NotNull ZipOutputStream jarOutputStream,
@NotNull File file,
SourceFileFilter filter,
@NotNull String relativePath,
String targetJarPath,
@NotNull Set<String> writtenItemRelativePaths,
List<String> packedFilePaths,
int rootIndex) throws IOException {
final String filePath = FileUtil.toSystemIndependentName(file.getAbsolutePath());
if (!filter.accept(filePath) || !filter.shouldBeCopied(filePath, myContext.getProjectDescriptor())) {
return;
}
if (file.isDirectory()) {
final String directoryPath = relativePath.length() == 0 ? "" : relativePath + "/";
if (!directoryPath.isEmpty()) {
addDirectoryEntry(jarOutputStream, directoryPath, writtenItemRelativePaths);
}
final File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
addFileOrDirRecursively(jarOutputStream, child, filter, directoryPath + child.getName(), targetJarPath, writtenItemRelativePaths,
packedFilePaths, rootIndex);
}
}
return;
}
final boolean added = ZipUtil.addFileToZip(jarOutputStream, file, relativePath, writtenItemRelativePaths, null);
if (rootIndex != -1) {
myOutSrcMapping.appendData(targetJarPath, rootIndex, filePath);
if (added) {
packedFilePaths.add(filePath);
}
}
}
private static String addParentDirectories(JarOutputStream jarOutputStream, Set<String> writtenPaths, String relativePath) throws IOException {
while (StringUtil.startsWithChar(relativePath, '/')) {
relativePath = relativePath.substring(1);
}
int i = relativePath.indexOf('/');
while (i != -1) {
String prefix = relativePath.substring(0, i+1);
if (prefix.length() > 1) {
addDirectoryEntry(jarOutputStream, prefix, writtenPaths);
}
i = relativePath.indexOf('/', i + 1);
}
return relativePath;
}
private static void addDirectoryEntry(final ZipOutputStream output, @NonNls final String relativePath, Set<String> writtenPaths) throws IOException {
if (!writtenPaths.add(relativePath)) return;
ZipEntry e = new ZipEntry(relativePath);
e.setMethod(ZipEntry.STORED);
e.setSize(0);
e.setCrc(0);
output.putNextEntry(e);
output.closeEntry();
}
private class JarsGraph implements GraphGenerator.SemiGraph<JarInfo> {
public Collection<JarInfo> getNodes() {
return myJarsToBuild;
}
public Iterator<JarInfo> getIn(final JarInfo n) {
Set<JarInfo> ins = new HashSet<JarInfo>();
final DestinationInfo destination = n.getDestination();
if (destination instanceof JarDestinationInfo) {
ins.add(((JarDestinationInfo)destination).getJarInfo());
}
return ins.iterator();
}
}
}