]> git.argeo.org Git - cc0/argeo-build.git/blob - build/Make.java
Releasing
[cc0/argeo-build.git] / build / Make.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
9 import java.io.File;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.io.OutputStream;
13 import java.io.PrintWriter;
14 import java.lang.System.Logger;
15 import java.lang.System.Logger.Level;
16 import java.lang.management.ManagementFactory;
17 import java.nio.file.DirectoryStream;
18 import java.nio.file.FileVisitResult;
19 import java.nio.file.Files;
20 import java.nio.file.Path;
21 import java.nio.file.PathMatcher;
22 import java.nio.file.Paths;
23 import java.nio.file.SimpleFileVisitor;
24 import java.nio.file.StandardCopyOption;
25 import java.nio.file.attribute.BasicFileAttributes;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Objects;
33 import java.util.Properties;
34 import java.util.StringJoiner;
35 import java.util.StringTokenizer;
36 import java.util.TreeMap;
37 import java.util.concurrent.CompletableFuture;
38 import java.util.jar.Attributes;
39 import java.util.jar.JarEntry;
40 import java.util.jar.JarOutputStream;
41 import java.util.jar.Manifest;
42 import java.util.zip.Deflater;
43
44 import org.eclipse.jdt.core.compiler.CompilationProgress;
45
46 import aQute.bnd.osgi.Analyzer;
47 import aQute.bnd.osgi.Jar;
48
49 /**
50 * Minimalistic OSGi compiler and packager, meant to be used as a single file
51 * without being itself compiled first. It depends on the Eclipse batch compiler
52 * (aka. ECJ) and the BND Libs library for OSGi metadata generation (which
53 * itselfs depends on slf4j).<br/>
54 * <br/>
55 * For example, a typical system call would be:<br/>
56 * <code>java -cp "/path/to/ECJ jar:/path/to/bndlib jar:/path/to/SLF4J jar" /path/to/cloned/argeo-build/src/org/argeo/build/Make.java action --option1 argument1 argument2 --option2 argument3 </code>
57 */
58 public class Make {
59 private final static Logger logger = System.getLogger(Make.class.getName());
60
61 /**
62 * Environment variable on whether sources should be packaged separately or
63 * integrated in the bundles.
64 */
65 private final static String ENV_SOURCE_BUNDLES = "SOURCE_BUNDLES";
66
67 /**
68 * Environment variable on whether legal files at the root of the sources should
69 * be included in the generated bundles. Should be set to true when building
70 * third-party software in order no to include the build harness license into
71 * the generated bundles.
72 */
73 private final static String ENV_NO_SDK_LEGAL = "NO_SDK_LEGAL";
74
75 /**
76 * Environment variable to override the default location for the Argeo Build
77 * configuration files. Typically used if Argeo Build has been compiled and
78 * packaged separately.
79 */
80 private final static String ENV_ARGEO_BUILD_CONFIG = "ARGEO_BUILD_CONFIG";
81
82 /** Make file variable (in {@link #SDK_MK}) with a path to the sources base. */
83 private final static String VAR_SDK_SRC_BASE = "SDK_SRC_BASE";
84
85 /**
86 * Make file variable (in {@link #SDK_MK}) with a path to the build output base.
87 */
88 private final static String VAR_SDK_BUILD_BASE = "SDK_BUILD_BASE";
89 /**
90 * Make file variable (in {@link #BRANCH_MK}) with the branch.
91 */
92 private final static String VAR_BRANCH = "BRANCH";
93
94 /** Name of the local-specific Makefile (sdk.mk). */
95 final static String SDK_MK = "sdk.mk";
96 /** Name of the branch definition Makefile (branch.mk). */
97 final static String BRANCH_MK = "branch.mk";
98
99 /** The execution directory (${user.dir}). */
100 final Path execDirectory;
101 /** Base of the source code, typically the cloned git repository. */
102 final Path sdkSrcBase;
103 /**
104 * The base of the builder, typically a submodule pointing to the INCLUDEpublic
105 * argeo-build directory.
106 */
107 final Path argeoBuildBase;
108 /** The base of the build for all layers. */
109 final Path sdkBuildBase;
110 /** The base of the build for this layer. */
111 final Path buildBase;
112 /** The base of the a2 output for all layers. */
113 final Path a2Output;
114 /** The base of the a2 sources when packaged separately. */
115 final Path a2srcOutput;
116
117 /** Whether sources should be packaged separately. */
118 final boolean sourceBundles;
119 /** Whether common legal files should be included. */
120 final boolean noSdkLegal;
121
122 /** Constructor initialises the base directories. */
123 public Make() throws IOException {
124 sourceBundles = Boolean.parseBoolean(System.getenv(ENV_SOURCE_BUNDLES));
125 if (sourceBundles)
126 logger.log(Level.INFO, "Sources will be packaged separately");
127 noSdkLegal = Boolean.parseBoolean(System.getenv(ENV_NO_SDK_LEGAL));
128 if (noSdkLegal)
129 logger.log(Level.INFO, "SDK legal files will NOT be included");
130
131 execDirectory = Paths.get(System.getProperty("user.dir"));
132 Path sdkMkP = findSdkMk(execDirectory);
133 Objects.requireNonNull(sdkMkP, "No " + SDK_MK + " found under " + execDirectory);
134
135 Map<String, String> context = readMakefileVariables(sdkMkP);
136 sdkSrcBase = Paths.get(context.computeIfAbsent(VAR_SDK_SRC_BASE, (key) -> {
137 throw new IllegalStateException(key + " not found");
138 })).toAbsolutePath();
139
140 Path argeoBuildBaseT = sdkSrcBase.resolve("sdk/argeo-build");
141 if (!Files.exists(argeoBuildBaseT)) {
142 String fromEnv = System.getenv(ENV_ARGEO_BUILD_CONFIG);
143 if (fromEnv != null)
144 argeoBuildBaseT = Paths.get(fromEnv);
145 if (fromEnv == null || !Files.exists(argeoBuildBaseT)) {
146 throw new IllegalStateException(
147 "Argeo Build not found. Did you initialise the git submodules or set the "
148 + ENV_ARGEO_BUILD_CONFIG + " environment variable?");
149 }
150 }
151 argeoBuildBase = argeoBuildBaseT;
152
153 sdkBuildBase = Paths.get(context.computeIfAbsent(VAR_SDK_BUILD_BASE, (key) -> {
154 throw new IllegalStateException(key + " not found");
155 })).toAbsolutePath();
156 buildBase = sdkBuildBase.resolve(sdkSrcBase.getFileName());
157 a2Output = sdkBuildBase.resolve("a2");
158 a2srcOutput = sdkBuildBase.resolve("a2.src");
159 }
160
161 /*
162 * ACTIONS
163 */
164 /** Compile and create the bundles in one go. */
165 void all(Map<String, List<String>> options) throws IOException {
166 compile(options);
167 bundle(options);
168 }
169
170 /** Compile all the bundles which have been passed via the --bundle argument. */
171 void compile(Map<String, List<String>> options) throws IOException {
172 List<String> bundles = options.get("--bundles");
173 Objects.requireNonNull(bundles, "--bundles argument must be set");
174 if (bundles.isEmpty())
175 return;
176
177 List<String> a2Categories = options.getOrDefault("--dep-categories", new ArrayList<>());
178 List<String> a2Bases = options.getOrDefault("--a2-bases", new ArrayList<>());
179 if (a2Bases.isEmpty() || !a2Bases.contains(a2Output.toString())) {
180 a2Bases.add(a2Output.toString());
181 }
182
183 List<String> compilerArgs = new ArrayList<>();
184
185 Path ecjArgs = argeoBuildBase.resolve("ecj.args");
186 compilerArgs.add("@" + ecjArgs);
187
188 // classpath
189 if (!a2Categories.isEmpty()) {
190 // We will keep only the highest major.minor
191 // and order by bundle name, for predictability
192 Map<String, A2Jar> a2Jars = new TreeMap<>();
193
194 // StringJoiner modulePath = new StringJoiner(File.pathSeparator);
195 for (String a2Base : a2Bases) {
196 categories: for (String a2Category : a2Categories) {
197 Path a2Dir = Paths.get(a2Base).resolve(a2Category);
198 if (!Files.exists(a2Dir))
199 continue categories;
200 // modulePath.add(a2Dir.toString());
201 for (Path jarP : Files.newDirectoryStream(a2Dir, (p) -> p.getFileName().toString().endsWith(".jar")
202 && !p.getFileName().toString().endsWith(".src.jar"))) {
203 A2Jar a2Jar = new A2Jar(jarP);
204 if (a2Jars.containsKey(a2Jar.name)) {
205 A2Jar current = a2Jars.get(a2Jar.name);
206 if (a2Jar.major > current.major)
207 a2Jars.put(a2Jar.name, a2Jar);
208 else if (a2Jar.major == current.major //
209 // if minor equals, we take the last one
210 && a2Jar.minor >= current.minor)
211 a2Jars.put(a2Jar.name, a2Jar);
212 } else {
213 a2Jars.put(a2Jar.name, a2Jar);
214 }
215 }
216 }
217 }
218
219 StringJoiner classPath = new StringJoiner(File.pathSeparator);
220 for (Iterator<A2Jar> it = a2Jars.values().iterator(); it.hasNext();)
221 classPath.add(it.next().path.toString());
222
223 compilerArgs.add("-cp");
224 compilerArgs.add(classPath.toString());
225 // compilerArgs.add("--module-path");
226 // compilerArgs.add(modulePath.toString());
227 }
228
229 // sources
230 boolean atLeastOneBundleToCompile = false;
231 bundles: for (String bundle : bundles) {
232 StringBuilder sb = new StringBuilder();
233 Path bundlePath = execDirectory.resolve(bundle);
234 if (!Files.exists(bundlePath)) {
235 if (bundles.size() == 1) {
236 logger.log(WARNING, "Bundle " + bundle + " not found in " + execDirectory
237 + ", assuming this is this directory, as only one bundle was requested.");
238 bundlePath = execDirectory;
239 } else
240 throw new IllegalArgumentException("Bundle " + bundle + " not found in " + execDirectory);
241 }
242 Path bundleSrc = bundlePath.resolve("src");
243 if (!Files.exists(bundleSrc)) {
244 logger.log(TRACE, bundleSrc + " does not exist, skipping it, as this is not a Java bundle");
245 continue bundles;
246 }
247 sb.append(bundleSrc);
248 sb.append("[-d");
249 compilerArgs.add(sb.toString());
250 sb = new StringBuilder();
251 sb.append(buildBase.resolve(bundle).resolve("bin"));
252 sb.append("]");
253 compilerArgs.add(sb.toString());
254 atLeastOneBundleToCompile = true;
255 }
256
257 if (!atLeastOneBundleToCompile)
258 return;
259
260 if (logger.isLoggable(INFO))
261 compilerArgs.add("-time");
262
263 if (logger.isLoggable(DEBUG)) {
264 logger.log(DEBUG, "Compiler arguments:");
265 for (String arg : compilerArgs)
266 logger.log(DEBUG, arg);
267 }
268
269 boolean success = org.eclipse.jdt.core.compiler.batch.BatchCompiler.compile(
270 compilerArgs.toArray(new String[compilerArgs.size()]), new PrintWriter(System.out),
271 new PrintWriter(System.err), new MakeCompilationProgress());
272 if (!success) // kill the process if compilation failed
273 throw new IllegalStateException("Compilation failed");
274 }
275
276 /** Package the bundles. */
277 void bundle(Map<String, List<String>> options) throws IOException {
278 // check arguments
279 List<String> bundles = options.get("--bundles");
280 Objects.requireNonNull(bundles, "--bundles argument must be set");
281 if (bundles.isEmpty())
282 return;
283
284 List<String> categories = options.get("--category");
285 Objects.requireNonNull(categories, "--category argument must be set");
286 if (categories.size() != 1)
287 throw new IllegalArgumentException("One and only one --category must be specified");
288 String category = categories.get(0);
289
290 final String branch;
291 Path branchMk = sdkSrcBase.resolve(BRANCH_MK);
292 if (Files.exists(branchMk)) {
293 Map<String, String> branchVariables = readMakefileVariables(branchMk);
294 branch = branchVariables.get(VAR_BRANCH);
295 } else {
296 branch = null;
297 }
298
299 long begin = System.currentTimeMillis();
300 // create jars in parallel
301 List<CompletableFuture<Void>> toDos = new ArrayList<>();
302 for (String bundle : bundles) {
303 toDos.add(CompletableFuture.runAsync(() -> {
304 try {
305 createBundle(branch, bundle, category);
306 } catch (IOException e) {
307 throw new RuntimeException("Packaging of " + bundle + " failed", e);
308 }
309 }));
310 }
311 CompletableFuture.allOf(toDos.toArray(new CompletableFuture[toDos.size()])).join();
312 long duration = System.currentTimeMillis() - begin;
313 logger.log(INFO, "Packaging took " + duration + " ms");
314 }
315
316 /** Install the bundles. */
317 void install(Map<String, List<String>> options, boolean uninstall) throws IOException {
318 // check arguments
319 List<String> bundles = options.get("--bundles");
320 Objects.requireNonNull(bundles, "--bundles argument must be set");
321 if (bundles.isEmpty())
322 return;
323
324 List<String> categories = options.get("--category");
325 Objects.requireNonNull(categories, "--category argument must be set");
326 if (categories.size() != 1)
327 throw new IllegalArgumentException("One and only one --category must be specified");
328 String category = categories.get(0);
329
330 List<String> targetDirs = options.get("--target");
331 Objects.requireNonNull(targetDirs, "--target argument must be set");
332 if (targetDirs.size() != 1)
333 throw new IllegalArgumentException("One and only one --target must be specified");
334 Path targetA2 = Paths.get(targetDirs.get(0));
335 logger.log(INFO, (uninstall ? "Uninstalling from " : "Installing to ") + targetA2);
336
337 final String branch;
338 Path branchMk = sdkSrcBase.resolve(BRANCH_MK);
339 if (Files.exists(branchMk)) {
340 Map<String, String> branchVariables = readMakefileVariables(branchMk);
341 branch = branchVariables.get(VAR_BRANCH);
342 } else {
343 throw new IllegalArgumentException(VAR_BRANCH + " variable must be set.");
344 }
345
346 Properties properties = new Properties();
347 Path branchBnd = sdkSrcBase.resolve("sdk/branches/" + branch + ".bnd");
348 if (Files.exists(branchBnd))
349 try (InputStream in = Files.newInputStream(branchBnd)) {
350 properties.load(in);
351 }
352 String major = properties.getProperty("major");
353 Objects.requireNonNull(major, "'major' must be set");
354 String minor = properties.getProperty("minor");
355 Objects.requireNonNull(minor, "'minor' must be set");
356
357 int count = 0;
358 for (String bundle : bundles) {
359 Path bundlePath = Paths.get(bundle);
360 Path bundleParent = bundlePath.getParent();
361 Path a2JarDirectory = bundleParent != null ? a2Output.resolve(bundleParent).resolve(category)
362 : a2Output.resolve(category);
363 Path jarP = a2JarDirectory.resolve(bundlePath.getFileName() + "." + major + "." + minor + ".jar");
364
365 Path targetJarP = targetA2.resolve(a2Output.relativize(jarP));
366 if (uninstall) { // uninstall
367 if (Files.exists(targetJarP)) {
368 Files.delete(targetJarP);
369 logger.log(DEBUG, "Removed " + targetJarP);
370 count++;
371 }
372 Path targetParent = targetJarP.getParent();
373 deleteEmptyParents(targetA2, targetParent);
374 } else { // install
375 Files.createDirectories(targetJarP.getParent());
376 boolean update = Files.exists(targetJarP);
377 Files.copy(jarP, targetJarP, StandardCopyOption.REPLACE_EXISTING);
378 logger.log(DEBUG, (update ? "Updated " : "Installed ") + targetJarP);
379 count++;
380 }
381 }
382 logger.log(INFO, uninstall ? count + " bundles removed" : count + " bundles installed or updated");
383 }
384
385 /** Delete empty parent directory up to the A2 target (included). */
386 void deleteEmptyParents(Path targetA2, Path targetParent) throws IOException {
387 if (!Files.isDirectory(targetParent))
388 throw new IllegalArgumentException(targetParent + " must be a directory");
389 boolean isA2target = Files.isSameFile(targetA2, targetParent);
390 if (!Files.list(targetParent).iterator().hasNext()) {
391 Files.delete(targetParent);
392 if (isA2target)
393 return;// stop after deleting A2 base
394 deleteEmptyParents(targetA2, targetParent.getParent());
395 }
396 }
397
398 /** Package a single bundle. */
399 void createBundle(String branch, String bundle, String category) throws IOException {
400 final Path source;
401 if (!Files.exists(execDirectory.resolve(bundle))) {
402 logger.log(WARNING,
403 "Bundle " + bundle + " not found in " + execDirectory + ", assuming this is this directory.");
404 source = execDirectory;
405 } else {
406 source = execDirectory.resolve(bundle);
407 }
408 Path srcP = source.resolve("src");
409
410 Path compiled = buildBase.resolve(bundle);
411 String bundleSymbolicName = source.getFileName().toString();
412
413 // Metadata
414 Properties properties = new Properties();
415 Path argeoBnd = argeoBuildBase.resolve("argeo.bnd");
416 try (InputStream in = Files.newInputStream(argeoBnd)) {
417 properties.load(in);
418 }
419
420 if (branch != null) {
421 Path branchBnd = sdkSrcBase.resolve("sdk/branches/" + branch + ".bnd");
422 if (Files.exists(branchBnd))
423 try (InputStream in = Files.newInputStream(branchBnd)) {
424 properties.load(in);
425 }
426 }
427
428 Path bndBnd = source.resolve("bnd.bnd");
429 if (Files.exists(bndBnd))
430 try (InputStream in = Files.newInputStream(bndBnd)) {
431 properties.load(in);
432 }
433
434 // Normalise
435 if (!properties.containsKey("Bundle-SymbolicName"))
436 properties.put("Bundle-SymbolicName", bundleSymbolicName);
437
438 // Calculate MANIFEST
439 Path binP = compiled.resolve("bin");
440 if (!Files.exists(binP))
441 Files.createDirectories(binP);
442 Manifest manifest;
443 try (Analyzer bndAnalyzer = new Analyzer()) {
444 bndAnalyzer.setProperties(properties);
445 Jar jar = new Jar(bundleSymbolicName, binP.toFile());
446 bndAnalyzer.setJar(jar);
447 manifest = bndAnalyzer.calcManifest();
448 } catch (Exception e) {
449 throw new RuntimeException("Bnd analysis of " + compiled + " failed", e);
450 }
451
452 String major = properties.getProperty("major");
453 Objects.requireNonNull(major, "'major' must be set");
454 String minor = properties.getProperty("minor");
455 Objects.requireNonNull(minor, "'minor' must be set");
456
457 // Write manifest
458 Path manifestP = compiled.resolve("META-INF/MANIFEST.MF");
459 Files.createDirectories(manifestP.getParent());
460 try (OutputStream out = Files.newOutputStream(manifestP)) {
461 manifest.write(out);
462 }
463
464 // Load excludes
465 List<PathMatcher> excludes = new ArrayList<>();
466 Path excludesP = argeoBuildBase.resolve("excludes.txt");
467 for (String line : Files.readAllLines(excludesP)) {
468 PathMatcher pathMatcher = excludesP.getFileSystem().getPathMatcher("glob:" + line);
469 excludes.add(pathMatcher);
470 }
471
472 Path bundleParent = Paths.get(bundle).getParent();
473 Path a2JarDirectory = bundleParent != null ? a2Output.resolve(bundleParent).resolve(category)
474 : a2Output.resolve(category);
475 Path jarP = a2JarDirectory.resolve(compiled.getFileName() + "." + major + "." + minor + ".jar");
476 Files.createDirectories(jarP.getParent());
477
478 try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarP), manifest)) {
479 jarOut.setLevel(Deflater.DEFAULT_COMPRESSION);
480 // add all classes first
481 Files.walkFileTree(binP, new SimpleFileVisitor<Path>() {
482 @Override
483 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
484 jarOut.putNextEntry(new JarEntry(binP.relativize(file).toString()));
485 Files.copy(file, jarOut);
486 return FileVisitResult.CONTINUE;
487 }
488 });
489
490 // add resources
491 Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
492 @Override
493 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
494 // skip output directory if it happens to be within the sources
495 if (Files.isSameFile(sdkBuildBase, dir))
496 return FileVisitResult.SKIP_SUBTREE;
497
498 // skip excluded patterns
499 Path relativeP = source.relativize(dir);
500 for (PathMatcher exclude : excludes)
501 if (exclude.matches(relativeP))
502 return FileVisitResult.SKIP_SUBTREE;
503
504 return FileVisitResult.CONTINUE;
505 }
506
507 @Override
508 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
509 Path relativeP = source.relativize(file);
510 for (PathMatcher exclude : excludes)
511 if (exclude.matches(relativeP))
512 return FileVisitResult.CONTINUE;
513 JarEntry entry = new JarEntry(relativeP.toString());
514 jarOut.putNextEntry(entry);
515 Files.copy(file, jarOut);
516 return FileVisitResult.CONTINUE;
517 }
518 });
519
520 // Add all resources from src/
521 Files.walkFileTree(srcP, new SimpleFileVisitor<Path>() {
522 @Override
523 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
524 // skip directories ending with .js
525 // TODO find something more robust?
526 if (dir.getFileName().toString().endsWith(".js"))
527 return FileVisitResult.SKIP_SUBTREE;
528 return super.preVisitDirectory(dir, attrs);
529 }
530
531 @Override
532 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
533 if (file.getFileName().toString().endsWith(".java")
534 || file.getFileName().toString().endsWith(".class"))
535 return FileVisitResult.CONTINUE;
536 jarOut.putNextEntry(new JarEntry(srcP.relativize(file).toString()));
537 if (!Files.isDirectory(file))
538 Files.copy(file, jarOut);
539 return FileVisitResult.CONTINUE;
540 }
541 });
542
543 // add legal notices and licenses
544 for (Path p : listLegalFilesToInclude(source).values()) {
545 jarOut.putNextEntry(new JarEntry(p.getFileName().toString()));
546 Files.copy(p, jarOut);
547 }
548
549 // add sources
550 // TODO add effective BND, Eclipse project file, etc., in order to be able to
551 // repackage
552 if (!sourceBundles) {
553 copySourcesToJar(srcP, jarOut, "OSGI-OPT/src/");
554 }
555 }
556
557 if (sourceBundles) {// create separate sources jar
558 Path a2srcJarDirectory = bundleParent != null ? a2srcOutput.resolve(bundleParent).resolve(category)
559 : a2srcOutput.resolve(category);
560 Files.createDirectories(a2srcJarDirectory);
561 Path srcJarP = a2srcJarDirectory.resolve(compiled.getFileName() + "." + major + "." + minor + ".src.jar");
562 Manifest srcManifest = new Manifest();
563 srcManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
564 srcManifest.getMainAttributes().putValue("Bundle-SymbolicName", bundleSymbolicName + ".src");
565 srcManifest.getMainAttributes().putValue("Bundle-Version",
566 manifest.getMainAttributes().getValue("Bundle-Version").toString());
567 srcManifest.getMainAttributes().putValue("Eclipse-SourceBundle",
568 bundleSymbolicName + ";version=\"" + manifest.getMainAttributes().getValue("Bundle-Version"));
569
570 try (JarOutputStream srcJarOut = new JarOutputStream(Files.newOutputStream(srcJarP), srcManifest)) {
571 copySourcesToJar(srcP, srcJarOut, "");
572 // add legal notices and licenses
573 for (Path p : listLegalFilesToInclude(source).values()) {
574 srcJarOut.putNextEntry(new JarEntry(p.getFileName().toString()));
575 Files.copy(p, srcJarOut);
576 }
577 }
578 }
579 }
580
581 /** List the relevant legal files to include, from the SDK source base. */
582 Map<String, Path> listLegalFilesToInclude(Path bundleBase) throws IOException {
583 Map<String, Path> toInclude = new HashMap<>();
584 if (!noSdkLegal) {
585 DirectoryStream<Path> sdkSrcLegal = Files.newDirectoryStream(sdkSrcBase, (p) -> {
586 String fileName = p.getFileName().toString();
587 return switch (fileName) {
588 case "NOTICE":
589 case "LICENSE":
590 case "COPYING":
591 case "COPYING.LESSER":
592 yield true;
593 default:
594 yield false;
595 };
596 });
597 for (Path p : sdkSrcLegal)
598 toInclude.put(p.getFileName().toString(), p);
599 }
600 for (Iterator<Map.Entry<String, Path>> entries = toInclude.entrySet().iterator(); entries.hasNext();) {
601 Map.Entry<String, Path> entry = entries.next();
602 Path inBundle = bundleBase.resolve(entry.getValue().getFileName());
603 // remove file if it is also defined at bundle level
604 // since it has already been copied
605 // and has priority
606 if (Files.exists(inBundle))
607 entries.remove();
608 }
609 return toInclude;
610 }
611
612 /*
613 * UTILITIES
614 */
615 /** Add sources to a jar file */
616 void copySourcesToJar(Path srcP, JarOutputStream srcJarOut, String prefix) throws IOException {
617 Files.walkFileTree(srcP, new SimpleFileVisitor<Path>() {
618 @Override
619 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
620 srcJarOut.putNextEntry(new JarEntry(prefix + srcP.relativize(file).toString()));
621 if (!Files.isDirectory(file))
622 Files.copy(file, srcJarOut);
623 return FileVisitResult.CONTINUE;
624 }
625 });
626 }
627
628 /**
629 * Recursively find the base source directory (which contains the
630 * <code>{@value #SDK_MK}</code> file).
631 */
632 Path findSdkMk(Path directory) {
633 Path sdkMkP = directory.resolve(SDK_MK);
634 if (Files.exists(sdkMkP)) {
635 return sdkMkP.toAbsolutePath();
636 }
637 if (directory.getParent() == null)
638 return null;
639 return findSdkMk(directory.getParent());
640 }
641
642 /**
643 * Reads Makefile variable assignments of the form =, :=, or ?=, ignoring white
644 * spaces. To be used with very simple included Makefiles only.
645 */
646 Map<String, String> readMakefileVariables(Path path) throws IOException {
647 Map<String, String> context = new HashMap<>();
648 List<String> sdkMkLines = Files.readAllLines(path);
649 lines: for (String line : sdkMkLines) {
650 StringTokenizer st = new StringTokenizer(line, " :=?");
651 if (!st.hasMoreTokens())
652 continue lines;
653 String key = st.nextToken();
654 if (!st.hasMoreTokens())
655 continue lines;
656 String value = st.nextToken();
657 if (st.hasMoreTokens()) // probably not a simple variable assignment
658 continue lines;
659 context.put(key, value);
660 }
661 return context;
662 }
663
664 /** Main entry point, interpreting actions and arguments. */
665 public static void main(String... args) {
666 if (args.length == 0)
667 throw new IllegalArgumentException("At least an action must be provided");
668 int actionIndex = 0;
669 String action = args[actionIndex];
670 if (args.length > actionIndex + 1 && !args[actionIndex + 1].startsWith("-"))
671 throw new IllegalArgumentException(
672 "Action " + action + " must be followed by an option: " + Arrays.asList(args));
673
674 Map<String, List<String>> options = new HashMap<>();
675 String currentOption = null;
676 for (int i = actionIndex + 1; i < args.length; i++) {
677 if (args[i].startsWith("-")) {
678 currentOption = args[i];
679 if (!options.containsKey(currentOption))
680 options.put(currentOption, new ArrayList<>());
681
682 } else {
683 options.get(currentOption).add(args[i]);
684 }
685 }
686
687 try {
688 Make argeoMake = new Make();
689 switch (action) {
690 case "compile" -> argeoMake.compile(options);
691 case "bundle" -> argeoMake.bundle(options);
692 case "all" -> argeoMake.all(options);
693 case "install" -> argeoMake.install(options, false);
694 case "uninstall" -> argeoMake.install(options, true);
695
696 default -> throw new IllegalArgumentException("Unkown action: " + action);
697 }
698
699 long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
700 logger.log(INFO, "Make.java action '" + action + "' succesfully completed after " + (jvmUptime / 1000) + "."
701 + (jvmUptime % 1000) + " s");
702 } catch (Exception e) {
703 long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
704 logger.log(ERROR, "Make.java action '" + action + "' failed after " + (jvmUptime / 1000) + "."
705 + (jvmUptime % 1000) + " s", e);
706 System.exit(1);
707 }
708 }
709
710 /** A jar file in A2 format */
711 static class A2Jar {
712 final Path path;
713 final String name;
714 final int major;
715 final int minor;
716
717 A2Jar(Path path) {
718 try {
719 this.path = path;
720 String fileName = path.getFileName().toString();
721 fileName = fileName.substring(0, fileName.lastIndexOf('.'));
722 minor = Integer.parseInt(fileName.substring(fileName.lastIndexOf('.') + 1));
723 fileName = fileName.substring(0, fileName.lastIndexOf('.'));
724 major = Integer.parseInt(fileName.substring(fileName.lastIndexOf('.') + 1));
725 name = fileName.substring(0, fileName.lastIndexOf('.'));
726 } catch (Exception e) {
727 throw new IllegalArgumentException("Badly formatted A2 jar " + path, e);
728 }
729 }
730 }
731
732 /**
733 * An ECJ {@link CompilationProgress} printing a progress bar while compiling.
734 */
735 static class MakeCompilationProgress extends CompilationProgress {
736 private int totalWork;
737 private long currentChunk = 0;
738 private long chunksCount = 80;
739
740 @Override
741 public void worked(int workIncrement, int remainingWork) {
742 if (!logger.isLoggable(Level.INFO)) // progress bar only at INFO level
743 return;
744 long chunk = ((totalWork - remainingWork) * chunksCount) / totalWork;
745 if (chunk != currentChunk) {
746 currentChunk = chunk;
747 for (long i = 0; i < currentChunk; i++) {
748 System.out.print("#");
749 }
750 for (long i = currentChunk; i < chunksCount; i++) {
751 System.out.print("-");
752 }
753 System.out.print("\r");
754 }
755 if (remainingWork == 0)
756 System.out.print("\n");
757 }
758
759 @Override
760 public void setTaskName(String name) {
761 }
762
763 @Override
764 public boolean isCanceled() {
765 return false;
766 }
767
768 @Override
769 public void done() {
770 }
771
772 @Override
773 public void begin(int remainingWork) {
774 this.totalWork = remainingWork;
775 }
776 }
777 }