Incremental build working
authorMathieu Baudier <mbaudier@argeo.org>
Sun, 9 Oct 2022 12:09:14 +0000 (14:09 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Sun, 9 Oct 2022 12:09:14 +0000 (14:09 +0200)
.classpath
build.properties
java/org/argeo/build/Make.java [deleted file]
osgi.mk
src/org/argeo/build/Make.java [new file with mode: 0644]

index d706197f22a4da7e777c25448b3a22beacd18b77..3628e336878e528db30e5202048bfc67be000fbd 100644 (file)
@@ -6,6 +6,6 @@
                </attributes>
        </classpathentry>
        <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
-       <classpathentry kind="src" path="java"/>
+       <classpathentry kind="src" path="src"/>
        <classpathentry kind="output" path="bin"/>
 </classpath>
index 31bbddc70b63fb3f71195b1a467eca92f213d95a..f8373a304fa4e437a54d46d23b471e379005605f 100644 (file)
@@ -1,4 +1,4 @@
 additional.bundles = org.slf4j.api
 bin.includes = META-INF/,\
-               java/org/
-source.. = java
\ No newline at end of file
+               src/org/
+source.. = src
diff --git a/java/org/argeo/build/Make.java b/java/org/argeo/build/Make.java
deleted file mode 100644 (file)
index f0ce443..0000000
+++ /dev/null
@@ -1,325 +0,0 @@
-package org.argeo.build;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.PathMatcher;
-import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Properties;
-import java.util.StringJoiner;
-import java.util.StringTokenizer;
-import java.util.jar.JarEntry;
-import java.util.jar.JarOutputStream;
-import java.util.jar.Manifest;
-
-import org.eclipse.jdt.core.compiler.CompilationProgress;
-
-import aQute.bnd.osgi.Analyzer;
-import aQute.bnd.osgi.Jar;
-
-public class Make {
-       private final static String SDK_MK = "sdk.mk";
-
-       final Path execDirectory;
-       final Path sdkSrcBase;
-       final Path argeoBuildBase;
-       final Path sdkBuildBase;
-       final Path buildBase;
-       final Path a2Output;
-
-       public Make() throws IOException {
-               execDirectory = Paths.get(System.getProperty("user.dir"));
-               Path sdkMkP = findSdkMk(execDirectory);
-               Objects.requireNonNull(sdkMkP, "No " + SDK_MK + " found under " + execDirectory);
-
-               Map<String, String> context = new HashMap<>();
-               List<String> sdkMkLines = Files.readAllLines(sdkMkP);
-               lines: for (String line : sdkMkLines) {
-                       StringTokenizer st = new StringTokenizer(line, " :=");
-                       if (!st.hasMoreTokens())
-                               continue lines;
-                       String key = st.nextToken();
-                       if (!st.hasMoreTokens())
-                               continue lines;
-                       String value = st.nextToken();
-                       context.put(key, value);
-               }
-
-               sdkSrcBase = Paths.get(context.computeIfAbsent("SDK_SRC_BASE", (key) -> {
-                       throw new IllegalStateException(key + " not found");
-               })).toAbsolutePath();
-               argeoBuildBase = sdkSrcBase.resolve("sdk/argeo-build");
-
-               sdkBuildBase = Paths.get(context.computeIfAbsent("SDK_BUILD_BASE", (key) -> {
-                       throw new IllegalStateException(key + " not found");
-               })).toAbsolutePath();
-               buildBase = sdkBuildBase.resolve(sdkSrcBase.getFileName());
-               a2Output = sdkBuildBase.resolve("a2");
-       }
-
-       /*
-        * ACTIONS
-        */
-
-       @SuppressWarnings("restriction")
-       void compile(Map<String, List<String>> options) throws IOException {
-               List<String> bundles = options.get("--bundles");
-               Objects.requireNonNull(bundles, "--bundles argument must be set");
-
-               List<String> a2Categories = options.getOrDefault("--dep-categories", new ArrayList<>());
-               List<String> a2Bases = options.getOrDefault("--a2-bases", new ArrayList<>());
-               if (a2Bases.isEmpty()) {
-                       a2Bases.add(a2Output.toString());
-               }
-
-               List<String> compilerArgs = new ArrayList<>();
-
-               Path ecjArgs = argeoBuildBase.resolve("ecj.args");
-               compilerArgs.add("@" + ecjArgs);
-
-               // classpath
-               if (!a2Categories.isEmpty()) {
-                       compilerArgs.add("-cp");
-                       StringJoiner classPath = new StringJoiner(File.pathSeparator);
-                       for (String a2Base : a2Bases) {
-                               for (String a2Category : a2Categories) {
-                                       Path a2Dir = Paths.get(a2Base).resolve(a2Category);
-                                       for (Path jarP : Files.newDirectoryStream(a2Dir,
-                                                       (p) -> p.getFileName().toString().endsWith(".jar"))) {
-                                               classPath.add(jarP.toString());
-                                       }
-                               }
-                       }
-                       compilerArgs.add(classPath.toString());
-               }
-
-               // sources
-               for (String bundle : bundles) {
-                       StringBuilder sb = new StringBuilder();
-                       sb.append(sdkSrcBase.resolve(bundle).resolve("src"));
-                       sb.append("[-d");
-                       compilerArgs.add(sb.toString());
-                       sb = new StringBuilder();
-                       sb.append(buildBase.resolve(bundle).resolve("bin"));
-                       sb.append("]");
-                       compilerArgs.add(sb.toString());
-               }
-
-               // System.out.println(compilerArgs);
-
-               // Use Main instead of BatchCompiler to workaround the fact that
-               // org.eclipse.jdt.core.compiler.batch is not exported
-               org.eclipse.jdt.internal.compiler.batch.Main.compile(compilerArgs.toArray(new String[compilerArgs.size()]),
-                               new PrintWriter(System.out), new PrintWriter(System.err), (CompilationProgress) null);
-       }
-
-       void bundle(Map<String, List<String>> options) throws IOException {
-               List<String> bundles = options.get("--bundles");
-               Objects.requireNonNull(bundles, "--bundles argument must be set");
-
-               List<String> categories = options.get("--category");
-               Objects.requireNonNull(bundles, "--bundles argument must be set");
-               if (categories.size() != 1)
-                       throw new IllegalArgumentException("One and only one category must be specified");
-               String category = categories.get(0);
-
-               // create jars
-               for (String bundle : bundles) {
-                       Path source = sdkSrcBase.resolve(bundle);
-                       Path compiled = buildBase.resolve(bundle);
-                       createBundle(source, compiled, category);
-               }
-       }
-
-       /*
-        * JAR PACKAGING
-        */
-       void createBundle(Path source, Path compiled, String category) throws IOException {
-               String bundleSymbolicName = source.getFileName().toString();
-
-               // Metadata
-               Properties properties = new Properties();
-               Path argeoBnd = argeoBuildBase.resolve("argeo.bnd");
-               try (InputStream in = Files.newInputStream(argeoBnd)) {
-                       properties.load(in);
-               }
-               // FIXME make it configurable
-               Path branchBnd = sdkSrcBase.resolve("cnf/unstable.bnd");
-               try (InputStream in = Files.newInputStream(branchBnd)) {
-                       properties.load(in);
-               }
-
-               Path bndBnd = source.resolve("bnd.bnd");
-               try (InputStream in = Files.newInputStream(bndBnd)) {
-                       properties.load(in);
-               }
-
-               // Normalise
-               properties.put("Bundle-SymbolicName", bundleSymbolicName);
-
-               // Calculate MANIFEST
-               Path binP = compiled.resolve("bin");
-               Manifest manifest;
-               try (Analyzer bndAnalyzer = new Analyzer()) {
-                       bndAnalyzer.setProperties(properties);
-                       Jar jar = new Jar(bundleSymbolicName, binP.toFile());
-                       bndAnalyzer.setJar(jar);
-                       manifest = bndAnalyzer.calcManifest();
-
-//                     keys: for (Object key : manifest.getMainAttributes().keySet()) {
-//                             System.out.println(key + ": " + manifest.getMainAttributes().getValue(key.toString()));
-//                     }
-               } catch (Exception e) {
-                       throw new RuntimeException("Bnd analysis of " + compiled + " failed", e);
-               }
-
-               String major = properties.getProperty("MAJOR");
-               Objects.requireNonNull(major, "MAJOR must be set");
-               String minor = properties.getProperty("MINOR");
-               Objects.requireNonNull(minor, "MINOR must be set");
-
-               // Write manifest
-//             Path manifestP = compiled.resolve("META-INF/MANIFEST.MF");
-//             Files.createDirectories(manifestP.getParent());
-//             try (OutputStream out = Files.newOutputStream(manifestP)) {
-//                     manifest.write(out);
-//             }
-//             
-//             // Load manifest
-//             Path manifestP = compiled.resolve("META-INF/MANIFEST.MF");
-//             if (!Files.exists(manifestP))
-//                     throw new IllegalStateException("Manifest " + manifestP + " not found");
-//             Manifest manifest;
-//             try (InputStream in = Files.newInputStream(manifestP)) {
-//                     manifest = new Manifest(in);
-//             } catch (IOException e) {
-//                     throw new IllegalStateException("Cannot read manifest " + manifestP, e);
-//             }
-
-               // Load excludes
-               List<PathMatcher> excludes = new ArrayList<>();
-               Path excludesP = argeoBuildBase.resolve("excludes.txt");
-               for (String line : Files.readAllLines(excludesP)) {
-                       PathMatcher pathMatcher = excludesP.getFileSystem().getPathMatcher("glob:" + line);
-                       excludes.add(pathMatcher);
-               }
-
-               Path jarP = a2Output.resolve(category).resolve(compiled.getFileName() + "." + major + "." + minor + ".jar");
-               Files.createDirectories(jarP.getParent());
-
-               try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarP), manifest)) {
-                       // add all classes first
-//                     Path binP = compiled.resolve("bin");
-                       Files.walkFileTree(binP, new SimpleFileVisitor<Path>() {
-
-                               @Override
-                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                                       jarOut.putNextEntry(new JarEntry(binP.relativize(file).toString()));
-                                       Files.copy(file, jarOut);
-                                       return FileVisitResult.CONTINUE;
-                               }
-                       });
-
-                       // add resources
-                       Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
-
-                               @Override
-                               public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
-                                       Path relativeP = source.relativize(dir);
-                                       for (PathMatcher exclude : excludes)
-                                               if (exclude.matches(relativeP))
-                                                       return FileVisitResult.SKIP_SUBTREE;
-
-                                       return FileVisitResult.CONTINUE;
-                               }
-
-                               @Override
-                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                                       Path relativeP = source.relativize(file);
-                                       for (PathMatcher exclude : excludes)
-                                               if (exclude.matches(relativeP))
-                                                       return FileVisitResult.CONTINUE;
-                                       JarEntry entry = new JarEntry(relativeP.toString());
-                                       jarOut.putNextEntry(entry);
-                                       Files.copy(file, jarOut);
-                                       return FileVisitResult.CONTINUE;
-                               }
-
-                       });
-
-                       // add sources
-                       // TODO add effective BND, Eclipse project file, etc., in order to be able to
-                       // repackage
-                       Path srcP = source.resolve("src");
-                       Files.walkFileTree(srcP, new SimpleFileVisitor<Path>() {
-
-                               @Override
-                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                                       jarOut.putNextEntry(new JarEntry("OSGI-OPT/src/" + srcP.relativize(file).toString()));
-                                       Files.copy(file, jarOut);
-                                       return FileVisitResult.CONTINUE;
-                               }
-                       });
-
-               }
-
-       }
-
-       private Path findSdkMk(Path directory) {
-               Path sdkMkP = directory.resolve(SDK_MK);
-               if (Files.exists(sdkMkP)) {
-                       return sdkMkP.toAbsolutePath();
-               }
-               if (directory.getParent() == null)
-                       return null;
-               return findSdkMk(directory.getParent());
-
-       }
-
-       public static void main(String... args) throws IOException {
-               if (args.length == 0)
-                       throw new IllegalArgumentException("At least an action must be provided");
-               int actionIndex = 0;
-               String action = args[actionIndex];
-               if (args.length > actionIndex + 1 && !args[actionIndex + 1].startsWith("-"))
-                       throw new IllegalArgumentException(
-                                       "Action " + action + " must be followed by an option: " + Arrays.asList(args));
-
-               Map<String, List<String>> options = new HashMap<>();
-               String currentOption = null;
-               for (int i = actionIndex + 1; i < args.length; i++) {
-                       if (args[i].startsWith("-")) {
-                               currentOption = args[i];
-                               if (!options.containsKey(currentOption))
-                                       options.put(currentOption, new ArrayList<>());
-
-                       } else {
-                               options.get(currentOption).add(args[i]);
-                       }
-               }
-
-               Make argeoMake = new Make();
-               switch (action) {
-               case "compile" -> argeoMake.compile(options);
-               case "bundle" -> argeoMake.bundle(options);
-               case "all" -> {
-                       argeoMake.compile(options);
-                       argeoMake.bundle(options);
-               }
-               default -> throw new IllegalArgumentException("Unkown action: " + action);
-               }
-
-       }
-}
diff --git a/osgi.mk b/osgi.mk
index 45f313e465230413a4b01101fcf4002e0ece663e..cef7609717a916f901155a7cd181ed476770c908 100644 (file)
--- a/osgi.mk
+++ b/osgi.mk
@@ -9,47 +9,56 @@ ECJ_JAR := $(A2_BASE)/org.argeo.tp.sdk/org.eclipse.jdt.core.compiler.batch.3.29.
 BNDLIB_JAR := $(A2_BASE)/org.argeo.tp.sdk/biz.aQute.bndlib.5.3.jar
 SLF4J_API_JAR := $(A2_BASE)/org.argeo.tp/org.slf4j.api.1.7.jar
 
