1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
3 package org.jetbrains.jps.incremental.artifacts.impl;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.util.Pair;
7 import com.intellij.openapi.util.Ref;
8 import com.intellij.openapi.util.io.FileUtil;
9 import com.intellij.openapi.util.text.StringUtil;
10 import com.intellij.util.ArrayUtil;
11 import com.intellij.util.graph.CachingSemiGraph;
12 import com.intellij.util.graph.DFSTBuilder;
13 import com.intellij.util.graph.GraphGenerator;
14 import com.intellij.util.graph.InboundSemiGraph;
15 import com.intellij.util.io.ZipUtil;
16 import gnu.trove.THashSet;
17 import org.jetbrains.annotations.NonNls;
18 import org.jetbrains.annotations.NotNull;
19 import org.jetbrains.annotations.Nullable;
20 import org.jetbrains.jps.builders.BuildOutputConsumer;
21 import org.jetbrains.jps.builders.logging.ProjectBuilderLogger;
22 import org.jetbrains.jps.incremental.CompileContext;
23 import org.jetbrains.jps.incremental.FSOperations;
24 import org.jetbrains.jps.incremental.ProjectBuildException;
25 import org.jetbrains.jps.incremental.artifacts.ArtifactOutputToSourceMapping;
26 import org.jetbrains.jps.incremental.artifacts.IncArtifactBuilder;
27 import org.jetbrains.jps.incremental.artifacts.instructions.*;
28 import org.jetbrains.jps.incremental.messages.BuildMessage;
29 import org.jetbrains.jps.incremental.messages.CompilerMessage;
30 import org.jetbrains.jps.incremental.messages.ProgressMessage;
34 import java.util.jar.JarFile;
35 import java.util.jar.JarOutputStream;
36 import java.util.jar.Manifest;
37 import java.util.zip.ZipEntry;
38 import java.util.zip.ZipOutputStream;
43 public class JarsBuilder {
44 private static final Logger LOG = Logger.getInstance("#com.intellij.compiler.impl.packagingCompiler.JarsBuilder");
45 private final Set<JarInfo> myJarsToBuild;
46 private final CompileContext myContext;
47 private Map<JarInfo, File> myBuiltJars;
48 private final BuildOutputConsumer myOutputConsumer;
49 private final ArtifactOutputToSourceMapping myOutSrcMapping;
51 public JarsBuilder(Set<JarInfo> jarsToBuild, CompileContext context, BuildOutputConsumer outputConsumer,
52 ArtifactOutputToSourceMapping outSrcMapping) {
53 myOutputConsumer = outputConsumer;
54 myOutSrcMapping = outSrcMapping;
55 DependentJarsEvaluator evaluator = new DependentJarsEvaluator();
56 for (JarInfo jarInfo : jarsToBuild) {
57 evaluator.addJarWithDependencies(jarInfo);
59 myJarsToBuild = evaluator.getJars();
63 public boolean buildJars() throws IOException, ProjectBuildException {
64 myContext.processMessage(new ProgressMessage("Building archives..."));
66 final JarInfo[] sortedJars = sortJars();
67 if (sortedJars == null) {
71 myBuiltJars = new HashMap<>();
73 for (JarInfo jar : sortedJars) {
74 myContext.checkCanceled();
78 myContext.processMessage(new ProgressMessage("Copying archives..."));
82 deleteTemporaryJars();
89 private void deleteTemporaryJars() {
90 for (File file : myBuiltJars.values()) {
91 FileUtil.delete(file);
95 private void copyJars() throws IOException {
96 for (Map.Entry<JarInfo, File> entry : myBuiltJars.entrySet()) {
97 File fromFile = entry.getValue();
98 final JarInfo jarInfo = entry.getKey();
99 DestinationInfo destination = jarInfo.getDestination();
100 if (destination instanceof ExplodedDestinationInfo) {
101 File toFile = new File(FileUtil.toSystemDependentName(destination.getOutputPath()));
102 FileUtil.rename(fromFile, toFile);
108 private JarInfo[] sortJars() {
109 final DFSTBuilder<JarInfo> builder = new DFSTBuilder<>(GraphGenerator.generate(CachingSemiGraph.cache(new JarsGraph())));
110 if (!builder.isAcyclic()) {
111 final Pair<JarInfo, JarInfo> dependency = builder.getCircularDependency();
112 String message = "Cannot build: circular dependency found between '" + dependency.getFirst().getPresentableDestination() +
113 "' and '" + dependency.getSecond().getPresentableDestination() + "'";
114 myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.ERROR, message));
118 JarInfo[] jars = myJarsToBuild.toArray(new JarInfo[0]);
119 Arrays.sort(jars, builder.comparator());
120 jars = ArrayUtil.reverseArray(jars);
124 private void buildJar(final JarInfo jar) throws IOException {
125 final String emptyArchiveMessage = "Archive '" + jar.getPresentableDestination() + "' doesn't contain files so it won't be created";
126 if (jar.getContent().isEmpty()) {
127 myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.WARNING, emptyArchiveMessage));
131 myContext.processMessage(new ProgressMessage("Building " + jar.getPresentableDestination() + "..."));
132 File jarFile = FileUtil.createTempFile("artifactCompiler", "tmp");
133 myBuiltJars.put(jar, jarFile);
135 FileUtil.createParentDirs(jarFile);
136 final String targetJarPath = jar.getDestination().getOutputFilePath();
137 List<String> packedFilePaths = new ArrayList<>();
138 Manifest manifest = loadManifest(jar, packedFilePaths);
139 final JarOutputStream jarOutputStream = createJarOutputStream(jarFile, manifest);
141 final THashSet<String> writtenPaths = new THashSet<>();
143 if (manifest != null) {
144 writtenPaths.add(JarFile.MANIFEST_NAME);
147 for (Pair<String, Object> pair : jar.getContent()) {
148 final String relativePath = pair.getFirst();
149 if (pair.getSecond() instanceof ArtifactRootDescriptor) {
150 final ArtifactRootDescriptor descriptor = (ArtifactRootDescriptor)pair.getSecond();
151 final int rootIndex = descriptor.getRootIndex();
152 if (descriptor instanceof FileBasedArtifactRootDescriptor) {
153 addFileToJar(jarOutputStream, jarFile, descriptor.getRootFile(), descriptor.getFilter(), relativePath, targetJarPath, writtenPaths,
154 packedFilePaths, rootIndex);
157 final String filePath = FileUtil.toSystemIndependentName(descriptor.getRootFile().getAbsolutePath());
158 packedFilePaths.add(filePath);
159 myOutSrcMapping.appendData(targetJarPath, rootIndex, filePath);
160 extractFileAndAddToJar(jarOutputStream, (JarBasedArtifactRootDescriptor)descriptor, relativePath, writtenPaths);
164 JarInfo nestedJar = (JarInfo)pair.getSecond();
165 File nestedJarFile = myBuiltJars.get(nestedJar);
166 if (nestedJarFile != null) {
167 addFileToJar(jarOutputStream, jarFile, nestedJarFile, SourceFileFilter.ALL, relativePath, targetJarPath, writtenPaths,
168 packedFilePaths, -1);
171 LOG.debug("nested JAR file " + relativePath + " for " + jar.getPresentableDestination() + " not found");
176 if (writtenPaths.isEmpty()) {
177 myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.WARNING, emptyArchiveMessage));
181 final ProjectBuilderLogger logger = myContext.getLoggingManager().getProjectBuilderLogger();
182 if (logger.isEnabled()) {
183 logger.logCompiledPaths(packedFilePaths, IncArtifactBuilder.BUILDER_NAME, "Packing files:");
185 myOutputConsumer.registerOutputFile(new File(targetJarPath), packedFilePaths);
189 if (writtenPaths.isEmpty()) {
191 jarOutputStream.close();
193 catch (IOException ignored) {
195 FileUtil.delete(jarFile);
196 myBuiltJars.remove(jar);
200 jarOutputStream.close();
202 catch (IOException e) {
203 String messageText = "Cannot create '" + jar.getPresentableDestination() + "': " + e.getMessage();
204 myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.ERROR, messageText));
211 private static JarOutputStream createJarOutputStream(File jarFile, @Nullable Manifest manifest) throws IOException {
212 final BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(jarFile));
213 if (manifest != null) {
214 return new JarOutputStream(outputStream, manifest);
216 return new JarOutputStream(outputStream);
220 private Manifest loadManifest(JarInfo jar, List<String> packedFilePaths) throws IOException {
221 for (Pair<String, Object> pair : jar.getContent()) {
222 if (pair.getSecond() instanceof ArtifactRootDescriptor) {
223 final String rootPath = pair.getFirst();
224 if (!JarFile.MANIFEST_NAME.startsWith(rootPath)) {
227 final String manifestPath = JpsArtifactPathUtil.trimForwardSlashes(JarFile.MANIFEST_NAME.substring(rootPath.length()));
228 final ArtifactRootDescriptor descriptor = (ArtifactRootDescriptor)pair.getSecond();
229 if (descriptor instanceof FileBasedArtifactRootDescriptor) {
230 final File manifestFile = new File(descriptor.getRootFile(), manifestPath);
231 if (manifestFile.exists()) {
232 final String fullManifestPath = FileUtil.toSystemIndependentName(manifestFile.getAbsolutePath());
233 packedFilePaths.add(fullManifestPath);
234 try (FileInputStream stream = new FileInputStream(manifestFile)) {
235 return createManifest(stream, manifestFile);
240 final Ref<Manifest> manifestRef = Ref.create(null);
241 ((JarBasedArtifactRootDescriptor)descriptor).processEntries(new JarBasedArtifactRootDescriptor.EntryProcessor() {
243 public void process(@Nullable InputStream inputStream, @NotNull String relativePath, ZipEntry entry) throws IOException {
244 if (manifestRef.isNull() && relativePath.equals(manifestPath) && inputStream != null) {
245 try (InputStream stream = inputStream) {
246 manifestRef.set(createManifest(stream, descriptor.getRootFile()));
251 if (!manifestRef.isNull()) {
252 return manifestRef.get();
261 private Manifest createManifest(InputStream manifestStream, File manifestFile) {
263 return new Manifest(manifestStream);
265 catch (IOException e) {
266 myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.ERROR,
267 "Cannot create MANIFEST.MF from " + manifestFile.getAbsolutePath() + ":" + e.getMessage()));
273 private void extractFileAndAddToJar(final JarOutputStream jarOutputStream, final JarBasedArtifactRootDescriptor root,
274 final String relativeOutputPath, final Set<String> writtenPaths)
276 final long timestamp = FSOperations.lastModified(root.getRootFile());
277 root.processEntries(new JarBasedArtifactRootDescriptor.EntryProcessor() {
279 public void process(@Nullable InputStream inputStream, @NotNull String relativePath, ZipEntry entry) throws IOException {
280 String pathInJar = addParentDirectories(jarOutputStream, writtenPaths, JpsArtifactPathUtil
281 .appendToPath(relativeOutputPath, relativePath));
283 if (inputStream == null) {
284 if (!pathInJar.endsWith("/")) {
285 addDirectoryEntry(jarOutputStream, pathInJar + "/", writtenPaths);
288 else if (writtenPaths.add(pathInJar)) {
289 ZipEntry newEntry = new ZipEntry(pathInJar);
290 newEntry.setTime(timestamp);
291 if (entry.getMethod() == ZipEntry.STORED) {
292 newEntry.setMethod(ZipEntry.STORED);
293 newEntry.setSize(entry.getSize());
294 newEntry.setCrc(entry.getCrc());
296 jarOutputStream.putNextEntry(newEntry);
297 FileUtil.copy(inputStream, jarOutputStream);
299 jarOutputStream.closeEntry();
301 catch (IOException e) {
302 String messageText = "Cannot extract '" + pathInJar + "' from '" + root.getRootFile().getAbsolutePath() + "' while building '" +
303 root.getTarget().getArtifact().getName() + "' artifact: " + e.getMessage();
304 myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.ERROR, messageText));
313 private void addFileToJar(final @NotNull JarOutputStream jarOutputStream, final @NotNull File jarFile, @NotNull File file,
314 SourceFileFilter filter, @NotNull String relativePath, String targetJarPath,
315 final @NotNull Set<String> writtenPaths, List<String> packedFilePaths, final int rootIndex) throws IOException {
316 if (!file.exists() || FileUtil.isAncestor(file, jarFile, false)) {
320 relativePath = addParentDirectories(jarOutputStream, writtenPaths, relativePath);
321 addFileOrDirRecursively(jarOutputStream, file, filter, relativePath, targetJarPath, writtenPaths, packedFilePaths, rootIndex);
324 private void addFileOrDirRecursively(@NotNull ZipOutputStream jarOutputStream,
326 SourceFileFilter filter,
327 @NotNull String relativePath,
328 String targetJarPath,
329 @NotNull Set<String> writtenItemRelativePaths,
330 List<String> packedFilePaths,
331 int rootIndex) throws IOException {
332 final String filePath = FileUtil.toSystemIndependentName(file.getAbsolutePath());
333 if (!filter.accept(filePath) || !filter.shouldBeCopied(filePath, myContext.getProjectDescriptor())) {
337 if (file.isDirectory()) {
338 final String directoryPath = relativePath.length() == 0 ? "" : relativePath + "/";
339 if (!directoryPath.isEmpty()) {
340 addDirectoryEntry(jarOutputStream, directoryPath, writtenItemRelativePaths);
342 final File[] children = file.listFiles();
343 if (children != null) {
344 for (File child : children) {
345 addFileOrDirRecursively(jarOutputStream, child, filter, directoryPath + child.getName(), targetJarPath, writtenItemRelativePaths,
346 packedFilePaths, rootIndex);
352 final boolean added = ZipUtil.addFileToZip(jarOutputStream, file, relativePath, writtenItemRelativePaths, null);
353 if (rootIndex != -1) {
354 myOutSrcMapping.appendData(targetJarPath, rootIndex, filePath);
356 packedFilePaths.add(filePath);
362 private static String addParentDirectories(JarOutputStream jarOutputStream, Set<String> writtenPaths, String relativePath) throws IOException {
363 while (StringUtil.startsWithChar(relativePath, '/')) {
364 relativePath = relativePath.substring(1);
366 int i = relativePath.indexOf('/');
368 String prefix = relativePath.substring(0, i+1);
369 if (prefix.length() > 1) {
370 addDirectoryEntry(jarOutputStream, prefix, writtenPaths);
372 i = relativePath.indexOf('/', i + 1);
377 private static void addDirectoryEntry(final ZipOutputStream output, @NonNls final String relativePath, Set<String> writtenPaths) throws IOException {
378 if (!writtenPaths.add(relativePath)) return;
380 ZipEntry e = new ZipEntry(relativePath);
381 e.setMethod(ZipEntry.STORED);
384 output.putNextEntry(e);
388 private class JarsGraph implements InboundSemiGraph<JarInfo> {
391 public Collection<JarInfo> getNodes() {
392 return myJarsToBuild;
397 public Iterator<JarInfo> getIn(final JarInfo n) {
398 Set<JarInfo> ins = new HashSet<>();
399 final DestinationInfo destination = n.getDestination();
400 if (destination instanceof JarDestinationInfo) {
401 ins.add(((JarDestinationInfo)destination).getJarInfo());
403 return ins.iterator();