Merge remote-tracking branch 'origin/merge-to-testing' into testing
authorMathieu <mbaudier@argeo.org>
Tue, 6 Dec 2022 05:04:41 +0000 (06:04 +0100)
committerMathieu <mbaudier@argeo.org>
Tue, 6 Dec 2022 05:04:41 +0000 (06:04 +0100)
14 files changed:
.classpath [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.project [deleted file]
.project.COPYME [new file with mode: 0644]
.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
META-INF/MANIFEST.MF [new file with mode: 0644]
argeo.bnd
build.properties [new file with mode: 0644]
configure [new file with mode: 0755]
ecj.args
excludes.txt
osgi.mk
src/org/argeo/build/Make.java [new file with mode: 0644]
src/org/argeo/build/Repackage.java [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..3628e33
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
+               <attributes>
+                       <attribute name="module" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..fbfcfb6
--- /dev/null
@@ -0,0 +1,3 @@
+.project
+*.class
+/bin/
diff --git a/.project b/.project
deleted file mode 100644 (file)
index 7518718..0000000
--- a/.project
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
-       <name>argeo-build</name>
-       <comment></comment>
-       <projects>
-       </projects>
-       <buildSpec>
-       </buildSpec>
-       <natures>
-       </natures>
-</projectDescription>
diff --git a/.project.COPYME b/.project.COPYME
new file mode 100644 (file)
index 0000000..197dde3
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>argeo-build-unstable</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..997d664
--- /dev/null
@@ -0,0 +1,104 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.builder.annotationPath.allLocations=disabled
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.problem.APILeak=warning
+org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
+org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info
+org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
+org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF
new file mode 100644 (file)
index 0000000..46ce004
--- /dev/null
@@ -0,0 +1,11 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Automatic-Module-Name: argeo-build
+Bundle-Name: argeo-build
+Bundle-RequiredExecutionEnvironment: JavaSE-17
+Bundle-SymbolicName: argeo-build
+Bundle-Version: 2.3.9.next
+Import-Package: aQute.bnd.osgi,
+ org.eclipse.jdt.core.compiler,
+ org.eclipse.jdt.internal.compiler.batch
+Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=17))"
index ee9dc01d2f25c4a45c813c01bf57b45a523656ff..9ab4015cd38bab02a243b3a5e1de50e227e49c39 100644 (file)
--- a/argeo.bnd
+++ b/argeo.bnd
@@ -1,14 +1,8 @@
-# Common
-Bundle-Version: ${MAJOR}.${MINOR}.${MICRO}${qualifier}
-Private-Package: *.internal.*
-Export-Package: !*.internal.*, *
+Bundle-Version: ${major}.${minor}.${micro}${qualifier}
+Export-Package: !*.internal.*, !config.*, !icons.*, !css.*, !swt.*, !rap.*, *
+Bundle-RequiredExecutionEnvironment=JavaSE-17
+
 #-consumer-policy : ${range;[==,=+)}
 -contract: !JavaServlet,*
--savemanifest : META-INF/MANIFEST.MF
--includeresource.default : bin,OSGI-INF/=-OSGI-INF/,e4xmi/=-e4xmi/,icons/=-icons/,img/=-img/
--compression STORE
--source true
 -removeheaders = Bnd-LastModified,Build-Jdk,Built-By,Tool,Created-By
-Automatic-Module-Name: ${bsn}
-SLC-Category=${category}
--groupid=${category}
+Automatic-Module-Name: ${bsn}
\ No newline at end of file
diff --git a/build.properties b/build.properties
new file mode 100644 (file)
index 0000000..f8373a3
--- /dev/null
@@ -0,0 +1,4 @@
+additional.bundles = org.slf4j.api
+bin.includes = META-INF/,\
+               src/org/
+source.. = src
diff --git a/configure b/configure
new file mode 100755 (executable)
index 0000000..56a8fb7
--- /dev/null
+++ b/configure
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+# We build where we are
+SDK_BUILD_BASE=$(pwd -P)/output
+
+SDK_MK=$SDK_SRC_BASE/sdk.mk
+
+if [ -f "$SDK_MK" ]; 
+then
+
+echo "File $SDK_MK already exists. Remove it in order to configure a new build location:"
+echo "rm $SDK_MK"
+exit 1
+
+else
+
+if [ -z "$JAVA_HOME" ]
+then
+echo "Environment variable JAVA_HOME must be set"
+exit 1
+fi
+
+# Create build directory, so that it can be used right away
+# and we check whether we have the rights
+mkdir -p $SDK_BUILD_BASE
+if [ -f "$SDK_MK" ];
+then
+echo "Cannot create $SDK_BUILD_BASE, SDK configuration has failed."
+exit 2
+fi
+
+# Generate sdk.mk
+cat > "$SDK_MK" <<EOF
+SDK_SRC_BASE := $SDK_SRC_BASE
+SDK_BUILD_BASE := $SDK_BUILD_BASE
+JAVA_HOME := $JAVA_HOME
+
+include \$(SDK_SRC_BASE)/branch.mk
+include \$(SDK_SRC_BASE)/sdk/branches/\$(BRANCH).bnd
+EOF
+
+
+echo SDK was configured.
+echo "JAVA_HOME        : $JAVA_HOME"
+echo "Base for sources : $SDK_SRC_BASE"
+echo "Base for builds  : $SDK_BUILD_BASE"
+exit 0
+fi
+
index 852a33d4a0272c9afa3f4e7ab4c5f3598d655e01..a4098400b18c3830c2d2cc99e99a0fde5a49053c 100644 (file)
--- a/ecj.args
+++ b/ecj.args
@@ -1,4 +1,3 @@
 -source 17
--target 11
--nowarn
--time 
\ No newline at end of file
+-target 17
+-nowarn
\ No newline at end of file
index 3f5b4e5017fe9aa83ab89a42072ddf412f198cfe..0b155d9e36eab0f3e93a82e2b81bd12a7372aff8 100644 (file)
@@ -5,4 +5,5 @@ bnd.bnd
 pom.xml
 build.properties
 bin
-generated
\ No newline at end of file
+generated
+META-INF/MANIFEST.MF
diff --git a/osgi.mk b/osgi.mk
index 17133d647e2d0986721d806def90ba2ef70c747a..b93e06ca04f9bd0a1b5d96d2ea81fc7ba3ac3417 100644 (file)
--- a/osgi.mk
+++ b/osgi.mk
@@ -1,73 +1,75 @@
-
-
 #
-# GENERIC
+# Common build routines to be included in Makefiles
 #
-JVM := $(JAVA_HOME)/bin/java
-JAVADOC := $(JAVA_HOME)/bin/javadoc
-ECJ_JAR := $(A2_BASE)/org.argeo.tp.sdk/org.eclipse.jdt.core.compiler.batch.3.28.jar
-BND_TOOL := /usr/bin/bnd
-
-BUILD_BASE = $(SDK_BUILD_BASE)/$(A2_CATEGORY)
-
-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_BUNDLES = $(foreach bundle, $(BUNDLES),$(A2_OUTPUT)/$(A2_CATEGORY)/$(shell basename $(bundle)).$(MAJOR).$(MINOR).jar)
+# The following variables are found in the sdk.mk file which is generated by the configure script:
+# SDK_SRC_BASE         the base of the source code, typically the root of the cloned git repository
+# SDK_BUILD_BASE       the base of the output
+# JAVA_HOME                    the base of the JDK used to build
+A2_OUTPUT := $(SDK_BUILD_BASE)/a2
+JVM ?= $(JAVA_HOME)/bin/java
+JAVADOC ?= $(JAVA_HOME)/bin/javadoc
+
+# The following variables should be declared in the Makefile:
+# BUNDLES                      the space-separated list of bundles to be built
+# A2_CATEGORY          the a2 category the bundles will belong to
+#
+# The following variables have default values which can be overriden in the Makefile
+# DEP_CATEGORIES       the a2 categories the compilation depends on
+# JAVADOC_PACKAGES     the space-separated list of packages for which javadoc will be generated
+# A2_BASE                      the space-separated directories where already built a2 categories can be found
+DEP_CATEGORIES ?=
+JAVADOC_PACKAGES ?=
+A2_BASE ?= $(A2_OUTPUT)
 
-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)/$(shell basename $(bundle))/bin])
+ECJ_JAR ?= $(A2_BASE)/org.argeo.tp.sdk/org.eclipse.jdt.core.compiler.batch.3.31.jar
+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/src/org/argeo/build/Make.java
+#ARGEO_MAKE = $(JVM) -cp $(ECJ_JAR):$(BNDLIB_JAR):$(SLF4J_API_JAR):$(BUILD_BASE)/bin org/argeo/build/Make
 