-ARGEO_MAKE := $(JVM) -cp $(ECJ_JAR):$(BNDLIB_JAR):$(SLF4J_API_JAR) $(SDK_SRC_BASE)/sdk/argeo-build/java/org/argeo/build/Make.java
+#ARGEO_MAKE = $(JVM) -cp $(ECJ_JAR):$(BNDLIB_JAR):$(SLF4J_API_JAR):$(BUILD_BASE)/bin org/argeo/build/Make
+ARGEO_MAKE := $(JVM) -cp $(ECJ_JAR):$(BNDLIB_JAR):$(SLF4J_API_JAR) $(SDK_SRC_BASE)/sdk/argeo-build/src/org/argeo/build/Make.java
 #BND_TOOL := /usr/bin/bnd
 
 BUILD_BASE = $(SDK_BUILD_BASE)/$(shell basename $(SDK_SRC_BASE))
 
-WORKSPACE_BNDS := $(shell cd $(SDK_SRC_BASE) && find cnf -name '*.bnd') sdk/argeo-build/argeo.bnd
-BUILD_WORKSPACE_BNDS := $(WORKSPACE_BNDS:%=$(BUILD_BASE)/%)
+#WORKSPACE_BNDS := $(shell cd $(SDK_SRC_BASE) && find cnf -name '*.bnd') sdk/argeo-build/argeo.bnd
+#BUILD_WORKSPACE_BNDS := $(WORKSPACE_BNDS:%=$(BUILD_BASE)/%)
 
