]> git.argeo.org Git - cc0/argeo-build.git/blob - Repackage.java
82a24f0b9a7bb3d093443797aff9ef91d5ed6b90
[cc0/argeo-build.git] / Repackage.java
1 package org.argeo.build;
2
3 import static java.lang.System.Logger.Level.DEBUG;
4 import static java.lang.System.Logger.Level.ERROR;
5 import static java.lang.System.Logger.Level.INFO;
6 import static java.lang.System.Logger.Level.TRACE;
7 import static java.lang.System.Logger.Level.WARNING;
8 import static java.nio.file.FileVisitResult.CONTINUE;
9 import static java.util.jar.Attributes.Name.MANIFEST_VERSION;
10 import static org.argeo.build.Repackage.ManifestConstants.ARGEO_ORIGIN_M2;
11 import static org.argeo.build.Repackage.ManifestConstants.ARGEO_ORIGIN_M2_MERGE;
12 import static org.argeo.build.Repackage.ManifestConstants.ARGEO_ORIGIN_M2_REPO;
13 import static org.argeo.build.Repackage.ManifestConstants.ARGEO_ORIGIN_URI;
14 import static org.argeo.build.Repackage.ManifestConstants.BUNDLE_LICENSE;
15 import static org.argeo.build.Repackage.ManifestConstants.BUNDLE_SYMBOLICNAME;
16 import static org.argeo.build.Repackage.ManifestConstants.BUNDLE_VERSION;
17 import static org.argeo.build.Repackage.ManifestConstants.ECLIPSE_SOURCE_BUNDLE;
18 import static org.argeo.build.Repackage.ManifestConstants.EXPORT_PACKAGE;
19 import static org.argeo.build.Repackage.ManifestConstants.IMPORT_PACKAGE;
20 import static org.argeo.build.Repackage.ManifestConstants.SPDX_LICENSE_IDENTIFIER;
21
22 import java.io.BufferedWriter;
23 import java.io.File;
24 import java.io.FileNotFoundException;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.lang.System.Logger;
29 import java.net.MalformedURLException;
30 import java.net.URL;
31 import java.nio.charset.StandardCharsets;
32 import java.nio.file.DirectoryStream;
33 import java.nio.file.FileSystem;
34 import java.nio.file.FileSystems;
35 import java.nio.file.FileVisitResult;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.PathMatcher;
39 import java.nio.file.Paths;
40 import java.nio.file.SimpleFileVisitor;
41 import java.nio.file.StandardOpenOption;
42 import java.nio.file.attribute.BasicFileAttributes;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.Iterator;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.Properties;
50 import java.util.Set;
51 import java.util.TreeMap;
52 import java.util.TreeSet;
53 import java.util.concurrent.CompletableFuture;
54 import java.util.jar.Attributes;
55 import java.util.jar.JarEntry;
56 import java.util.jar.JarInputStream;
57 import java.util.jar.JarOutputStream;
58 import java.util.jar.Manifest;
59 import java.util.zip.Deflater;
60
61 import aQute.bnd.osgi.Analyzer;
62 import aQute.bnd.osgi.Jar;
63
64 /**
65 * Simple tool repackaging existing jar files into OSGi bundles in an A2
66 * repository.
67 */
68 public class Repackage {
69 final static Logger logger = System.getLogger(Repackage.class.getName());
70
71 /**
72 * Environment variable on whether sources should be packaged separately or
73 * integrated in the bundles.
74 */
75 final static String ENV_SOURCE_BUNDLES = "SOURCE_BUNDLES";
76
77 /** Whether repackaging should run in parallel or sequentially. */
78 final static boolean parallel = true;
79
80 // cache
81 /** Summary of all license seen during the repackaging. */
82 final static Map<String, Set<String>> licensesUsed = new TreeMap<>();
83
84 /** Main entry point. */
85 public static void main(String[] args) {
86 if (args.length < 2) {
87 System.err.println("Usage: <path to a2 output dir> <category1> <category2> ...");
88 System.exit(1);
89 }
90 Path a2Base = Paths.get(args[0]).toAbsolutePath().normalize();
91 Path descriptorsBase = Paths.get(".").toAbsolutePath().normalize();
92 Repackage factory = new Repackage(a2Base, descriptorsBase);
93
94 List<CompletableFuture<Void>> toDos = new ArrayList<>();
95 for (int i = 1; i < args.length; i++) {
96 Path p = Paths.get(args[i]);
97 if (parallel)
98 toDos.add(CompletableFuture.runAsync(() -> factory.processCategory(p)));
99 else
100 factory.processCategory(p);
101 }
102 CompletableFuture.allOf(toDos.toArray(new CompletableFuture[toDos.size()])).join();
103
104 // Summary
105 StringBuilder sb = new StringBuilder();
106 for (String licenseId : licensesUsed.keySet())
107 for (String name : licensesUsed.get(licenseId))
108 sb.append((licenseId.equals("") ? "Proprietary" : licenseId) + "\t\t" + name + "\n");
109 logger.log(INFO, "# License summary:\n" + sb);
110 }
111
112 /** Name of the file centralising information for multiple M2 artifacts. */
113 final static String COMMON_BND = "common.bnd";
114 /** Name of the file centralising information for mergin M2 artifacts. */
115 final static String MERGE_BND = "merge.bnd";
116 /**
117 * Subdirectory of the jar file where origin informations (changes, legal
118 * notices etc. are stored)
119 */
120 final static String A2_ORIGIN = "A2-ORIGIN";
121
122 /** Directory where to download archives */
123 Path originBase;
124 /** Directory where to download Maven artifacts */
125 Path mavenBase;
126
127 /** A2 repository base for binary bundles */
128 Path a2Base;
129 /** A2 repository base for source bundles */
130 Path a2SrcBase;
131 /** A2 base for native components */
132 Path a2LibBase;
133 /** Location of the descriptors driving the packaging */
134 Path descriptorsBase;
135 /** URIs of archives to download */
136 Properties uris = new Properties();
137 /** Mirrors for archive download. Key is URI prefix, value list of base URLs */
138 Map<String, List<String>> mirrors = new HashMap<String, List<String>>();
139
140 /** Whether sources should be packaged separately */
141 final boolean sourceBundles;
142
143 /** Constructor initialises the various variables */
144 public Repackage(Path a2Base, Path descriptorsBase) {
145 sourceBundles = Boolean.parseBoolean(System.getenv(ENV_SOURCE_BUNDLES));
146 if (sourceBundles)
147 logger.log(INFO, "Sources will be packaged separately");
148
149 Objects.requireNonNull(a2Base);
150 Objects.requireNonNull(descriptorsBase);
151 this.originBase = Paths.get(System.getProperty("user.home"), ".cache", "argeo/build/origin");
152 this.mavenBase = Paths.get(System.getProperty("user.home"), ".m2", "repository");
153
154 // TODO define and use a build base
155 this.a2Base = a2Base;
156 this.a2SrcBase = a2Base.getParent().resolve(a2Base.getFileName() + ".src");
157 this.a2LibBase = a2Base.resolve("lib");
158 this.descriptorsBase = descriptorsBase;
159 if (!Files.exists(this.descriptorsBase))
160 throw new IllegalArgumentException(this.descriptorsBase + " does not exist");
161
162 // URIs mapping
163 Path urisPath = this.descriptorsBase.resolve("uris.properties");
164 if (Files.exists(urisPath)) {
165 try (InputStream in = Files.newInputStream(urisPath)) {
166 uris.load(in);
167 } catch (IOException e) {
168 throw new IllegalStateException("Cannot load " + urisPath, e);
169 }
170 }
171
172 // Eclipse mirrors
173 Path eclipseMirrorsPath = this.descriptorsBase.resolve("eclipse.mirrors.txt");
174 List<String> eclipseMirrors = new ArrayList<>();
175 if (Files.exists(eclipseMirrorsPath)) {
176 try {
177 eclipseMirrors = Files.readAllLines(eclipseMirrorsPath, StandardCharsets.UTF_8);
178 } catch (IOException e) {
179 throw new IllegalStateException("Cannot load " + eclipseMirrorsPath, e);
180 }
181 for (Iterator<String> it = eclipseMirrors.iterator(); it.hasNext();) {
182 String value = it.next();
183 if (value.strip().equals(""))
184 it.remove();
185 }
186 }
187 mirrors.put("http://www.eclipse.org/downloads", eclipseMirrors);
188 }
189
190 /*
191 * MAVEN ORIGIN
192 */
193 /** Process a whole category/group id. */
194 void processCategory(Path categoryRelativePath) {
195 try {
196 Path targetCategoryBase = descriptorsBase.resolve(categoryRelativePath);
197 DirectoryStream<Path> bnds = Files.newDirectoryStream(targetCategoryBase,
198 (p) -> p.getFileName().toString().endsWith(".bnd") && !p.getFileName().toString().equals(COMMON_BND)
199 && !p.getFileName().toString().equals(MERGE_BND));
200 for (Path p : bnds) {
201 processSingleM2ArtifactDistributionUnit(p);
202 }
203
204 DirectoryStream<Path> dus = Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p));
205 for (Path duDir : dus) {
206 if (duDir.getFileName().toString().startsWith("eclipse-")) {
207 processEclipseArchive(duDir);
208 } else {
209 processM2BasedDistributionUnit(duDir);
210 }
211 }
212 } catch (IOException e) {
213 throw new RuntimeException("Cannot process category " + categoryRelativePath, e);
214 }
215 }
216
217 /** Process a standalone Maven artifact. */
218 void processSingleM2ArtifactDistributionUnit(Path bndFile) {
219 try {
220 Path categoryRelativePath = descriptorsBase.relativize(bndFile.getParent());
221 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
222
223 Properties fileProps = new Properties();
224 try (InputStream in = Files.newInputStream(bndFile)) {
225 fileProps.load(in);
226 }
227 String repoStr = fileProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
228 ? fileProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
229 : null;
230
231 if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
232 // use file name as symbolic name
233 String symbolicName = bndFile.getFileName().toString();
234 symbolicName = symbolicName.substring(0, symbolicName.length() - ".bnd".length());
235 fileProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
236 }
237
238 String m2Coordinates = fileProps.getProperty(ARGEO_ORIGIN_M2.toString());
239 if (m2Coordinates == null)
240 throw new IllegalArgumentException("No M2 coordinates available for " + bndFile);
241 M2Artifact artifact = new M2Artifact(m2Coordinates);
242 URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
243 Path downloaded = downloadMaven(url, artifact);
244
245 A2Origin origin = new A2Origin();
246 Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, fileProps, artifact, origin);
247
248 downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir, false);
249
250 createJar(targetBundleDir, origin);
251 } catch (Exception e) {
252 throw new RuntimeException("Cannot process " + bndFile, e);
253 }
254 }
255
256 /**
257 * Process multiple Maven artifacts coming from a same project and therefore
258 * with information in common (typically the version), generating single bundles
259 * or merging them if necessary.
260 */
261 void processM2BasedDistributionUnit(Path duDir) {
262 try {
263 Path categoryRelativePath = descriptorsBase.relativize(duDir.getParent());
264 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
265
266 Path mergeBnd = duDir.resolve(MERGE_BND);
267 if (Files.exists(mergeBnd)) // merge
268 mergeM2Artifacts(mergeBnd);
269
270 Path commonBnd = duDir.resolve(COMMON_BND);
271 if (!Files.exists(commonBnd))
272 return;
273
274 Properties commonProps = new Properties();
275 try (InputStream in = Files.newInputStream(commonBnd)) {
276 commonProps.load(in);
277 }
278
279 String m2Version = commonProps.getProperty(ARGEO_ORIGIN_M2.toString());
280 if (m2Version == null) {
281 logger.log(WARNING, "Ignoring " + duDir + " as it is not an M2-based distribution unit");
282 return;// ignore, this is probably an Eclipse archive
283 }
284 if (!m2Version.startsWith(":")) {
285 throw new IllegalStateException("Only the M2 version can be specified: " + m2Version);
286 }
287 m2Version = m2Version.substring(1);
288
289 DirectoryStream<Path> ds = Files.newDirectoryStream(duDir,
290 (p) -> p.getFileName().toString().endsWith(".bnd") && !p.getFileName().toString().equals(COMMON_BND)
291 && !p.getFileName().toString().equals(MERGE_BND));
292 for (Path p : ds) {
293 Properties fileProps = new Properties();
294 try (InputStream in = Files.newInputStream(p)) {
295 fileProps.load(in);
296 }
297 String m2Coordinates = fileProps.getProperty(ARGEO_ORIGIN_M2.toString());
298 M2Artifact artifact = new M2Artifact(m2Coordinates);
299 artifact.setVersion(m2Version);
300
301 // prepare manifest entries
302 Properties mergeProps = new Properties();
303 mergeProps.putAll(commonProps);
304
305 fileEntries: for (Object key : fileProps.keySet()) {
306 if (ARGEO_ORIGIN_M2.toString().equals(key))
307 continue fileEntries;
308 String value = fileProps.getProperty(key.toString());
309 Object previousValue = mergeProps.put(key.toString(), value);
310 if (previousValue != null) {
311 logger.log(WARNING,
312 commonBnd + ": " + key + " was " + previousValue + ", overridden with " + value);
313 }
314 }
315 mergeProps.put(ARGEO_ORIGIN_M2.toString(), artifact.toM2Coordinates());
316 if (!mergeProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
317 // use file name as symbolic name
318 String symbolicName = p.getFileName().toString();
319 symbolicName = symbolicName.substring(0, symbolicName.length() - ".bnd".length());
320 mergeProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
321 }
322
323 String repoStr = mergeProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
324 ? mergeProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
325 : null;
326
327 // download
328 URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
329 Path downloaded = downloadMaven(url, artifact);
330
331 A2Origin origin = new A2Origin();
332 Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, mergeProps, artifact, origin);
333 downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir, false);
334 createJar(targetBundleDir, origin);
335 }
336 } catch (IOException e) {
337 throw new RuntimeException("Cannot process " + duDir, e);
338 }
339 }
340
341 /** Merge multiple Maven artifacts. */
342 void mergeM2Artifacts(Path mergeBnd) throws IOException {
343 Path duDir = mergeBnd.getParent();
344 String category = duDir.getParent().getFileName().toString();
345 Path targetCategoryBase = a2Base.resolve(category);
346
347 Properties mergeProps = new Properties();
348 try (InputStream in = Files.newInputStream(mergeBnd)) {
349 mergeProps.load(in);
350 }
351
352 String m2Version = mergeProps.getProperty(ARGEO_ORIGIN_M2.toString());
353 if (m2Version == null) {
354 logger.log(WARNING, "Ignoring " + duDir + " as it is not an M2-based distribution unit");
355 return;// ignore, this is probably an Eclipse archive
356 }
357 if (!m2Version.startsWith(":")) {
358 throw new IllegalStateException("Only the M2 version can be specified: " + m2Version);
359 }
360 m2Version = m2Version.substring(1);
361 mergeProps.put(BUNDLE_VERSION.toString(), m2Version);
362
363 String artifactsStr = mergeProps.getProperty(ARGEO_ORIGIN_M2_MERGE.toString());
364 if (artifactsStr == null)
365 throw new IllegalArgumentException(mergeBnd + ": " + ARGEO_ORIGIN_M2_MERGE + " must be set");
366
367 String repoStr = mergeProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
368 ? mergeProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
369 : null;
370
371 String bundleSymbolicName = mergeProps.getProperty(BUNDLE_SYMBOLICNAME.toString());
372 if (bundleSymbolicName == null)
373 throw new IllegalArgumentException("Bundle-SymbolicName must be set in " + mergeBnd);
374 CategoryNameVersion nameVersion = new M2Artifact(category + ":" + bundleSymbolicName + ":" + m2Version);
375
376 A2Origin origin = new A2Origin();
377 Path bundleDir = targetCategoryBase.resolve(bundleSymbolicName + "." + nameVersion.getBranch());
378
379 String[] artifacts = artifactsStr.split(",");
380 artifacts: for (String str : artifacts) {
381 String m2Coordinates = str.trim();
382 if ("".equals(m2Coordinates))
383 continue artifacts;
384 M2Artifact artifact = new M2Artifact(m2Coordinates.trim());
385 if (artifact.getVersion() == null)
386 artifact.setVersion(m2Version);
387 URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
388 Path downloaded = downloadMaven(url, artifact);
389 JarEntry entry;
390 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(downloaded), false)) {
391 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
392 if (entry.isDirectory())
393 continue entries;
394 if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".SF")) {
395 origin.deleted.add("cryptographic signatures from " + artifact);
396 continue entries;
397 }
398 if (entry.getName().endsWith("module-info.class")) { // skip Java 9 module info
399 origin.deleted.add("Java module information (module-info.class) from " + artifact);
400 continue entries;
401 }
402 if (entry.getName().startsWith("META-INF/versions/")) { // skip multi-version
403 origin.deleted.add("additional Java versions (META-INF/versions) from " + artifact);
404 continue entries;
405 }
406 if (entry.getName().equals("META-INF/DEPENDENCIES")) {
407 origin.deleted.add("dependency list (META-INF/DEPENDENCIES) from " + artifact);
408 continue entries;
409 }
410 if (entry.getName().startsWith("META-INF/maven/")) {
411 origin.deleted.add("Maven information (META-INF/maven) from " + artifact);
412 continue entries;
413 }
414 if (entry.getName().startsWith(".cache/")) { // Apache SSHD
415 origin.deleted.add("cache directory (.cache) from " + artifact);
416 continue entries;
417 }
418
419 if (entry.getName().endsWith("NOTICE") || entry.getName().endsWith("NOTICE.txt")
420 || entry.getName().endsWith("LICENSE") || entry.getName().endsWith("LICENSE.md")
421 || entry.getName().endsWith("LICENSE-notice.md") || entry.getName().endsWith("COPYING")
422 || entry.getName().endsWith("COPYING.LESSER")) {
423 Path artifactOriginDir = bundleDir.resolve(A2_ORIGIN).resolve(artifact.getGroupId())
424 .resolve(artifact.getArtifactId());
425 Path target = artifactOriginDir.resolve(entry.getName());
426 Files.createDirectories(target.getParent());
427 Files.copy(jarIn, target);
428 origin.moved.add(entry.getName() + " in " + artifact + " to " + bundleDir.relativize(target));
429 continue entries;
430 }
431 Path target = bundleDir.resolve(entry.getName());
432 Files.createDirectories(target.getParent());
433 if (!Files.exists(target)) {
434 Files.copy(jarIn, target);
435 } else {
436 if (entry.getName().startsWith("META-INF/services/")) {
437 try (OutputStream out = Files.newOutputStream(target, StandardOpenOption.APPEND)) {
438 out.write("\n".getBytes());
439 jarIn.transferTo(out);
440 logger.log(DEBUG, artifact.getArtifactId() + " - Appended " + entry.getName());
441 }
442 origin.modified.add(entry.getName() + ", merging from " + artifact);
443 } else if (entry.getName().startsWith("org/apache/batik/")) {
444 logger.log(TRACE, "Skip " + entry.getName());
445 continue entries;
446 } else {
447 throw new IllegalStateException("File " + target + " from " + artifact + " already exists");
448 }
449 }
450 logger.log(TRACE, () -> "Copied " + target);
451 }
452 }
453 origin.added.add("binary content of " + artifact);
454
455 // process sources
456 downloadAndProcessM2Sources(repoStr, artifact, bundleDir, true);
457 }
458
459 // additional service files
460 Path servicesDir = duDir.resolve("services");
461 if (Files.exists(servicesDir)) {
462 for (Path p : Files.newDirectoryStream(servicesDir)) {
463 Path target = bundleDir.resolve("META-INF/services/").resolve(p.getFileName());
464 try (InputStream in = Files.newInputStream(p);
465 OutputStream out = Files.newOutputStream(target, StandardOpenOption.APPEND);) {
466 out.write("\n".getBytes());
467 in.transferTo(out);
468 logger.log(DEBUG, "Appended " + p);
469 }
470 origin.added.add(bundleDir.relativize(target).toString());
471 }
472 }
473
474 Map<String, String> entries = new TreeMap<>();
475 try (Analyzer bndAnalyzer = new Analyzer()) {
476 bndAnalyzer.setProperties(mergeProps);
477 Jar jar = new Jar(bundleDir.toFile());
478 bndAnalyzer.setJar(jar);
479 Manifest manifest = bndAnalyzer.calcManifest();
480
481 keys: for (Object key : manifest.getMainAttributes().keySet()) {
482 Object value = manifest.getMainAttributes().get(key);
483
484 switch (key.toString()) {
485 case "Tool":
486 case "Bnd-LastModified":
487 case "Created-By":
488 continue keys;
489 }
490 if ("Require-Capability".equals(key.toString())
491 && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\""))
492 continue keys;// hack for very old classes
493 entries.put(key.toString(), value.toString());
494 }
495 } catch (Exception e) {
496 throw new RuntimeException("Cannot process " + mergeBnd, e);
497 }
498
499 Manifest manifest = new Manifest();
500 Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
501 Files.createDirectories(manifestPath.getParent());
502 for (String key : entries.keySet()) {
503 String value = entries.get(key);
504 manifest.getMainAttributes().putValue(key, value);
505 }
506
507 try (OutputStream out = Files.newOutputStream(manifestPath)) {
508 manifest.write(out);
509 }
510 createJar(bundleDir, origin);
511 }
512
513 /** Generate MANIFEST using BND. */
514 Path processBndJar(Path downloaded, Path targetCategoryBase, Properties fileProps, M2Artifact artifact,
515 A2Origin origin) {
516
517 try {
518 Map<String, String> additionalEntries = new TreeMap<>();
519 boolean doNotModify = Boolean.parseBoolean(fileProps
520 .getOrDefault(ManifestConstants.ARGEO_ORIGIN_MANIFEST_NOT_MODIFIED.toString(), "false").toString());
521
522 // Note: we always force the symbolic name
523 if (doNotModify) {
524 fileEntries: for (Object key : fileProps.keySet()) {
525 if (ManifestConstants.ARGEO_ORIGIN_M2.toString().equals(key))
526 continue fileEntries;
527 String value = fileProps.getProperty(key.toString());
528 additionalEntries.put(key.toString(), value);
529 }
530 } else {
531 if (artifact != null) {
532 if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
533 fileProps.put(BUNDLE_SYMBOLICNAME.toString(), artifact.getName());
534 }
535 if (!fileProps.containsKey(BUNDLE_VERSION.toString())) {
536 fileProps.put(BUNDLE_VERSION.toString(), artifact.getVersion());
537 }
538 }
539
540 if (!fileProps.containsKey(EXPORT_PACKAGE.toString())) {
541 fileProps.put(EXPORT_PACKAGE.toString(),
542 "*;version=\"" + fileProps.getProperty(BUNDLE_VERSION.toString()) + "\"");
543 }
544
545 try (Analyzer bndAnalyzer = new Analyzer()) {
546 bndAnalyzer.setProperties(fileProps);
547 Jar jar = new Jar(downloaded.toFile());
548 bndAnalyzer.setJar(jar);
549 Manifest manifest = bndAnalyzer.calcManifest();
550
551 keys: for (Object key : manifest.getMainAttributes().keySet()) {
552 Object value = manifest.getMainAttributes().get(key);
553
554 switch (key.toString()) {
555 case "Tool":
556 case "Bnd-LastModified":
557 case "Created-By":
558 continue keys;
559 }
560 if ("Require-Capability".equals(key.toString())
561 && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\""))
562 continue keys;// !! hack for very old classes
563 additionalEntries.put(key.toString(), value.toString());
564 }
565 }
566 }
567 Path targetBundleDir = processBundleJar(downloaded, targetCategoryBase, additionalEntries, origin);
568 logger.log(DEBUG, () -> "Processed " + downloaded);
569 return targetBundleDir;
570 } catch (Exception e) {
571 throw new RuntimeException("Cannot BND process " + downloaded, e);
572 }
573
574 }
575
576 /** Download and integrates sources for a single Maven artifact. */
577 void downloadAndProcessM2Sources(String repoStr, M2Artifact artifact, Path targetBundleDir, boolean merging)
578 throws IOException {
579 try {
580 M2Artifact sourcesArtifact = new M2Artifact(artifact.toM2Coordinates(), "sources");
581 URL sourcesUrl = M2ConventionsUtils.mavenRepoUrl(repoStr, sourcesArtifact);
582 Path sourcesDownloaded = downloadMaven(sourcesUrl, artifact, true);
583 processM2SourceJar(sourcesDownloaded, targetBundleDir, merging ? artifact : null);
584 logger.log(TRACE, () -> "Processed source " + sourcesDownloaded);
585 } catch (Exception e) {
586 logger.log(ERROR, () -> "Cannot download source for " + artifact);
587 }
588
589 }
590
591 /** Integrate sources from a downloaded jar file. */
592 void processM2SourceJar(Path file, Path bundleDir, M2Artifact mergingFrom) throws IOException {
593 A2Origin origin = new A2Origin();
594 Path sourceDir = sourceBundles ? bundleDir.getParent().resolve(bundleDir.toString() + ".src")
595 : bundleDir.resolve("OSGI-OPT/src");
596 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
597
598 String mergingMsg = "";
599 if (mergingFrom != null)
600 mergingMsg = " of " + mergingFrom;
601
602 Files.createDirectories(sourceDir);
603 JarEntry entry;
604 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
605 if (entry.isDirectory())
606 continue entries;
607 if (entry.getName().startsWith("META-INF")) {// skip META-INF entries
608 origin.deleted.add("META-INF directory from the sources" + mergingMsg);
609 continue entries;
610 }
611 if (entry.getName().startsWith("module-info.java")) {// skip Java module information
612 origin.deleted.add("Java module information from the sources (module-info.java)" + mergingMsg);
613 continue entries;
614 }
615 if (entry.getName().startsWith("/")) { // absolute paths
616 // TODO does it really happen?
617 logger.log(WARNING, entry.getName() + " has an absolute path");
618 origin.deleted.add(entry.getName() + " from the sources" + mergingMsg);
619 continue entries;
620 }
621 Path target = sourceDir.resolve(entry.getName());
622 Files.createDirectories(target.getParent());
623 if (!Files.exists(target)) {
624 Files.copy(jarIn, target);
625 logger.log(TRACE, () -> "Copied source " + target);
626 } else {
627 logger.log(TRACE, () -> target + " already exists, skipping...");
628 }
629 }
630 }
631 // write the changes
632 if (sourceBundles) {
633 origin.appendChanges(sourceDir);
634 } else {
635 origin.added.add("source code under OSGI-OPT/src");
636 origin.appendChanges(bundleDir);
637 }
638 }
639
640 /** Download a Maven artifact. */
641 Path downloadMaven(URL url, M2Artifact artifact) throws IOException {
642 return downloadMaven(url, artifact, false);
643 }
644
645 /** Download a Maven artifact. */
646 Path downloadMaven(URL url, M2Artifact artifact, boolean sources) throws IOException {
647 return download(url, mavenBase, artifact.getGroupId().replace(".", "/") //
648 + '/' + artifact.getArtifactId() + '/' + artifact.getVersion() //
649 + '/' + artifact.getArtifactId() + "-" + artifact.getVersion() + (sources ? "-sources" : "") + ".jar");
650 }
651
652 /*
653 * ECLIPSE ORIGIN
654 */
655 /** Process an archive in Eclipse format. */
656 void processEclipseArchive(Path duDir) {
657 try {
658 Path categoryRelativePath = descriptorsBase.relativize(duDir.getParent());
659 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
660 Files.createDirectories(targetCategoryBase);
661 // first delete all directories from previous builds
662 for (Path dir : Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p)))
663 deleteDirectory(dir);
664
665 Files.createDirectories(originBase);
666
667 Path commonBnd = duDir.resolve(COMMON_BND);
668 Properties commonProps = new Properties();
669 try (InputStream in = Files.newInputStream(commonBnd)) {
670 commonProps.load(in);
671 }
672 String url = commonProps.getProperty(ARGEO_ORIGIN_URI.toString());
673 if (url == null) {
674 url = uris.getProperty(duDir.getFileName().toString());
675 if (url == null)
676 throw new IllegalStateException("No url available for " + duDir);
677 commonProps.put(ARGEO_ORIGIN_URI.toString(), url);
678 }
679 Path downloaded = tryDownloadArchive(url, originBase);
680
681 FileSystem zipFs = FileSystems.newFileSystem(downloaded, (ClassLoader) null);
682
683 // filters
684 List<PathMatcher> includeMatchers = new ArrayList<>();
685 Properties includes = new Properties();
686 try (InputStream in = Files.newInputStream(duDir.resolve("includes.properties"))) {
687 includes.load(in);
688 }
689 for (Object pattern : includes.keySet()) {
690 PathMatcher pathMatcher = zipFs.getPathMatcher("glob:/" + pattern);
691 includeMatchers.add(pathMatcher);
692 }
693
694 List<PathMatcher> excludeMatchers = new ArrayList<>();
695 Path excludeFile = duDir.resolve("excludes.properties");
696 if (Files.exists(excludeFile)) {
697 Properties excludes = new Properties();
698 try (InputStream in = Files.newInputStream(excludeFile)) {
699 excludes.load(in);
700 }
701 for (Object pattern : excludes.keySet()) {
702 PathMatcher pathMatcher = zipFs.getPathMatcher("glob:/" + pattern);
703 excludeMatchers.add(pathMatcher);
704 }
705 }
706
707 // keys are the bundle directories
708 Map<Path, A2Origin> origins = new HashMap<>();
709 Files.walkFileTree(zipFs.getRootDirectories().iterator().next(), new SimpleFileVisitor<Path>() {
710
711 @Override
712 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
713 includeMatchers: for (PathMatcher includeMatcher : includeMatchers) {
714 if (includeMatcher.matches(file)) {
715 for (PathMatcher excludeMatcher : excludeMatchers) {
716 if (excludeMatcher.matches(file)) {
717 logger.log(TRACE, "Skipping excluded " + file);
718 return FileVisitResult.CONTINUE;
719 }
720 }
721 if (file.getFileName().toString().contains(".source_")) {
722 processEclipseSourceJar(file, targetCategoryBase);
723 logger.log(DEBUG, () -> "Processed source " + file);
724 } else {
725 Map<String, String> map = new HashMap<>();
726 for (Object key : commonProps.keySet())
727 map.put(key.toString(), commonProps.getProperty(key.toString()));
728 A2Origin origin = new A2Origin();
729 Path bundleDir = processBundleJar(file, targetCategoryBase, map, origin);
730 origins.put(bundleDir, origin);
731 logger.log(DEBUG, () -> "Processed " + file);
732 }
733 break includeMatchers;
734 }
735 }
736 return FileVisitResult.CONTINUE;
737 }
738 });
739
740 DirectoryStream<Path> dirs = Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p)
741 && p.getFileName().toString().indexOf('.') >= 0 && !p.getFileName().toString().endsWith(".src"));
742 for (Path bundleDir : dirs) {
743 A2Origin origin = origins.get(bundleDir);
744 Objects.requireNonNull(origin, "No A2 origin found for " + bundleDir);
745 createJar(bundleDir, origin);
746 }
747 } catch (IOException e) {
748 throw new RuntimeException("Cannot process " + duDir, e);
749 }
750
751 }
752
753 /** Process sources in Eclipse format. */
754 void processEclipseSourceJar(Path file, Path targetBase) throws IOException {
755 try {
756 A2Origin origin = new A2Origin();
757 Path bundleDir;
758 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
759 Manifest manifest = jarIn.getManifest();
760
761 String[] relatedBundle = manifest.getMainAttributes().getValue(ECLIPSE_SOURCE_BUNDLE.toString())
762 .split(";");
763 String version = relatedBundle[1].substring("version=\"".length());
764 version = version.substring(0, version.length() - 1);
765 NameVersion nameVersion = new NameVersion(relatedBundle[0], version);
766 bundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
767
768 Path sourceDir = sourceBundles ? bundleDir.getParent().resolve(bundleDir.toString() + ".src")
769 : bundleDir.resolve("OSGI-OPT/src");
770
771 Files.createDirectories(sourceDir);
772 JarEntry entry;
773 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
774 if (entry.isDirectory())
775 continue entries;
776 if (entry.getName().startsWith("META-INF"))// skip META-INF entries
777 continue entries;
778 Path target = sourceDir.resolve(entry.getName());
779 Files.createDirectories(target.getParent());
780 Files.copy(jarIn, target);
781 logger.log(TRACE, () -> "Copied source " + target);
782 }
783
784 // write the changes
785 if (sourceBundles) {
786 origin.appendChanges(sourceDir);
787 } else {
788 origin.added.add("source code under OSGI-OPT/src");
789 origin.appendChanges(bundleDir);
790 }
791 }
792 } catch (IOException e) {
793 throw new IllegalStateException("Cannot process " + file, e);
794 }
795 }
796
797 /*
798 * COMMON PROCESSING
799 */
800 /** Normalise a bundle. */
801 Path processBundleJar(Path file, Path targetBase, Map<String, String> entries, A2Origin origin) throws IOException {
802 NameVersion nameVersion;
803 Path bundleDir;
804 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
805 Manifest sourceManifest = jarIn.getManifest();
806 Manifest manifest = sourceManifest != null ? new Manifest(sourceManifest) : new Manifest();
807
808 // singleton
809 boolean isSingleton = false;
810 String rawSourceSymbolicName = manifest.getMainAttributes().getValue(BUNDLE_SYMBOLICNAME.toString());
811 if (rawSourceSymbolicName != null) {
812 // make sure there is no directive
813 String[] arr = rawSourceSymbolicName.split(";");
814 for (int i = 1; i < arr.length; i++) {
815 if (arr[i].trim().equals("singleton:=true"))
816 isSingleton = true;
817 logger.log(DEBUG, file.getFileName() + " is a singleton");
818 }
819 }
820 // remove problematic entries in MANIFEST
821 manifest.getEntries().clear();
822
823 String ourSymbolicName = entries.get(BUNDLE_SYMBOLICNAME.toString());
824 String ourVersion = entries.get(BUNDLE_VERSION.toString());
825
826 if (ourSymbolicName != null && ourVersion != null) {
827 nameVersion = new NameVersion(ourSymbolicName, ourVersion);
828 } else {
829 nameVersion = nameVersionFromManifest(manifest);
830 if (ourVersion != null && !nameVersion.getVersion().equals(ourVersion)) {
831 logger.log(WARNING,
832 "Original version is " + nameVersion.getVersion() + " while new version is " + ourVersion);
833 entries.put(BUNDLE_VERSION.toString(), ourVersion);
834 }
835 if (ourSymbolicName != null) {
836 // we always force our symbolic name
837 nameVersion.setName(ourSymbolicName);
838 }
839 }
840 bundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
841
842 // force Java 9 module name
843 entries.put(ManifestConstants.AUTOMATIC_MODULE_NAME.toString(), nameVersion.getName());
844
845 boolean isNative = false;
846 String os = null;
847 String arch = null;
848 if (bundleDir.startsWith(a2LibBase)) {
849 isNative = true;
850 Path libRelativePath = a2LibBase.relativize(bundleDir);
851 os = libRelativePath.getName(0).toString();
852 arch = libRelativePath.getName(1).toString();
853 }
854
855 // copy entries
856 JarEntry entry;
857 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
858 if (entry.isDirectory())
859 continue entries;
860 if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".SF")) {
861 origin.deleted.add("cryptographic signatures");
862 continue entries;
863 }
864 if (entry.getName().endsWith("module-info.class")) { // skip Java 9 module info
865 origin.deleted.add("Java module information (module-info.class)");
866 continue entries;
867 }
868 if (entry.getName().startsWith("META-INF/versions/")) { // skip multi-version
869 origin.deleted.add("additional Java versions (META-INF/versions)");
870 continue entries;
871 }
872 // skip file system providers as they cause issues with native image
873 if (entry.getName().startsWith("META-INF/services/java.nio.file.spi.FileSystemProvider")) {
874 origin.deleted
875 .add("file system providers (META-INF/services/java.nio.file.spi.FileSystemProvider)");
876 continue entries;
877 }
878 if (entry.getName().startsWith("OSGI-OPT/src/")) { // skip embedded sources
879 origin.deleted.add("embedded sources");
880 continue entries;
881 }
882 Path target = bundleDir.resolve(entry.getName());
883 Files.createDirectories(target.getParent());
884 Files.copy(jarIn, target);
885
886 // native libraries
887 if (isNative && (entry.getName().endsWith(".so") || entry.getName().endsWith(".dll")
888 || entry.getName().endsWith(".jnilib"))) {
889 Path categoryDir = bundleDir.getParent();
890 boolean copyDll = false;
891 Path targetDll = categoryDir.resolve(bundleDir.relativize(target));
892 if (nameVersion.getName().equals("com.sun.jna")) {
893 if (arch.equals("x86_64"))
894 arch = "x86-64";
895 if (os.equals("macosx"))
896 os = "darwin";
897 if (target.getParent().getFileName().toString().equals(os + "-" + arch)) {
898 copyDll = true;
899 }
900 targetDll = categoryDir.resolve(target.getFileName());
901 } else {
902 copyDll = true;
903 }
904 if (copyDll) {
905 Files.createDirectories(targetDll.getParent());
906 if (Files.exists(targetDll))
907 Files.delete(targetDll);
908 Files.copy(target, targetDll);
909 }
910 Files.delete(target);
911 }
912 logger.log(TRACE, () -> "Copied " + target);
913 }
914
915 // copy MANIFEST
916 Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
917 Files.createDirectories(manifestPath.getParent());
918
919 if (isSingleton && entries.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
920 entries.put(BUNDLE_SYMBOLICNAME.toString(),
921 entries.get(BUNDLE_SYMBOLICNAME.toString()) + ";singleton:=true");
922 }
923
924 // Final MANIFEST decisions
925 // This also where we check the original OSGi metadata and compare with our
926 // changes
927 for (String key : entries.keySet()) {
928 String value = entries.get(key);
929 String previousValue = manifest.getMainAttributes().getValue(key);
930 boolean wasDifferent = previousValue != null && !previousValue.equals(value);
931 boolean keepPrevious = false;
932 if (wasDifferent) {
933 if (SPDX_LICENSE_IDENTIFIER.toString().equals(key) && previousValue != null)
934 keepPrevious = true;
935 else if (BUNDLE_VERSION.toString().equals(key) && wasDifferent)
936 if (previousValue.equals(value + ".0")) // typically a Maven first release
937 keepPrevious = true;
938
939 if (keepPrevious) {
940 if (logger.isLoggable(DEBUG))
941 logger.log(DEBUG, file.getFileName() + ": " + key + " was NOT modified, value kept is "
942 + previousValue + ", not overriden with " + value);
943 value = previousValue;
944 }
945 }
946
947 manifest.getMainAttributes().putValue(key, value);
948 if (wasDifferent && !keepPrevious) {
949 if (IMPORT_PACKAGE.toString().equals(key) || EXPORT_PACKAGE.toString().equals(key))
950 logger.log(TRACE, () -> file.getFileName() + ": " + key + " was modified");
951 else
952 logger.log(WARNING, file.getFileName() + ": " + key + " was " + previousValue
953 + ", overridden with " + value);
954 }
955
956 // !! hack to remove unresolvable
957 if (key.equals("Provide-Capability") || key.equals("Require-Capability"))
958 if (nameVersion.getName().equals("osgi.core") || nameVersion.getName().equals("osgi.cmpn")) {
959 manifest.getMainAttributes().remove(key);
960 }
961 }
962
963 // license checks
964 String spdxLicenceId = manifest.getMainAttributes().getValue(SPDX_LICENSE_IDENTIFIER.toString());
965 String bundleLicense = manifest.getMainAttributes().getValue(BUNDLE_LICENSE.toString());
966 if (spdxLicenceId == null) {
967 logger.log(ERROR, file.getFileName() + ": " + SPDX_LICENSE_IDENTIFIER + " not available, "
968 + BUNDLE_LICENSE + " is " + bundleLicense);
969 } else {
970 if (!licensesUsed.containsKey(spdxLicenceId))
971 licensesUsed.put(spdxLicenceId, new TreeSet<>());
972 licensesUsed.get(spdxLicenceId).add(nameVersion.toString());
973 }
974
975 origin.modified.add("jar MANIFEST (META-INF/MANIFEST.MF)");
976 // write the MANIFEST
977 try (OutputStream out = Files.newOutputStream(manifestPath)) {
978 manifest.write(out);
979 }
980 }
981 return bundleDir;
982 }
983
984 /*
985 * UTILITIES
986 */
987 /** Recursively deletes a directory. */
988 static void deleteDirectory(Path path) throws IOException {
989 if (!Files.exists(path))
990 return;
991 Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
992 @Override
993 public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
994 if (e != null)
995 throw e;
996 Files.delete(directory);
997 return CONTINUE;
998 }
999
1000 @Override
1001 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1002 Files.delete(file);
1003 return CONTINUE;
1004 }
1005 });
1006 }
1007
1008 /** Extract name/version from a MANIFEST. */
1009 NameVersion nameVersionFromManifest(Manifest manifest) {
1010 Attributes attrs = manifest.getMainAttributes();
1011 // symbolic name
1012 String symbolicName = attrs.getValue(ManifestConstants.BUNDLE_SYMBOLICNAME.toString());
1013 if (symbolicName == null)
1014 return null;
1015 // make sure there is no directive
1016 symbolicName = symbolicName.split(";")[0];
1017
1018 String version = attrs.getValue(ManifestConstants.BUNDLE_VERSION.toString());
1019 return new NameVersion(symbolicName, version);
1020 }
1021
1022 /** Try to download from an URI. */
1023 Path tryDownloadArchive(String uri, Path dir) throws IOException {
1024 // find mirror
1025 List<String> urlBases = null;
1026 String uriPrefix = null;
1027 uriPrefixes: for (String uriPref : mirrors.keySet()) {
1028 if (uri.startsWith(uriPref)) {
1029 if (mirrors.get(uriPref).size() > 0) {
1030 urlBases = mirrors.get(uriPref);
1031 uriPrefix = uriPref;
1032 break uriPrefixes;
1033 }
1034 }
1035 }
1036 if (urlBases == null)
1037 try {
1038 return downloadArchive(new URL(uri), dir);
1039 } catch (FileNotFoundException e) {
1040 throw new FileNotFoundException("Cannot find " + uri);
1041 }
1042
1043 // try to download
1044 for (String urlBase : urlBases) {
1045 String relativePath = uri.substring(uriPrefix.length());
1046 URL url = new URL(urlBase + relativePath);
1047 try {
1048 return downloadArchive(url, dir);
1049 } catch (FileNotFoundException e) {
1050 logger.log(WARNING, "Cannot download " + url + ", trying another mirror");
1051 }
1052 }
1053 throw new FileNotFoundException("Cannot find " + uri);
1054 }
1055
1056 /**
1057 * Effectively download. Synchronised in order to avoid downloading twice in
1058 * parallel.
1059 */
1060 synchronized Path downloadArchive(URL url, Path dir) throws IOException {
1061 return download(url, dir, (String) null);
1062 }
1063
1064 /** Effectively download. */
1065 Path download(URL url, Path dir, String name) throws IOException {
1066
1067 Path dest;
1068 if (name == null) {
1069 // We use also use parent directory in case the archive itself has a fixed name
1070 String[] segments = url.getPath().split("/");
1071 name = segments.length > 1 ? segments[segments.length - 2] + '-' + segments[segments.length - 1]
1072 : segments[segments.length - 1];
1073 }
1074
1075 dest = dir.resolve(name);
1076 if (Files.exists(dest)) {
1077 logger.log(TRACE, () -> "File " + dest + " already exists for " + url + ", not downloading again");
1078 return dest;
1079 } else {
1080 Files.createDirectories(dest.getParent());
1081 }
1082
1083 try (InputStream in = url.openStream()) {
1084 Files.copy(in, dest);
1085 logger.log(DEBUG, () -> "Downloaded " + dest + " from " + url);
1086 }
1087 return dest;
1088 }
1089
1090 /** Create a JAR file from a directory. */
1091 Path createJar(Path bundleDir, A2Origin origin) throws IOException {
1092 // write changes
1093 origin.appendChanges(bundleDir);
1094 // Create the jar
1095 Path jarPath = bundleDir.getParent().resolve(bundleDir.getFileName() + ".jar");
1096 Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
1097 Manifest manifest;
1098 try (InputStream in = Files.newInputStream(manifestPath)) {
1099 manifest = new Manifest(in);
1100 }
1101 try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarPath), manifest)) {
1102 jarOut.setLevel(Deflater.DEFAULT_COMPRESSION);
1103 Files.walkFileTree(bundleDir, new SimpleFileVisitor<Path>() {
1104
1105 @Override
1106 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1107 if (file.getFileName().toString().equals("MANIFEST.MF"))
1108 return super.visitFile(file, attrs);
1109 JarEntry entry = new JarEntry(
1110 bundleDir.relativize(file).toString().replace(File.separatorChar, '/'));
1111 jarOut.putNextEntry(entry);
1112 Files.copy(file, jarOut);
1113 return super.visitFile(file, attrs);
1114 }
1115
1116 });
1117 }
1118 deleteDirectory(bundleDir);
1119
1120 if (sourceBundles) {
1121 Path bundleCategoryDir = bundleDir.getParent();
1122 Path sourceDir = bundleCategoryDir.resolve(bundleDir.toString() + ".src");
1123 if (!Files.exists(sourceDir)) {
1124 logger.log(WARNING, sourceDir + " does not exist, skipping...");
1125 return jarPath;
1126
1127 }
1128
1129 Path relPath = a2Base.relativize(bundleCategoryDir);
1130 Path srcCategoryDir = a2SrcBase.resolve(relPath);
1131 Path srcJarP = srcCategoryDir.resolve(sourceDir.getFileName() + ".jar");
1132 Files.createDirectories(srcJarP.getParent());
1133
1134 String bundleSymbolicName = manifest.getMainAttributes().getValue("Bundle-SymbolicName").toString();
1135 // in case there are additional directives
1136 bundleSymbolicName = bundleSymbolicName.split(";")[0];
1137 Manifest srcManifest = new Manifest();
1138 srcManifest.getMainAttributes().put(MANIFEST_VERSION, "1.0");
1139 srcManifest.getMainAttributes().putValue(BUNDLE_SYMBOLICNAME.toString(), bundleSymbolicName + ".src");
1140 srcManifest.getMainAttributes().putValue(BUNDLE_VERSION.toString(),
1141 manifest.getMainAttributes().getValue(BUNDLE_VERSION.toString()).toString());
1142 srcManifest.getMainAttributes().putValue(ECLIPSE_SOURCE_BUNDLE.toString(), bundleSymbolicName
1143 + ";version=\"" + manifest.getMainAttributes().getValue(BUNDLE_VERSION.toString()));
1144
1145 try (JarOutputStream srcJarOut = new JarOutputStream(Files.newOutputStream(srcJarP), srcManifest)) {
1146 srcJarOut.setLevel(Deflater.BEST_COMPRESSION);
1147 Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
1148
1149 @Override
1150 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1151 if (file.getFileName().toString().equals("MANIFEST.MF"))
1152 return super.visitFile(file, attrs);
1153 JarEntry entry = new JarEntry(
1154 sourceDir.relativize(file).toString().replace(File.separatorChar, '/'));
1155 srcJarOut.putNextEntry(entry);
1156 Files.copy(file, srcJarOut);
1157 return super.visitFile(file, attrs);
1158 }
1159
1160 });
1161 }
1162 deleteDirectory(sourceDir);
1163 }
1164
1165 return jarPath;
1166 }
1167
1168 /** MANIFEST headers. */
1169 enum ManifestConstants {
1170 // OSGi
1171 /** OSGi bundle symbolic name. */
1172 BUNDLE_SYMBOLICNAME("Bundle-SymbolicName"), //
1173 /** OSGi bundle version. */
1174 BUNDLE_VERSION("Bundle-Version"), //
1175 /** OSGi bundle license. */
1176 BUNDLE_LICENSE("Bundle-License"), //
1177 /** OSGi exported packages list. */
1178 EXPORT_PACKAGE("Export-Package"), //
1179 /** OSGi imported packages list. */
1180 IMPORT_PACKAGE("Import-Package"), //
1181 // Java
1182 /** Java module name. */
1183 AUTOMATIC_MODULE_NAME("Automatic-Module-Name"), //
1184 // Eclipse
1185 /** Eclipse source bundle. */
1186 ECLIPSE_SOURCE_BUNDLE("Eclipse-SourceBundle"), //
1187 // SPDX
1188 /**
1189 * SPDX license identifier.
1190 *
1191 * @see https://spdx.org/licenses/
1192 */
1193 SPDX_LICENSE_IDENTIFIER("SPDX-License-Identifier"), //
1194 // Argeo Origin
1195 /**
1196 * Maven coordinates of the origin, possibly partial when using common.bnd or
1197 * merge.bnd.
1198 */
1199 ARGEO_ORIGIN_M2("Argeo-Origin-M2"), //
1200 /** List of Maven coordinates to merge. */
1201 ARGEO_ORIGIN_M2_MERGE("Argeo-Origin-M2-Merge"), //
1202 /** Maven repository, if not the default one. */
1203 ARGEO_ORIGIN_M2_REPO("Argeo-Origin-M2-Repo"), //
1204 /**
1205 * Do not perform BND analysis of the origin component. Typically IMport_package
1206 * and Export-Package will be kept untouched.
1207 */
1208 ARGEO_ORIGIN_MANIFEST_NOT_MODIFIED("Argeo-Origin-ManifestNotModified"), //
1209 /**
1210 * Origin (non-Maven) URI of the component. It may be anything (jar, archive,
1211 * etc.).
1212 */
1213 ARGEO_ORIGIN_URI("Argeo-Origin-URI"), //
1214 ;
1215
1216 final String value;
1217
1218 private ManifestConstants(String value) {
1219 this.value = value;
1220 }
1221
1222 @Override
1223 public String toString() {
1224 return value;
1225 }
1226 }
1227 }
1228
1229 /**
1230 * Gathers modifications performed on the original binaries and sources,
1231 * especially in order to comply with their license requirements.
1232 */
1233 class A2Origin {
1234 Set<String> modified = new TreeSet<>();
1235 Set<String> deleted = new TreeSet<>();
1236 Set<String> added = new TreeSet<>();
1237 Set<String> moved = new TreeSet<>();
1238
1239 /** Append changes to the A2-ORIGIN/changes file. */
1240 void appendChanges(Path baseDirectory) throws IOException {
1241 Path changesFile = baseDirectory.resolve("A2-ORIGIN/changes");
1242 Files.createDirectories(changesFile.getParent());
1243 try (BufferedWriter writer = Files.newBufferedWriter(changesFile, StandardOpenOption.APPEND,
1244 StandardOpenOption.CREATE)) {
1245 for (String msg : added)
1246 writer.write("- Added " + msg + ".\n");
1247 for (String msg : modified)
1248 writer.write("- Modified " + msg + ".\n");
1249 for (String msg : moved)
1250 writer.write("- Moved " + msg + ".\n");
1251 for (String msg : deleted)
1252 writer.write("- Deleted " + msg + ".\n");
1253 }
1254 }
1255 }
1256
1257 /** Simple representation of an M2 artifact. */
1258 class M2Artifact extends CategoryNameVersion {
1259 private String classifier;
1260
1261 M2Artifact(String m2coordinates) {
1262 this(m2coordinates, null);
1263 }
1264
1265 M2Artifact(String m2coordinates, String classifier) {
1266 String[] parts = m2coordinates.split(":");
1267 setCategory(parts[0]);
1268 setName(parts[1]);
1269 if (parts.length > 2) {
1270 setVersion(parts[2]);
1271 }
1272 this.classifier = classifier;
1273 }
1274
1275 String getGroupId() {
1276 return super.getCategory();
1277 }
1278
1279 String getArtifactId() {
1280 return super.getName();
1281 }
1282
1283 String toM2Coordinates() {
1284 return getCategory() + ":" + getName() + (getVersion() != null ? ":" + getVersion() : "");
1285 }
1286
1287 String getClassifier() {
1288 return classifier != null ? classifier : "";
1289 }
1290
1291 String getExtension() {
1292 return "jar";
1293 }
1294 }
1295
1296 /** Utilities around Maven (conventions based). */
1297 class M2ConventionsUtils {
1298 final static String MAVEN_CENTRAL_BASE_URL = "https://repo1.maven.org/maven2/";
1299
1300 /** The file name of this artifact when stored */
1301 static String artifactFileName(M2Artifact artifact) {
1302 return artifact.getArtifactId() + '-' + artifact.getVersion()
1303 + (artifact.getClassifier().equals("") ? "" : '-' + artifact.getClassifier()) + '.'
1304 + artifact.getExtension();
1305 }
1306
1307 /** Absolute path to the file */
1308 static String artifactPath(String artifactBasePath, M2Artifact artifact) {
1309 return artifactParentPath(artifactBasePath, artifact) + '/' + artifactFileName(artifact);
1310 }
1311
1312 /** Absolute path to the file */
1313 static String artifactUrl(String repoUrl, M2Artifact artifact) {
1314 if (repoUrl.endsWith("/"))
1315 return repoUrl + artifactPath("/", artifact).substring(1);
1316 else
1317 return repoUrl + artifactPath("/", artifact);
1318 }
1319
1320 /** Absolute path to the file */
1321 static URL mavenRepoUrl(String repoBase, M2Artifact artifact) {
1322 String url = artifactUrl(repoBase == null ? MAVEN_CENTRAL_BASE_URL : repoBase, artifact);
1323 try {
1324 return new URL(url);
1325 } catch (MalformedURLException e) {
1326 // it should not happen
1327 throw new IllegalStateException(e);
1328 }
1329 }
1330
1331 /** Absolute path to the directories where the files will be stored */
1332 static String artifactParentPath(String artifactBasePath, M2Artifact artifact) {
1333 return artifactBasePath + (artifactBasePath.endsWith("/") ? "" : "/") + artifactParentPath(artifact);
1334 }
1335
1336 /** Relative path to the directories where the files will be stored */
1337 static String artifactParentPath(M2Artifact artifact) {
1338 return artifact.getGroupId().replace('.', '/') + '/' + artifact.getArtifactId() + '/' + artifact.getVersion();
1339 }
1340
1341 /** Singleton */
1342 private M2ConventionsUtils() {
1343 }
1344 }
1345
1346 /** Combination of a category, a name and a version. */
1347 class CategoryNameVersion extends NameVersion {
1348 private String category;
1349
1350 CategoryNameVersion() {
1351 }
1352
1353 CategoryNameVersion(String category, String name, String version) {
1354 super(name, version);
1355 this.category = category;
1356 }
1357
1358 CategoryNameVersion(String category, NameVersion nameVersion) {
1359 super(nameVersion);
1360 this.category = category;
1361 }
1362
1363 String getCategory() {
1364 return category;
1365 }
1366
1367 void setCategory(String category) {
1368 this.category = category;
1369 }
1370
1371 @Override
1372 public String toString() {
1373 return category + ":" + super.toString();
1374 }
1375
1376 }
1377
1378 /** Combination of a name and a version. */
1379 class NameVersion implements Comparable<NameVersion> {
1380 private String name;
1381 private String version;
1382
1383 NameVersion() {
1384 }
1385
1386 /** Interprets string in OSGi-like format my.module.name;version=0.0.0 */
1387 NameVersion(String nameVersion) {
1388 int index = nameVersion.indexOf(";version=");
1389 if (index < 0) {
1390 setName(nameVersion);
1391 setVersion(null);
1392 } else {
1393 setName(nameVersion.substring(0, index));
1394 setVersion(nameVersion.substring(index + ";version=".length()));
1395 }
1396 }
1397
1398 NameVersion(String name, String version) {
1399 this.name = name;
1400 this.version = version;
1401 }
1402
1403 NameVersion(NameVersion nameVersion) {
1404 this.name = nameVersion.getName();
1405 this.version = nameVersion.getVersion();
1406 }
1407
1408 String getName() {
1409 return name;
1410 }
1411
1412 void setName(String name) {
1413 this.name = name;
1414 }
1415
1416 String getVersion() {
1417 return version;
1418 }
1419
1420 void setVersion(String version) {
1421 this.version = version;
1422 }
1423
1424 String getBranch() {
1425 String[] parts = getVersion().split("\\.");
1426 if (parts.length < 2)
1427 throw new IllegalStateException("Version " + getVersion() + " cannot be interpreted as branch.");
1428 return parts[0] + "." + parts[1];
1429 }
1430
1431 @Override
1432 public boolean equals(Object obj) {
1433 if (obj instanceof NameVersion) {
1434 NameVersion nameVersion = (NameVersion) obj;
1435 return name.equals(nameVersion.getName()) && version.equals(nameVersion.getVersion());
1436 } else
1437 return false;
1438 }
1439
1440 @Override
1441 public int hashCode() {
1442 return name.hashCode();
1443 }
1444
1445 @Override
1446 public String toString() {
1447 return name + ":" + version;
1448 }
1449
1450 public int compareTo(NameVersion o) {
1451 if (o.getName().equals(name))
1452 return version.compareTo(o.getVersion());
1453 else
1454 return name.compareTo(o.getName());
1455 }
1456 }