[duplicates] enable duplicates analysis in PyCharm/WebStorm/PhpStorm/RubyMine
[idea/community.git] / jps / jps-builders / src / org / jetbrains / jps / incremental / artifacts / impl / JarsBuilder.java
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.
2
3 package org.jetbrains.jps.incremental.artifacts.impl;
4
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;
31
32 import java.io.*;
33 import java.util.*;
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;
39
40 /**
41  * @author nik
42  */
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;
50
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);
58     }
59     myJarsToBuild = evaluator.getJars();
60     myContext = context;
61   }
62
63   public boolean buildJars() throws IOException, ProjectBuildException {
64     myContext.processMessage(new ProgressMessage("Building archives..."));
65
66     final JarInfo[] sortedJars = sortJars();
67     if (sortedJars == null) {
68       return false;
69     }
70
71     myBuiltJars = new HashMap<>();
72     try {
73       for (JarInfo jar : sortedJars) {
74         myContext.checkCanceled();
75         buildJar(jar);
76       }
77
78       myContext.processMessage(new ProgressMessage("Copying archives..."));
79       copyJars();
80     }
81     finally {
82       deleteTemporaryJars();
83     }
84
85
86     return true;
87   }
88
89   private void deleteTemporaryJars() {
90     for (File file : myBuiltJars.values()) {
91       FileUtil.delete(file);
92     }
93   }
94
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);
103       }
104     }
105   }
106
107   @Nullable
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));
115       return null;
116     }
117
118     JarInfo[] jars = myJarsToBuild.toArray(new JarInfo[0]);
119     Arrays.sort(jars, builder.comparator());
120     jars = ArrayUtil.reverseArray(jars);
121     return jars;
122   }
123
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));
128       return;
129     }
130
131     myContext.processMessage(new ProgressMessage("Building " + jar.getPresentableDestination() + "..."));
132     File jarFile = FileUtil.createTempFile("artifactCompiler", "tmp");
133     myBuiltJars.put(jar, jarFile);
134
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);
140
141     final THashSet<String> writtenPaths = new THashSet<>();
142     try {
143       if (manifest != null) {
144         writtenPaths.add(JarFile.MANIFEST_NAME);
145       }
146
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);
155           }
156           else {
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);
161           }
162         }
163         else {
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);
169           }
170           else {
171             LOG.debug("nested JAR file " + relativePath + " for " + jar.getPresentableDestination() + " not found");
172           }
173         }
174       }
175
176       if (writtenPaths.isEmpty()) {
177         myContext.processMessage(new CompilerMessage(IncArtifactBuilder.BUILDER_NAME, BuildMessage.Kind.WARNING, emptyArchiveMessage));
178         return;
179       }
180
181       final ProjectBuilderLogger logger = myContext.getLoggingManager().getProjectBuilderLogger();
182       if (logger.isEnabled()) {
183         logger.logCompiledPaths(packedFilePaths, IncArtifactBuilder.BUILDER_NAME, "Packing files:");
184       }
185       myOutputConsumer.registerOutputFile(new File(targetJarPath), packedFilePaths);
186
187     }
188     finally {
189       if (writtenPaths.isEmpty()) {
190         try {
191           jarOutputStream.close();
192         }
193         catch (IOException ignored) {
194         }
195         FileUtil.delete(jarFile);
196         myBuiltJars.remove(jar);
197       }
198       else {
199         try {
200           jarOutputStream.close();
201         }
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));
205           LOG.debug(e);
206         }
207       }
208     }
209   }
210
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);
215     }
216     return new JarOutputStream(outputStream);
217   }
218
219   @Nullable
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)) {
225           continue;
226         }
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);
236             }
237           }
238         }
239         else {
240           final Ref<Manifest> manifestRef = Ref.create(null);
241           ((JarBasedArtifactRootDescriptor)descriptor).processEntries(new JarBasedArtifactRootDescriptor.EntryProcessor() {
242             @Override
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()));
247                 }
248               }
249             }
250           });
251           if (!manifestRef.isNull()) {
252             return manifestRef.get();
253           }
254         }
255       }
256     }
257     return null;
258   }
259
260   @Nullable
261   private Manifest createManifest(InputStream manifestStream, File manifestFile) {
262     try {
263       return new Manifest(manifestStream);
264     }
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()));
268       LOG.debug(e);
269       return null;
270     }
271   }
272
273   private void extractFileAndAddToJar(final JarOutputStream jarOutputStream, final JarBasedArtifactRootDescriptor root,
274                                       final String relativeOutputPath, final Set<String> writtenPaths)
275     throws IOException {
276     final long timestamp = FSOperations.lastModified(root.getRootFile());
277     root.processEntries(new JarBasedArtifactRootDescriptor.EntryProcessor() {
278       @Override
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));
282
283         if (inputStream == null) {
284           if (!pathInJar.endsWith("/")) {
285             addDirectoryEntry(jarOutputStream, pathInJar + "/", writtenPaths);
286           }
287         }
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());
295           }
296           jarOutputStream.putNextEntry(newEntry);
297           FileUtil.copy(inputStream, jarOutputStream);
298           try {
299             jarOutputStream.closeEntry();
300           }
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));
305             LOG.debug(e);
306           }
307         }
308       }
309     });
310
311   }
312
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)) {
317       return;
318     }
319
320     relativePath = addParentDirectories(jarOutputStream, writtenPaths, relativePath);
321     addFileOrDirRecursively(jarOutputStream, file, filter, relativePath, targetJarPath, writtenPaths, packedFilePaths, rootIndex);
322   }
323
324   private void addFileOrDirRecursively(@NotNull ZipOutputStream jarOutputStream,
325                                        @NotNull File file,
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())) {
334       return;
335     }
336
337     if (file.isDirectory()) {
338       final String directoryPath = relativePath.length() == 0 ? "" : relativePath + "/";
339       if (!directoryPath.isEmpty()) {
340         addDirectoryEntry(jarOutputStream, directoryPath, writtenItemRelativePaths);
341       }
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);
347         }
348       }
349       return;
350     }
351
352     final boolean added = ZipUtil.addFileToZip(jarOutputStream, file, relativePath, writtenItemRelativePaths, null);
353     if (rootIndex != -1) {
354       myOutSrcMapping.appendData(targetJarPath, rootIndex, filePath);
355       if (added) {
356         packedFilePaths.add(filePath);
357       }
358     }
359   }
360
361
362   private static String addParentDirectories(JarOutputStream jarOutputStream, Set<String> writtenPaths, String relativePath) throws IOException {
363     while (StringUtil.startsWithChar(relativePath, '/')) {
364       relativePath = relativePath.substring(1);
365     }
366     int i = relativePath.indexOf('/');
367     while (i != -1) {
368       String prefix = relativePath.substring(0, i+1);
369       if (prefix.length() > 1) {
370         addDirectoryEntry(jarOutputStream, prefix, writtenPaths);
371       }
372       i = relativePath.indexOf('/', i + 1);
373     }
374     return relativePath;
375   }
376
377   private static void addDirectoryEntry(final ZipOutputStream output, @NonNls final String relativePath, Set<String> writtenPaths) throws IOException {
378     if (!writtenPaths.add(relativePath)) return;
379
380     ZipEntry e = new ZipEntry(relativePath);
381     e.setMethod(ZipEntry.STORED);
382     e.setSize(0);
383     e.setCrc(0);
384     output.putNextEntry(e);
385     output.closeEntry();
386   }
387
388   private class JarsGraph implements InboundSemiGraph<JarInfo> {
389     @Override
390     @NotNull
391     public Collection<JarInfo> getNodes() {
392       return myJarsToBuild;
393     }
394
395     @NotNull
396     @Override
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());
402       }
403       return ins.iterator();
404     }
405   }
406 }