-A2_JARS = $(foreach category, $(DEP_CATEGORIES), $(shell find $(A2_BASE)/$(category) -name '*.jar'))
-A2_CLASSPATH = $(subst $(space),$(pathsep),$(strip $(A2_JARS)))
+#A2_JARS = $(foreach category, $(DEP_CATEGORIES), $(shell find $(A2_BASE)/$(category) -name '*.jar'))
+#A2_CLASSPATH = $(subst $(space),$(pathsep),$(strip $(A2_JARS)))
 
-A2_BUNDLES = $(foreach bundle, $(BUNDLES),$(A2_OUTPUT)/$(A2_CATEGORY)/$(shell basename $(bundle)).$(MAJOR).$(MINOR).jar)
+#A2_BUNDLES = $(foreach bundle, $(BUNDLES),$(A2_OUTPUT)/$(A2_CATEGORY)/$(shell basename $(bundle)).$(MAJOR).$(MINOR).jar)
 
-JAVA_SRCS = $(foreach bundle, $(BUNDLES), $(shell find $(bundle) -name '*.java'))
-BNDS = $(foreach bundle, $(BUNDLES), $(BUILD_BASE)/$(shell basename $(bundle))/bnd.bnd)
-ECJ_SRCS = $(foreach bundle, $(BUNDLES), $(bundle)/src[-d $(BUILD_BASE)/$(bundle)/bin])
+#JAVA_SRCS = $(foreach bundle, $(BUNDLES), $(shell find $(bundle) -name '*.java'))
+#BNDS = $(foreach bundle, $(BUNDLES), $(BUILD_BASE)/$(shell basename $(bundle))/bnd.bnd)
+#ECJ_SRCS = $(foreach bundle, $(BUNDLES), $(bundle)/src[-d $(BUILD_BASE)/$(bundle)/bin])
 
 JAVADOC_SRCS = $(foreach bundle, $(JAVADOC_BUNDLES),$(bundle)/src)
 