-JAVADOC_SRCS = $(foreach bundle, $(JAVADOC_BUNDLES),$(bundle)/src)
+JAVADOC_SRCS = $(foreach bundle, $(BUNDLES), $(bundle)/src)
 
-osgi: $(BUILD_WORKSPACE_BNDS) $(A2_BUNDLES)
+BUILD_BASE = $(SDK_BUILD_BASE)/$(shell basename $(SDK_SRC_BASE))
 
-javadoc: $(BUILD_BASE)/java-compiled
-       $(JAVADOC) -d $(BUILD_BASE)/api --source-path $(subst $(space),$(pathsep),$(strip $(JAVADOC_SRCS))) -subpackages $(JAVADOC_PACKAGES)
+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) 
 
+## Needed in order to be able to expand $$ variables
+.SECONDEXPANSION:
+.PHONY: osgi manifests javadoc
 
-# 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)/%.jar
-       mkdir -p $(dir $@)
-       cp $< $@
+osgi: $(BUILD_BASE)/built
 
-$(BUILD_BASE)/%.jar: $(BUILD_BASE)/jars-built
-       mv $(basename $@)/generated/*.jar $(basename $@).jar
+javadoc: $(BUILD_BASE)/built
+       $(JAVADOC) -quiet -Xmaxwarns 1 -d $(BUILD_BASE)/api --source-path $(subst $(space),$(pathsep),$(strip $(JAVADOC_SRCS))) -subpackages $(JAVADOC_PACKAGES)
 
-# Build level
-$(BUILD_BASE)/jars-built: $(BNDS)
-       cd $(BUILD_BASE) && $(BND_TOOL) build
-       touch $@
+# Actual build (compilation + bundle packaging)
+$(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 
 
-$(BUILD_BASE)/%/bnd.bnd : %/bnd.bnd $(BUILD_BASE)/java-compiled 
-       mkdir -p $(dir $@)bin
-       rsync -r --exclude "*.java" $(dir  $<)src/ $(dir $@)bin
-       rsync -r --exclude-from $(SDK_SRC_BASE)/sdk/argeo-build/excludes.txt $(dir  $<) $(dir $@)bin
-       if [ -d "$(dir  $<)OSGI-INF" ]; then rsync -r $(dir  $<)OSGI-INF/ $(dir $@)/OSGI-INF; fi
-       cp $< $@
-       echo "\n-sourcepath:$(SDK_SRC_BASE)/$(dir  $<)src\n" >> $@
+$(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)/java-compiled : $(JAVA_SRCS)
-       $(JVM) -jar $(ECJ_JAR) @$(SDK_SRC_BASE)/sdk/argeo-build/ecj.args -cp $(A2_CLASSPATH) $(ECJ_SRCS)
-       touch $@
+$(BUILD_BASE)/%/to-build : $$(shell find % -type f -not -path 'bin/*' -not -path '*/MANIFEST.MF' | sed 's/ /\\ /g')
+       @rm -rf $(dir $@)
+       @mkdir -p $(dir $@) 
+       @touch $@
 
 # 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;)
+
+# Local build of the builder, not used as the performance gain is negligible   
+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]
 
+# Make variables used to replace spaces by a separator, typically in order to generate classpaths
+# for example: CLASSPATH = $(subst $(space),$(pathsep),$(strip $(JARS)))
 null  :=
 space := $(null) #
