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