-osgi: $(BUILD_BASE)/jars-built
+osgi: $(BUILD_BASE)/built
 
 javadoc: $(BUILD_BASE)/java-compiled
        $(JAVADOC) -d $(BUILD_BASE)/api --source-path $(subst $(space),$(pathsep),$(strip $(JAVADOC_SRCS))) -subpackages $(JAVADOC_PACKAGES)
 
 
-# SDK level
-#$(BUILD_BASE)/cnf/%.bnd: cnf/%.bnd
-#      mkdir -p $(dir $@)
-#      cp $< $@
-       
-#$(BUILD_BASE)/sdk/argeo-build/%.bnd: sdk/argeo-build/%.bnd
-#      mkdir -p $(dir $@)
-#      cp $< $@
-       
-#$(A2_OUTPUT)/$(A2_CATEGORY)/%.$(MAJOR).$(MINOR).jar : $(BUILD_BASE)/jars-built:
-#      touch $@
-#      mkdir -p $(dir $@)
-#      cp $< $@
+TARGET_BUNDLES :=  $(abspath $(foreach bundle, $(BUNDLES),$(A2_OUTPUT)/$(shell dirname $(bundle))/$(A2_CATEGORY)/$(shell basename $(bundle)).$(MAJOR).$(MINOR).jar))
+TODOS := $(foreach bundle, $(BUNDLES),$(BUILD_BASE)/$(bundle)/to-build) 
+
+.SECONDEXPANSION:
+
+$(BUILD_BASE)/built : BUNDLES_TO_BUILD = $(subst $(abspath $(BUILD_BASE))/,, $(subst to-build,, $?))
+$(BUILD_BASE)/built : $(TODOS)
+       $(ARGEO_MAKE) all --a2-bases $(A2_BASE) --dep-categories $(DEP_CATEGORIES) --category $(A2_CATEGORY) --bundles $(BUNDLES_TO_BUILD)
+       touch $(BUILD_BASE)/built 
+
+$(A2_OUTPUT)/%.$(MAJOR).$(MINOR).jar : $(BUILD_BASE)/$$(subst $(A2_CATEGORY)/,,$$*)/to-build
+       $(ARGEO_MAKE) all --a2-bases $(A2_BASE) --dep-categories $(DEP_CATEGORIES) --category $(A2_CATEGORY) --bundles $(subst $(A2_CATEGORY)/,,$*)
+
+$(BUILD_BASE)/%/to-build : $$(shell find $(SDK_SRC_BASE)/% -type f -not -path 'bin/*' -not -path '*/MANIFEST.MF')
+       @rm -rf $(dir $@)
+       @mkdir -p $(dir $@) 
+       @touch $@
 