-pathsep := :
+pathsep := :
\ No newline at end of file
diff --git a/src/org/argeo/build/Make.java b/src/org/argeo/build/Make.java
new file mode 100644 (file)
index 0000000..b9fd48d
--- /dev/null
@@ -0,0 +1,488 @@
+package org.argeo.build;
+
+import static java.lang.System.Logger.Level.ERROR;
+import static java.lang.System.Logger.Level.INFO;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+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.concurrent.CompletableFuture;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.Deflater;
+
+import org.eclipse.jdt.core.compiler.CompilationProgress;
+
+import aQute.bnd.osgi.Analyzer;
+import aQute.bnd.osgi.Jar;
+
+/**
+ * Minimalistic OSGi compiler and packager, meant to be used as a single file
+ * without being itself compiled first. It depends on the Eclipse batch compiler
+ * (aka. ECJ) and the BND Libs library for OSGi metadata generation (which
+ * itselfs depends on slf4j).<br/>
+ * <br/>
+ * For example, a typical system call would be:<br/>
+ * <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>
+ */
+public class Make {
+       private final static Logger logger = System.getLogger(Make.class.getName());
+
+       /**
+        * Environment properties on whether sources should be packaged separately or
+        * integrated in the bundles.
+        */
+       private final static String ENV_BUILD_SOURCE_BUNDLES = "BUILD_SOURCE_BUNDLES";
+
+       /** Name of the local-specific Makefile (sdk.mk). */
+       final static String SDK_MK = "sdk.mk";
+       /** Name of the branch definition Makefile (branch.mk). */
+       final static String BRANCH_MK = "branch.mk";
+
+       /** The execution directory (${user.dir}). */
+       final Path execDirectory;
+       /** Base of the source code, typically the cloned git repository. */
+       final Path sdkSrcBase;
+       /**
+        * The base of the builder, typically a submodule pointing to the public
+        * argeo-build directory.
+        */
+       final Path argeoBuildBase;
+       /** The base of the build for all layers. */
+       final Path sdkBuildBase;
+       /** The base of the build for this layer. */
+       final Path buildBase;
+       /** The base of the a2 output for all layers. */
+       final Path a2Output;
+
+       /** Whether sources should be packaged separately */
+       final boolean sourceBundles;
+
+       /** Constructor initialises the base directories. */
+       public Make() throws IOException {
+               sourceBundles = Boolean.parseBoolean(System.getenv(ENV_BUILD_SOURCE_BUNDLES));
+               if (sourceBundles)
+                       logger.log(Level.INFO, "Sources will be packaged separately");
+
+               execDirectory = Paths.get(System.getProperty("user.dir"));
+               Path sdkMkP = findSdkMk(execDirectory);
+               Objects.requireNonNull(sdkMkP, "No " + SDK_MK + " found under " + execDirectory);
+
+               Map<String, String> context = readeMakefileVariables(sdkMkP);
+               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
+        */
+       /** Compile and create the bundles in one go. */
+       void all(Map<String, List<String>> options) throws IOException {
+               compile(options);
+               bundle(options);
+       }
+
+       /** Compile all the bundles which have been passed via the --bundle argument. */
+       @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()) {
+                       StringJoiner classPath = new StringJoiner(File.pathSeparator);
+                       StringJoiner modulePath = new StringJoiner(File.pathSeparator);
+                       for (String a2Base : a2Bases) {
+                               for (String a2Category : a2Categories) {
+                                       Path a2Dir = Paths.get(a2Base).resolve(a2Category);
+                                       if (!Files.exists(a2Dir))
+                                               Files.createDirectories(a2Dir);
+                                       modulePath.add(a2Dir.toString());
+                                       for (Path jarP : Files.newDirectoryStream(a2Dir,
+                                                       (p) -> p.getFileName().toString().endsWith(".jar"))) {
+                                               classPath.add(jarP.toString());
+                                       }
+                               }
+                       }
+                       compilerArgs.add("-cp");
+                       compilerArgs.add(classPath.toString());
+//                     compilerArgs.add("--module-path");
+//                     compilerArgs.add(modulePath.toString());
+               }
+
+               // sources
+               for (String bundle : bundles) {
+                       StringBuilder sb = new StringBuilder();
+                       Path bundlePath = execDirectory.resolve(bundle);
+                       if (!Files.exists(bundlePath))
+                               throw new IllegalArgumentException("Bundle " + bundle + " not found in " + execDirectory);
+                       sb.append(bundlePath.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());
+               }
+
+               if (logger.isLoggable(INFO))
+                       compilerArgs.add("-time");
+
+//             for (String arg : compilerArgs)
+//                     System.out.println(arg);
+
+               boolean success = org.eclipse.jdt.core.compiler.batch.BatchCompiler.compile(
+                               compilerArgs.toArray(new String[compilerArgs.size()]), new PrintWriter(System.out),
+                               new PrintWriter(System.err), new MakeCompilationProgress());
+               if (!success) // kill the process if compilation failed
+                       throw new IllegalStateException("Compilation failed");
+       }
+
+       /** Package the bundles. */
+       void bundle(Map<String, List<String>> options) throws IOException {
+               // check arguments
+               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, "--category argument must be set");
+               if (categories.size() != 1)
+                       throw new IllegalArgumentException("One and only one --category must be specified");
+               String category = categories.get(0);
+
+               Path branchMk = sdkSrcBase.resolve(BRANCH_MK);
+               if (!Files.exists(branchMk))
+                       throw new IllegalStateException("No " + branchMk + " file available");
+               Map<String, String> branchVariables = readeMakefileVariables(branchMk);
+
+               String branch = branchVariables.get("BRANCH");
+
+               long begin = System.currentTimeMillis();
+               // create jars in parallel
+               List<CompletableFuture<Void>> toDos = new ArrayList<>();
+               for (String bundle : bundles) {
+                       toDos.add(CompletableFuture.runAsync(() -> {
+                               try {
+                                       createBundle(branch, bundle, category);
+                               } catch (IOException e) {
+                                       throw new RuntimeException("Packaging of " + bundle + " failed", e);
+                               }
+                       }));
+               }
+               CompletableFuture.allOf(toDos.toArray(new CompletableFuture[toDos.size()])).join();
+               long duration = System.currentTimeMillis() - begin;
+               logger.log(INFO, "Packaging took " + duration + " ms");
+       }
+
+       /*
+        * UTILITIES
+        */
+       /** Package a single bundle. */
+       void createBundle(String branch, String bundle, String category) throws IOException {
+               Path source = execDirectory.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);
+               }
+
+               Path branchBnd = sdkSrcBase.resolve("sdk/branches/" + branch + ".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
+               if (!properties.containsKey("Bundle-SymbolicName"))
+                       properties.put("Bundle-SymbolicName", bundleSymbolicName);
+
+               // Calculate MANIFEST
+               Path binP = compiled.resolve("bin");
+               if (!Files.exists(binP))
+                       Files.createDirectories(binP);
+               Manifest manifest;
+               try (Analyzer bndAnalyzer = new Analyzer()) {
+                       bndAnalyzer.setProperties(properties);
+                       Jar jar = new Jar(bundleSymbolicName, binP.toFile());
+                       bndAnalyzer.setJar(jar);
+                       manifest = bndAnalyzer.calcManifest();
+               } 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 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)) {
+                       jarOut.setLevel(Deflater.DEFAULT_COMPRESSION);
+                       // add all classes first
+                       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;
+                               }
+                       });
+
+                       Path srcP = source.resolve("src");
+                       // Add all resources from src/
+                       Files.walkFileTree(srcP, new SimpleFileVisitor<Path>() {
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       if (file.getFileName().toString().endsWith(".java")
+                                                       || file.getFileName().toString().endsWith(".class"))
+                                               return FileVisitResult.CONTINUE;
+                                       jarOut.putNextEntry(new JarEntry(srcP.relativize(file).toString()));
+                                       if (!Files.isDirectory(file))
+                                               Files.copy(file, jarOut);
+                                       return FileVisitResult.CONTINUE;
+                               }
+                       });
+
+                       // add sources
+                       // TODO add effective BND, Eclipse project file, etc., in order to be able to
+                       // repackage
+                       if (sourceBundles) {
+                               // TODO package sources separately
+                       } else {
+                               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()));
+                                               if (!Files.isDirectory(file))
+                                                       Files.copy(file, jarOut);
+                                               return FileVisitResult.CONTINUE;
+                                       }
+                               });
+                       }
+               }
+       }
+
+       /**
+        * Recursively find the base source directory (which contains the
+        * <code>{@value #SDK_MK}</code> file).
+        */
+       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());
+       }
+
+       /**
+        * Reads Makefile variable assignments of the form =, :=, or ?=, ignoring white
+        * spaces. To be used with very simple included Makefiles only.
+        */
+       Map<String, String> readeMakefileVariables(Path path) throws IOException {
+               Map<String, String> context = new HashMap<>();
+               List<String> sdkMkLines = Files.readAllLines(path);
+               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();
+                       if (st.hasMoreTokens()) // probably not a simple variable assignment
+                               continue lines;
+                       context.put(key, value);
+               }
+               return context;
+       }
+
+       /** Main entry point, interpreting actions and arguments. */
+       public static void main(String... args) {
+               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]);
+                       }
+               }
+
+               try {
+                       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();
+                       logger.log(INFO, "Make.java action '" + action + "' succesfully completed after " + (jvmUptime / 1000) + "."
+                                       + (jvmUptime % 1000) + " s");
+               } catch (Exception e) {
+                       long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
+                       logger.log(ERROR, "Make.java action '" + action + "' failed after " + (jvmUptime / 1000) + "."
+                                       + (jvmUptime % 1000) + " s", e);
+                       System.exit(1);
+               }
+       }
+
+       /**
+        * An ECJ {@link CompilationProgress} printing a progress bar while compiling.
+        */
+       static class MakeCompilationProgress extends CompilationProgress {
+               private int totalWork;
+               private long currentChunk = 0;
+               private long chunksCount = 80;
+
+               @Override
+               public void worked(int workIncrement, int remainingWork) {
+                       if (!logger.isLoggable(Level.INFO)) // progress bar only at INFO level
+                               return;
+                       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;
+               }
+       }
+}
diff --git a/src/org/argeo/build/Repackage.java b/src/org/argeo/build/Repackage.java
new file mode 100644 (file)
index 0000000..51d125e
--- /dev/null
@@ -0,0 +1,1225 @@
+package org.argeo.build;
+
+import static java.lang.System.Logger.Level.DEBUG;
+import static org.argeo.build.Repackage.ManifestConstants.BUNDLE_SYMBOLICNAME;
+import static org.argeo.build.Repackage.ManifestConstants.BUNDLE_VERSION;
+import static org.argeo.build.Repackage.ManifestConstants.EXPORT_PACKAGE;
+import static org.argeo.build.Repackage.ManifestConstants.SLC_ORIGIN_M2;
+import static org.argeo.build.Repackage.ManifestConstants.SLC_ORIGIN_M2_REPO;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+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.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.Deflater;
+
+import aQute.bnd.osgi.Analyzer;
+import aQute.bnd.osgi.Jar;
+
+/** The central class for A2 packaging. */
+public class Repackage {
+       private final static Logger logger = System.getLogger(Repackage.class.getName());
+
+       private final static String ENV_BUILD_SOURCE_BUNDLES = "BUILD_SOURCE_BUNDLES";
+
+       /** Main entry point. */
+       public static void main(String[] args) {
+               if (args.length < 2) {
+                       System.err.println("Usage: <path to a2 output dir> <category1> <category2> ...");
+                       System.exit(1);
+               }
+               Path a2Base = Paths.get(args[0]).toAbsolutePath().normalize();
+               Path descriptorsBase = Paths.get(".").toAbsolutePath().normalize();
+               Repackage factory = new Repackage(a2Base, descriptorsBase);
+
+               List<CompletableFuture<Void>> toDos = new ArrayList<>();
+               for (int i = 1; i < args.length; i++) {
+                       Path p = Paths.get(args[i]);
+                       toDos.add(CompletableFuture.runAsync(() -> factory.processCategory(p)));
+               }
+               CompletableFuture.allOf(toDos.toArray(new CompletableFuture[toDos.size()])).join();
+       }
+
+       private final static String COMMON_BND = "common.bnd";
+       private final static String MERGE_BND = "merge.bnd";
+
+       private Path originBase;
+       private Path a2Base;
+       private Path a2LibBase;
+       private Path descriptorsBase;
+
+       private Properties uris = new Properties();
+
+       /** key is URI prefix, value list of base URLs */
+       private Map<String, List<String>> mirrors = new HashMap<String, List<String>>();
+
+       private final boolean sourceBundles;
+
+       public Repackage(Path a2Base, Path descriptorsBase) {
+               sourceBundles = Boolean.parseBoolean(System.getenv(ENV_BUILD_SOURCE_BUNDLES));
+               if (sourceBundles)
+                       logger.log(Level.INFO, "Sources will be packaged separately");
+
+               Objects.requireNonNull(a2Base);
+               Objects.requireNonNull(descriptorsBase);
+               this.originBase = Paths.get(System.getProperty("user.home"), ".cache", "argeo/build/origin");
+               // TODO define and use a build base
+               this.a2Base = a2Base;
+               this.a2LibBase = a2Base.resolve("lib");
+               this.descriptorsBase = descriptorsBase;
+               if (!Files.exists(this.descriptorsBase))
+                       throw new IllegalArgumentException(this.descriptorsBase + " does not exist");
+
+               // URIs mapping
+               Path urisPath = this.descriptorsBase.resolve("uris.properties");
+               if (Files.exists(urisPath)) {
+                       try (InputStream in = Files.newInputStream(urisPath)) {
+                               uris.load(in);
+                       } catch (IOException e) {
+                               throw new IllegalStateException("Cannot load " + urisPath, e);
+                       }
+               }
+
+               // Eclipse mirrors
+               Path eclipseMirrorsPath = this.descriptorsBase.resolve("eclipse.mirrors.txt");
+               List<String> eclipseMirrors = new ArrayList<>();
+               if (Files.exists(eclipseMirrorsPath)) {
+                       try {
+                               eclipseMirrors = Files.readAllLines(eclipseMirrorsPath, StandardCharsets.UTF_8);
+                       } catch (IOException e) {
+                               throw new IllegalStateException("Cannot load " + eclipseMirrorsPath, e);
+                       }
+                       for (Iterator<String> it = eclipseMirrors.iterator(); it.hasNext();) {
+                               String value = it.next();
+                               if (value.strip().equals(""))
+                                       it.remove();
+                       }
+               }
+
+               mirrors.put("http://www.eclipse.org/downloads", eclipseMirrors);
+       }
+
+       /*
+        * MAVEN ORIGIN
+        */
+
+       /** Process a whole category/group id. */
+       public void processCategory(Path categoryRelativePath) {
+               try {
+                       Path targetCategoryBase = descriptorsBase.resolve(categoryRelativePath);
+                       DirectoryStream<Path> bnds = Files.newDirectoryStream(targetCategoryBase,
+                                       (p) -> p.getFileName().toString().endsWith(".bnd") && !p.getFileName().toString().equals(COMMON_BND)
+                                                       && !p.getFileName().toString().equals(MERGE_BND));
+                       for (Path p : bnds) {
+                               processSingleM2ArtifactDistributionUnit(p);
+                       }
+
+                       DirectoryStream<Path> dus = Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p));
+                       for (Path duDir : dus) {
+                               if (duDir.getFileName().toString().startsWith("eclipse-")) {
+                                       processEclipseArchive(duDir);
+                               } else {
+                                       processM2BasedDistributionUnit(duDir);
+                               }
+                       }
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot process category " + categoryRelativePath, e);
+               }
+       }
+
+       /** Process a standalone Maven artifact. */
+       public void processSingleM2ArtifactDistributionUnit(Path bndFile) {
+               try {
+//                     String category = bndFile.getParent().getFileName().toString();
+                       Path categoryRelativePath = descriptorsBase.relativize(bndFile.getParent());
+                       Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
+
+                       Properties fileProps = new Properties();
+                       try (InputStream in = Files.newInputStream(bndFile)) {
+                               fileProps.load(in);
+                       }
+                       String repoStr = fileProps.containsKey(SLC_ORIGIN_M2_REPO.toString())
+                                       ? fileProps.getProperty(SLC_ORIGIN_M2_REPO.toString())
+                                       : null;
+
+                       if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
+                               // use file name as symbolic name
+                               String symbolicName = bndFile.getFileName().toString();
+                               symbolicName = symbolicName.substring(0, symbolicName.length() - ".bnd".length());
+                               fileProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
+                       }
+
+                       String m2Coordinates = fileProps.getProperty(SLC_ORIGIN_M2.toString());
+                       if (m2Coordinates == null)
+                               throw new IllegalArgumentException("No M2 coordinates available for " + bndFile);
+                       M2Artifact artifact = new M2Artifact(m2Coordinates);
+                       URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
+                       Path downloaded = download(url, originBase, artifact);
+
+                       Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, fileProps, artifact);
+
+                       downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir);
+
+                       createJar(targetBundleDir);
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot process " + bndFile, e);
+               }
+       }
+
+       /** Process multiple Maven artifacts. */
+       public void processM2BasedDistributionUnit(Path duDir) {
+               try {
+                       // String category = duDir.getParent().getFileName().toString();
+                       Path categoryRelativePath = descriptorsBase.relativize(duDir.getParent());
+                       Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
+
+                       // merge
+                       Path mergeBnd = duDir.resolve(MERGE_BND);
+                       if (Files.exists(mergeBnd)) {
+                               mergeM2Artifacts(mergeBnd);
+//                             return;
+                       }
+
+                       Path commonBnd = duDir.resolve(COMMON_BND);
+                       if (!Files.exists(commonBnd)) {
+                               return;
+                       }
+                       Properties commonProps = new Properties();
+                       try (InputStream in = Files.newInputStream(commonBnd)) {
+                               commonProps.load(in);
+                       }
+
+                       String m2Version = commonProps.getProperty(SLC_ORIGIN_M2.toString());
+                       if (m2Version == null) {
+                               logger.log(Level.WARNING, "Ignoring " + duDir + " as it is not an M2-based distribution unit");
+                               return;// ignore, this is probably an Eclipse archive
+                       }
+                       if (!m2Version.startsWith(":")) {
+                               throw new IllegalStateException("Only the M2 version can be specified: " + m2Version);
+                       }
+                       m2Version = m2Version.substring(1);
+
+                       DirectoryStream<Path> ds = Files.newDirectoryStream(duDir,
+                                       (p) -> p.getFileName().toString().endsWith(".bnd") && !p.getFileName().toString().equals(COMMON_BND)
+                                                       && !p.getFileName().toString().equals(MERGE_BND));
+                       for (Path p : ds) {
+                               Properties fileProps = new Properties();
+                               try (InputStream in = Files.newInputStream(p)) {
+                                       fileProps.load(in);
+                               }
+                               String m2Coordinates = fileProps.getProperty(SLC_ORIGIN_M2.toString());
+                               M2Artifact artifact = new M2Artifact(m2Coordinates);
+
+                               artifact.setVersion(m2Version);
+
+                               // prepare manifest entries
+                               Properties mergeProps = new Properties();
+                               mergeProps.putAll(commonProps);
+
+                               fileEntries: for (Object key : fileProps.keySet()) {
+                                       if (ManifestConstants.SLC_ORIGIN_M2.toString().equals(key))
+                                               continue fileEntries;
+                                       String value = fileProps.getProperty(key.toString());
+                                       Object previousValue = mergeProps.put(key.toString(), value);
+                                       if (previousValue != null) {
+                                               logger.log(Level.WARNING,
+                                                               commonBnd + ": " + key + " was " + previousValue + ", overridden with " + value);
+                                       }
+                               }
+                               mergeProps.put(ManifestConstants.SLC_ORIGIN_M2.toString(), artifact.toM2Coordinates());
+                               if (!mergeProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
+                                       // use file name as symbolic name
+                                       String symbolicName = p.getFileName().toString();
+                                       symbolicName = symbolicName.substring(0, symbolicName.length() - ".bnd".length());
+                                       mergeProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
+                               }
+
+                               String repoStr = mergeProps.containsKey(SLC_ORIGIN_M2_REPO.toString())
+                                               ? mergeProps.getProperty(SLC_ORIGIN_M2_REPO.toString())
+                                               : null;
+
+                               // download
+                               URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
+                               Path downloaded = download(url, originBase, artifact);
+
+                               Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, mergeProps, artifact);
+//                             logger.log(Level.DEBUG, () -> "Processed " + downloaded);
+
+                               // sources
+                               downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir);
+
+                               createJar(targetBundleDir);
+                       }
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot process " + duDir, e);
+               }
+
+       }
+
+       /** Merge multiple Maven artifacts. */
+       protected void mergeM2Artifacts(Path mergeBnd) throws IOException {
+               Path duDir = mergeBnd.getParent();
+               String category = duDir.getParent().getFileName().toString();
+               Path targetCategoryBase = a2Base.resolve(category);
+
+               Properties mergeProps = new Properties();
+               try (InputStream in = Files.newInputStream(mergeBnd)) {
+                       mergeProps.load(in);
+               }
+
+               // Version
+               String m2Version = mergeProps.getProperty(SLC_ORIGIN_M2.toString());
+               if (m2Version == null) {
+                       logger.log(Level.WARNING, "Ignoring " + duDir + " as it is not an M2-based distribution unit");
+                       return;// ignore, this is probably an Eclipse archive
+               }
+               if (!m2Version.startsWith(":")) {
+                       throw new IllegalStateException("Only the M2 version can be specified: " + m2Version);
+               }
+               m2Version = m2Version.substring(1);
+               mergeProps.put(ManifestConstants.BUNDLE_VERSION.toString(), m2Version);
+
+               String artifactsStr = mergeProps.getProperty(ManifestConstants.SLC_ORIGIN_M2_MERGE.toString());
+               if (artifactsStr == null)
+                       throw new IllegalArgumentException(
+                                       mergeBnd + ": " + ManifestConstants.SLC_ORIGIN_M2_MERGE + " must be set");
+               
+               String repoStr = mergeProps.containsKey(SLC_ORIGIN_M2_REPO.toString())
+                               ? mergeProps.getProperty(SLC_ORIGIN_M2_REPO.toString())
+                               : null;
+
+               String bundleSymbolicName = mergeProps.getProperty(ManifestConstants.BUNDLE_SYMBOLICNAME.toString());
+               if (bundleSymbolicName == null)
+                       throw new IllegalArgumentException("Bundle-SymbolicName must be set in " + mergeBnd);
+               CategoryNameVersion nameVersion = new M2Artifact(category + ":" + bundleSymbolicName + ":" + m2Version);
+               Path targetBundleDir = targetCategoryBase.resolve(bundleSymbolicName + "." + nameVersion.getBranch());
+
+               String[] artifacts = artifactsStr.split(",");
+               artifacts: for (String str : artifacts) {
+                       String m2Coordinates = str.trim();
+                       if ("".equals(m2Coordinates))
+                               continue artifacts;
+                       M2Artifact artifact = new M2Artifact(m2Coordinates.trim());
+                       if (artifact.getVersion() == null)
+                               artifact.setVersion(m2Version);
+                       URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
+                       Path downloaded = download(url, originBase, artifact);
+                       JarEntry entry;
+                       try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(downloaded), false)) {
+                               entries: while ((entry = jarIn.getNextJarEntry()) != null) {
+                                       if (entry.isDirectory())
+                                               continue entries;
+                                       else if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".SF"))
+                                               continue entries;
+                                       else if (entry.getName().startsWith("META-INF/versions/"))
+                                               continue entries;
+                                       else if (entry.getName().startsWith("META-INF/maven/"))
+                                               continue entries;
+                                       else if (entry.getName().equals("module-info.class"))
+                                               continue entries;
+                                       else if (entry.getName().equals("META-INF/NOTICE"))
+                                               continue entries;
+                                       else if (entry.getName().equals("META-INF/NOTICE.txt"))
+                                               continue entries;
+                                       else if (entry.getName().equals("META-INF/LICENSE"))
+                                               continue entries;
+                                       else if (entry.getName().equals("META-INF/LICENSE.md"))
+                                               continue entries;
+                                       else if (entry.getName().equals("META-INF/LICENSE-notice.md"))
+                                               continue entries;
+                                       else if (entry.getName().equals("META-INF/DEPENDENCIES"))
+                                               continue entries;
+                                       if (entry.getName().startsWith(".cache/")) // Apache SSHD
+                                               continue entries;
+                                       Path target = targetBundleDir.resolve(entry.getName());
+                                       Files.createDirectories(target.getParent());
+                                       if (!Files.exists(target)) {
+                                               Files.copy(jarIn, target);
+                                       } else {
+                                               if (entry.getName().startsWith("META-INF/services/")) {
+                                                       try (OutputStream out = Files.newOutputStream(target, StandardOpenOption.APPEND)) {
+                                                               out.write("\n".getBytes());
+                                                               jarIn.transferTo(out);
+                                                               logger.log(Level.WARNING, artifact.getArtifactId() + " - Appended " + entry.getName());
+                                                       }
+                                               } else if (entry.getName().startsWith("org/apache/batik/")) {
+                                                       logger.log(Level.WARNING, "Skip " + entry.getName());
+                                                       continue entries;
+                                               } else {
+                                                       throw new IllegalStateException("File " + target + " from " + artifact + " already exists");
+                                               }
+                                       }
+                                       logger.log(Level.TRACE, () -> "Copied " + target);
+                               }
+
+                       }
+                       downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir);
+               }
+
+               // additional service files
+               Path servicesDir = duDir.resolve("services");
+               if (Files.exists(servicesDir)) {
+                       for (Path p : Files.newDirectoryStream(servicesDir)) {
+                               Path target = targetBundleDir.resolve("META-INF/services/").resolve(p.getFileName());
+                               try (InputStream in = Files.newInputStream(p);
+                                               OutputStream out = Files.newOutputStream(target, StandardOpenOption.APPEND);) {
+                                       out.write("\n".getBytes());
+                                       in.transferTo(out);
+                                       logger.log(Level.WARNING, "Appended " + p);
+                               }
+                       }
+               }
+
+               Map<String, String> entries = new TreeMap<>();
+               try (Analyzer bndAnalyzer = new Analyzer()) {
+                       bndAnalyzer.setProperties(mergeProps);
+                       Jar jar = new Jar(targetBundleDir.toFile());
+                       bndAnalyzer.setJar(jar);
+                       Manifest manifest = bndAnalyzer.calcManifest();
+
+                       keys: for (Object key : manifest.getMainAttributes().keySet()) {
+                               Object value = manifest.getMainAttributes().get(key);
+
+                               switch (key.toString()) {
+                               case "Tool":
+                               case "Bnd-LastModified":
+                               case "Created-By":
+                                       continue keys;
+                               }
+                               if ("Require-Capability".equals(key.toString())
+                                               && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\""))
+                                       continue keys;// hack for very old classes
+                               entries.put(key.toString(), value.toString());
+                               // logger.log(DEBUG, () -> key + "=" + value);
+
+                       }
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot process " + mergeBnd, e);
+               }
+
+               Manifest manifest = new Manifest();
+               Path manifestPath = targetBundleDir.resolve("META-INF/MANIFEST.MF");
+               Files.createDirectories(manifestPath.getParent());
+               for (String key : entries.keySet()) {
+                       String value = entries.get(key);
+                       manifest.getMainAttributes().putValue(key, value);
+               }
+
+               try (OutputStream out = Files.newOutputStream(manifestPath)) {
+                       manifest.write(out);
+               }
+               createJar(targetBundleDir);
+       }
+
+       /** Generate MANIFEST using BND. */
+       protected Path processBndJar(Path downloaded, Path targetCategoryBase, Properties fileProps, M2Artifact artifact) {
+
+               try {
+                       Map<String, String> additionalEntries = new TreeMap<>();
+                       boolean doNotModify = Boolean.parseBoolean(fileProps
+                                       .getOrDefault(ManifestConstants.SLC_ORIGIN_MANIFEST_NOT_MODIFIED.toString(), "false").toString());
+
+                       // we always force the symbolic name
+
+                       if (doNotModify) {
+                               fileEntries: for (Object key : fileProps.keySet()) {
+                                       if (ManifestConstants.SLC_ORIGIN_M2.toString().equals(key))
+                                               continue fileEntries;
+                                       String value = fileProps.getProperty(key.toString());
+                                       additionalEntries.put(key.toString(), value);
+                               }
+                       } else {
+                               if (artifact != null) {
+                                       if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
+                                               fileProps.put(BUNDLE_SYMBOLICNAME.toString(), artifact.getName());
+                                       }
+                                       if (!fileProps.containsKey(BUNDLE_VERSION.toString())) {
+                                               fileProps.put(BUNDLE_VERSION.toString(), artifact.getVersion());
+                                       }
+                               }
+
+                               if (!fileProps.containsKey(EXPORT_PACKAGE.toString())) {
+                                       fileProps.put(EXPORT_PACKAGE.toString(),
+                                                       "*;version=\"" + fileProps.getProperty(BUNDLE_VERSION.toString()) + "\"");
+                               }
+
+                               try (Analyzer bndAnalyzer = new Analyzer()) {
+                                       bndAnalyzer.setProperties(fileProps);
+                                       Jar jar = new Jar(downloaded.toFile());
+                                       bndAnalyzer.setJar(jar);
+                                       Manifest manifest = bndAnalyzer.calcManifest();
+
+                                       keys: for (Object key : manifest.getMainAttributes().keySet()) {
+                                               Object value = manifest.getMainAttributes().get(key);
+
+                                               switch (key.toString()) {
+                                               case "Tool":
+                                               case "Bnd-LastModified":
+                                               case "Created-By":
+                                                       continue keys;
+                                               }
+                                               if ("Require-Capability".equals(key.toString())
+                                                               && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\""))
+                                                       continue keys;// hack for very old classes
+                                               additionalEntries.put(key.toString(), value.toString());
+                                               // logger.log(DEBUG, () -> key + "=" + value);
+
+                                       }
+                               }
+                       }
+                       Path targetBundleDir = processBundleJar(downloaded, targetCategoryBase, additionalEntries);
+                       logger.log(Level.DEBUG, () -> "Processed " + downloaded);
+                       return targetBundleDir;
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot BND process " + downloaded, e);
+               }
+
+       }
+
+       /** Download and integrates sources for a single Maven artifact. */
+       protected void downloadAndProcessM2Sources(String repoStr, M2Artifact artifact, Path targetBundleDir)
+                       throws IOException {
+               if (sourceBundles)
+                       return;
+               try {
+                       M2Artifact sourcesArtifact = new M2Artifact(artifact.toM2Coordinates(), "sources");
+                       URL sourcesUrl = M2ConventionsUtils.mavenRepoUrl(repoStr, sourcesArtifact);
+                       Path sourcesDownloaded = download(sourcesUrl, originBase, artifact, true);
+                       processM2SourceJar(sourcesDownloaded, targetBundleDir);
+                       logger.log(Level.TRACE, () -> "Processed source " + sourcesDownloaded);
+               } catch (Exception e) {
+                       logger.log(Level.ERROR, () -> "Cannot download source for  " + artifact);
+               }
+
+       }
+
+       /** Integrate sources from a downloaded jar file. */
+       protected void processM2SourceJar(Path file, Path targetBundleDir) throws IOException {
+               try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
+                       Path targetSourceDir = targetBundleDir.resolve("OSGI-OPT/src");
+
+                       // TODO make it less dangerous?
+                       if (Files.exists(targetSourceDir)) {
+//                             deleteDirectory(targetSourceDir);
+                       } else {
+                               Files.createDirectories(targetSourceDir);
+                       }
+
+                       // copy entries
+                       JarEntry entry;
+                       entries: while ((entry = jarIn.getNextJarEntry()) != null) {
+                               if (entry.isDirectory())
+                                       continue entries;
+                               if (entry.getName().startsWith("META-INF"))// skip META-INF entries
+                                       continue entries;
+                               if (entry.getName().startsWith("module-info.java"))// skip META-INF entries
+                                       continue entries;
+                               if (entry.getName().startsWith("/")) // absolute paths
+                                       continue entries;
+                               Path target = targetSourceDir.resolve(entry.getName());
+                               Files.createDirectories(target.getParent());
+                               if (!Files.exists(target)) {
+                                       Files.copy(jarIn, target);
+                                       logger.log(Level.TRACE, () -> "Copied source " + target);
+                               } else {
+                                       logger.log(Level.WARNING, () -> target + " already exists, skipping...");
+                               }
+                       }
+               }
+
+       }
+
+       /** Download a Maven artifact. */
+       protected Path download(URL url, Path dir, M2Artifact artifact) throws IOException {
+               return download(url, dir, artifact, false);
+       }
+
+       /** Download a Maven artifact. */
+       protected Path download(URL url, Path dir, M2Artifact artifact, boolean sources) throws IOException {
+               return download(url, dir, artifact.getGroupId() + '/' + artifact.getArtifactId() + "-" + artifact.getVersion()
+                               + (sources ? "-sources" : "") + ".jar");
+       }
+
+       /*
+        * ECLIPSE ORIGIN
+        */
+
+       /** Process an archive in Eclipse format. */
+       public void processEclipseArchive(Path duDir) {
+               try {
+                       Path categoryRelativePath = descriptorsBase.relativize(duDir.getParent());
+                       // String category = categoryRelativePath.getFileName().toString();
+                       Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
+                       Files.createDirectories(targetCategoryBase);
+                       // first delete all directories from previous builds
+                       for (Path dir : Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p))) {
+                               deleteDirectory(dir);
+                       }
+
+                       Files.createDirectories(originBase);
+
+                       Path commonBnd = duDir.resolve(COMMON_BND);
+                       Properties commonProps = new Properties();
+                       try (InputStream in = Files.newInputStream(commonBnd)) {
+                               commonProps.load(in);
+                       }
+                       String url = commonProps.getProperty(ManifestConstants.SLC_ORIGIN_URI.toString());
+                       if (url == null) {
+                               url = uris.getProperty(duDir.getFileName().toString());
+                               if (url == null)
+                                       throw new IllegalStateException("No url available for " + duDir);
+                               commonProps.put(ManifestConstants.SLC_ORIGIN_URI.toString(), url);
+                       }
+                       Path downloaded = tryDownload(url, originBase);
+
+                       FileSystem zipFs = FileSystems.newFileSystem(downloaded, (ClassLoader) null);
+
+                       // filters
+                       List<PathMatcher> includeMatchers = new ArrayList<>();
+                       Properties includes = new Properties();
+                       try (InputStream in = Files.newInputStream(duDir.resolve("includes.properties"))) {
+                               includes.load(in);
+                       }
+                       for (Object pattern : includes.keySet()) {
+                               PathMatcher pathMatcher = zipFs.getPathMatcher("glob:/" + pattern);
+                               includeMatchers.add(pathMatcher);
+                       }
+
+                       List<PathMatcher> excludeMatchers = new ArrayList<>();
+                       Path excludeFile = duDir.resolve("excludes.properties");
+                       if (Files.exists(excludeFile)) {
+                               Properties excludes = new Properties();
+                               try (InputStream in = Files.newInputStream(excludeFile)) {
+                                       excludes.load(in);
+                               }
+                               for (Object pattern : excludes.keySet()) {
+                                       PathMatcher pathMatcher = zipFs.getPathMatcher("glob:/" + pattern);
+                                       excludeMatchers.add(pathMatcher);
+                               }
+                       }
+
+                       Files.walkFileTree(zipFs.getRootDirectories().iterator().next(), new SimpleFileVisitor<Path>() {
+
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       includeMatchers: for (PathMatcher includeMatcher : includeMatchers) {
+                                               if (includeMatcher.matches(file)) {
+                                                       for (PathMatcher excludeMatcher : excludeMatchers) {
+                                                               if (excludeMatcher.matches(file)) {
+                                                                       logger.log(Level.TRACE, "Skipping excluded " + file);
+                                                                       return FileVisitResult.CONTINUE;
+                                                               }
+                                                       }
+                                                       if (file.getFileName().toString().contains(".source_")) {
+                                                               if (!sourceBundles) {
+                                                                       processEclipseSourceJar(file, targetCategoryBase);
+                                                                       logger.log(Level.DEBUG, () -> "Processed source " + file);
+                                                               }
+
+                                                       } else {
+                                                               Map<String, String> map = new HashMap<>();
+                                                               for (Object key : commonProps.keySet())
+                                                                       map.put(key.toString(), commonProps.getProperty(key.toString()));
+                                                               processBundleJar(file, targetCategoryBase, map);
+                                                               logger.log(Level.DEBUG, () -> "Processed " + file);
+                                                       }
+                                                       break includeMatchers;
+                                               }
+                                       }
+                                       return FileVisitResult.CONTINUE;
+                               }
+                       });
+
+                       DirectoryStream<Path> dirs = Files.newDirectoryStream(targetCategoryBase,
+                                       (p) -> Files.isDirectory(p) && p.getFileName().toString().indexOf('.') >= 0);
+                       for (Path dir : dirs) {
+                               createJar(dir);
+                       }
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot process " + duDir, e);
+               }
+
+       }
+
+       /** Process sources in Eclipse format. */
+       protected void processEclipseSourceJar(Path file, Path targetBase) throws IOException {
+               try {
+                       Path targetBundleDir;
+                       try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
+                               Manifest manifest = jarIn.getManifest();
+
+                               String[] relatedBundle = manifest.getMainAttributes().getValue("Eclipse-SourceBundle").split(";");
+                               String version = relatedBundle[1].substring("version=\"".length());
+                               version = version.substring(0, version.length() - 1);
+                               NameVersion nameVersion = new NameVersion(relatedBundle[0], version);
+                               targetBundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
+
+                               Path targetSourceDir = targetBundleDir.resolve("OSGI-OPT/src");
+
+                               // TODO make it less dangerous?
+                               if (Files.exists(targetSourceDir)) {
+//                             deleteDirectory(targetSourceDir);
+                               } else {
+                                       Files.createDirectories(targetSourceDir);
+                               }
+
+                               // copy entries
+                               JarEntry entry;
+                               entries: while ((entry = jarIn.getNextJarEntry()) != null) {
+                                       if (entry.isDirectory())
+                                               continue entries;
+                                       if (entry.getName().startsWith("META-INF"))// skip META-INF entries
+                                               continue entries;
+                                       Path target = targetSourceDir.resolve(entry.getName());
+                                       Files.createDirectories(target.getParent());
+                                       Files.copy(jarIn, target);
+                                       logger.log(Level.TRACE, () -> "Copied source " + target);
+                               }
+
+                               // copy MANIFEST
+                       }
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot process " + file, e);
+               }
+
+       }
+
+       /*
+        * COMMON PROCESSING
+        */
+       /** Normalise a bundle. */
+       protected Path processBundleJar(Path file, Path targetBase, Map<String, String> entries) throws IOException {
+               NameVersion nameVersion;
+               Path targetBundleDir;
+               try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
+                       Manifest sourceManifest = jarIn.getManifest();
+                       Manifest manifest = sourceManifest != null ? new Manifest(sourceManifest) : new Manifest();
+
+                       // singleton
+                       boolean isSingleton = false;
+                       String rawSourceSymbolicName = manifest.getMainAttributes()
+                                       .getValue(ManifestConstants.BUNDLE_SYMBOLICNAME.toString());
+                       if (rawSourceSymbolicName != null) {
+
+                               // make sure there is no directive
+                               String[] arr = rawSourceSymbolicName.split(";");
+                               for (int i = 1; i < arr.length; i++) {
+                                       if (arr[i].trim().equals("singleton:=true"))
+                                               isSingleton = true;
+                                       logger.log(DEBUG, file.getFileName() + " is a singleton");
+                               }
+                       }
+
+                       // remove problematic entries in MANIFEST
+                       manifest.getEntries().clear();
+
+                       String ourSymbolicName = entries.get(BUNDLE_SYMBOLICNAME.toString());
+                       String ourVersion = entries.get(BUNDLE_VERSION.toString());
+
+                       if (ourSymbolicName != null && ourVersion != null) {
+                               nameVersion = new NameVersion(ourSymbolicName, ourVersion);
+                       } else {
+                               nameVersion = nameVersionFromManifest(manifest);
+                               if (ourVersion != null && !nameVersion.getVersion().equals(ourVersion)) {
+                                       logger.log(Level.WARNING,
+                                                       "Original version is " + nameVersion.getVersion() + " while new version is " + ourVersion);
+                                       entries.put(BUNDLE_VERSION.toString(), ourVersion);
+                               }
+                               if (ourSymbolicName != null) {
+                                       // we always force our symbolic name
+                                       nameVersion.setName(ourSymbolicName);
+                               }
+                       }
+                       targetBundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
+
+                       // force Java 9 module name
+                       entries.put(ManifestConstants.AUTOMATIC_MODULE_NAME.toString(), nameVersion.getName());
+
+                       boolean isNative = false;
+                       String os = null;
+                       String arch = null;
+                       if (targetBundleDir.startsWith(a2LibBase)) {
+                               isNative = true;
+                               Path libRelativePath = a2LibBase.relativize(targetBundleDir);
+                               os = libRelativePath.getName(0).toString();
+                               arch = libRelativePath.getName(1).toString();
+                       }
+
+                       // copy entries
+                       JarEntry entry;
+                       entries: while ((entry = jarIn.getNextJarEntry()) != null) {
+                               if (entry.isDirectory())
+                                       continue entries;
+                               if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".SF"))
+                                       continue entries;
+                               if (entry.getName().endsWith("module-info.class")) // skip Java 9 module info
+                                       continue entries;
+                               if (entry.getName().startsWith("META-INF/versions/")) // skip multi-version
+                                       continue entries;
+                               // skip file system providers as they cause issues with native image
+                               if (entry.getName().startsWith("META-INF/services/java.nio.file.spi.FileSystemProvider"))
+                                       continue entries;
+                               if (entry.getName().startsWith("OSGI-OPT/src/")) // skip embedded sources
+                                       continue entries;
+                               Path target = targetBundleDir.resolve(entry.getName());
+                               Files.createDirectories(target.getParent());
+                               Files.copy(jarIn, target);
+
+                               // native libraries
+                               if (isNative && (entry.getName().endsWith(".so") || entry.getName().endsWith(".dll")
+                                               || entry.getName().endsWith(".jnilib"))) {
+                                       Path categoryDir = targetBundleDir.getParent();
+//                                     String[] segments = categoryDir.getFileName().toString().split("\\.");
+//                                     String arch = segments[segments.length - 1];
+//                                     String os = segments[segments.length - 2];
+                                       boolean copyDll = false;
+                                       Path targetDll = categoryDir.resolve(targetBundleDir.relativize(target));
+                                       if (nameVersion.getName().equals("com.sun.jna")) {
+                                               if (arch.equals("x86_64"))
+                                                       arch = "x86-64";
+                                               if (os.equals("macosx"))
+                                                       os = "darwin";
+                                               if (target.getParent().getFileName().toString().equals(os + "-" + arch)) {
+                                                       copyDll = true;
+                                               }
+                                               targetDll = categoryDir.resolve(target.getFileName());
+                                       } else {
+                                               copyDll = true;
+                                       }
+                                       if (copyDll) {
+                                               Files.createDirectories(targetDll.getParent());
+                                               if (Files.exists(targetDll))
+                                                       Files.delete(targetDll);
+                                               Files.copy(target, targetDll);
+                                       }
+                                       Files.delete(target);
+                               }
+                               logger.log(Level.TRACE, () -> "Copied " + target);
+                       }
+
+                       // copy MANIFEST
+                       Path manifestPath = targetBundleDir.resolve("META-INF/MANIFEST.MF");
+                       Files.createDirectories(manifestPath.getParent());
+
+                       if (isSingleton && entries.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
+                               entries.put(BUNDLE_SYMBOLICNAME.toString(),
+                                               entries.get(BUNDLE_SYMBOLICNAME.toString()) + ";singleton:=true");
+                       }
+
+                       for (String key : entries.keySet()) {
+                               String value = entries.get(key);
+                               Object previousValue = manifest.getMainAttributes().putValue(key, value);
+                               if (previousValue != null && !previousValue.equals(value)) {
+                                       if (ManifestConstants.IMPORT_PACKAGE.toString().equals(key)
+                                                       || ManifestConstants.EXPORT_PACKAGE.toString().equals(key)
+                                                       || ManifestConstants.BUNDLE_LICENSE.toString().equals(key))
+                                               logger.log(Level.TRACE, file.getFileName() + ": " + key + " was modified");
+                                       else
+                                               logger.log(Level.WARNING, file.getFileName() + ": " + key + " was " + previousValue
+                                                               + ", overridden with " + value);
+                               }
+
+                               // hack to remove unresolvable
+                               if (key.equals("Provide-Capability") || key.equals("Require-Capability"))
+                                       if (nameVersion.getName().equals("osgi.core") || nameVersion.getName().equals("osgi.cmpn")) {
+                                               manifest.getMainAttributes().remove(key);
+                                       }
+                       }
+                       try (OutputStream out = Files.newOutputStream(manifestPath)) {
+                               manifest.write(out);
+                       }
+               }
+               return targetBundleDir;
+       }
+
+       /*
+        * UTILITIES
+        */
+
+       /** Recursively deletes a directory. */
+       private static void deleteDirectory(Path path) throws IOException {
+               if (!Files.exists(path))
+                       return;
+               Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+                       @Override
+                       public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
+                               if (e != null)
+                                       throw e;
+                               Files.delete(directory);
+                               return FileVisitResult.CONTINUE;
+                       }
+
+                       @Override
+                       public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                               Files.delete(file);
+                               return FileVisitResult.CONTINUE;
+                       }
+               });
+       }
+
+       /** Extract name/version from a MANIFEST. */
+       protected NameVersion nameVersionFromManifest(Manifest manifest) {
+               Attributes attrs = manifest.getMainAttributes();
+               // symbolic name
+               String symbolicName = attrs.getValue(ManifestConstants.BUNDLE_SYMBOLICNAME.toString());
+               if (symbolicName == null)
+                       return null;
+               // make sure there is no directive
+               symbolicName = symbolicName.split(";")[0];
+
+               String version = attrs.getValue(ManifestConstants.BUNDLE_VERSION.toString());
+               return new NameVersion(symbolicName, version);
+       }
+
+       /** Try to download from an URI. */
+       protected Path tryDownload(String uri, Path dir) throws IOException {
+               // find mirror
+               List<String> urlBases = null;
+               String uriPrefix = null;
+               uriPrefixes: for (String uriPref : mirrors.keySet()) {
+                       if (uri.startsWith(uriPref)) {
+                               if (mirrors.get(uriPref).size() > 0) {
+                                       urlBases = mirrors.get(uriPref);
+                                       uriPrefix = uriPref;
+                                       break uriPrefixes;
+                               }
+                       }
+               }
+               if (urlBases == null)
+                       try {
+                               return download(new URL(uri), dir);
+                       } catch (FileNotFoundException e) {
+                               throw new FileNotFoundException("Cannot find " + uri);
+                       }
+
+               // try to download
+               for (String urlBase : urlBases) {
+                       String relativePath = uri.substring(uriPrefix.length());
+                       URL url = new URL(urlBase + relativePath);
+                       try {
+                               return download(url, dir);
+                       } catch (FileNotFoundException e) {
+                               logger.log(Level.WARNING, "Cannot download " + url + ", trying another mirror");
+                       }
+               }
+               throw new FileNotFoundException("Cannot find " + uri);
+       }
+
+       /** Effectively download. */
+       protected Path download(URL url, Path dir) throws IOException {
+               return download(url, dir, (String) null);
+       }
+
+       /** Effectively download. */
+       protected Path download(URL url, Path dir, String name) throws IOException {
+
+               Path dest;
+               if (name == null) {
+                       name = url.getPath().substring(url.getPath().lastIndexOf('/') + 1);
+               }
+
+               dest = dir.resolve(name);
+               if (Files.exists(dest)) {
+                       logger.log(Level.TRACE, () -> "File " + dest + " already exists for " + url + ", not downloading again");
+                       return dest;
+               } else {
+                       Files.createDirectories(dest.getParent());
+               }
+
+               try (InputStream in = url.openStream()) {
+                       Files.copy(in, dest);
+                       logger.log(Level.DEBUG, () -> "Downloaded " + dest + " from " + url);
+               }
+               return dest;
+       }
+
+       /** Create a JAR file from a directory. */
+       protected Path createJar(Path bundleDir) throws IOException {
+               // Create the jar
+               Path jarPath = bundleDir.getParent().resolve(bundleDir.getFileName() + ".jar");
+               Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
+               Manifest manifest;
+               try (InputStream in = Files.newInputStream(manifestPath)) {
+                       manifest = new Manifest(in);
+               }
+               try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarPath), manifest)) {
+                       jarOut.setLevel(Deflater.DEFAULT_COMPRESSION);
+                       Files.walkFileTree(bundleDir, new SimpleFileVisitor<Path>() {
+
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       if (file.getFileName().toString().equals("MANIFEST.MF"))
+                                               return super.visitFile(file, attrs);
+                                       JarEntry entry = new JarEntry(bundleDir.relativize(file).toString());
+                                       jarOut.putNextEntry(entry);
+                                       Files.copy(file, jarOut);
+                                       return super.visitFile(file, attrs);
+                               }
+
+                       });
+               }
+               deleteDirectory(bundleDir);
+               return jarPath;
+       }
+
+       enum ManifestConstants {
+               // OSGi
+               BUNDLE_SYMBOLICNAME("Bundle-SymbolicName"), //
+               BUNDLE_VERSION("Bundle-Version"), //
+               BUNDLE_LICENSE("Bundle-License"), //
+               EXPORT_PACKAGE("Export-Package"), //
+               IMPORT_PACKAGE("Import-Package"), //
+               // JAVA
+               AUTOMATIC_MODULE_NAME("Automatic-Module-Name"), //
+               // SLC
+               SLC_CATEGORY("SLC-Category"), //
+               SLC_ORIGIN_M2("SLC-Origin-M2"), //
+               SLC_ORIGIN_M2_MERGE("SLC-Origin-M2-Merge"), //
+               SLC_ORIGIN_M2_REPO("SLC-Origin-M2-Repo"), //
+               SLC_ORIGIN_MANIFEST_NOT_MODIFIED("SLC-Origin-ManifestNotModified"), //
+               SLC_ORIGIN_URI("SLC-Origin-URI"),//
+               ;
+
+               final String value;
+
+               private ManifestConstants(String value) {
+                       this.value = value;
+               }
+
+               @Override
+               public String toString() {
+                       return value;
+               }
+
+       }
+
+}
+
+/** Simple representation of an M2 artifact. */
+class M2Artifact extends CategoryNameVersion {
+       private String classifier;
+
+       M2Artifact(String m2coordinates) {
+               this(m2coordinates, null);
+       }
+
+       M2Artifact(String m2coordinates, String classifier) {
+               String[] parts = m2coordinates.split(":");
+               setCategory(parts[0]);
+               setName(parts[1]);
+               if (parts.length > 2) {
+                       setVersion(parts[2]);
+               }
+               this.classifier = classifier;
+       }
+
+       String getGroupId() {
+               return super.getCategory();
+       }
+
+       String getArtifactId() {
+               return super.getName();
+       }
+
+       String toM2Coordinates() {
+               return getCategory() + ":" + getName() + (getVersion() != null ? ":" + getVersion() : "");
+       }
+
+       String getClassifier() {
+               return classifier != null ? classifier : "";
+       }
+
+       String getExtension() {
+               return "jar";
+       }
+}
+
+/** Utilities around Maven (conventions based). */
+class M2ConventionsUtils {
+       final static String MAVEN_CENTRAL_BASE_URL = "https://repo1.maven.org/maven2/";
+
+       /** The file name of this artifact when stored */
+       static String artifactFileName(M2Artifact artifact) {
+               return artifact.getArtifactId() + '-' + artifact.getVersion()
+                               + (artifact.getClassifier().equals("") ? "" : '-' + artifact.getClassifier()) + '.'
+                               + artifact.getExtension();
+       }
+
+       /** Absolute path to the file */
+       static String artifactPath(String artifactBasePath, M2Artifact artifact) {
+               return artifactParentPath(artifactBasePath, artifact) + '/' + artifactFileName(artifact);
+       }
+
+       /** Absolute path to the file */
+       static String artifactUrl(String repoUrl, M2Artifact artifact) {
+               if (repoUrl.endsWith("/"))
+                       return repoUrl + artifactPath("/", artifact).substring(1);
+               else
+                       return repoUrl + artifactPath("/", artifact);
+       }
+
+       /** Absolute path to the file */
+       static URL mavenRepoUrl(String repoBase, M2Artifact artifact) {
+               String url = artifactUrl(repoBase == null ? MAVEN_CENTRAL_BASE_URL : repoBase, artifact);
+               try {
+                       return new URL(url);
+               } catch (MalformedURLException e) {
+                       // it should not happen
+                       throw new IllegalStateException(e);
+               }
+       }
+
+       /** Absolute path to the directories where the files will be stored */
+       static String artifactParentPath(String artifactBasePath, M2Artifact artifact) {
+               return artifactBasePath + (artifactBasePath.endsWith("/") ? "" : "/") + artifactParentPath(artifact);
+       }
+
+       /** Relative path to the directories where the files will be stored */
+       static String artifactParentPath(M2Artifact artifact) {
+               return artifact.getGroupId().replace('.', '/') + '/' + artifact.getArtifactId() + '/' + artifact.getVersion();
+       }
+
+       /** Singleton */
+       private M2ConventionsUtils() {
+       }
+}
+
+class CategoryNameVersion extends NameVersion {
+       private String category;
+
+       CategoryNameVersion() {
+       }
+
+       CategoryNameVersion(String category, String name, String version) {
+               super(name, version);
+               this.category = category;
+       }
+
+       CategoryNameVersion(String category, NameVersion nameVersion) {
+               super(nameVersion);
+               this.category = category;
+       }
+
+       String getCategory() {
+               return category;
+       }
+
+       void setCategory(String category) {
+               this.category = category;
+       }
+
+       @Override
+       public String toString() {
+               return category + ":" + super.toString();
+       }
+
+}
+
+class NameVersion implements Comparable<NameVersion> {
+       private String name;
+       private String version;
+
+       NameVersion() {
+       }
+
+       /** Interprets string in OSGi-like format my.module.name;version=0.0.0 */
+       NameVersion(String nameVersion) {
+               int index = nameVersion.indexOf(";version=");
+               if (index < 0) {
+                       setName(nameVersion);
+                       setVersion(null);
+               } else {
+                       setName(nameVersion.substring(0, index));
+                       setVersion(nameVersion.substring(index + ";version=".length()));
+               }
+       }
+
+       NameVersion(String name, String version) {
+               this.name = name;
+               this.version = version;
+       }
+
+       NameVersion(NameVersion nameVersion) {
+               this.name = nameVersion.getName();
+               this.version = nameVersion.getVersion();
+       }
+
+       String getName() {
+               return name;
+       }
+
+       void setName(String name) {
+               this.name = name;
+       }
+
+       String getVersion() {
+               return version;
+       }
+
+       void setVersion(String version) {
+               this.version = version;
+       }
+
+       String getBranch() {
+               String[] parts = getVersion().split("\\.");
+               if (parts.length < 2)
+                       throw new IllegalStateException("Version " + getVersion() + " cannot be interpreted as branch.");
+               return parts[0] + "." + parts[1];
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof NameVersion) {
+                       NameVersion nameVersion = (NameVersion) obj;
+                       return name.equals(nameVersion.getName()) && version.equals(nameVersion.getVersion());
+               } else
+                       return false;
+       }
+
+       @Override
+       public int hashCode() {
+               return name.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               return name + ":" + version;
+       }
+
+       public int compareTo(NameVersion o) {
+               if (o.getName().equals(name))
+                       return version.compareTo(o.getVersion());
+               else
+                       return name.compareTo(o.getName());
+       }
+}