Do not override exported packages version
[cc0/argeo-build.git] / src / org / argeo / build / Repackage.java
index 09f78f746912239c9c218cece9974bfcbf7310e4..18b1487cb91707ccd5e30d0ed28bd34fe12935eb 100644 (file)
@@ -9,12 +9,14 @@ import static java.nio.file.FileVisitResult.CONTINUE;
 import static java.nio.file.StandardOpenOption.APPEND;
 import static java.nio.file.StandardOpenOption.CREATE;
 import static java.util.jar.Attributes.Name.MANIFEST_VERSION;
-import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_EMBED;
+import static org.argeo.build.Repackage.ManifestHeader.ARGEO_DO_NOT_MODIFY;
 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_M2;
 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_M2_MERGE;
 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_M2_REPO;
 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_NO_METADATA_GENERATION;
+import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_SOURCES_URI;
 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_URI;
+import static org.argeo.build.Repackage.ManifestHeader.AUTOMATIC_MODULE_NAME;
 import static org.argeo.build.Repackage.ManifestHeader.BUNDLE_LICENSE;
 import static org.argeo.build.Repackage.ManifestHeader.BUNDLE_SYMBOLICNAME;
 import static org.argeo.build.Repackage.ManifestHeader.BUNDLE_VERSION;
@@ -156,11 +158,11 @@ public class Repackage {
                 * and Export-Package will be kept untouched.
                 */
                ARGEO_ORIGIN_NO_METADATA_GENERATION("Argeo-Origin-NoMetadataGeneration"), //
-               /**
-                * Embed the original jar without modifying it (may be required by some
-                * proprietary licenses, such as JCR Day License).
-                */
-               ARGEO_ORIGIN_EMBED("Argeo-Origin-Embed"), //
+//             /**
+//              * Embed the original jar without modifying it (may be required by some
+//              * proprietary licenses, such as JCR Day License).
+//              */
+//             ARGEO_ORIGIN_EMBED("Argeo-Origin-Embed"), //
                /**
                 * Do not modify original jar (may be required by some proprietary licenses,
                 * such as JCR Day License).
@@ -171,17 +173,42 @@ public class Repackage {
                 * etc.).
                 */
                ARGEO_ORIGIN_URI("Argeo-Origin-URI"), //
+               /**
+                * Origin (non-Maven) URI of the source of the component. It may be anything
+                * (jar, archive, code repository, etc.).
+                */
+               ARGEO_ORIGIN_SOURCES_URI("Argeo-Origin-Sources-URI"), //
                ;
 
-               final String value;
+               final String headerName;
 
-               private ManifestHeader(String value) {
-                       this.value = value;
+               private ManifestHeader(String headerName) {
+                       this.headerName = headerName;
                }
 
                @Override
                public String toString() {
-                       return value;
+                       return headerName;
+               }
+
+               /** Get the value from either a {@link Manifest} or a {@link Properties}. */
+               String get(Object map) {
+                       if (map instanceof Manifest manifest)
+                               return manifest.getMainAttributes().getValue(headerName);
+                       else if (map instanceof Properties props)
+                               return props.getProperty(headerName);
+                       else
+                               throw new IllegalArgumentException("Unsupported mapping " + map.getClass());
+               }
+
+               /** Put the value into either a {@link Manifest} or a {@link Properties}. */
+               void put(Object map, String value) {
+                       if (map instanceof Manifest manifest)
+                               manifest.getMainAttributes().putValue(headerName, value);
+                       else if (map instanceof Properties props)
+                               props.setProperty(headerName, value);
+                       else
+                               throw new IllegalArgumentException("Unsupported mapping " + map.getClass());
                }
        }
 
@@ -193,9 +220,9 @@ public class Repackage {
         * Subdirectory of the jar file where origin informations (changes, legal
         * notices etc. are stored)
         */
-       final static String A2_ORIGIN = "A2-ORIGIN";
+       final static String ARGEO_ORIGIN = "ARGEO-ORIGIN";
        /** File detailing modifications to the original component. */
-       final static String CHANGES = A2_ORIGIN + "/changes";
+       final static String CHANGES = ARGEO_ORIGIN + "/changes";
        /**
         * Name of the file at the root of the repackaged jar, which prominently
         * notifies that the component has be repackaged.
@@ -225,12 +252,12 @@ public class Repackage {
        final Map<String, List<String>> mirrors = new HashMap<String, List<String>>();
 
        /** Whether sources should be packaged separately */
-       final boolean sourceBundles;
+       final boolean separateSources;
 
        /** Constructor initialises the various variables */
        public Repackage(Path a2Base, Path descriptorsBase) {
-               sourceBundles = Boolean.parseBoolean(System.getenv(ENV_SOURCE_BUNDLES));
-               if (sourceBundles)
+               separateSources = Boolean.parseBoolean(System.getenv(ENV_SOURCE_BUNDLES));
+               if (separateSources)
                        logger.log(INFO, "Sources will be packaged separately");
 
                Objects.requireNonNull(a2Base);
@@ -240,7 +267,7 @@ public class Repackage {
 
                // TODO define and use a build base
                this.a2Base = a2Base;
-               this.a2SrcBase = a2Base.getParent().resolve(a2Base.getFileName() + ".src");
+               this.a2SrcBase = separateSources ? a2Base.getParent().resolve(a2Base.getFileName() + ".src") : a2Base;
                this.a2LibBase = a2Base.resolve("lib");
                this.descriptorsBase = descriptorsBase;
                if (!Files.exists(this.descriptorsBase))
@@ -311,10 +338,6 @@ public class Repackage {
                        try (InputStream in = Files.newInputStream(bndFile)) {
                                fileProps.load(in);
                        }
-                       String repoStr = fileProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
-                                       ? fileProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
-                                       : null;
-
                        // use file name as symbolic name
                        if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
                                String symbolicName = bndFile.getFileName().toString();
@@ -326,32 +349,20 @@ public class Repackage {
                        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 = downloadMaven(url, artifact);
-
-                       // some proprietary artifacts do not allow any modification
-                       // when releasing (with separate sources) we just copy it
-                       boolean doNotModify = Boolean.parseBoolean(
-                                       fileProps.getOrDefault(ManifestHeader.ARGEO_DO_NOT_MODIFY.toString(), "false").toString());
-                       if (doNotModify && sourceBundles) {
-                               Path unmodifiedTarget = targetCategoryBase.resolve(
-                                               fileProps.getProperty(BUNDLE_SYMBOLICNAME.toString()) + "." + artifact.getBranch() + ".jar");
-                               Files.copy(downloaded, unmodifiedTarget, StandardCopyOption.REPLACE_EXISTING);
-                               Path bundleDir = targetCategoryBase
-                                               .resolve(fileProps.getProperty(BUNDLE_SYMBOLICNAME.toString()) + "." + artifact.getBranch());
-                               downloadAndProcessM2Sources(repoStr, artifact, bundleDir, false);
-                               Manifest manifest;
-                               try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(unmodifiedTarget))) {
-                                       manifest = jarIn.getManifest();
-                               }
-                               createSourceJar(bundleDir, manifest);
+
+                       Path downloaded = downloadMaven(fileProps, artifact);
+
+                       boolean doNotModify = Boolean
+                                       .parseBoolean(fileProps.getOrDefault(ARGEO_DO_NOT_MODIFY.toString(), "false").toString());
+                       if (doNotModify) {
+                               processNotModified(targetCategoryBase, downloaded, fileProps, artifact);
                                return;
                        }
 
                        // regular processing
                        A2Origin origin = new A2Origin();
                        Path bundleDir = processBndJar(downloaded, targetCategoryBase, fileProps, artifact, origin);
-                       downloadAndProcessM2Sources(repoStr, artifact, bundleDir, false);
+                       downloadAndProcessM2Sources(fileProps, artifact, bundleDir, false, false);
                        createJar(bundleDir, origin);
                } catch (Exception e) {
                        throw new RuntimeException("Cannot process " + bndFile, e);
@@ -404,42 +415,48 @@ public class Repackage {
                                }
                                String m2Coordinates = fileProps.getProperty(ARGEO_ORIGIN_M2.toString());
                                M2Artifact artifact = new M2Artifact(m2Coordinates);
-                               artifact.setVersion(m2Version);
+                               if (artifact.getVersion() == null) {
+                                       artifact.setVersion(m2Version);
+                               } else {
+                                       logger.log(WARNING, p.getFileName() + " : Using version " + artifact.getVersion()
+                                                       + " specified in descriptor rather than " + m2Version + " specified in " + COMMON_BND);
+                               }
 
                                // prepare manifest entries
-                               Properties mergeProps = new Properties();
-                               mergeProps.putAll(commonProps);
+                               Properties mergedProps = new Properties();
+                               mergedProps.putAll(commonProps);
 
                                fileEntries: for (Object key : fileProps.keySet()) {
                                        if (ARGEO_ORIGIN_M2.toString().equals(key))
                                                continue fileEntries;
                                        String value = fileProps.getProperty(key.toString());
-                                       Object previousValue = mergeProps.put(key.toString(), value);
+                                       Object previousValue = mergedProps.put(key.toString(), value);
                                        if (previousValue != null) {
                                                logger.log(WARNING,
                                                                commonBnd + ": " + key + " was " + previousValue + ", overridden with " + value);
                                        }
                                }
-                               mergeProps.put(ARGEO_ORIGIN_M2.toString(), artifact.toM2Coordinates());
-                               if (!mergeProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
+                               mergedProps.put(ARGEO_ORIGIN_M2.toString(), artifact.toM2Coordinates());
+                               if (!mergedProps.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);
+                                       mergedProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
                                }
 
-                               String repoStr = mergeProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
-                                               ? mergeProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
-                                               : null;
-
                                // download
-                               URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
-                               Path downloaded = downloadMaven(url, artifact);
+                               Path downloaded = downloadMaven(mergedProps, artifact);
 
-                               A2Origin origin = new A2Origin();
-                               Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, mergeProps, artifact, origin);
-                               downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir, false);
-                               createJar(targetBundleDir, origin);
+                               boolean doNotModify = Boolean
+                                               .parseBoolean(mergedProps.getOrDefault(ARGEO_DO_NOT_MODIFY.toString(), "false").toString());
+                               if (doNotModify) {
+                                       processNotModified(targetCategoryBase, downloaded, mergedProps, artifact);
+                               } else {
+                                       A2Origin origin = new A2Origin();
+                                       Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, mergedProps, artifact, origin);
+                                       downloadAndProcessM2Sources(mergedProps, artifact, targetBundleDir, false, false);
+                                       createJar(targetBundleDir, origin);
+                               }
                        }
                } catch (IOException e) {
                        throw new RuntimeException("Cannot process " + duDir, e);
@@ -453,13 +470,20 @@ public class Repackage {
                Path targetCategoryBase = a2Base.resolve(category);
 
                Properties mergeProps = new Properties();
+               // first, load common properties
+               Path commonBnd = duDir.resolve(COMMON_BND);
+               if (Files.exists(commonBnd))
+                       try (InputStream in = Files.newInputStream(commonBnd)) {
+                               mergeProps.load(in);
+                       }
+               // then, the merge properties themselves
                try (InputStream in = Files.newInputStream(mergeBnd)) {
                        mergeProps.load(in);
                }
 
                String m2Version = mergeProps.getProperty(ARGEO_ORIGIN_M2.toString());
                if (m2Version == null) {
-                       logger.log(WARNING, "Ignoring " + duDir + " as it is not an M2-based distribution unit");
+                       logger.log(WARNING, "Ignoring merging in " + duDir + " as it is not an M2-based distribution unit");
                        return;// ignore, this is probably an Eclipse archive
                }
                if (!m2Version.startsWith(":")) {
@@ -472,10 +496,6 @@ public class Repackage {
                if (artifactsStr == null)
                        throw new IllegalArgumentException(mergeBnd + ": " + ARGEO_ORIGIN_M2_MERGE + " must be set");
 
-               String repoStr = mergeProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
-                               ? mergeProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
-                               : null;
-
                String bundleSymbolicName = mergeProps.getProperty(BUNDLE_SYMBOLICNAME.toString());
                if (bundleSymbolicName == null)
                        throw new IllegalArgumentException("Bundle-SymbolicName must be set in " + mergeBnd);
@@ -494,8 +514,7 @@ public class Repackage {
                        if (artifact.getVersion() == null)
                                artifact.setVersion(m2Version);
                        originDesc.add(artifact.toString());
-                       URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
-                       Path downloaded = downloadMaven(url, artifact);
+                       Path downloaded = downloadMaven(mergeProps, artifact);
                        JarEntry entry;
                        try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(downloaded), false)) {
                                entries: while ((entry = jarIn.getNextJarEntry()) != null) {
@@ -527,7 +546,7 @@ public class Repackage {
                                                continue entries;
                                        }
                                        if (entry.getName().equals("META-INF/MANIFEST.MF")) {
-                                               Path originalManifest = bundleDir.resolve(A2_ORIGIN).resolve(artifact.getGroupId())
+                                               Path originalManifest = bundleDir.resolve(ARGEO_ORIGIN).resolve(artifact.getGroupId())
                                                                .resolve(artifact.getArtifactId()).resolve("MANIFEST.MF");
                                                Files.createDirectories(originalManifest.getParent());
                                                try (OutputStream out = Files.newOutputStream(originalManifest)) {
@@ -542,7 +561,7 @@ public class Repackage {
                                                        || entry.getName().endsWith("LICENSE") || entry.getName().endsWith("LICENSE.md")
                                                        || entry.getName().endsWith("LICENSE-notice.md") || entry.getName().endsWith("COPYING")
                                                        || entry.getName().endsWith("COPYING.LESSER")) {
-                                               Path artifactOriginDir = bundleDir.resolve(A2_ORIGIN).resolve(artifact.getGroupId())
+                                               Path artifactOriginDir = bundleDir.resolve(ARGEO_ORIGIN).resolve(artifact.getGroupId())
                                                                .resolve(artifact.getArtifactId());
                                                Path target = artifactOriginDir.resolve(entry.getName());
                                                Files.createDirectories(target.getParent());
@@ -565,6 +584,10 @@ public class Repackage {
                                                } else if (entry.getName().startsWith("org/apache/batik/")) {
                                                        logger.log(TRACE, "Skip " + entry.getName());
                                                        continue entries;
+                                               } else if (entry.getName().startsWith("META-INF/NOTICE")) {
+                                                       logger.log(WARNING, "Skip " + entry.getName() + " from " + artifact);
+                                                       // TODO merge them?
+                                                       continue entries;
                                                } else {
                                                        throw new IllegalStateException("File " + target + " from " + artifact + " already exists");
                                                }
@@ -575,7 +598,7 @@ public class Repackage {
                        origin.added.add("binary content of " + artifact);
 
                        // process sources
-                       downloadAndProcessM2Sources(repoStr, artifact, bundleDir, true);
+                       downloadAndProcessM2Sources(mergeProps, artifact, bundleDir, true, false);
                }
 
                // additional service files
@@ -611,8 +634,10 @@ public class Repackage {
                                        continue keys;
                                }
                                if ("Require-Capability".equals(key.toString())
-                                               && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\""))
+                                               && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\"")) {
+                                       origin.deleted.add("MANIFEST header " + key);
                                        continue keys;// hack for very old classes
+                               }
                                entries.put(key.toString(), value.toString());
                        }
                } catch (Exception e) {
@@ -662,8 +687,7 @@ public class Repackage {
                                }
 
                                if (!fileProps.containsKey(EXPORT_PACKAGE.toString())) {
-                                       fileProps.put(EXPORT_PACKAGE.toString(),
-                                                       "*;version=\"" + fileProps.getProperty(BUNDLE_VERSION.toString()) + "\"");
+                                       fileProps.put(EXPORT_PACKAGE.toString(), "*");
                                }
 
                                // BND analysis
@@ -700,14 +724,38 @@ public class Repackage {
 
        }
 
-       /** Download and integrates sources for a single Maven artifact. */
-       void downloadAndProcessM2Sources(String repoStr, M2Artifact artifact, Path targetBundleDir, boolean merging)
+       /** Process an artifact that should not be modified. */
+       void processNotModified(Path targetCategoryBase, Path downloaded, Properties fileProps, M2Artifact artifact)
                        throws IOException {
+               // Some proprietary or signed artifacts do not allow any modification
+               // When releasing (with separate sources), we just copy it
+               Path unmodifiedTarget = targetCategoryBase
+                               .resolve(fileProps.getProperty(BUNDLE_SYMBOLICNAME.toString()) + "." + artifact.getBranch() + ".jar");
+               Files.createDirectories(unmodifiedTarget.getParent());
+               Files.copy(downloaded, unmodifiedTarget, StandardCopyOption.REPLACE_EXISTING);
+               Path bundleDir = targetCategoryBase
+                               .resolve(fileProps.getProperty(BUNDLE_SYMBOLICNAME.toString()) + "." + artifact.getBranch());
+               downloadAndProcessM2Sources(fileProps, artifact, bundleDir, false, true);
+               Manifest manifest;
+               try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(unmodifiedTarget))) {
+                       manifest = jarIn.getManifest();
+               }
+               createSourceJar(bundleDir, manifest, fileProps);
+       }
+
+       /** Download and integrates sources for a single Maven artifact. */
+       void downloadAndProcessM2Sources(Properties props, M2Artifact artifact, Path targetBundleDir, boolean merging,
+                       boolean unmodified) throws IOException {
                try {
+                       String repoStr = props.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
+                                       ? props.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
+                                       : null;
+                       String alternateUri = props.getProperty(ARGEO_ORIGIN_SOURCES_URI.toString());
                        M2Artifact sourcesArtifact = new M2Artifact(artifact.toM2Coordinates(), "sources");
-                       URL sourcesUrl = M2ConventionsUtils.mavenRepoUrl(repoStr, sourcesArtifact);
-                       Path sourcesDownloaded = downloadMaven(sourcesUrl, artifact, true);
-                       processM2SourceJar(sourcesDownloaded, targetBundleDir, merging ? artifact : null);
+                       URL sourcesUrl = alternateUri != null ? new URL(alternateUri)
+                                       : M2ConventionsUtils.mavenRepoUrl(repoStr, sourcesArtifact);
+                       Path sourcesDownloaded = downloadMaven(sourcesUrl, sourcesArtifact);
+                       processM2SourceJar(sourcesDownloaded, targetBundleDir, merging ? artifact : null, unmodified);
                        logger.log(TRACE, () -> "Processed source " + sourcesDownloaded);
                } catch (Exception e) {
                        logger.log(ERROR, () -> "Cannot download source for  " + artifact);
@@ -716,9 +764,9 @@ public class Repackage {
        }
 
        /** Integrate sources from a downloaded jar file. */
-       void processM2SourceJar(Path file, Path bundleDir, M2Artifact mergingFrom) throws IOException {
+       void processM2SourceJar(Path file, Path bundleDir, M2Artifact mergingFrom, boolean unmodified) throws IOException {
                A2Origin origin = new A2Origin();
-               Path sourceDir = sourceBundles ? bundleDir.getParent().resolve(bundleDir.toString() + ".src")
+               Path sourceDir = separateSources || unmodified ? bundleDir.getParent().resolve(bundleDir.toString() + ".src")
                                : bundleDir.resolve("OSGI-OPT/src");
                try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
 
@@ -732,24 +780,26 @@ public class Repackage {
                                String relPath = entry.getName();
                                if (entry.isDirectory())
                                        continue entries;
-                               if (entry.getName().startsWith("META-INF")) {// skip META-INF entries
-                                       origin.deleted.add("META-INF directory from the sources" + mergingMsg);
-                                       continue entries;
-                               }
-                               if (entry.getName().startsWith("module-info.java")) {// skip Java module information
-                                       origin.deleted.add("Java module information from the sources (module-info.java)" + mergingMsg);
+                               if (entry.getName().equals("META-INF/MANIFEST.MF")) {// skip META-INF entries
+                                       origin.deleted.add("MANIFEST.MF from the sources" + mergingMsg);
                                        continue entries;
                                }
-                               if (entry.getName().startsWith("/")) { // absolute paths
-                                       int metaInfIndex = entry.getName().indexOf("META-INF");
-                                       if (metaInfIndex >= 0) {
-                                               relPath = entry.getName().substring(metaInfIndex);
-                                               origin.moved.add(" to " + relPath + " entry with absolute path " + entry.getName());
-                                       } else {
-                                               logger.log(WARNING, entry.getName() + " has an absolute path");
-                                               origin.deleted.add(entry.getName() + " from the sources" + mergingMsg);
+                               if (!unmodified) {
+                                       if (entry.getName().startsWith("module-info.java")) {// skip Java module information
+                                               origin.deleted.add("Java module information from the sources (module-info.java)" + mergingMsg);
+                                               continue entries;
+                                       }
+                                       if (entry.getName().startsWith("/")) { // absolute paths
+                                               int metaInfIndex = entry.getName().indexOf("META-INF");
+                                               if (metaInfIndex >= 0) {
+                                                       relPath = entry.getName().substring(metaInfIndex);
+                                                       origin.moved.add(" to " + relPath + " entry with absolute path " + entry.getName());
+                                               } else {
+                                                       logger.log(WARNING, entry.getName() + " has an absolute path");
+                                                       origin.deleted.add(entry.getName() + " from the sources" + mergingMsg);
+                                               }
+                                               continue entries;
                                        }
-                                       continue entries;
                                }
                                Path target = sourceDir.resolve(relPath);
                                Files.createDirectories(target.getParent());
@@ -762,7 +812,7 @@ public class Repackage {
                        }
                }
                // write the changes
-               if (sourceBundles) {
+               if (separateSources || unmodified) {
                        origin.appendChanges(sourceDir);
                } else {
                        origin.added.add("source code under OSGI-OPT/src");
@@ -771,15 +821,18 @@ public class Repackage {
        }
 
        /** Download a Maven artifact. */
-       Path downloadMaven(URL url, M2Artifact artifact) throws IOException {
-               return downloadMaven(url, artifact, false);
+       Path downloadMaven(Properties props, M2Artifact artifact) throws IOException {
+               String repoStr = props.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
+                               ? props.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
+                               : null;
+               String alternateUri = props.getProperty(ARGEO_ORIGIN_URI.toString());
+               URL url = alternateUri != null ? new URL(alternateUri) : M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
+               return downloadMaven(url, artifact);
        }
 
        /** Download a Maven artifact. */
-       Path downloadMaven(URL url, M2Artifact artifact, boolean sources) throws IOException {
-               return download(url, mavenBase, artifact.getGroupId().replace(".", "/") //
-                               + '/' + artifact.getArtifactId() + '/' + artifact.getVersion() //
-                               + '/' + artifact.getArtifactId() + "-" + artifact.getVersion() + (sources ? "-sources" : "") + ".jar");
+       Path downloadMaven(URL url, M2Artifact artifact) throws IOException {
+               return download(url, mavenBase, M2ConventionsUtils.artifactPath("", artifact));
        }
 
        /*
@@ -898,7 +951,7 @@ public class Repackage {
                                NameVersion nameVersion = new NameVersion(relatedBundle[0], version);
                                bundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
 
-                               Path sourceDir = sourceBundles ? bundleDir.getParent().resolve(bundleDir.toString() + ".src")
+                               Path sourceDir = separateSources ? bundleDir.getParent().resolve(bundleDir.toString() + ".src")
                                                : bundleDir.resolve("OSGI-OPT/src");
 
                                Files.createDirectories(sourceDir);
@@ -915,7 +968,7 @@ public class Repackage {
                                }
 
                                // write the changes
-                               if (sourceBundles) {
+                               if (separateSources) {
                                        origin.appendChanges(sourceDir);
                                } else {
                                        origin.added.add("source code under OSGI-OPT/src");
@@ -932,7 +985,9 @@ public class Repackage {
         */
        /** Normalise a single (that is, non-merged) bundle. */
        Path processBundleJar(Path file, Path targetBase, Map<String, String> entries, A2Origin origin) throws IOException {
-               boolean embed = Boolean.parseBoolean(entries.getOrDefault(ARGEO_ORIGIN_EMBED.toString(), "false").toString());
+//             boolean embed = Boolean.parseBoolean(entries.getOrDefault(ARGEO_ORIGIN_EMBED.toString(), "false").toString());
+               boolean doNotModify = Boolean
+                               .parseBoolean(entries.getOrDefault(ManifestHeader.ARGEO_DO_NOT_MODIFY.toString(), "false").toString());
                NameVersion nameVersion;
                Path bundleDir;
                // singleton
@@ -977,7 +1032,7 @@ public class Repackage {
 
                        // copy original MANIFEST
                        if (sourceManifest != null) {
-                               Path originalManifest = bundleDir.resolve(A2_ORIGIN).resolve("MANIFEST.MF");
+                               Path originalManifest = bundleDir.resolve(ARGEO_ORIGIN).resolve("MANIFEST.MF");
                                Files.createDirectories(originalManifest.getParent());
                                try (OutputStream out = Files.newOutputStream(originalManifest)) {
                                        sourceManifest.write(out);
@@ -998,12 +1053,13 @@ public class Repackage {
                                arch = libRelativePath.getName(1).toString();
                        }
 
-                       if (!embed) {
-                               // copy entries
-                               JarEntry entry;
-                               entries: while ((entry = jarIn.getNextJarEntry()) != null) {
-                                       if (entry.isDirectory())
-                                               continue entries;
+//                     if (!embed) {
+                       // copy entries
+                       JarEntry entry;
+                       entries: while ((entry = jarIn.getNextJarEntry()) != null) {
+                               if (entry.isDirectory())
+                                       continue entries;
+                               if (!doNotModify) {
                                        if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".DSA")
                                                        || entry.getName().endsWith(".SF")) {
                                                origin.deleted.add("cryptographic signatures");
@@ -1027,43 +1083,44 @@ public class Repackage {
                                                                .add("file system providers (META-INF/services/java.nio.file.spi.FileSystemProvider)");
                                                continue entries;
                                        }
-                                       if (entry.getName().startsWith("OSGI-OPT/src/")) { // skip embedded sources
-                                               origin.deleted.add("embedded sources");
-                                               continue entries;
-                                       }
-                                       Path target = bundleDir.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 = bundleDir.getParent();
-                                               boolean copyDll = false;
-                                               Path targetDll = categoryDir.resolve(bundleDir.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 {
+                               }
+                               if (entry.getName().startsWith("OSGI-OPT/src/")) { // skip embedded sources
+                                       origin.deleted.add("embedded sources");
+                                       continue entries;
+                               }
+                               Path target = bundleDir.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 = bundleDir.getParent();
+                                       boolean copyDll = false;
+                                       Path targetDll = categoryDir.resolve(bundleDir.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;
                                                }
-                                               if (copyDll) {
-                                                       Files.createDirectories(targetDll.getParent());
-                                                       if (Files.exists(targetDll))
-                                                               Files.delete(targetDll);
-                                                       Files.copy(target, targetDll);
-                                               }
-                                               Files.delete(target);
-                                               origin.deleted.add(bundleDir.relativize(target).toString());
+                                               targetDll = categoryDir.resolve(target.getFileName());
+                                       } else {
+                                               copyDll = true;
                                        }
-                                       logger.log(TRACE, () -> "Copied " + target);
+                                       if (copyDll) {
+                                               Files.createDirectories(targetDll.getParent());
+                                               if (Files.exists(targetDll))
+                                                       Files.delete(targetDll);
+                                               Files.copy(target, targetDll);
+                                       }
+                                       Files.delete(target);
+                                       origin.deleted.add(bundleDir.relativize(target).toString());
                                }
+                               logger.log(TRACE, () -> "Copied " + target);
+//                             }
                        }
                }
 
@@ -1076,10 +1133,10 @@ public class Repackage {
                                        entries.get(BUNDLE_SYMBOLICNAME.toString()) + ";singleton:=true");
                }
 
-               if (embed) {// copy embedded jar
-                       Files.copy(file, bundleDir.resolve(file.getFileName()));
-                       entries.put(ManifestHeader.BUNDLE_CLASSPATH.toString(), file.getFileName().toString());
-               }
+//             if (embed) {// copy embedded jar
+//                     Files.copy(file, bundleDir.resolve(file.getFileName()));
+//                     entries.put(ManifestHeader.BUNDLE_CLASSPATH.toString(), file.getFileName().toString());
+//             }
 
                // Final MANIFEST decisions
                // We also check the original OSGi metadata and compare with our changes
@@ -1107,6 +1164,9 @@ public class Repackage {
                        if (wasDifferent && !keepPrevious) {
                                if (IMPORT_PACKAGE.toString().equals(key) || EXPORT_PACKAGE.toString().equals(key))
                                        logger.log(TRACE, () -> file.getFileName() + ": " + key + " was modified");
+                               else if (BUNDLE_SYMBOLICNAME.toString().equals(key) || AUTOMATIC_MODULE_NAME.toString().equals(key))
+                                       logger.log(DEBUG,
+                                                       file.getFileName() + ": " + key + " was " + previousValue + ", overridden with " + value);
                                else
                                        logger.log(WARNING,
                                                        file.getFileName() + ": " + key + " was " + previousValue + ", overridden with " + value);
@@ -1325,21 +1385,21 @@ public class Repackage {
                }
                deleteDirectory(bundleDir);
 
-               if (sourceBundles)
-                       createSourceJar(bundleDir, manifest);
+               if (separateSources)
+                       createSourceJar(bundleDir, manifest, null);
 
                return jarPath;
        }
 
        /** Package sources separately, in the Eclipse-SourceBundle format. */
-       void createSourceJar(Path bundleDir, Manifest manifest) throws IOException {
+       void createSourceJar(Path bundleDir, Manifest manifest, Properties props) throws IOException {
+               boolean unmodified = props != null;
                Path bundleCategoryDir = bundleDir.getParent();
                Path sourceDir = bundleCategoryDir.resolve(bundleDir.toString() + ".src");
                if (!Files.exists(sourceDir)) {
                        logger.log(WARNING, sourceDir + " does not exist, skipping...");
                        return;
                }
-               createReadMe(sourceDir, manifest);
 
                Path relPath = a2Base.relativize(bundleCategoryDir);
                Path srcCategoryDir = a2SrcBase.resolve(relPath);
@@ -1351,14 +1411,16 @@ public class Repackage {
                bundleSymbolicName = bundleSymbolicName.split(";")[0];
                Manifest srcManifest = new Manifest();
                srcManifest.getMainAttributes().put(MANIFEST_VERSION, "1.0");
-               srcManifest.getMainAttributes().putValue(BUNDLE_SYMBOLICNAME.toString(), bundleSymbolicName + ".src");
-               srcManifest.getMainAttributes().putValue(BUNDLE_VERSION.toString(),
-                               manifest.getMainAttributes().getValue(BUNDLE_VERSION.toString()).toString());
-               srcManifest.getMainAttributes().putValue(ECLIPSE_SOURCE_BUNDLE.toString(), bundleSymbolicName + ";version=\""
-                               + manifest.getMainAttributes().getValue(BUNDLE_VERSION.toString()) + "\"");
-
+               BUNDLE_SYMBOLICNAME.put(srcManifest, bundleSymbolicName + ".src");
+               BUNDLE_VERSION.put(srcManifest, BUNDLE_VERSION.get(manifest));
+               ECLIPSE_SOURCE_BUNDLE.put(srcManifest,
+                               bundleSymbolicName + ";version=\"" + BUNDLE_VERSION.get(manifest) + "\"");
+
+               // metadata
+               createReadMe(sourceDir, unmodified ? props : manifest);
+               // create jar
                try (JarOutputStream srcJarOut = new JarOutputStream(Files.newOutputStream(srcJarP), srcManifest)) {
-                       srcJarOut.setLevel(Deflater.BEST_COMPRESSION);
+                       // srcJarOut.setLevel(Deflater.BEST_COMPRESSION);
                        Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
 
                                @Override
@@ -1381,10 +1443,10 @@ public class Repackage {
         * Generate a readme clarifying and prominently notifying of the repackaging and
         * modifications.
         */
-       void createReadMe(Path jarDir, Manifest manifest) throws IOException {
+       void createReadMe(Path jarDir, Object mapping) throws IOException {
                // write repackaged README
                try (BufferedWriter writer = Files.newBufferedWriter(jarDir.resolve(README_REPACKAGED))) {
-                       boolean merged = manifest.getMainAttributes().getValue(ARGEO_ORIGIN_M2_MERGE.toString()) != null;
+                       boolean merged = ARGEO_ORIGIN_M2_MERGE.get(mapping) != null;
                        if (merged)
                                writer.append("This component is a merging of third party components"
                                                + " in order to comply with A2 packaging standards.\n");
@@ -1393,7 +1455,7 @@ public class Repackage {
                                                + " in order to comply with A2 packaging standards.\n");
 
                        // license
-                       String spdxLicenseId = manifest.getMainAttributes().getValue(SPDX_LICENSE_IDENTIFIER.toString());
+                       String spdxLicenseId = SPDX_LICENSE_IDENTIFIER.get(mapping);
                        if (spdxLicenseId == null)
                                throw new IllegalStateException("An SPDX license id must have beend defined at this stage.");
                        writer.append("\nIt is redistributed under the following license:\n\n");
@@ -1410,39 +1472,42 @@ public class Repackage {
                                        writer.append("which is available here: https://spdx.org/licenses/" + spdxLicenseId + "\n");
                                }
                        } else {
-                               String url = manifest.getMainAttributes().getValue(BUNDLE_LICENSE.toString());
+                               String url = BUNDLE_LICENSE.get(mapping);
                                if (url != null) {
                                        writer.write("which is available here: " + url + "\n");
                                } else {
-                                       logger.log(ERROR, "No licne URL for " + jarDir);
+                                       logger.log(ERROR, "No licence URL for " + jarDir);
                                }
                        }
-                       writer.write("\n");
 
                        // origin
-                       String m2Repo = manifest.getMainAttributes().getValue(ARGEO_ORIGIN_M2_REPO.toString());
-                       String originDesc = manifest.getMainAttributes().getValue(ARGEO_ORIGIN_M2.toString());
+                       String originDesc = ARGEO_ORIGIN_URI.get(mapping);
                        if (originDesc != null)
-                               writer.append("The original component has M2 coordinates " + originDesc.replace(',', '\n') + "\n"
-                                               + (m2Repo != null ? "\nin M2 repository" + m2Repo + "\n" : "") + "\n");
+                               writer.append("\nThe original component comes from " + originDesc + ".\n");
                        else {
-                               originDesc = manifest.getMainAttributes().getValue(ARGEO_ORIGIN_URI.toString());
+                               String m2Repo = ARGEO_ORIGIN_M2_REPO.get(mapping);
+                               originDesc = ARGEO_ORIGIN_M2.get(mapping);
                                if (originDesc != null)
-                                       writer.append("The original component comes from " + originDesc + ".\n");
+                                       writer.append("\nThe original component has M2 coordinates:\n" + originDesc.replace(',', '\n')
+                                                       + "\n" + (m2Repo != null ? "\nin M2 repository " + m2Repo + "\n" : ""));
                                else
                                        logger.log(ERROR, "Cannot find origin information in " + jarDir);
                        }
+                       String originSources = ARGEO_ORIGIN_SOURCES_URI.get(mapping);
+                       if (originSources != null)
+                               writer.append("\nThe original sources come from " + originSources + ".\n");
 
-                       writer.append("\nA detailed list of changes is available under " + CHANGES + ".\n");
-                       if (!jarDir.getFileName().endsWith(".src")) {// binary archive
-                               if (sourceBundles)
+                       if (Files.exists(jarDir.resolve(CHANGES)))
+                               writer.append("\nA detailed list of changes is available under " + CHANGES + ".\n");
+
+                       if (!jarDir.getFileName().toString().endsWith(".src")) {// binary archive
+                               if (separateSources)
                                        writer.append("Corresponding sources are available in the related archive named "
                                                        + jarDir.toString() + ".src.jar.\n");
                                else
                                        writer.append("Corresponding sources are available under OSGI-OPT/src.\n");
                        }
                }
-
        }
 
        /**
@@ -1461,6 +1526,8 @@ public class Repackage {
 
                /** Append changes to the A2-ORIGIN/changes file. */
                void appendChanges(Path baseDirectory) throws IOException {
+                       if (modified.isEmpty() && deleted.isEmpty() && added.isEmpty() && moved.isEmpty())
+                               return; // no changes
                        Path changesFile = baseDirectory.resolve(CHANGES);
                        Files.createDirectories(changesFile.getParent());
                        try (BufferedWriter writer = Files.newBufferedWriter(changesFile, APPEND, CREATE)) {
@@ -1553,7 +1620,8 @@ class M2ConventionsUtils {
 
        /** Absolute path to the directories where the files will be stored */
        static String artifactParentPath(String artifactBasePath, M2Artifact artifact) {
-               return artifactBasePath + (artifactBasePath.endsWith("/") ? "" : "/") + artifactParentPath(artifact);
+               return artifactBasePath + (artifactBasePath.endsWith("/") || artifactBasePath.equals("") ? "" : "/")
+                               + artifactParentPath(artifact);
        }
 
        /** Relative path to the directories where the files will be stored */