-#$(BUILD_BASE)/%.jar: $(BUILD_BASE)/jars-built
-       #mv $(basename $@)/generated/*.jar $(basename $@).jar
+$(BUILD_BASE)/%/sources-modified : $$(shell find $(SDK_SRC_BASE)/$$* -name '*.java')
+       touch $@        
+
+$(BUILD_BASE)/%/java-compiled: $(BUILD_BASE)/%/sources-modified
+       $(ARGEO_MAKE) compile --a2-bases $(A2_BASE) --dep-categories $(DEP_CATEGORIES) --bundles $(BUNDLES)
+       touch $@        
 
 # Build level
 $(BUILD_BASE)/jars-built: $(BUILD_BASE)/java-compiled 
@@ -59,14 +68,20 @@ $(BUILD_BASE)/jars-built: $(BUILD_BASE)/java-compiled
 $(BUILD_BASE)/java-compiled : $(JAVA_SRCS)
        $(ARGEO_MAKE) compile --a2-bases $(A2_BASE) --dep-categories $(DEP_CATEGORIES) --bundles $(BUNDLES)
        touch $@
-       
-argeo-all:
-       $(ARGEO_MAKE) all --a2-bases $(A2_BASE) --dep-categories $(DEP_CATEGORIES) --category $(A2_CATEGORY) --bundles $(BUNDLES)
+
+clean-a2 :
+       rm -rf $(TARGET_BUNDLES)
+       rm -rf $(BUILD_BASE)/built
 
 # Local manifests
 manifests : osgi
-       $(foreach bundle, $(BUNDLES), mkdir -p  $(bundle)/META-INF/;)
-       $(foreach bundle, $(BUNDLES), cp -v $(BUILD_BASE)/$(shell basename $(bundle))/META-INF/MANIFEST.MF  $(bundle)/META-INF/MANIFEST.MF;)
+       @mkdir -p $(foreach bundle, $(BUNDLES), $(bundle)/META-INF/);
+       @$(foreach bundle, $(BUNDLES), cp -v $(BUILD_BASE)/$(bundle)/META-INF/MANIFEST.MF  $(bundle)/META-INF/MANIFEST.MF;)
+       
+builder: $(BUILD_BASE)/bin/org/argeo/build/Make.class
+
+$(BUILD_BASE)/bin/org/argeo/build/Make.class : $(SDK_SRC_BASE)/sdk/argeo-build/java/org/argeo/build/Make.java
+       $(JVM) -jar $(ECJ_JAR) -cp $(ECJ_JAR):$(BNDLIB_JAR):$(SLF4J_API_JAR) @$(SDK_SRC_BASE)/sdk/argeo-build/ecj.args $(SDK_SRC_BASE)/sdk/argeo-build/src[-d $(BUILD_BASE)/bin]
 
 null  :=
 space := $(null) #
