]> git.argeo.org Git - cc0/argeo-build.git/blob - src/org/argeo/build/Make.java
4d9caf93145e8bc717bf90c415519896a9cf174b
[cc0/argeo-build.git] / src / org / argeo / build / Make.java
1 package org.argeo.build;
2
3 import static java.lang.System.Logger.Level.ERROR;
4 import static java.lang.System.Logger.Level.INFO;
5
6 import java.io.File;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.io.OutputStream;
10 import java.io.PrintWriter;
11 import java.lang.System.Logger;
12 import java.lang.System.Logger.Level;
13 import java.lang.management.ManagementFactory;
14 import java.nio.file.FileVisitResult;
15 import java.nio.file.Files;
16 import java.nio.file.Path;
17 import java.nio.file.PathMatcher;
18 import java.nio.file.Paths;
19 import java.nio.file.SimpleFileVisitor;
20 import java.nio.file.attribute.BasicFileAttributes;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.Properties;
28 import java.util.StringJoiner;
29 import java.util.StringTokenizer;
30 import java.util.concurrent.CompletableFuture;
31 import java.util.jar.Attributes;
32 import java.util.jar.JarEntry;
33 import java.util.jar.JarOutputStream;
34 import java.util.jar.Manifest;
35 import java.util.zip.Deflater;
36
37 import org.eclipse.jdt.core.compiler.CompilationProgress;
38
39 import aQute.bnd.osgi.Analyzer;
40 import aQute.bnd.osgi.Jar;
41
42 /**
43 * Minimalistic OSGi compiler and packager, meant to be used as a single file
44 * without being itself compiled first. It depends on the Eclipse batch compiler
45 * (aka. ECJ) and the BND Libs library for OSGi metadata generation (which
46 * itselfs depends on slf4j).<br/>
47 * <br/>
48 * For example, a typical system call would be:<br/>
49 * <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>
50 */
51 public class Make {
52 private final static Logger logger = System.getLogger(Make.class.getName());
53
54 /**
55 * Environment properties on whether sources should be packaged separately or
56 * integrated in the bundles.
57 */
58 private final static String ENV_BUILD_SOURCE_BUNDLES = "BUILD_SOURCE_BUNDLES";
59
60 /** Name of the local-specific Makefile (sdk.mk). */
61 final static String SDK_MK = "sdk.mk";
62 /** Name of the branch definition Makefile (branch.mk). */
63 final static String BRANCH_MK = "branch.mk";
64
65 /** The execution directory (${user.dir}). */
66 final Path execDirectory;
67 /** Base of the source code, typically the cloned git repository. */
68 final Path sdkSrcBase;
69 /**
70 * The base of the builder, typically a submodule pointing to the public
71 * argeo-build directory.
72 */
73 final Path argeoBuildBase;
74 /** The base of the build for all layers. */
75 final Path sdkBuildBase;
76 /** The base of the build for this layer. */
77 final Path buildBase;
78 /** The base of the a2 output for all layers. */
79 final Path a2Output;
80
81 /** Whether sources should be packaged separately */
82 final boolean sourceBundles;
83
84 /** Constructor initialises the base directories. */
85 public Make() throws IOException {
86 if (System.getenv(ENV_BUILD_SOURCE_BUNDLES) != null) {
87 sourceBundles = Boolean.parseBoolean(System.getenv(ENV_BUILD_SOURCE_BUNDLES));
88 if (sourceBundles)
89 logger.log(Level.INFO, "Sources will be packaged separately");
90 } else {
91 sourceBundles = true;
92 }
93
94 execDirectory = Paths.get(System.getProperty("user.dir"));
95 Path sdkMkP = findSdkMk(execDirectory);
96 Objects.requireNonNull(sdkMkP, "No " + SDK_MK + " found under " + execDirectory);
97
98 Map<String, String> context = readeMakefileVariables(sdkMkP);
99 sdkSrcBase = Paths.get(context.computeIfAbsent("SDK_SRC_BASE", (key) -> {
100 throw new IllegalStateException(key + " not found");
101 })).toAbsolutePath();
102 argeoBuildBase = sdkSrcBase.resolve("sdk/argeo-build");
103
104 sdkBuildBase = Paths.get(context.computeIfAbsent("SDK_BUILD_BASE", (key) -> {
105 throw new IllegalStateException(key + " not found");
106 })).toAbsolutePath();
107 buildBase = sdkBuildBase.resolve(sdkSrcBase.getFileName());
108 a2Output = sdkBuildBase.resolve("a2");
109 }
110
111 /*
112 * ACTIONS
113 */
114 /** Compile and create the bundles in one go. */
115 void all(Map<String, List<String>> options) throws IOException {
116 compile(options);
117 bundle(options);
118 }
119
120 /** Compile all the bundles which have been passed via the --bundle argument. */
121 @SuppressWarnings("restriction")
122 void compile(Map<String, List<String>> options) throws IOException {
123 List<String> bundles = options.get("--bundles");
124 Objects.requireNonNull(bundles, "--bundles argument must be set");
125 if (bundles.isEmpty())
126 return;
127
128 List<String> a2Categories = options.getOrDefault("--dep-categories", new ArrayList<>());
129 List<String> a2Bases = options.getOrDefault("--a2-bases", new ArrayList<>());
130 if (a2Bases.isEmpty()) {
131 a2Bases.add(a2Output.toString());
132 }
133
134 List<String> compilerArgs = new ArrayList<>();
135
136 Path ecjArgs = argeoBuildBase.resolve("ecj.args");
137 compilerArgs.add("@" + ecjArgs);
138
139 // classpath
140 if (!a2Categories.isEmpty()) {
141 StringJoiner classPath = new StringJoiner(File.pathSeparator);
142 StringJoiner modulePath = new StringJoiner(File.pathSeparator);
143 for (String a2Base : a2Bases) {
144 for (String a2Category : a2Categories) {
145 Path a2Dir = Paths.get(a2Base).resolve(a2Category);
146 if (!Files.exists(a2Dir))
147 Files.createDirectories(a2Dir);
148 modulePath.add(a2Dir.toString());
149 for (Path jarP : Files.newDirectoryStream(a2Dir,
150 (p) -> p.getFileName().toString().endsWith(".jar"))) {
151 classPath.add(jarP.toString());
152 }
153 }
154 }
155 compilerArgs.add("-cp");
156 compilerArgs.add(classPath.toString());
157 // compilerArgs.add("--module-path");
158 // compilerArgs.add(modulePath.toString());
159 }
160
161 // sources
162 for (String bundle : bundles) {
163 StringBuilder sb = new StringBuilder();
164 Path bundlePath = execDirectory.resolve(bundle);
165 if (!Files.exists(bundlePath))
166 throw new IllegalArgumentException("Bundle " + bundle + " not found in " + execDirectory);
167 sb.append(bundlePath.resolve("src"));
168 sb.append("[-d");
169 compilerArgs.add(sb.toString());
170 sb = new StringBuilder();
171 sb.append(buildBase.resolve(bundle).resolve("bin"));
172 sb.append("]");
173 compilerArgs.add(sb.toString());
174 }
175
176 if (logger.isLoggable(INFO))
177 compilerArgs.add("-time");
178
179 // for (String arg : compilerArgs)
180 // System.out.println(arg);
181
182 boolean success = org.eclipse.jdt.core.compiler.batch.BatchCompiler.compile(
183 compilerArgs.toArray(new String[compilerArgs.size()]), new PrintWriter(System.out),
184 new PrintWriter(System.err), new MakeCompilationProgress());
185 if (!success) // kill the process if compilation failed
186 throw new IllegalStateException("Compilation failed");
187 }
188
189 /** Package the bundles. */
190 void bundle(Map<String, List<String>> options) throws IOException {
191 // check arguments
192 List<String> bundles = options.get("--bundles");
193 Objects.requireNonNull(bundles, "--bundles argument must be set");
194 if (bundles.isEmpty())
195 return;
196
197 List<String> categories = options.get("--category");
198 Objects.requireNonNull(bundles, "--category argument must be set");
199 if (categories.size() != 1)
200 throw new IllegalArgumentException("One and only one --category must be specified");
201 String category = categories.get(0);
202
203 final String branch;
204 Path branchMk = sdkSrcBase.resolve(BRANCH_MK);
205 if (Files.exists(branchMk)) {
206 Map<String, String> branchVariables = readeMakefileVariables(branchMk);
207 branch = branchVariables.get("BRANCH");
208 } else {
209 branch = null;
210 }
211
212 long begin = System.currentTimeMillis();
213 // create jars in parallel
214 List<CompletableFuture<Void>> toDos = new ArrayList<>();
215 for (String bundle : bundles) {
216 toDos.add(CompletableFuture.runAsync(() -> {
217 try {
218 createBundle(branch, bundle, category);
219 } catch (IOException e) {
220 throw new RuntimeException("Packaging of " + bundle + " failed", e);
221 }
222 }));
223 }
224 CompletableFuture.allOf(toDos.toArray(new CompletableFuture[toDos.size()])).join();
225 long duration = System.currentTimeMillis() - begin;
226 logger.log(INFO, "Packaging took " + duration + " ms");
227 }
228
229 /*
230 * UTILITIES
231 */
232 /** Package a single bundle. */
233 void createBundle(String branch, String bundle, String category) throws IOException {
234 Path source = execDirectory.resolve(bundle);
235 Path compiled = buildBase.resolve(bundle);
236 String bundleSymbolicName = source.getFileName().toString();
237
238 // Metadata
239 Properties properties = new Properties();
240 Path argeoBnd = argeoBuildBase.resolve("argeo.bnd");
241 try (InputStream in = Files.newInputStream(argeoBnd)) {
242 properties.load(in);
243 }
244
245 if (branch != null) {
246 Path branchBnd = sdkSrcBase.resolve("sdk/branches/" + branch + ".bnd");
247 if (Files.exists(branchBnd))
248 try (InputStream in = Files.newInputStream(branchBnd)) {
249 properties.load(in);
250 }
251 }
252
253 Path bndBnd = source.resolve("bnd.bnd");
254 if (Files.exists(bndBnd))
255 try (InputStream in = Files.newInputStream(bndBnd)) {
256 properties.load(in);
257 }
258
259 // Normalise
260 if (!properties.containsKey("Bundle-SymbolicName"))
261 properties.put("Bundle-SymbolicName", bundleSymbolicName);
262
263 // Calculate MANIFEST
264 Path binP = compiled.resolve("bin");
265 if (!Files.exists(binP))
266 Files.createDirectories(binP);
267 Manifest manifest;
268 try (Analyzer bndAnalyzer = new Analyzer()) {
269 bndAnalyzer.setProperties(properties);
270 Jar jar = new Jar(bundleSymbolicName, binP.toFile());
271 bndAnalyzer.setJar(jar);
272 manifest = bndAnalyzer.calcManifest();
273 } catch (Exception e) {
274 throw new RuntimeException("Bnd analysis of " + compiled + " failed", e);
275 }
276
277 String major = properties.getProperty("major");
278 Objects.requireNonNull(major, "'major' must be set");
279 String minor = properties.getProperty("minor");
280 Objects.requireNonNull(minor, "'minor' must be set");
281
282 // Write manifest
283 Path manifestP = compiled.resolve("META-INF/MANIFEST.MF");
284 Files.createDirectories(manifestP.getParent());
285 try (OutputStream out = Files.newOutputStream(manifestP)) {
286 manifest.write(out);
287 }
288
289 // Load excludes
290 List<PathMatcher> excludes = new ArrayList<>();
291 Path excludesP = argeoBuildBase.resolve("excludes.txt");
292 for (String line : Files.readAllLines(excludesP)) {
293 PathMatcher pathMatcher = excludesP.getFileSystem().getPathMatcher("glob:" + line);
294 excludes.add(pathMatcher);
295 }
296
297 Path bundleParent = Paths.get(bundle).getParent();
298 Path a2JarDirectory = bundleParent != null ? a2Output.resolve(bundleParent).resolve(category)
299 : a2Output.resolve(category);
300 Path jarP = a2JarDirectory.resolve(compiled.getFileName() + "." + major + "." + minor + ".jar");
301 Files.createDirectories(jarP.getParent());
302
303 try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarP), manifest)) {
304 jarOut.setLevel(Deflater.DEFAULT_COMPRESSION);
305 // add all classes first
306 Files.walkFileTree(binP, new SimpleFileVisitor<Path>() {
307 @Override
308 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
309 jarOut.putNextEntry(new JarEntry(binP.relativize(file).toString()));
310 Files.copy(file, jarOut);
311 return FileVisitResult.CONTINUE;
312 }
313 });
314
315 // add resources
316 Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
317 @Override
318 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
319 Path relativeP = source.relativize(dir);
320 for (PathMatcher exclude : excludes)
321 if (exclude.matches(relativeP))
322 return FileVisitResult.SKIP_SUBTREE;
323
324 return FileVisitResult.CONTINUE;
325 }
326
327 @Override
328 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
329 Path relativeP = source.relativize(file);
330 for (PathMatcher exclude : excludes)
331 if (exclude.matches(relativeP))
332 return FileVisitResult.CONTINUE;
333 JarEntry entry = new JarEntry(relativeP.toString());
334 jarOut.putNextEntry(entry);
335 Files.copy(file, jarOut);
336 return FileVisitResult.CONTINUE;
337 }
338 });
339
340 Path srcP = source.resolve("src");
341 // Add all resources from src/
342 Files.walkFileTree(srcP, new SimpleFileVisitor<Path>() {
343 @Override
344 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
345 if (file.getFileName().toString().endsWith(".java")
346 || file.getFileName().toString().endsWith(".class"))
347 return FileVisitResult.CONTINUE;
348 jarOut.putNextEntry(new JarEntry(srcP.relativize(file).toString()));
349 if (!Files.isDirectory(file))
350 Files.copy(file, jarOut);
351 return FileVisitResult.CONTINUE;
352 }
353 });
354
355 // add sources
356 // TODO add effective BND, Eclipse project file, etc., in order to be able to
357 // repackage
358 if (sourceBundles) {
359 Path srcJarP = a2JarDirectory.resolve(compiled.getFileName() + "." + major + "." + minor + ".src.jar");
360 Manifest srcManifest = new Manifest();
361 srcManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
362 srcManifest.getMainAttributes().putValue("Bundle-SymbolicName", bundleSymbolicName + ".src");
363 srcManifest.getMainAttributes().putValue("Bundle-Version",
364 manifest.getMainAttributes().getValue("Bundle-Version").toString());
365 srcManifest.getMainAttributes().putValue("Eclipse-SourceBundle",
366 bundleSymbolicName + ";version=\"" + manifest.getMainAttributes().getValue("Bundle-Version"));
367
368 try (JarOutputStream srcJarOut = new JarOutputStream(Files.newOutputStream(srcJarP), srcManifest)) {
369 copySourcesToJar(srcP, srcJarOut, "");
370 }
371 } else {
372 copySourcesToJar(srcP, jarOut, "OSGI-OPT/src/");
373 }
374 }
375 }
376
377 void copySourcesToJar(Path srcP, JarOutputStream srcJarOut, String prefix) throws IOException {
378 Files.walkFileTree(srcP, new SimpleFileVisitor<Path>() {
379 @Override
380 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
381 srcJarOut.putNextEntry(new JarEntry(prefix + srcP.relativize(file).toString()));
382 if (!Files.isDirectory(file))
383 Files.copy(file, srcJarOut);
384 return FileVisitResult.CONTINUE;
385 }
386 });
387 }
388
389 /**
390 * Recursively find the base source directory (which contains the
391 * <code>{@value #SDK_MK}</code> file).
392 */
393 Path findSdkMk(Path directory) {
394 Path sdkMkP = directory.resolve(SDK_MK);
395 if (Files.exists(sdkMkP)) {
396 return sdkMkP.toAbsolutePath();
397 }
398 if (directory.getParent() == null)
399 return null;
400 return findSdkMk(directory.getParent());
401 }
402
403 /**
404 * Reads Makefile variable assignments of the form =, :=, or ?=, ignoring white
405 * spaces. To be used with very simple included Makefiles only.
406 */
407 Map<String, String> readeMakefileVariables(Path path) throws IOException {
408 Map<String, String> context = new HashMap<>();
409 List<String> sdkMkLines = Files.readAllLines(path);
410 lines: for (String line : sdkMkLines) {
411 StringTokenizer st = new StringTokenizer(line, " :=?");
412 if (!st.hasMoreTokens())
413 continue lines;
414 String key = st.nextToken();
415 if (!st.hasMoreTokens())
416 continue lines;
417 String value = st.nextToken();
418 if (st.hasMoreTokens()) // probably not a simple variable assignment
419 continue lines;
420 context.put(key, value);
421 }
422 return context;
423 }
424
425 /** Main entry point, interpreting actions and arguments. */
426 public static void main(String... args) {
427 if (args.length == 0)
428 throw new IllegalArgumentException("At least an action must be provided");
429 int actionIndex = 0;
430 String action = args[actionIndex];
431 if (args.length > actionIndex + 1 && !args[actionIndex + 1].startsWith("-"))
432 throw new IllegalArgumentException(
433 "Action " + action + " must be followed by an option: " + Arrays.asList(args));
434
435 Map<String, List<String>> options = new HashMap<>();
436 String currentOption = null;
437 for (int i = actionIndex + 1; i < args.length; i++) {
438 if (args[i].startsWith("-")) {
439 currentOption = args[i];
440 if (!options.containsKey(currentOption))
441 options.put(currentOption, new ArrayList<>());
442
443 } else {
444 options.get(currentOption).add(args[i]);
445 }
446 }
447
448 try {
449 Make argeoMake = new Make();
450 switch (action) {
451 case "compile" -> argeoMake.compile(options);
452 case "bundle" -> argeoMake.bundle(options);
453 case "all" -> argeoMake.all(options);
454
455 default -> throw new IllegalArgumentException("Unkown action: " + action);
456 }
457
458 long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
459 logger.log(INFO, "Make.java action '" + action + "' succesfully completed after " + (jvmUptime / 1000) + "."
460 + (jvmUptime % 1000) + " s");
461 } catch (Exception e) {
462 long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
463 logger.log(ERROR, "Make.java action '" + action + "' failed after " + (jvmUptime / 1000) + "."
464 + (jvmUptime % 1000) + " s", e);
465 System.exit(1);
466 }
467 }
468
469 /**
470 * An ECJ {@link CompilationProgress} printing a progress bar while compiling.
471 */
472 static class MakeCompilationProgress extends CompilationProgress {
473 private int totalWork;
474 private long currentChunk = 0;
475 private long chunksCount = 80;
476
477 @Override
478 public void worked(int workIncrement, int remainingWork) {
479 if (!logger.isLoggable(Level.INFO)) // progress bar only at INFO level
480 return;
481 long chunk = ((totalWork - remainingWork) * chunksCount) / totalWork;
482 if (chunk != currentChunk) {
483 currentChunk = chunk;
484 for (long i = 0; i < currentChunk; i++) {
485 System.out.print("#");
486 }
487 for (long i = currentChunk; i < chunksCount; i++) {
488 System.out.print("-");
489 }
490 System.out.print("\r");
491 }
492 if (remainingWork == 0)
493 System.out.print("\n");
494 }
495
496 @Override
497 public void setTaskName(String name) {
498 }
499
500 @Override
501 public boolean isCanceled() {
502 return false;
503 }
504
505 @Override
506 public void done() {
507 }
508
509 @Override
510 public void begin(int remainingWork) {
511 this.totalWork = remainingWork;
512 }
513 }
514 }