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