diff --git a/src/org/argeo/build/Make.java b/src/org/argeo/build/Make.java
new file mode 100644 (file)
index 0000000..7e29e14
--- /dev/null
@@ -0,0 +1,399 @@
+package org.argeo.build;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.lang.management.ManagementFactory;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.StringJoiner;
+import java.util.StringTokenizer;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import org.eclipse.jdt.core.compiler.CompilationProgress;
+
+import aQute.bnd.osgi.Analyzer;
+import aQute.bnd.osgi.Jar;
+
+public class Make {
+       private final static String SDK_MK = "sdk.mk";
+
+       final Path execDirectory;
+       final Path sdkSrcBase;
+       final Path argeoBuildBase;
+       final Path sdkBuildBase;
+       final Path buildBase;
+       final Path a2Output;
+
+       public Make() throws IOException {
+               execDirectory = Paths.get(System.getProperty("user.dir"));
+               Path sdkMkP = findSdkMk(execDirectory);
+               Objects.requireNonNull(sdkMkP, "No " + SDK_MK + " found under " + execDirectory);
+
+               Map<String, String> context = new HashMap<>();
+               List<String> sdkMkLines = Files.readAllLines(sdkMkP);
+               lines: for (String line : sdkMkLines) {
+                       StringTokenizer st = new StringTokenizer(line, " :=");
+                       if (!st.hasMoreTokens())
+                               continue lines;
+                       String key = st.nextToken();
+                       if (!st.hasMoreTokens())
+                               continue lines;
+                       String value = st.nextToken();
+                       context.put(key, value);
+               }
+
+               sdkSrcBase = Paths.get(context.computeIfAbsent("SDK_SRC_BASE", (key) -> {
+                       throw new IllegalStateException(key + " not found");
+               })).toAbsolutePath();
+               argeoBuildBase = sdkSrcBase.resolve("sdk/argeo-build");
+
+               sdkBuildBase = Paths.get(context.computeIfAbsent("SDK_BUILD_BASE", (key) -> {
+                       throw new IllegalStateException(key + " not found");
+               })).toAbsolutePath();
+               buildBase = sdkBuildBase.resolve(sdkSrcBase.getFileName());
+               a2Output = sdkBuildBase.resolve("a2");
+       }
+
+       /*
+        * ACTIONS
+        */
+
+       void all(Map<String, List<String>> options) throws IOException {
+//             List<String> a2Bundles = options.get("--a2-bundles");
+//             if (a2Bundles == null)
+//                     throw new IllegalArgumentException("--a2-bundles must be specified");
+//             List<String> bundles = new ArrayList<>();
+//             for (String a2Bundle : a2Bundles) {
+//                     Path a2BundleP = Paths.get(a2Bundle);
+//                     Path bundleP = a2Output.relativize(a2BundleP.getParent().getParent().resolve(a2BundleP.getFileName()));
+//                     bundles.add(bundleP.toString());
+//             }
+//             options.put("--bundles", bundles);
+               compile(options);
+               bundle(options);
+       }
+
+       @SuppressWarnings("restriction")
+       void compile(Map<String, List<String>> options) throws IOException {
+               List<String> bundles = options.get("--bundles");
+               Objects.requireNonNull(bundles, "--bundles argument must be set");
+               if (bundles.isEmpty())
+                       return;
+
+               List<String> a2Categories = options.getOrDefault("--dep-categories", new ArrayList<>());
+               List<String> a2Bases = options.getOrDefault("--a2-bases", new ArrayList<>());
+               if (a2Bases.isEmpty()) {
+                       a2Bases.add(a2Output.toString());
+               }
+
+               List<String> compilerArgs = new ArrayList<>();
+
+               Path ecjArgs = argeoBuildBase.resolve("ecj.args");
+               compilerArgs.add("@" + ecjArgs);
+
+               // classpath
+               if (!a2Categories.isEmpty()) {
+                       compilerArgs.add("-cp");
+                       StringJoiner classPath = new StringJoiner(File.pathSeparator);
+                       for (String a2Base : a2Bases) {
+                               for (String a2Category : a2Categories) {
+                                       Path a2Dir = Paths.get(a2Base).resolve(a2Category);
+                                       for (Path jarP : Files.newDirectoryStream(a2Dir,
+                                                       (p) -> p.getFileName().toString().endsWith(".jar"))) {
+                                               classPath.add(jarP.toString());
+                                       }
+                               }
+                       }
+                       compilerArgs.add(classPath.toString());
+               }
+
+               // sources
+               for (String bundle : bundles) {
+                       StringBuilder sb = new StringBuilder();
+                       sb.append(sdkSrcBase.resolve(bundle).resolve("src"));
+                       sb.append("[-d");
+                       compilerArgs.add(sb.toString());
+                       sb = new StringBuilder();
+                       sb.append(buildBase.resolve(bundle).resolve("bin"));
+                       sb.append("]");
+                       compilerArgs.add(sb.toString());
+               }
+
+               // System.out.println(compilerArgs);
+
+               CompilationProgress compilationProgress = new CompilationProgress() {
+                       int totalWork;
+                       long currentChunk = 0;
+
+                       long chunksCount = 80;
+
+                       @Override
+                       public void worked(int workIncrement, int remainingWork) {
+                               long chunk = ((totalWork - remainingWork) * chunksCount) / totalWork;
+                               if (chunk != currentChunk) {
+                                       currentChunk = chunk;
+                                       for (long i = 0; i < currentChunk; i++) {
+                                               System.out.print("#");
+                                       }
+                                       for (long i = currentChunk; i < chunksCount; i++) {
+                                               System.out.print("-");
+                                       }
+                                       System.out.print("\r");
+                               }
+                               if (remainingWork == 0)
+                                       System.out.print("\n");
+                       }
+
+                       @Override
+                       public void setTaskName(String name) {
+                       }
+
+                       @Override
+                       public boolean isCanceled() {
+                               return false;
+                       }
+
+                       @Override
+                       public void done() {
+                       }
+
+                       @Override
+                       public void begin(int remainingWork) {
+                               this.totalWork = remainingWork;
+                       }
+               };
+               // Use Main instead of BatchCompiler to workaround the fact that
+               // org.eclipse.jdt.core.compiler.batch is not exported
+               boolean success = org.eclipse.jdt.internal.compiler.batch.Main.compile(
+                               compilerArgs.toArray(new String[compilerArgs.size()]), new PrintWriter(System.out),
+                               new PrintWriter(System.err), (CompilationProgress) compilationProgress);
+               if (!success) {
+                       System.exit(1);
+               }
+       }
+
+       void bundle(Map<String, List<String>> options) throws IOException {
+               List<String> bundles = options.get("--bundles");
+               Objects.requireNonNull(bundles, "--bundles argument must be set");
+               if (bundles.isEmpty())
+                       return;
+
+               List<String> categories = options.get("--category");
+               Objects.requireNonNull(bundles, "--bundles argument must be set");
+               if (categories.size() != 1)
+                       throw new IllegalArgumentException("One and only one category must be specified");
+               String category = categories.get(0);
+
+               // create jars
+               for (String bundle : bundles) {
+                       createBundle(bundle, category);
+               }
+       }
+
+       /*
+        * JAR PACKAGING
+        */
+       void createBundle(String bundle, String category) throws IOException {
+               Path source = sdkSrcBase.resolve(bundle);
+               Path compiled = buildBase.resolve(bundle);
+               String bundleSymbolicName = source.getFileName().toString();
+
+               // Metadata
+               Properties properties = new Properties();
+               Path argeoBnd = argeoBuildBase.resolve("argeo.bnd");
+               try (InputStream in = Files.newInputStream(argeoBnd)) {
+                       properties.load(in);
+               }
+               // FIXME make it configurable
+               Path branchBnd = sdkSrcBase.resolve("cnf/unstable.bnd");
+               try (InputStream in = Files.newInputStream(branchBnd)) {
+                       properties.load(in);
+               }
+
+               Path bndBnd = source.resolve("bnd.bnd");
+               try (InputStream in = Files.newInputStream(bndBnd)) {
+                       properties.load(in);
+               }
+
+               // Normalise
+               properties.put("Bundle-SymbolicName", bundleSymbolicName);
+
+               // Calculate MANIFEST
+               Path binP = compiled.resolve("bin");
+               Manifest manifest;
+               try (Analyzer bndAnalyzer = new Analyzer()) {
+                       bndAnalyzer.setProperties(properties);
+                       Jar jar = new Jar(bundleSymbolicName, binP.toFile());
+                       bndAnalyzer.setJar(jar);
+                       manifest = bndAnalyzer.calcManifest();
+
+//                     keys: for (Object key : manifest.getMainAttributes().keySet()) {
+//                             System.out.println(key + ": " + manifest.getMainAttributes().getValue(key.toString()));
+//                     }
+               } catch (Exception e) {
+                       throw new RuntimeException("Bnd analysis of " + compiled + " failed", e);
+               }
+
+               String major = properties.getProperty("MAJOR");
+               Objects.requireNonNull(major, "MAJOR must be set");
+               String minor = properties.getProperty("MINOR");
+               Objects.requireNonNull(minor, "MINOR must be set");
+
+               // Write manifest
+               Path manifestP = compiled.resolve("META-INF/MANIFEST.MF");
+               Files.createDirectories(manifestP.getParent());
+               try (OutputStream out = Files.newOutputStream(manifestP)) {
+                       manifest.write(out);
+               }
+//             
+//             // Load manifest
+//             Path manifestP = compiled.resolve("META-INF/MANIFEST.MF");
+//             if (!Files.exists(manifestP))
+//                     throw new IllegalStateException("Manifest " + manifestP + " not found");
+//             Manifest manifest;
+//             try (InputStream in = Files.newInputStream(manifestP)) {
+//                     manifest = new Manifest(in);
+//             } catch (IOException e) {
+//                     throw new IllegalStateException("Cannot read manifest " + manifestP, e);
+//             }
+
+               // Load excludes
+               List<PathMatcher> excludes = new ArrayList<>();
+               Path excludesP = argeoBuildBase.resolve("excludes.txt");
+               for (String line : Files.readAllLines(excludesP)) {
+                       PathMatcher pathMatcher = excludesP.getFileSystem().getPathMatcher("glob:" + line);
+                       excludes.add(pathMatcher);
+               }
+
+               Path bundleParent = Paths.get(bundle).getParent();
+               Path a2JarDirectory = bundleParent != null ? a2Output.resolve(bundleParent).resolve(category)
+                               : a2Output.resolve(category);
+               Path jarP = a2JarDirectory.resolve(compiled.getFileName() + "." + major + "." + minor + ".jar");
+               Files.createDirectories(jarP.getParent());
+
+               try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarP), manifest)) {
+                       // add all classes first
+//                     Path binP = compiled.resolve("bin");
+                       Files.walkFileTree(binP, new SimpleFileVisitor<Path>() {
+
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       jarOut.putNextEntry(new JarEntry(binP.relativize(file).toString()));
+                                       Files.copy(file, jarOut);
+                                       return FileVisitResult.CONTINUE;
+                               }
+                       });
+
+                       // add resources
+                       Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
+
+                               @Override
+                               public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+                                       Path relativeP = source.relativize(dir);
+                                       for (PathMatcher exclude : excludes)
+                                               if (exclude.matches(relativeP))
+                                                       return FileVisitResult.SKIP_SUBTREE;
+
+                                       return FileVisitResult.CONTINUE;
+                               }
+
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       Path relativeP = source.relativize(file);
+                                       for (PathMatcher exclude : excludes)
+                                               if (exclude.matches(relativeP))
+                                                       return FileVisitResult.CONTINUE;
+                                       JarEntry entry = new JarEntry(relativeP.toString());
+                                       jarOut.putNextEntry(entry);
+                                       Files.copy(file, jarOut);
+                                       return FileVisitResult.CONTINUE;
+                               }
+
+                       });
+
+                       // add sources
+                       // TODO add effective BND, Eclipse project file, etc., in order to be able to
+                       // repackage
+                       Path srcP = source.resolve("src");
+                       Files.walkFileTree(srcP, new SimpleFileVisitor<Path>() {
+
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       jarOut.putNextEntry(new JarEntry("OSGI-OPT/src/" + srcP.relativize(file).toString()));
+                                       Files.copy(file, jarOut);
+                                       return FileVisitResult.CONTINUE;
+                               }
+                       });
+
+               }
+
+       }
+
+       private Path findSdkMk(Path directory) {
+               Path sdkMkP = directory.resolve(SDK_MK);
+               if (Files.exists(sdkMkP)) {
+                       return sdkMkP.toAbsolutePath();
+               }
+               if (directory.getParent() == null)
+                       return null;
+               return findSdkMk(directory.getParent());
+
+       }
+
+       public static void main(String... args) {
+               try {
+                       if (args.length == 0)
+                               throw new IllegalArgumentException("At least an action must be provided");
+                       int actionIndex = 0;
+                       String action = args[actionIndex];
+                       if (args.length > actionIndex + 1 && !args[actionIndex + 1].startsWith("-"))
+                               throw new IllegalArgumentException(
+                                               "Action " + action + " must be followed by an option: " + Arrays.asList(args));
+
+                       Map<String, List<String>> options = new HashMap<>();
+                       String currentOption = null;
+                       for (int i = actionIndex + 1; i < args.length; i++) {
+                               if (args[i].startsWith("-")) {
+                                       currentOption = args[i];
+                                       if (!options.containsKey(currentOption))
+                                               options.put(currentOption, new ArrayList<>());
+
+                               } else {
+                                       options.get(currentOption).add(args[i]);
+                               }
+                       }
+
+                       Make argeoMake = new Make();
+                       switch (action) {
+                       case "compile" -> argeoMake.compile(options);
+                       case "bundle" -> argeoMake.bundle(options);
+                       case "all" -> argeoMake.all(options);
+
+                       default -> throw new IllegalArgumentException("Unkown action: " + action);
+                       }
+
+                       long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
+                       System.out.println("Completed after " + jvmUptime + " ms");
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       System.exit(1);
+               }
+       }
+}