diff --git a/pom.xml b/pom.xml
index a90ea182..b9a58d05 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,16 @@
asm-commons
${asmVersion}
+
+ org.ow2.asm
+ asm-tree
+ ${asmVersion}
+
+
+ org.ow2.asm
+ asm-analysis
+ ${asmVersion}
+
org.jdom
jdom2
@@ -257,6 +267,7 @@
src/it/projects/rerun-with-reloc/src/main/resources/some-ordinary-resource.txt
src/it/projects/rerun-without-reloc/src/main/resources/some-ordinary-resource.txt
src/it/projects/MSHADE-182/src/main/resources/META-INF/services/relocateme.Service
+ src/it/projects/module-info-merge/automatic/src/main/resources/META-INF/services/*
src/it/projects/MSHADE-390-sisu-index/**
diff --git a/src/it/projects/MSHADE-215/app/pom.xml b/src/it/projects/MSHADE-215/app/pom.xml
new file mode 100644
index 00000000..64a8c467
--- /dev/null
+++ b/src/it/projects/MSHADE-215/app/pom.xml
@@ -0,0 +1,68 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.maven.its.shade.mshade215
+ parent
+ 1.0
+
+
+ app
+
+
+
+ ${project.groupId}
+ base
+ ${project.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ @project.version@
+
+
+ package
+
+ shade
+
+
+ false
+
+
+
+ MSHADE-215
+
+
+
+
+
+
+
+
+
+
diff --git a/src/it/projects/MSHADE-215/base/pom.xml b/src/it/projects/MSHADE-215/base/pom.xml
new file mode 100644
index 00000000..4ab5a90f
--- /dev/null
+++ b/src/it/projects/MSHADE-215/base/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.maven.its.shade.mshade215
+ parent
+ 1.0
+
+
+ base
+
diff --git a/src/it/projects/MSHADE-215/base/src/main/java/org/apache/maven/its/shade/mshade215/Base.java b/src/it/projects/MSHADE-215/base/src/main/java/org/apache/maven/its/shade/mshade215/Base.java
new file mode 100644
index 00000000..a89d7113
--- /dev/null
+++ b/src/it/projects/MSHADE-215/base/src/main/java/org/apache/maven/its/shade/mshade215/Base.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.maven.its.shade.mshade215;
+
+public class Base {}
diff --git a/src/it/projects/MSHADE-215/invoker.properties b/src/it/projects/MSHADE-215/invoker.properties
new file mode 100644
index 00000000..95122c94
--- /dev/null
+++ b/src/it/projects/MSHADE-215/invoker.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Running test as a second lifecycle task makes Maven expose the reactor dependency's output directory.
+invoker.goals = clean package test
diff --git a/src/it/projects/MSHADE-215/pom.xml b/src/it/projects/MSHADE-215/pom.xml
new file mode 100644
index 00000000..24114d6a
--- /dev/null
+++ b/src/it/projects/MSHADE-215/pom.xml
@@ -0,0 +1,50 @@
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.shade.mshade215
+ parent
+ 1.0
+ pom
+
+
+ base
+ app
+
+
+
+ 8
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ @version.maven-compiler-plugin@
+
+
+
+
+
diff --git a/src/it/projects/MSHADE-215/verify.groovy b/src/it/projects/MSHADE-215/verify.groovy
new file mode 100644
index 00000000..0b3ea6d4
--- /dev/null
+++ b/src/it/projects/MSHADE-215/verify.groovy
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.jar.JarFile
+
+def shadedJar = new File(basedir, 'app/target/app-1.0.jar')
+assert shadedJar.isFile()
+
+new JarFile(shadedJar).withCloseable { jar ->
+ assert jar.getEntry('org/apache/maven/its/shade/mshade215/Base.class') != null
+ assert jar.manifest.mainAttributes.getValue('Test-Case') == 'MSHADE-215'
+}
diff --git a/src/it/projects/module-info-merge/app/pom.xml b/src/it/projects/module-info-merge/app/pom.xml
new file mode 100644
index 00000000..9ef29860
--- /dev/null
+++ b/src/it/projects/module-info-merge/app/pom.xml
@@ -0,0 +1,107 @@
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.shade.modules
+ module-info-merge-parent
+ 1.0
+
+ app
+
+
+
+ ${project.groupId}
+ embedded
+
+
+ ${project.groupId}
+ automatic
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ @project.version@
+
+
+ package
+
+ shade
+
+
+ false
+ merge
+
+ shaded.app.module
+ merge
+
+
+ external.module
+ true
+
+
+ optional.module
+ true
+ true
+
+
+
+
+ embedded.internal
+
+
+
+
+ embedded.internal
+
+
+
+
+
+ org.apache.maven.its.shade.modules:external
+
+
+
+
+ automatic.library
+ shaded.automatic
+
+
+ embedded.api
+ shaded.library
+
+
+ embedded.internal
+ shaded.internal
+
+
+
+
+
+
+
+
+
diff --git a/src/it/projects/module-info-merge/app/src/main/java/app/api/Application.java b/src/it/projects/module-info-merge/app/src/main/java/app/api/Application.java
new file mode 100644
index 00000000..84123caa
--- /dev/null
+++ b/src/it/projects/module-info-merge/app/src/main/java/app/api/Application.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package app.api;
+
+import automatic.library.AutomaticLibrary;
+
+public final class Application {
+ private Application() {}
+
+ public static String loadAutomaticService() {
+ return AutomaticLibrary.loadService();
+ }
+}
diff --git a/src/it/projects/module-info-merge/app/src/main/java/module-info.java b/src/it/projects/module-info-merge/app/src/main/java/module-info.java
new file mode 100644
index 00000000..9bd3f723
--- /dev/null
+++ b/src/it/projects/module-info-merge/app/src/main/java/module-info.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module app.module {
+ requires automatic.module;
+ requires embedded.module;
+
+ exports app.api;
+}
diff --git a/src/it/projects/module-info-merge/automatic/pom.xml b/src/it/projects/module-info-merge/automatic/pom.xml
new file mode 100644
index 00000000..0a5a20ba
--- /dev/null
+++ b/src/it/projects/module-info-merge/automatic/pom.xml
@@ -0,0 +1,54 @@
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.shade.modules
+ module-info-merge-parent
+ 1.0
+
+ automatic
+
+
+
+ ${project.groupId}
+ external
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @version.maven-jar-plugin@
+
+
+
+ automatic.module
+
+
+
+
+
+
+
diff --git a/src/it/projects/module-info-merge/automatic/src/main/java/automatic/library/AutomaticLibrary.java b/src/it/projects/module-info-merge/automatic/src/main/java/automatic/library/AutomaticLibrary.java
new file mode 100644
index 00000000..249f893c
--- /dev/null
+++ b/src/it/projects/module-info-merge/automatic/src/main/java/automatic/library/AutomaticLibrary.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package automatic.library;
+
+import java.sql.Driver;
+import java.util.ServiceLoader;
+
+import external.api.AutomaticService;
+import external.api.Service;
+
+public final class AutomaticLibrary {
+ private static final Class>[] REQUIREMENTS = {Driver.class, Service.class};
+
+ private AutomaticLibrary() {}
+
+ public static String loadService() {
+ return ServiceLoader.load(AutomaticService.class).findFirst().orElseThrow().name();
+ }
+}
diff --git a/src/it/projects/module-info-merge/automatic/src/main/java/automatic/library/AutomaticProvider.java b/src/it/projects/module-info-merge/automatic/src/main/java/automatic/library/AutomaticProvider.java
new file mode 100644
index 00000000..49626de1
--- /dev/null
+++ b/src/it/projects/module-info-merge/automatic/src/main/java/automatic/library/AutomaticProvider.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package automatic.library;
+
+import external.api.AutomaticService;
+
+public final class AutomaticProvider implements AutomaticService {
+ @Override
+ public String name() {
+ return "automatic";
+ }
+}
diff --git a/src/it/projects/module-info-merge/automatic/src/main/resources/META-INF/services/external.api.AutomaticService b/src/it/projects/module-info-merge/automatic/src/main/resources/META-INF/services/external.api.AutomaticService
new file mode 100644
index 00000000..f3e7c586
--- /dev/null
+++ b/src/it/projects/module-info-merge/automatic/src/main/resources/META-INF/services/external.api.AutomaticService
@@ -0,0 +1 @@
+automatic.library.AutomaticProvider
diff --git a/src/it/projects/module-info-merge/embedded/pom.xml b/src/it/projects/module-info-merge/embedded/pom.xml
new file mode 100644
index 00000000..c9f479f1
--- /dev/null
+++ b/src/it/projects/module-info-merge/embedded/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.shade.modules
+ module-info-merge-parent
+ 1.0
+
+ embedded
+
+
+
+ ${project.groupId}
+ external
+
+
+
diff --git a/src/it/projects/module-info-merge/embedded/src/main/java/embedded/api/Library.java b/src/it/projects/module-info-merge/embedded/src/main/java/embedded/api/Library.java
new file mode 100644
index 00000000..1fe10f9f
--- /dev/null
+++ b/src/it/projects/module-info-merge/embedded/src/main/java/embedded/api/Library.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package embedded.api;
+
+public final class Library {
+ private Library() {}
+}
diff --git a/src/it/projects/module-info-merge/embedded/src/main/java/embedded/internal/Provider.java b/src/it/projects/module-info-merge/embedded/src/main/java/embedded/internal/Provider.java
new file mode 100644
index 00000000..1922c460
--- /dev/null
+++ b/src/it/projects/module-info-merge/embedded/src/main/java/embedded/internal/Provider.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package embedded.internal;
+
+import external.api.Service;
+
+public final class Provider implements Service {
+ @Override
+ public String name() {
+ return "embedded";
+ }
+}
diff --git a/src/it/projects/module-info-merge/embedded/src/main/java/module-info.java b/src/it/projects/module-info-merge/embedded/src/main/java/module-info.java
new file mode 100644
index 00000000..10fc107f
--- /dev/null
+++ b/src/it/projects/module-info-merge/embedded/src/main/java/module-info.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module embedded.module {
+ requires transitive external.module;
+
+ exports embedded.api;
+ uses external.api.Service;
+ provides external.api.Service with embedded.internal.Provider;
+}
diff --git a/src/it/projects/module-info-merge/external/pom.xml b/src/it/projects/module-info-merge/external/pom.xml
new file mode 100644
index 00000000..57629f36
--- /dev/null
+++ b/src/it/projects/module-info-merge/external/pom.xml
@@ -0,0 +1,30 @@
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.shade.modules
+ module-info-merge-parent
+ 1.0
+
+ external
+
diff --git a/src/it/projects/module-info-merge/external/src/main/java/external/api/AutomaticService.java b/src/it/projects/module-info-merge/external/src/main/java/external/api/AutomaticService.java
new file mode 100644
index 00000000..9a16b6e9
--- /dev/null
+++ b/src/it/projects/module-info-merge/external/src/main/java/external/api/AutomaticService.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package external.api;
+
+public interface AutomaticService {
+ String name();
+}
diff --git a/src/it/projects/module-info-merge/external/src/main/java/external/api/Service.java b/src/it/projects/module-info-merge/external/src/main/java/external/api/Service.java
new file mode 100644
index 00000000..2d3ad3cf
--- /dev/null
+++ b/src/it/projects/module-info-merge/external/src/main/java/external/api/Service.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package external.api;
+
+public interface Service {
+ String name();
+}
diff --git a/src/it/projects/module-info-merge/external/src/main/java/module-info.java b/src/it/projects/module-info-merge/external/src/main/java/module-info.java
new file mode 100644
index 00000000..b6a0e592
--- /dev/null
+++ b/src/it/projects/module-info-merge/external/src/main/java/module-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module external.module {
+ exports external.api;
+}
diff --git a/src/it/projects/module-info-merge/invoker.properties b/src/it/projects/module-info-merge/invoker.properties
new file mode 100644
index 00000000..cabde7db
--- /dev/null
+++ b/src/it/projects/module-info-merge/invoker.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+invoker.goals = clean package
+invoker.java.version = 11+
diff --git a/src/it/projects/module-info-merge/pom.xml b/src/it/projects/module-info-merge/pom.xml
new file mode 100644
index 00000000..6e985d46
--- /dev/null
+++ b/src/it/projects/module-info-merge/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.shade.modules
+ module-info-merge-parent
+ 1.0
+ pom
+
+
+ external
+ embedded
+ automatic
+ app
+
+
+
+ 11
+
+
+
+
+
+ ${project.groupId}
+ external
+ ${project.version}
+
+
+ ${project.groupId}
+ embedded
+ ${project.version}
+
+
+ ${project.groupId}
+ automatic
+ ${project.version}
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ @version.maven-compiler-plugin@
+
+
+
+
+
diff --git a/src/it/projects/module-info-merge/verify.groovy b/src/it/projects/module-info-merge/verify.groovy
new file mode 100644
index 00000000..0b97f62d
--- /dev/null
+++ b/src/it/projects/module-info-merge/verify.groovy
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.lang.module.ModuleDescriptor
+import java.lang.module.ModuleFinder
+import java.util.jar.JarFile
+
+def shaded = new File( basedir, "app/target/app-1.0.jar" )
+assert shaded.isFile()
+
+def shadedFinder = ModuleFinder.of( shaded.toPath() )
+assert !shadedFinder.find( "app.module" ).isPresent()
+def reference = shadedFinder.find( "shaded.app.module" ).orElseThrow {
+ new AssertionError( "The shaded JAR is not the shaded.app.module module" )
+}
+def descriptor = reference.descriptor()
+
+assert descriptor.name() == "shaded.app.module"
+assert descriptor.requires()*.name().contains( "external.module" )
+assert descriptor.requires()*.name().contains( "java.sql" )
+assert !descriptor.requires()*.name().contains( "automatic.module" )
+assert !descriptor.requires()*.name().contains( "embedded.module" )
+assert descriptor.requires().find { it.name() == "external.module" }
+ .modifiers().contains( ModuleDescriptor.Requires.Modifier.TRANSITIVE )
+def optional = descriptor.requires().find { it.name() == "optional.module" }
+assert optional != null
+assert optional.modifiers().contains( ModuleDescriptor.Requires.Modifier.STATIC )
+assert optional.modifiers().contains( ModuleDescriptor.Requires.Modifier.TRANSITIVE )
+assert ( descriptor.exports()*.source() as Set ) ==
+ [ "app.api", "shaded.automatic", "shaded.internal", "shaded.library" ] as Set
+assert ( descriptor.opens()*.source() as Set ) ==
+ [ "shaded.automatic", "shaded.internal" ] as Set
+assert descriptor.packages() ==
+ [ "app.api", "shaded.automatic", "shaded.internal", "shaded.library" ] as Set
+assert descriptor.uses() ==
+ [ "external.api.AutomaticService", "external.api.Service" ] as Set
+
+def service = descriptor.provides().find { it.service() == "external.api.Service" }
+assert service != null
+assert service.providers() == [ "shaded.internal.Provider" ]
+def automaticService =
+ descriptor.provides().find { it.service() == "external.api.AutomaticService" }
+assert automaticService != null
+assert automaticService.providers() == [ "shaded.automatic.AutomaticProvider" ]
+
+def external = new File( basedir, "external/target/external-1.0.jar" )
+assert external.isFile()
+
+def finder = ModuleFinder.of( shaded.toPath(), external.toPath() )
+def configuration = ModuleLayer.boot().configuration()
+ .resolve( finder, ModuleFinder.of(), [ "shaded.app.module" ] as Set )
+def layer = ModuleLayer.boot().defineModulesWithOneLoader(
+ configuration, ClassLoader.getSystemClassLoader() )
+def library = layer.findLoader( "shaded.app.module" ).loadClass( "shaded.library.Library" )
+assert library.module.name == "shaded.app.module"
+def automaticLibrary =
+ Class.forName( "shaded.automatic.AutomaticLibrary", true, layer.findLoader( "shaded.app.module" ) )
+assert automaticLibrary.module.name == "shaded.app.module"
+def application = layer.findLoader( "shaded.app.module" ).loadClass( "app.api.Application" )
+def thread = Thread.currentThread()
+def contextLoader = thread.contextClassLoader
+try
+{
+ thread.contextClassLoader = layer.findLoader( "shaded.app.module" )
+ assert application.getMethod( "loadAutomaticService" ).invoke( null ) == "automatic"
+}
+finally
+{
+ thread.contextClassLoader = contextLoader
+}
+
+def jar = new JarFile( shaded )
+try
+{
+ assert jar.getJarEntry( "module-info.class" ) == null
+ assert jar.getJarEntry( "META-INF/versions/11/module-info.class" ) != null
+ assert jar.getJarEntry( "META-INF/services/external.api.AutomaticService" ) != null
+ assert jar.getJarEntry( "shaded/automatic/AutomaticLibrary.class" ) != null
+ assert jar.getJarEntry( "shaded/automatic/AutomaticProvider.class" ) != null
+ assert jar.getJarEntry( "shaded/library/Library.class" ) != null
+ assert jar.getJarEntry( "shaded/internal/Provider.class" ) != null
+ assert jar.getJarEntry( "embedded/api/Library.class" ) == null
+ assert jar.getJarEntry( "embedded/internal/Provider.class" ) == null
+ assert jar.getJarEntry( "automatic/library/AutomaticLibrary.class" ) == null
+ assert jar.manifest.mainAttributes.getValue( "Automatic-Module-Name" ) == "shaded.app.module"
+ assert jar.manifest.mainAttributes.getValue( "Multi-Release" ) == "true"
+}
+finally
+{
+ jar.close()
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/AutomaticModuleAnalyzer.java b/src/main/java/org/apache/maven/plugins/shade/AutomaticModuleAnalyzer.java
new file mode 100644
index 00000000..085d9d55
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/AutomaticModuleAnalyzer.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.shade;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.InvokeDynamicInsnNode;
+import org.objectweb.asm.tree.LdcInsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.analysis.Analyzer;
+import org.objectweb.asm.tree.analysis.AnalyzerException;
+import org.objectweb.asm.tree.analysis.BasicInterpreter;
+import org.objectweb.asm.tree.analysis.BasicValue;
+import org.objectweb.asm.tree.analysis.Frame;
+
+/**
+ * Extracts bytecode-visible dependencies and service uses from classes retained from automatic modules.
+ */
+final class AutomaticModuleAnalyzer {
+ private static final String SERVICE_LOADER = "java/util/ServiceLoader";
+
+ private AutomaticModuleAnalyzer() {}
+
+ static Analysis analyze(byte[] bytecode) {
+ Set references = new TreeSet<>();
+ ClassVisitor collector = new ClassRemapper(new ClassWriter(0), new Remapper(Opcodes.ASM9) {
+ @Override
+ public String map(String internalName) {
+ if (internalName != null) {
+ references.add(internalName);
+ }
+ return internalName;
+ }
+ });
+ ClassReader reader = new ClassReader(bytecode);
+ reader.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+
+ Set serviceUses = new TreeSet<>();
+ SortedSet unresolvedServiceUses = new TreeSet<>();
+ ClassNode classNode = new ClassNode(Opcodes.ASM9);
+ reader.accept(classNode, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ for (MethodNode method : classNode.methods) {
+ analyzeServiceUses(classNode.name, method, serviceUses, unresolvedServiceUses);
+ }
+ return new Analysis(reader.readShort(6), references, serviceUses, unresolvedServiceUses);
+ }
+
+ private static void analyzeServiceUses(
+ String className, MethodNode method, Set serviceUses, SortedSet unresolvedServiceUses) {
+ boolean hasDirectInvocation = false;
+ for (AbstractInsnNode instruction : method.instructions) {
+ if (instruction instanceof MethodInsnNode
+ && SERVICE_LOADER.equals(((MethodInsnNode) instruction).owner)
+ && ((MethodInsnNode) instruction).name.startsWith("load")) {
+ hasDirectInvocation = true;
+ } else if (instruction instanceof InvokeDynamicInsnNode) {
+ InvokeDynamicInsnNode dynamic = (InvokeDynamicInsnNode) instruction;
+ if (isServiceLoaderHandle(dynamic.bsm) || containsServiceLoaderHandle(dynamic.bsmArgs)) {
+ unresolvedServiceUses.add(
+ "ServiceLoader method handle in " + toClassName(className) + '.' + method.name);
+ }
+ } else if (instruction instanceof LdcInsnNode && isServiceLoaderHandle(((LdcInsnNode) instruction).cst)) {
+ unresolvedServiceUses.add(
+ "ServiceLoader method handle in " + toClassName(className) + '.' + method.name);
+ }
+ }
+ if (!hasDirectInvocation) {
+ return;
+ }
+
+ Frame[] frames;
+ try {
+ frames = new Analyzer<>(new ClassLiteralInterpreter()).analyze(className, method);
+ } catch (AnalyzerException e) {
+ unresolvedServiceUses.add(
+ "ServiceLoader call in " + toClassName(className) + '.' + method.name + method.desc);
+ return;
+ }
+
+ for (int index = 0; index < method.instructions.size(); index++) {
+ AbstractInsnNode instruction = method.instructions.get(index);
+ if (!(instruction instanceof MethodInsnNode)) {
+ continue;
+ }
+ MethodInsnNode invocation = (MethodInsnNode) instruction;
+ int serviceArgument = serviceArgumentIndex(invocation);
+ if (serviceArgument < 0) {
+ if (SERVICE_LOADER.equals(invocation.owner) && invocation.name.startsWith("load")) {
+ unresolvedServiceUses.add("ServiceLoader." + invocation.name + invocation.desc + " in "
+ + toClassName(className) + '.' + method.name);
+ }
+ continue;
+ }
+
+ Frame frame = frames[index];
+ Type[] arguments = Type.getArgumentTypes(invocation.desc);
+ int stackIndex = frame == null ? -1 : frame.getStackSize() - arguments.length + serviceArgument;
+ BasicValue value = stackIndex < 0 ? null : frame.getStack(stackIndex);
+ if (value instanceof ClassLiteralValue) {
+ serviceUses.addAll(((ClassLiteralValue) value).classNames);
+ } else {
+ unresolvedServiceUses.add("ServiceLoader." + invocation.name + invocation.desc + " in "
+ + toClassName(className) + '.' + method.name);
+ }
+ }
+ }
+
+ private static int serviceArgumentIndex(MethodInsnNode invocation) {
+ if (!SERVICE_LOADER.equals(invocation.owner) || invocation.getOpcode() != Opcodes.INVOKESTATIC) {
+ return -1;
+ }
+ if ("loadInstalled".equals(invocation.name)
+ && "(Ljava/lang/Class;)Ljava/util/ServiceLoader;".equals(invocation.desc)) {
+ return 0;
+ }
+ if (!"load".equals(invocation.name)) {
+ return -1;
+ }
+ if ("(Ljava/lang/Class;)Ljava/util/ServiceLoader;".equals(invocation.desc)
+ || "(Ljava/lang/Class;Ljava/lang/ClassLoader;)Ljava/util/ServiceLoader;".equals(invocation.desc)) {
+ return 0;
+ }
+ if ("(Ljava/lang/ModuleLayer;Ljava/lang/Class;)Ljava/util/ServiceLoader;".equals(invocation.desc)) {
+ return 1;
+ }
+ return -1;
+ }
+
+ private static boolean containsServiceLoaderHandle(Object[] values) {
+ for (Object value : values) {
+ if (isServiceLoaderHandle(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isServiceLoaderHandle(Object value) {
+ if (!(value instanceof Handle)) {
+ return false;
+ }
+ Handle handle = (Handle) value;
+ return SERVICE_LOADER.equals(handle.getOwner()) && handle.getName().startsWith("load");
+ }
+
+ private static String toClassName(String internalName) {
+ return internalName.replace('/', '.');
+ }
+
+ static final class Analysis {
+ final int classVersion;
+ final Set references;
+ final Set serviceUses;
+ final SortedSet unresolvedServiceUses;
+
+ private Analysis(
+ int classVersion,
+ Set references,
+ Set serviceUses,
+ SortedSet unresolvedServiceUses) {
+ this.classVersion = classVersion;
+ this.references = references;
+ this.serviceUses = serviceUses;
+ this.unresolvedServiceUses = unresolvedServiceUses;
+ }
+ }
+
+ private static final class ClassLiteralInterpreter extends BasicInterpreter {
+ private ClassLiteralInterpreter() {
+ super(Opcodes.ASM9);
+ }
+
+ @Override
+ public BasicValue newOperation(AbstractInsnNode instruction) throws AnalyzerException {
+ if (instruction.getOpcode() == Opcodes.LDC) {
+ Object value = ((LdcInsnNode) instruction).cst;
+ if (value instanceof Type && ((Type) value).getSort() == Type.OBJECT) {
+ return new ClassLiteralValue(Collections.singleton(((Type) value).getInternalName()));
+ }
+ }
+ return super.newOperation(instruction);
+ }
+
+ @Override
+ public BasicValue merge(BasicValue first, BasicValue second) {
+ if (first instanceof ClassLiteralValue && second instanceof ClassLiteralValue) {
+ SortedSet classNames = new TreeSet<>(((ClassLiteralValue) first).classNames);
+ classNames.addAll(((ClassLiteralValue) second).classNames);
+ return classNames.equals(((ClassLiteralValue) first).classNames)
+ ? first
+ : new ClassLiteralValue(classNames);
+ }
+ return super.merge(first, second);
+ }
+ }
+
+ private static final class ClassLiteralValue extends BasicValue {
+ private final SortedSet classNames;
+
+ private ClassLiteralValue(Collection classNames) {
+ super(Type.getObjectType("java/lang/Class"));
+ this.classNames = Collections.unmodifiableSortedSet(new TreeSet<>(classNames));
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return object instanceof ClassLiteralValue && classNames.equals(((ClassLiteralValue) object).classNames);
+ }
+
+ @Override
+ public int hashCode() {
+ return classNames.hashCode();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
index d4f99674..3c03f46c 100644
--- a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
+++ b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
@@ -52,6 +52,7 @@
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
@@ -64,6 +65,7 @@
import org.apache.maven.plugins.shade.resource.ManifestResourceTransformer;
import org.apache.maven.plugins.shade.resource.ReproducibleResourceTransformer;
import org.apache.maven.plugins.shade.resource.ResourceTransformer;
+import org.apache.maven.plugins.shade.resource.ServicesResourceTransformer;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.io.CachingOutputStream;
import org.objectweb.asm.ClassReader;
@@ -82,6 +84,9 @@
public class DefaultShader implements Shader {
private static final int BUFFER_SIZE = 32 * 1024;
+ private static final Pattern VERSIONED_CLASS_ENTRY =
+ Pattern.compile("^(META-INF/versions/[1-9][0-9]*/)(.+[.]class)$");
+
private final Logger logger;
public DefaultShader() {
@@ -135,12 +140,54 @@ public void shade(ShadeRequest shadeRequest) throws IOException, MojoExecutionEx
Set resources = new HashSet<>();
ManifestResourceTransformer manifestTransformer = null;
+ String forceAutomaticModuleName = null;
+ boolean forceMultiRelease = false;
+ ServicesResourceTransformer servicesTransformer = null;
List transformers = new ArrayList<>(shadeRequest.getResourceTransformers());
for (Iterator it = transformers.iterator(); it.hasNext(); ) {
ResourceTransformer transformer = it.next();
if (transformer instanceof ManifestResourceTransformer) {
manifestTransformer = (ManifestResourceTransformer) transformer;
it.remove();
+ } else if (transformer instanceof ServicesResourceTransformer) {
+ servicesTransformer = (ServicesResourceTransformer) transformer;
+ }
+ }
+
+ ModuleInfoProcessor moduleInfoProcessor = null;
+ if (shadeRequest.getModuleInfoMode() == ModuleInfoMode.MERGE) {
+ moduleInfoProcessor = new ModuleInfoProcessor(shadeRequest, logger);
+ if (manifestTransformer != null && manifestTransformer.isMultiReleaseExplicitlyEnabled()) {
+ moduleInfoProcessor.enableMultiReleaseOutput();
+ }
+ if (servicesTransformer == null) {
+ servicesTransformer = new ServicesResourceTransformer();
+ transformers.add(servicesTransformer);
+ }
+ if (moduleInfoProcessor.hasPrimaryModule()) {
+ if (manifestTransformer == null) {
+ manifestTransformer = new ManifestResourceTransformer();
+ }
+ forceAutomaticModuleName = moduleInfoProcessor.getOutputModuleName();
+ }
+ }
+
+ if (moduleInfoProcessor != null) {
+ List multiReleaseInputs = findMultiReleaseInputs(shadeRequest);
+ if (!multiReleaseInputs.isEmpty() || moduleInfoProcessor.requiresMultiReleaseOutput()) {
+ if (manifestTransformer == null) {
+ manifestTransformer = new ManifestResourceTransformer();
+ } else if (manifestTransformer.isMultiReleaseExplicitlyDisabled()) {
+ if (multiReleaseInputs.isEmpty()) {
+ logger.warn("Configured Multi-Release: false is overridden because module descriptor merging "
+ + "produces versioned output.");
+ } else {
+ logger.warn(
+ "Configured Multi-Release: false is overridden because shaded input {} is a multi-release JAR.",
+ multiReleaseInputs.get(0));
+ }
+ }
+ forceMultiRelease = true;
}
}
@@ -151,13 +198,14 @@ public void shade(ShadeRequest shadeRequest) throws IOException, MojoExecutionEx
try (JarOutputStream out =
new JarOutputStream(new BufferedOutputStream(new CachingOutputStream(shadeRequest.getUberJar())))) {
- goThroughAllJarEntriesForManifestTransformer(shadeRequest, resources, manifestTransformer, out);
+ goThroughAllJarEntriesForManifestTransformer(
+ shadeRequest, resources, manifestTransformer, forceMultiRelease, forceAutomaticModuleName, out);
// CHECKSTYLE_OFF: MagicNumber
Map> duplicates = new HashMap<>();
// CHECKSTYLE_ON: MagicNumber
- shadeJars(shadeRequest, resources, transformers, out, duplicates, packageMapper);
+ shadeJars(shadeRequest, resources, transformers, out, duplicates, packageMapper, moduleInfoProcessor);
// CHECKSTYLE_OFF: MagicNumber
Map, HashSet> overlapping = new HashMap<>();
@@ -177,6 +225,10 @@ public void shade(ShadeRequest shadeRequest) throws IOException, MojoExecutionEx
showOverlappingWarning();
}
+ if (moduleInfoProcessor != null) {
+ moduleInfoProcessor.writeDescriptors(out, servicesTransformer);
+ }
+
for (ResourceTransformer transformer : transformers) {
if (transformer.hasTransformedResource()) {
transformer.modifyOutputStream(out);
@@ -189,6 +241,30 @@ public void shade(ShadeRequest shadeRequest) throws IOException, MojoExecutionEx
}
}
+ private List findMultiReleaseInputs(ShadeRequest shadeRequest) throws IOException {
+ List result = new ArrayList<>();
+ for (File jar : shadeRequest.getJars()) {
+ Manifest manifest = null;
+ if (jar.isDirectory()) {
+ File manifestFile = new File(jar, JarFile.MANIFEST_NAME);
+ if (manifestFile.isFile()) {
+ try (InputStream input = Files.newInputStream(manifestFile.toPath())) {
+ manifest = new Manifest(input);
+ }
+ }
+ } else {
+ try (JarFile jarFile = newJarFile(jar)) {
+ manifest = jarFile.getManifest();
+ }
+ }
+ if (manifest != null
+ && "true".equalsIgnoreCase(manifest.getMainAttributes().getValue("Multi-Release"))) {
+ result.add(jar);
+ }
+ }
+ return result;
+ }
+
/**
* {@link InputStream} that can peek ahead at zip header bytes.
*/
@@ -248,13 +324,18 @@ private void shadeJars(
List transformers,
JarOutputStream jos,
Map> duplicates,
- DefaultPackageMapper packageMapper)
+ DefaultPackageMapper packageMapper,
+ ModuleInfoProcessor moduleInfoProcessor)
throws IOException {
for (File jar : shadeRequest.getJars()) {
logger.debug("Processing JAR " + jar);
List jarFilters = getFilters(jar, shadeRequest.getFilters());
+ if (moduleInfoProcessor != null) {
+ // Class analysis depends on the effective descriptor view, so select descriptors first.
+ moduleInfoProcessor.selectDescriptors(jar, jarFilters);
+ }
if (jar.isDirectory()) {
shadeDir(
shadeRequest,
@@ -266,9 +347,19 @@ private void shadeJars(
jar,
jar,
"",
- jarFilters);
+ jarFilters,
+ moduleInfoProcessor);
} else {
- shadeJar(shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar, jarFilters);
+ shadeJar(
+ shadeRequest,
+ resources,
+ transformers,
+ packageMapper,
+ jos,
+ duplicates,
+ jar,
+ jarFilters,
+ moduleInfoProcessor);
}
}
}
@@ -284,7 +375,8 @@ private void shadeDir(
File jar,
File current,
String prefix,
- List jarFilters)
+ List jarFilters,
+ ModuleInfoProcessor moduleInfoProcessor)
throws IOException {
final File[] children = current.listFiles();
if (children == null) {
@@ -304,15 +396,25 @@ private void shadeDir(
jar,
file,
prefix + file.getName() + '/',
- jarFilters);
+ jarFilters,
+ moduleInfoProcessor);
continue;
} catch (Exception e) {
throw new IOException(String.format("Problem shading JAR %s entry %s: %s", current, name, e), e);
}
}
- if (isFiltered(jarFilters, name) || isExcludedEntry(name)) {
+ if (moduleInfoProcessor != null && ModuleInfoProcessor.isModuleInfo(name)) {
+ continue;
+ }
+ if (isFiltered(jarFilters, name)) {
continue;
}
+ if (isExcludedEntry(name)) {
+ continue;
+ }
+ if (moduleInfoProcessor != null && ModuleInfoProcessor.isServiceConfiguration(name)) {
+ moduleInfoProcessor.includeServiceConfiguration(jar, name);
+ }
try {
shadeJarEntry(
@@ -331,7 +433,8 @@ public InputStream call() throws Exception {
},
name,
file.lastModified(),
- -1 /*ignore*/);
+ -1 /*ignore*/,
+ moduleInfoProcessor);
} catch (Exception e) {
throw new IOException(String.format("Problem shading JAR %s entry %s: %s", current, name, e), e);
}
@@ -347,7 +450,8 @@ private void shadeJar(
JarOutputStream jos,
Map> duplicates,
File jar,
- List jarFilters)
+ List jarFilters,
+ ModuleInfoProcessor moduleInfoProcessor)
throws IOException {
try (JarFile jarFile = newJarFile(jar)) {
@@ -356,9 +460,21 @@ private void shadeJar(
String name = entry.getName();
- if (entry.isDirectory() || isFiltered(jarFilters, name) || isExcludedEntry(name)) {
+ if (entry.isDirectory()) {
+ continue;
+ }
+ if (moduleInfoProcessor != null && ModuleInfoProcessor.isModuleInfo(name)) {
+ continue;
+ }
+ if (isFiltered(jarFilters, name)) {
+ continue;
+ }
+ if (isExcludedEntry(name)) {
continue;
}
+ if (moduleInfoProcessor != null && ModuleInfoProcessor.isServiceConfiguration(name)) {
+ moduleInfoProcessor.includeServiceConfiguration(jar, name);
+ }
try {
shadeJarEntry(
@@ -377,7 +493,8 @@ public InputStream call() throws Exception {
},
name,
getTime(entry),
- entry.getMethod());
+ entry.getMethod(),
+ moduleInfoProcessor);
} catch (Exception e) {
throw new IOException(String.format("Problem shading JAR %s entry %s: %s", jar, name, e), e);
}
@@ -393,7 +510,7 @@ private boolean isExcludedEntry(final String name) {
return true;
}
- if ("module-info.class".equals(name)) {
+ if (ModuleInfoProcessor.isModuleInfo(name)) {
logger.warn("Discovered module-info.class. " + "Shading will break its strong encapsulation.");
return true;
}
@@ -412,7 +529,8 @@ private void shadeJarEntry(
Callable inputProvider,
String name,
long time,
- int method)
+ int method,
+ ModuleInfoProcessor moduleInfoProcessor)
throws Exception {
try (InputStream in = inputProvider.call()) {
String mappedName = packageMapper.map(name, true, false);
@@ -428,7 +546,7 @@ private void shadeJarEntry(
duplicates.computeIfAbsent(name, k -> new HashSet<>()).add(jar);
if (name.endsWith(".class")) {
- addRemappedClass(jos, jar, name, time, in, packageMapper);
+ addRemappedClass(jos, jar, name, time, in, packageMapper, moduleInfoProcessor);
} else if (shadeRequest.isShadeSourcesContent() && name.endsWith(".java")) {
// Avoid duplicates
if (resources.contains(mappedName)) {
@@ -456,10 +574,26 @@ private void goThroughAllJarEntriesForManifestTransformer(
ShadeRequest shadeRequest,
Set resources,
ManifestResourceTransformer manifestTransformer,
+ boolean forceMultiRelease,
+ String forceAutomaticModuleName,
JarOutputStream jos)
throws IOException {
if (manifestTransformer != null) {
for (File jar : shadeRequest.getJars()) {
+ if (jar.isDirectory()) {
+ File manifestFile = new File(jar, JarFile.MANIFEST_NAME);
+ if (manifestFile.isFile()) {
+ resources.add(JarFile.MANIFEST_NAME);
+ try (InputStream input = Files.newInputStream(manifestFile.toPath())) {
+ manifestTransformer.processResource(
+ JarFile.MANIFEST_NAME,
+ input,
+ shadeRequest.getRelocators(),
+ manifestFile.lastModified());
+ }
+ }
+ continue;
+ }
try (JarFile jarFile = newJarFile(jar)) {
for (Enumeration en = jarFile.entries(); en.hasMoreElements(); ) {
JarEntry entry = en.nextElement();
@@ -476,6 +610,8 @@ private void goThroughAllJarEntriesForManifestTransformer(
}
}
if (manifestTransformer.hasTransformedResource()) {
+ manifestTransformer.setForceMultiRelease(forceMultiRelease);
+ manifestTransformer.setForceAutomaticModuleName(forceAutomaticModuleName);
manifestTransformer.modifyOutputStream(jos);
}
}
@@ -589,20 +725,32 @@ private void addDirectory(Set resources, JarOutputStream jos, String nam
resources.add(name);
}
- private void addRemappedClass(
- JarOutputStream jos, File jar, String name, long time, InputStream is, DefaultPackageMapper packageMapper)
+ private String addRemappedClass(
+ JarOutputStream jos,
+ File jar,
+ String name,
+ long time,
+ InputStream is,
+ DefaultPackageMapper packageMapper,
+ ModuleInfoProcessor moduleInfoProcessor)
throws IOException, MojoExecutionException {
if (packageMapper.relocators.isEmpty()) {
try {
JarEntry entry = new JarEntry(name);
entry.setTime(time);
jos.putNextEntry(entry);
- IOUtil.copy(is, jos);
+ if (moduleInfoProcessor == null) {
+ IOUtil.copy(is, jos);
+ } else {
+ byte[] classBytes = IOUtil.toByteArray(is);
+ jos.write(classBytes);
+ moduleInfoProcessor.recordClass(jar, name, classBytes);
+ }
+ return name;
} catch (ZipException e) {
logger.debug("We have a duplicate " + name + " in " + jar);
+ return null;
}
-
- return;
}
// Keep the original class, in case nothing was relocated by ShadeClassRemapper. This avoids binary
@@ -639,7 +787,7 @@ private void addRemappedClass(
}
// Need to take the .class off for remapping evaluation
- String mappedName = packageMapper.map(name.substring(0, name.indexOf('.')), true, false);
+ String mappedName = mapClassEntry(name, packageMapper);
try {
// Now we put it back on so the class file is written out with the right extension.
@@ -648,9 +796,26 @@ private void addRemappedClass(
jos.putNextEntry(entry);
jos.write(renamedClass);
+ if (moduleInfoProcessor != null) {
+ moduleInfoProcessor.recordClass(jar, mappedName + ".class", renamedClass);
+ }
+ return mappedName + ".class";
} catch (ZipException e) {
logger.debug("We have a duplicate " + mappedName + " in " + jar);
+ return null;
+ }
+ }
+
+ private static String mapClassEntry(String name, DefaultPackageMapper packageMapper) {
+ Matcher versionedClass = VERSIONED_CLASS_ENTRY.matcher(name);
+ String prefix = "";
+ String classEntry = name;
+ if (versionedClass.matches()) {
+ prefix = versionedClass.group(1);
+ classEntry = versionedClass.group(2);
}
+ return prefix
+ + packageMapper.map(classEntry.substring(0, classEntry.length() - ".class".length()), true, false);
}
private boolean isFiltered(List filters, String name) {
diff --git a/src/main/java/org/apache/maven/plugins/shade/ModuleInfoConfiguration.java b/src/main/java/org/apache/maven/plugins/shade/ModuleInfoConfiguration.java
new file mode 100644
index 00000000..5f7a7a76
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/ModuleInfoConfiguration.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.shade;
+
+import javax.lang.model.SourceVersion;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Additional choices used while constructing the module descriptor for amalgamated shaded contents.
+ */
+public class ModuleInfoConfiguration {
+ private String moduleName;
+
+ private String publicBoundary = ModuleInfoPublicBoundary.PRIMARY.name();
+
+ private Map analysisJdkToolchain = Collections.emptyMap();
+
+ private List additionalExports = Collections.emptyList();
+
+ private List additionalOpens = Collections.emptyList();
+
+ private List additionalRequires = Collections.emptyList();
+
+ private Set additionalUses = Collections.emptySet();
+
+ private Set dynamicUses = Collections.emptySet();
+
+ public String getModuleName() {
+ return moduleName;
+ }
+
+ public void setModuleName(String moduleName) {
+ this.moduleName = moduleName;
+ }
+
+ static boolean isValidModuleName(String moduleName) {
+ if (moduleName == null || !SourceVersion.isName(moduleName)) {
+ return false;
+ }
+ for (String identifier : moduleName.split("[.]", -1)) {
+ if ("_".equals(identifier)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public ModuleInfoPublicBoundary getPublicBoundary() {
+ return ModuleInfoPublicBoundary.fromString(publicBoundary);
+ }
+
+ public void setPublicBoundary(String publicBoundary) {
+ this.publicBoundary = publicBoundary == null ? ModuleInfoPublicBoundary.PRIMARY.name() : publicBoundary;
+ }
+
+ public Map getAnalysisJdkToolchain() {
+ return analysisJdkToolchain;
+ }
+
+ public void setAnalysisJdkToolchain(Map analysisJdkToolchain) {
+ this.analysisJdkToolchain =
+ analysisJdkToolchain == null ? Collections.emptyMap() : analysisJdkToolchain;
+ }
+
+ public List getAdditionalExports() {
+ return additionalExports;
+ }
+
+ public void setAdditionalExports(List additionalExports) {
+ this.additionalExports =
+ additionalExports == null ? Collections.emptyList() : additionalExports;
+ }
+
+ public List getAdditionalOpens() {
+ return additionalOpens;
+ }
+
+ public void setAdditionalOpens(List additionalOpens) {
+ this.additionalOpens = additionalOpens == null ? Collections.emptyList() : additionalOpens;
+ }
+
+ public List getAdditionalRequires() {
+ return additionalRequires;
+ }
+
+ public void setAdditionalRequires(List additionalRequires) {
+ this.additionalRequires =
+ additionalRequires == null ? Collections.emptyList() : additionalRequires;
+ }
+
+ public Set getAdditionalUses() {
+ return additionalUses;
+ }
+
+ public void setAdditionalUses(Set additionalUses) {
+ this.additionalUses = additionalUses == null ? Collections.emptySet() : additionalUses;
+ }
+
+ public Set getDynamicUses() {
+ return dynamicUses;
+ }
+
+ public void setDynamicUses(Set dynamicUses) {
+ this.dynamicUses = dynamicUses == null ? Collections.emptySet() : dynamicUses;
+ }
+
+ /**
+ * An additional export or open directive. An empty target set denotes an unqualified directive.
+ */
+ public static class PackageDirective {
+ private String packageName;
+
+ private Set targets = Collections.emptySet();
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public void setPackageName(String packageName) {
+ this.packageName = packageName;
+ }
+
+ public void setPackage(String packageName) {
+ this.packageName = packageName;
+ }
+
+ public Set getTargets() {
+ return targets;
+ }
+
+ public void setTargets(Set targets) {
+ this.targets = targets == null ? Collections.emptySet() : targets;
+ }
+ }
+
+ /**
+ * An additional module requirement.
+ */
+ public static class Requirement {
+ private String module;
+
+ private boolean staticRequirement;
+
+ private boolean transitive;
+
+ public String getModule() {
+ return module;
+ }
+
+ public void setModule(String module) {
+ this.module = module;
+ }
+
+ public boolean isStaticRequirement() {
+ return staticRequirement;
+ }
+
+ public void setStaticRequirement(boolean staticRequirement) {
+ this.staticRequirement = staticRequirement;
+ }
+
+ public void setStatic(boolean staticRequirement) {
+ this.staticRequirement = staticRequirement;
+ }
+
+ public boolean isTransitive() {
+ return transitive;
+ }
+
+ public void setTransitive(boolean transitive) {
+ this.transitive = transitive;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/ModuleInfoMode.java b/src/main/java/org/apache/maven/plugins/shade/ModuleInfoMode.java
new file mode 100644
index 00000000..fc763456
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/ModuleInfoMode.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.shade;
+
+import java.util.Locale;
+
+/**
+ * Controls how module descriptors are handled while shading.
+ */
+public enum ModuleInfoMode {
+ /** Discard module descriptors, retaining the historical behavior. */
+ DISCARD,
+
+ /** Merge module descriptors into the descriptor of the primary artifact. */
+ MERGE;
+
+ /**
+ * Parses a module descriptor handling mode without regard to case.
+ *
+ * @param value module descriptor handling mode
+ * @return parsed module descriptor handling mode
+ * @throws IllegalArgumentException if the value is not a supported mode
+ */
+ public static ModuleInfoMode fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("Module info mode must not be null.");
+ }
+ try {
+ return valueOf(value.toUpperCase(Locale.ROOT).trim());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Unknown module info mode '" + value + "'. Expected one of: discard, merge.");
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/ModuleInfoProcessor.java b/src/main/java/org/apache/maven/plugins/shade/ModuleInfoProcessor.java
new file mode 100644
index 00000000..5d274c01
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/ModuleInfoProcessor.java
@@ -0,0 +1,1993 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.shade;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipException;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.shade.filter.Filter;
+import org.apache.maven.plugins.shade.relocation.Relocator;
+import org.apache.maven.plugins.shade.resource.ServicesResourceTransformer;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ByteVector;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.ModuleVisitor;
+import org.objectweb.asm.Opcodes;
+import org.slf4j.Logger;
+
+/**
+ * Collects, merges and writes module descriptors after the shaded archive contents are known.
+ */
+final class ModuleInfoProcessor {
+ private static final Pattern VERSIONED_MODULE_INFO =
+ Pattern.compile("^META-INF/versions/([1-9][0-9]*)/module-info[.]class$");
+
+ private static final Pattern VERSIONED_CLASS = Pattern.compile("^META-INF/versions/([1-9][0-9]*)/(.+)[.]class$");
+
+ private static final Pattern SERVICE_CONFIGURATION = Pattern.compile("^META-INF/services/([^/]+)$");
+
+ private static final int FIRST_MODULE_RELEASE = 9;
+
+ private final Logger logger;
+
+ private final File primaryArtifact;
+
+ private final List relocators;
+
+ private final ModuleInfoConfiguration configuration;
+
+ private final File analysisJdkHome;
+
+ private final Map artifacts = new LinkedHashMap<>();
+
+ private final Map dependencyArtifacts = new LinkedHashMap<>();
+
+ private final Map> outputClasses = new TreeMap<>();
+
+ private final Map> outputPackages = new TreeMap<>();
+
+ private final Map>> artifactOutputPackages = new LinkedHashMap<>();
+
+ private final Map> retainedClasses = new TreeMap<>();
+
+ private final Map providerOrigins = new HashMap<>();
+
+ private final Map transitiveRequirementOrigins = new HashMap<>();
+
+ private final Set warnedDroppedAttributes = new HashSet<>();
+
+ private final Set warnedAutomaticAnalysis = new HashSet<>();
+
+ private final Set warnedDynamicUses = new HashSet<>();
+
+ private String primaryModuleName;
+
+ private String outputModuleName;
+
+ private boolean multiReleaseOutput;
+
+ private int modularFloor;
+
+ private List floorReasons = Collections.emptyList();
+
+ private PlatformModuleIndex platformModules;
+
+ ModuleInfoProcessor(ShadeRequest request, Logger logger) throws IOException, MojoExecutionException {
+ this.logger = logger;
+ this.primaryArtifact = request.getPrimaryArtifact();
+ this.relocators = request.getRelocators();
+ this.configuration = request.getModuleInfoConfiguration();
+ this.analysisJdkHome = request.getModuleInfoAnalysisJdkHome();
+ for (File artifact : request.getJars()) {
+ ArtifactData data = scanArtifact(artifact);
+ artifacts.put(artifact, data);
+ multiReleaseOutput |= data.multiRelease;
+ }
+ for (File artifact : request.getDependencyAnalysisArtifacts()) {
+ if (!artifacts.containsKey(artifact) && artifact != null && artifact.exists()) {
+ try {
+ dependencyArtifacts.put(artifact, scanArtifact(artifact, false));
+ } catch (ZipException e) {
+ logger.debug("Skipping non-archive dependency-analysis input " + artifact);
+ }
+ }
+ }
+ determinePrimaryModuleName();
+ determineOutputModuleName();
+ multiReleaseOutput |= primaryRootDescriptorRequiresVersionedOutput();
+ }
+
+ static boolean isModuleInfo(String name) {
+ return "module-info.class".equals(name)
+ || VERSIONED_MODULE_INFO.matcher(name).matches();
+ }
+
+ static boolean isServiceConfiguration(String name) {
+ return SERVICE_CONFIGURATION.matcher(name).matches();
+ }
+
+ String getOutputModuleName() {
+ return outputModuleName;
+ }
+
+ boolean hasPrimaryModule() {
+ return primaryModuleName != null;
+ }
+
+ void selectDescriptors(File artifact, List filters) {
+ ArtifactData data = artifacts.get(artifact);
+ if (data != null) {
+ for (DescriptorEntry descriptor : data.descriptorsByName.values()) {
+ descriptor.included = true;
+ for (Filter filter : filters) {
+ if (filter.isFiltered(descriptor.name)) {
+ descriptor.included = false;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ void includeServiceConfiguration(File artifact, String name) {
+ ArtifactData data = artifacts.get(artifact);
+ if (data != null) {
+ ServiceConfiguration configuration = data.serviceConfigurations.get(name);
+ if (configuration != null) {
+ configuration.included = true;
+ }
+ }
+ }
+
+ void recordClass(File artifact, String outputName, byte[] bytecode) {
+ Matcher matcher = VERSIONED_CLASS.matcher(outputName);
+ int release = 0;
+ String className;
+ if (matcher.matches()) {
+ release = Integer.parseInt(matcher.group(1));
+ className = matcher.group(2);
+ } else if (outputName.endsWith(".class")) {
+ className = outputName.substring(0, outputName.length() - ".class".length());
+ } else {
+ return;
+ }
+ if ("module-info".equals(className)) {
+ return;
+ }
+ if (release > 0 && !multiReleaseOutput) {
+ return;
+ }
+ outputClasses.computeIfAbsent(release, key -> new LinkedHashSet<>()).add(className);
+ ArtifactData origin = artifacts.get(artifact);
+ int viewRelease = release == 0 ? FIRST_MODULE_RELEASE : release;
+ if (origin != null && !artifact.equals(primaryArtifact) && origin.effectiveDescriptor(viewRelease) == null) {
+ AutomaticModuleAnalyzer.Analysis analysis = AutomaticModuleAnalyzer.analyze(bytecode);
+ retainedClasses
+ .computeIfAbsent(release, key -> new LinkedHashMap<>())
+ .put(
+ className,
+ new RetainedClass(
+ origin,
+ className,
+ Math.max(
+ release == 0 ? FIRST_MODULE_RELEASE : release,
+ classRelease(analysis.classVersion)),
+ analysis.references,
+ analysis.serviceUses,
+ analysis.unresolvedServiceUses));
+ }
+ int separator = className.lastIndexOf('/');
+ if (separator < 0) {
+ outputPackages
+ .computeIfAbsent(release, key -> new LinkedHashSet<>())
+ .add("");
+ } else {
+ String packaze = className.substring(0, separator).replace('/', '.');
+ outputPackages
+ .computeIfAbsent(release, key -> new LinkedHashSet<>())
+ .add(packaze);
+ artifactOutputPackages
+ .computeIfAbsent(artifact, key -> new TreeMap<>())
+ .computeIfAbsent(release, key -> new LinkedHashSet<>())
+ .add(packaze);
+ }
+ }
+
+ void writeDescriptors(JarOutputStream output, ServicesResourceTransformer servicesTransformer)
+ throws IOException, MojoExecutionException {
+ if (primaryArtifact == null || !artifacts.containsKey(primaryArtifact)) {
+ logger.warn("Module descriptor merging was requested, but the shaded output has no primary artifact.");
+ return;
+ }
+
+ SortedSet releases = collectReleaseBreakpoints();
+ if (releases.isEmpty()) {
+ logger.warn("Module descriptor merging was requested, but the primary artifact has no module-info.class.");
+ return;
+ }
+
+ int earliestRelease = findEarliestPrimaryRelease(releases);
+ if (earliestRelease < 0) {
+ logger.warn("Module descriptor merging was requested, but the primary module descriptor was filtered.");
+ return;
+ }
+
+ if (requiresPlatformAnalysis(releases)) {
+ PlatformModuleIndex platform = platformModules();
+ if (releases.last() > platform.getRelease()) {
+ throw analysisJdkTooOld(releases.last(), platform.getRelease());
+ }
+ for (int release = earliestRelease; release <= platform.getRelease(); release++) {
+ releases.add(release);
+ }
+ }
+
+ List candidates = new ArrayList<>();
+ for (int release : releases.tailSet(earliestRelease)) {
+ DescriptorEntry primary = artifacts.get(primaryArtifact).effectiveDescriptor(release);
+ if (primary != null && primary.included) {
+ candidates.add(mergeRelease(release, primary));
+ }
+ }
+ if (candidates.isEmpty()) {
+ logger.warn("Module descriptor merging was requested, but the primary module descriptor was filtered.");
+ return;
+ }
+
+ validatePrimaryBoundary(candidates);
+ normalizeProvidersAndFloor(candidates, earliestRelease);
+ normalizeInvariantRequirements(candidates);
+ validateConfiguredRequirements(candidates);
+ SortedSet allPackages = collectAllPackages(candidates);
+ if (allPackages.contains("")) {
+ throw new MojoExecutionException("Cannot create module " + outputModuleName
+ + ": the shaded JAR contains a class in the unnamed package.");
+ }
+ for (MergedDescriptor descriptor : candidates) {
+ descriptor.packages.clear();
+ descriptor.packages.addAll(allPackages);
+ }
+
+ MergedDescriptor previous = null;
+ boolean first = true;
+ for (MergedDescriptor descriptor : candidates) {
+ if (descriptor.release < modularFloor) {
+ continue;
+ }
+ if (!first && descriptor.sameModuleSemantics(previous)) {
+ continue;
+ }
+ String entryName = first && modularFloor == FIRST_MODULE_RELEASE && hasIncludedRootPrimaryDescriptor()
+ ? "module-info.class"
+ : "META-INF/versions/" + descriptor.release + "/module-info.class";
+ writeDescriptor(output, entryName, descriptor);
+ previous = descriptor;
+ first = false;
+ }
+
+ bridgeAutomaticServices(candidates, earliestRelease, servicesTransformer);
+ logRaisedFloor(earliestRelease);
+ }
+
+ boolean requiresMultiReleaseOutput() {
+ return multiReleaseOutput;
+ }
+
+ void enableMultiReleaseOutput() {
+ multiReleaseOutput = true;
+ }
+
+ private ArtifactData scanArtifact(File artifact) throws IOException, MojoExecutionException {
+ return scanArtifact(artifact, true);
+ }
+
+ private ArtifactData scanArtifact(File artifact, boolean scanServices) throws IOException, MojoExecutionException {
+ ArtifactData result = new ArtifactData(artifact);
+ if (artifact.isDirectory()) {
+ File manifestFile = new File(artifact, JarFile.MANIFEST_NAME);
+ if (manifestFile.isFile()) {
+ try (InputStream input = Files.newInputStream(manifestFile.toPath())) {
+ result.readManifest(new Manifest(input));
+ }
+ }
+ scanDirectory(result, artifact, artifact, "", scanServices);
+ } else {
+ try (JarFile jar = new JarFile(artifact)) {
+ result.readManifest(jar.getManifest());
+ Enumeration entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ if (isModuleInfo(entry.getName())) {
+ try (InputStream input = jar.getInputStream(entry)) {
+ result.addDescriptor(
+ entry.getName(),
+ parseDescriptor(input, artifact, entry.getName()),
+ entry.getTime());
+ }
+ } else if (entry.getName().endsWith(".class")) {
+ result.addClass(entry.getName());
+ } else if (scanServices && isServiceConfiguration(entry.getName())) {
+ try (InputStream input = jar.getInputStream(entry)) {
+ result.addServiceConfiguration(parseServiceConfiguration(input, artifact, entry.getName()));
+ }
+ }
+ }
+ }
+ }
+ if (result.automaticModuleName == null) {
+ result.automaticModuleName = deriveAutomaticModuleName(artifact.getName());
+ }
+ return result;
+ }
+
+ private void scanDirectory(ArtifactData data, File root, File current, String prefix, boolean scanServices)
+ throws IOException, MojoExecutionException {
+ File[] children = current.listFiles();
+ if (children == null) {
+ return;
+ }
+ Arrays.sort(children, Comparator.comparing(File::getName));
+ for (File child : children) {
+ String name = prefix + child.getName();
+ if (child.isDirectory()) {
+ scanDirectory(data, root, child, name + '/', scanServices);
+ } else if (isModuleInfo(name)) {
+ try (InputStream input = Files.newInputStream(child.toPath())) {
+ data.addDescriptor(name, parseDescriptor(input, root, name), child.lastModified());
+ }
+ } else if (name.endsWith(".class")) {
+ data.addClass(name);
+ } else if (scanServices && isServiceConfiguration(name)) {
+ try (InputStream input = Files.newInputStream(child.toPath())) {
+ data.addServiceConfiguration(parseServiceConfiguration(input, root, name));
+ }
+ }
+ }
+ }
+
+ private ServiceConfiguration parseServiceConfiguration(InputStream input, File artifact, String entry)
+ throws IOException, MojoExecutionException {
+ Matcher matcher = SERVICE_CONFIGURATION.matcher(entry);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException(entry);
+ }
+ String service = matcher.group(1);
+ validateServiceClassName(service, artifact, entry);
+ SortedSet providers = new TreeSet<>();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ int comment = line.indexOf('#');
+ String provider = (comment < 0 ? line : line.substring(0, comment)).trim();
+ if (!provider.isEmpty()) {
+ validateServiceClassName(provider, artifact, entry);
+ providers.add(provider);
+ }
+ }
+ }
+ return new ServiceConfiguration(entry, service, providers);
+ }
+
+ private void validateServiceClassName(String name, File artifact, String entry) throws MojoExecutionException {
+ if (!isBinaryClassName(name)) {
+ throw new MojoExecutionException(
+ "Invalid service class name " + name + " in " + artifact + '!' + entry + '.');
+ }
+ }
+
+ private static boolean isBinaryClassName(String name) {
+ boolean start = true;
+ for (int offset = 0; offset < name.length(); ) {
+ int character = name.codePointAt(offset);
+ if (character == '.') {
+ if (start) {
+ return false;
+ }
+ start = true;
+ } else if (start
+ ? !Character.isJavaIdentifierStart(character)
+ : !Character.isJavaIdentifierPart(character)) {
+ return false;
+ } else {
+ start = false;
+ }
+ offset += Character.charCount(character);
+ }
+ return !start;
+ }
+
+ private ModuleDescriptorData parseDescriptor(InputStream input, File artifact, String entry)
+ throws IOException, MojoExecutionException {
+ try {
+ final ModuleDescriptorData descriptor = new ModuleDescriptorData();
+ ClassReader reader = new ClassReader(input);
+ reader.accept(
+ new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ descriptor.classVersion = version;
+ descriptor.classAccess = access;
+ }
+
+ @Override
+ public ModuleVisitor visitModule(String name, int access, String version) {
+ descriptor.name = name;
+ descriptor.access = access;
+ descriptor.version = version;
+ return new ModuleVisitor(Opcodes.ASM9) {
+ @Override
+ public void visitMainClass(String mainClass) {
+ descriptor.mainClass = mainClass;
+ }
+
+ @Override
+ public void visitPackage(String packaze) {
+ descriptor.packages.add(packaze);
+ }
+
+ @Override
+ public void visitRequire(String module, int access, String version) {
+ descriptor.requires.put(module, new Require(module, access, version));
+ }
+
+ @Override
+ public void visitExport(String packaze, int access, String... modules) {
+ descriptor.exports.put(packaze, new Export(packaze, access, asSortedSet(modules)));
+ }
+
+ @Override
+ public void visitOpen(String packaze, int access, String... modules) {
+ descriptor.opens.put(packaze, new Open(packaze, access, asSortedSet(modules)));
+ }
+
+ @Override
+ public void visitUse(String service) {
+ descriptor.uses.add(service);
+ }
+
+ @Override
+ public void visitProvide(String service, String... providers) {
+ descriptor
+ .provides
+ .computeIfAbsent(service, key -> new TreeSet<>())
+ .addAll(Arrays.asList(providers));
+ }
+ };
+ }
+
+ @Override
+ public void visitAttribute(Attribute attribute) {
+ if (attribute instanceof ModuleTargetAttribute) {
+ descriptor.targetPlatform = ((ModuleTargetAttribute) attribute).targetPlatform;
+ } else if (attribute instanceof ModuleResolutionAttribute) {
+ descriptor.resolutionFlags = ((ModuleResolutionAttribute) attribute).resolutionFlags;
+ } else {
+ descriptor.attributes.add(attribute.type);
+ }
+ }
+ },
+ new Attribute[] {new ModuleTargetAttribute(), new ModuleResolutionAttribute()},
+ 0);
+ if (descriptor.name == null) {
+ throw new MojoExecutionException(
+ "Invalid module descriptor " + entry + " in " + artifact + ": missing Module attribute.");
+ }
+ return descriptor;
+ } catch (IllegalArgumentException e) {
+ throw new MojoExecutionException("Cannot parse module descriptor " + entry + " in " + artifact, e);
+ }
+ }
+
+ private MergedDescriptor mergeRelease(int release, DescriptorEntry primaryEntry) throws MojoExecutionException {
+ ModuleDescriptorData primary = primaryEntry.descriptor;
+ MergedDescriptor result = new MergedDescriptor(release, primaryEntry);
+ result.name = outputModuleName;
+ result.access = primary.access;
+ result.version = primary.version;
+ result.classVersion = primary.classVersion;
+ result.classAccess = primary.classAccess;
+ result.mainClass = relocateClass(primary.mainClass);
+ result.targetPlatform = primary.targetPlatform;
+ result.resolutionFlags = primary.resolutionFlags;
+ warnDroppedAttributes(primary, new Origin(primaryArtifact, primaryEntry));
+
+ Set embeddedNames = new HashSet<>();
+ for (ArtifactData artifact : artifacts.values()) {
+ if (!artifact.file.equals(primaryArtifact)) {
+ embeddedNames.add(artifact.configuredModuleName(release));
+ }
+ }
+ embeddedNames.remove(null);
+ embeddedNames.add(primaryModuleName);
+ embeddedNames.add(outputModuleName);
+
+ for (Export export : primary.exports.values()) {
+ String packaze = relocatePackage(export.packaze);
+ SortedSet targets = removeEmbeddedTargets(export.targets, embeddedNames);
+ if (export.targets == null || !targets.isEmpty()) {
+ result.exports.put(packaze, new Export(packaze, export.access, targetsOrNull(export.targets, targets)));
+ }
+ }
+ for (Open open : primary.opens.values()) {
+ String packaze = relocatePackage(open.packaze);
+ SortedSet targets = removeEmbeddedTargets(open.targets, embeddedNames);
+ if (open.targets == null || !targets.isEmpty()) {
+ result.opens.put(packaze, new Open(packaze, open.access, targetsOrNull(open.targets, targets)));
+ }
+ }
+ result.primaryExports.putAll(result.exports);
+ result.primaryOpens.putAll(result.opens);
+
+ addRequirements(
+ result, primary.requires.values(), embeddedNames, new Origin(primaryArtifact, primaryEntry), true);
+ addServices(result, primary, true, new Origin(primaryArtifact, primaryEntry));
+
+ for (ArtifactData artifact : artifacts.values()) {
+ if (artifact.file.equals(primaryArtifact)) {
+ continue;
+ }
+ DescriptorEntry descriptor = artifact.effectiveDescriptor(release);
+ if (descriptor == null) {
+ addAutomaticServices(result, artifact, release);
+ addAutomaticBoundary(result, artifact, release);
+ continue;
+ }
+ if (!descriptor.included) {
+ continue;
+ }
+ warnDroppedAttributes(descriptor.descriptor, new Origin(artifact.file, descriptor));
+ addRequirements(
+ result,
+ descriptor.descriptor.requires.values(),
+ embeddedNames,
+ new Origin(artifact.file, descriptor),
+ false);
+ addServices(result, descriptor.descriptor, false, new Origin(artifact.file, descriptor));
+ addEmbeddedBoundary(result, artifact, descriptor.descriptor, embeddedNames, release);
+ }
+
+ addAutomaticRequirements(result, release);
+ addConfiguredDirectives(result, embeddedNames, release);
+ result.packages.addAll(effectivePackages(release));
+ validateProjectDirectives(result, primary, release);
+ if ((result.access & Opcodes.ACC_OPEN) != 0) {
+ result.opens.clear();
+ }
+ return result;
+ }
+
+ private void addAutomaticServices(MergedDescriptor target, ArtifactData artifact, int release) {
+ if (artifact.effectiveDescriptor(release) != null) {
+ return;
+ }
+ for (ServiceConfiguration configuration : artifact.serviceConfigurations.values()) {
+ if (!configuration.included) {
+ continue;
+ }
+ String service = relocateClass(configuration.service);
+ for (String provider : configuration.providers) {
+ String relocatedProvider = relocateClass(provider);
+ if (hasClass(relocatedProvider, release)) {
+ target.provides
+ .computeIfAbsent(service, key -> new TreeSet<>())
+ .add(relocatedProvider);
+ providerOrigins.putIfAbsent(
+ new ServiceProvider(service, relocatedProvider),
+ new Origin(artifact.file, configuration.name));
+ } else {
+ logger.warn("Omitting automatic-module provider " + relocatedProvider + " from "
+ + artifact.file + '!' + configuration.name
+ + " because the class is not present in the shaded output.");
+ }
+ }
+ }
+ }
+
+ private void addAutomaticBoundary(MergedDescriptor target, ArtifactData artifact, int release) {
+ if (configuration.getPublicBoundary() != ModuleInfoPublicBoundary.MERGE) {
+ return;
+ }
+ for (String packaze : effectiveArtifactPackages(artifact.file, release)) {
+ mergeExport(target.exports, new Export(packaze, 0, null));
+ mergeOpen(target.opens, new Open(packaze, 0, null));
+ }
+ }
+
+ private void addEmbeddedBoundary(
+ MergedDescriptor target,
+ ArtifactData artifact,
+ ModuleDescriptorData descriptor,
+ Set embeddedNames,
+ int release) {
+ if (configuration.getPublicBoundary() != ModuleInfoPublicBoundary.MERGE) {
+ return;
+ }
+ Set packages = effectivePackages(release);
+ for (Export export : descriptor.exports.values()) {
+ String packaze = relocatePackage(export.packaze);
+ if (!packages.contains(packaze)) {
+ logger.warn("Omitting embedded export " + packaze
+ + " because the package is not present in the shaded output at Java " + release + '.');
+ continue;
+ }
+ SortedSet targets = removeEmbeddedTargets(export.targets, embeddedNames);
+ if (export.targets == null || !targets.isEmpty()) {
+ mergeExport(target.exports, new Export(packaze, 0, targetsOrNull(export.targets, targets)));
+ }
+ }
+ if ((descriptor.access & Opcodes.ACC_OPEN) != 0) {
+ for (String packaze : effectiveArtifactPackages(artifact.file, release)) {
+ mergeOpen(target.opens, new Open(packaze, 0, null));
+ }
+ } else {
+ for (Open open : descriptor.opens.values()) {
+ String packaze = relocatePackage(open.packaze);
+ if (!packages.contains(packaze)) {
+ logger.warn("Omitting embedded open package " + packaze
+ + " because the package is not present in the shaded output at Java " + release + '.');
+ continue;
+ }
+ SortedSet targets = removeEmbeddedTargets(open.targets, embeddedNames);
+ if (open.targets == null || !targets.isEmpty()) {
+ mergeOpen(target.opens, new Open(packaze, 0, targetsOrNull(open.targets, targets)));
+ }
+ }
+ }
+ }
+
+ private void addConfiguredDirectives(MergedDescriptor target, Set embeddedNames, int release)
+ throws MojoExecutionException {
+ for (ModuleInfoConfiguration.PackageDirective configured : configuration.getAdditionalExports()) {
+ Export export = configuredExport(configured, embeddedNames, "export");
+ if (export != null) {
+ mergeExport(target.exports, export);
+ }
+ }
+ for (ModuleInfoConfiguration.PackageDirective configured : configuration.getAdditionalOpens()) {
+ Open open = configuredOpen(configured, embeddedNames);
+ if (open != null) {
+ mergeOpen(target.opens, open);
+ }
+ }
+ for (ModuleInfoConfiguration.Requirement configured : configuration.getAdditionalRequires()) {
+ String module = configured.getModule();
+ if (module == null || module.trim().isEmpty()) {
+ throw new MojoExecutionException(
+ "moduleInfo.additionalRequires contains a requirement with no module.");
+ }
+ if (embeddedNames.contains(module)) {
+ throw new MojoExecutionException("moduleInfo.additionalRequires cannot require embedded module "
+ + module + " in the amalgamated output.");
+ }
+ if (isPlatformModule(module) && !platformModules().hasModule(module, release)) {
+ continue;
+ }
+ int access = 0;
+ if (configured.isStaticRequirement()) {
+ access |= Opcodes.ACC_STATIC_PHASE;
+ }
+ if (configured.isTransitive()) {
+ access |= Opcodes.ACC_TRANSITIVE;
+ }
+ target.requires.merge(module, new Require(module, access, null), Require::strongest);
+ }
+ for (String service : configuration.getAdditionalUses()) {
+ addConfiguredUse(target, service, "moduleInfo.additionalUses", release);
+ }
+ for (String service : configuration.getDynamicUses()) {
+ addConfiguredUse(target, service, "moduleInfo.dynamicUses", release);
+ }
+ }
+
+ private void addConfiguredUse(MergedDescriptor target, String configured, String parameter, int release)
+ throws MojoExecutionException {
+ String service = relocateConfiguredClass(configured, parameter);
+ if (hasClass(service, release)) {
+ target.uses.add(service);
+ return;
+ }
+ SortedSet owners = findModuleOwners(toInternalName(service), release);
+ if (owners.size() > 1) {
+ throw new MojoExecutionException(
+ "Configured service type " + service + " is owned by multiple modules " + owners + '.');
+ }
+ if (!owners.isEmpty()) {
+ target.uses.add(service);
+ String owner = owners.first();
+ if (!"java.base".equals(owner) && !isAmalgamatedModuleName(owner)) {
+ target.requires.merge(owner, new Require(owner, 0, null), Require::strongest);
+ }
+ }
+ }
+
+ private Export configuredExport(
+ ModuleInfoConfiguration.PackageDirective configured, Set embeddedNames, String directive)
+ throws MojoExecutionException {
+ String packaze = configuredPackage(configured, directive);
+ SortedSet targets = configuredTargets(configured, embeddedNames);
+ return configured.getTargets().isEmpty() || !targets.isEmpty()
+ ? new Export(packaze, 0, configured.getTargets().isEmpty() ? null : targets)
+ : null;
+ }
+
+ private Open configuredOpen(ModuleInfoConfiguration.PackageDirective configured, Set embeddedNames)
+ throws MojoExecutionException {
+ String packaze = configuredPackage(configured, "open");
+ SortedSet targets = configuredTargets(configured, embeddedNames);
+ return configured.getTargets().isEmpty() || !targets.isEmpty()
+ ? new Open(packaze, 0, configured.getTargets().isEmpty() ? null : targets)
+ : null;
+ }
+
+ private String configuredPackage(ModuleInfoConfiguration.PackageDirective configured, String directive)
+ throws MojoExecutionException {
+ if (configured.getPackageName() == null
+ || configured.getPackageName().trim().isEmpty()) {
+ throw new MojoExecutionException("moduleInfo.additional" + directive + "s contains an empty package.");
+ }
+ return relocatePackage(configured.getPackageName());
+ }
+
+ private SortedSet configuredTargets(
+ ModuleInfoConfiguration.PackageDirective configured, Set embeddedNames) {
+ SortedSet targets = new TreeSet<>(configured.getTargets());
+ targets.removeAll(embeddedNames);
+ return targets;
+ }
+
+ private String relocateConfiguredClass(String className, String parameter) throws MojoExecutionException {
+ if (className == null || className.trim().isEmpty()) {
+ throw new MojoExecutionException(parameter + " contains an empty service class.");
+ }
+ return relocateClass(className);
+ }
+
+ private boolean requiresPlatformAnalysis(SortedSet releases) {
+ for (int release : releases) {
+ for (RetainedClass retainedClass : effectiveRetainedClasses(release).values()) {
+ if (retainedClass.origin.effectiveDescriptor(release) == null) {
+ return true;
+ }
+ }
+ }
+ for (ModuleInfoConfiguration.Requirement requirement : configuration.getAdditionalRequires()) {
+ if (requirement.getModule() != null && isPlatformModule(requirement.getModule())) {
+ return true;
+ }
+ }
+ return !configuration.getAdditionalUses().isEmpty()
+ || !configuration.getDynamicUses().isEmpty();
+ }
+
+ private void validateConfiguredRequirements(List candidates) throws MojoExecutionException {
+ for (ModuleInfoConfiguration.Requirement requirement : configuration.getAdditionalRequires()) {
+ String module = requirement.getModule();
+ boolean found = false;
+ for (MergedDescriptor candidate : candidates) {
+ if (candidate.requires.containsKey(module)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ throw new MojoExecutionException("Configured moduleInfo requirement " + module
+ + " is not available through Java " + candidates.get(candidates.size() - 1).release
+ + " in the selected analysis JDK or resolved dependency hull.");
+ }
+ }
+ validateConfiguredUses(candidates, configuration.getAdditionalUses(), "moduleInfo.additionalUses");
+ validateConfiguredUses(candidates, configuration.getDynamicUses(), "moduleInfo.dynamicUses");
+ }
+
+ private void validateConfiguredUses(List candidates, Set configured, String parameter)
+ throws MojoExecutionException {
+ for (String service : configured) {
+ String relocated = relocateConfiguredClass(service, parameter);
+ boolean found = false;
+ for (MergedDescriptor candidate : candidates) {
+ if (candidate.uses.contains(relocated)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ throw new MojoExecutionException("Configured service type " + relocated
+ + " is not available through Java " + candidates.get(candidates.size() - 1).release
+ + " in the shaded output, selected analysis JDK, or resolved dependency hull.");
+ }
+ }
+ }
+
+ private static void mergeExport(Map exports, Export addition) {
+ Export existing = exports.get(addition.packaze);
+ if (existing == null) {
+ exports.put(addition.packaze, addition);
+ } else {
+ exports.put(
+ addition.packaze,
+ new Export(
+ addition.packaze,
+ existing.access | addition.access,
+ mergeDirectiveTargets(existing.targets, addition.targets)));
+ }
+ }
+
+ private static void mergeOpen(Map opens, Open addition) {
+ Open existing = opens.get(addition.packaze);
+ if (existing == null) {
+ opens.put(addition.packaze, addition);
+ } else {
+ opens.put(
+ addition.packaze,
+ new Open(
+ addition.packaze,
+ existing.access | addition.access,
+ mergeDirectiveTargets(existing.targets, addition.targets)));
+ }
+ }
+
+ private static SortedSet mergeDirectiveTargets(SortedSet existing, SortedSet addition) {
+ if (existing == null || addition == null) {
+ return null;
+ }
+ SortedSet targets = new TreeSet<>(existing);
+ targets.addAll(addition);
+ return targets;
+ }
+
+ private void addRequirements(
+ MergedDescriptor target,
+ Collection requirements,
+ Set embeddedNames,
+ Origin origin,
+ boolean primary) {
+ for (Require requirement : requirements) {
+ if (embeddedNames.contains(requirement.module)) {
+ continue;
+ }
+ Require retained = primary
+ ? requirement
+ : new Require(
+ requirement.module, requirement.access & Opcodes.ACC_STATIC_PHASE, requirement.version);
+ target.requires.merge(retained.module, retained, Require::strongest);
+ if (retained.isTransitive()) {
+ transitiveRequirementOrigins.putIfAbsent(requirement.module, origin);
+ }
+ }
+ }
+
+ private void addAutomaticRequirements(MergedDescriptor target, int release) throws MojoExecutionException {
+ for (RetainedClass retainedClass : effectiveRetainedClasses(release).values()) {
+ if (retainedClass.origin.effectiveDescriptor(release) != null) {
+ continue;
+ }
+
+ PlatformModuleIndex platform = platformModules();
+ if (retainedClass.effectiveRelease > platform.getRelease()) {
+ throw analysisJdkTooOld(retainedClass.effectiveRelease, platform.getRelease());
+ }
+ if (warnedAutomaticAnalysis.add(retainedClass.origin.file)) {
+ logger.warn("Inferring module requirements from bytecode retained from automatic module "
+ + retainedClass.origin.effectiveOutputModuleName(release) + " in " + retainedClass.origin.file
+ + "; reflective and string-only dependencies cannot be inferred.");
+ }
+
+ if (!retainedClass.unresolvedServiceUses.isEmpty()) {
+ if (configuration.getDynamicUses().isEmpty()) {
+ throw new MojoExecutionException("Cannot preserve ServiceLoader semantics for automatic module "
+ + retainedClass.origin.effectiveOutputModuleName(release) + " in "
+ + retainedClass.origin.file
+ + ": retained class " + toClassName(retainedClass.className) + " invokes "
+ + retainedClass.unresolvedServiceUses.first()
+ + ", but its service type cannot be determined from bytecode. List every possible service "
+ + "type in moduleInfo.dynamicUses.");
+ }
+ if (warnedDynamicUses.add(retainedClass.origin.file)) {
+ logger.warn("Using moduleInfo.dynamicUses as the complete service-type set for dynamic "
+ + "ServiceLoader calls retained from " + retainedClass.origin.file + '.');
+ }
+ }
+ for (String service : retainedClass.serviceUses) {
+ target.uses.add(toClassName(service));
+ }
+
+ for (String reference : retainedClass.references) {
+ if (hasClass(reference, release)) {
+ continue;
+ }
+ SortedSet owners = findModuleOwners(reference, release);
+ if (owners.isEmpty()) {
+ throw new MojoExecutionException("Cannot infer module requirements for automatic module "
+ + retainedClass.origin.effectiveOutputModuleName(release) + " in "
+ + retainedClass.origin.file
+ + ": retained class " + toClassName(retainedClass.className) + " references "
+ + toClassName(reference)
+ + ", which is not present in the shaded output, any resolved dependency, or the "
+ + "Java " + release + " platform represented by " + effectiveAnalysisJdkHome()
+ + '.');
+ }
+ if (owners.size() > 1) {
+ throw new MojoExecutionException("Cannot infer module requirements for automatic module "
+ + retainedClass.origin.effectiveOutputModuleName(release) + " in "
+ + retainedClass.origin.file
+ + ": retained class " + toClassName(retainedClass.className) + " references "
+ + toClassName(reference) + ", which is owned by multiple modules " + owners + '.');
+ }
+ String module = owners.first();
+ if ("java.base".equals(module) || isAmalgamatedModuleName(module)) {
+ continue;
+ }
+ target.requires.merge(module, new Require(module, 0, null), Require::strongest);
+ logger.debug("Inferred requirement on " + module + " from automatic-module reference "
+ + toClassName(retainedClass.className) + " -> " + toClassName(reference));
+ }
+ }
+ }
+
+ private Map effectiveRetainedClasses(int release) {
+ Map result = new LinkedHashMap<>();
+ Map root = retainedClasses.get(0);
+ if (root != null) {
+ result.putAll(root);
+ }
+ for (Map.Entry> entry : retainedClasses.entrySet()) {
+ if (entry.getKey() > 0 && entry.getKey() <= release) {
+ result.putAll(entry.getValue());
+ }
+ }
+ return result;
+ }
+
+ private SortedSet findModuleOwners(String className, int release) throws MojoExecutionException {
+ SortedSet owners = platformModules().findOwners(className, release);
+ for (ArtifactData artifact : dependencyArtifacts.values()) {
+ if (artifact.hasClass(className, release)) {
+ String moduleName = artifact.configuredModuleName(release);
+ if (moduleName != null) {
+ owners.add(moduleName);
+ }
+ }
+ }
+ return owners;
+ }
+
+ private PlatformModuleIndex platformModules() throws MojoExecutionException {
+ if (platformModules == null) {
+ platformModules = new PlatformModuleIndex(effectiveAnalysisJdkHome());
+ }
+ return platformModules;
+ }
+
+ private File effectiveAnalysisJdkHome() {
+ return analysisJdkHome == null ? new File(System.getProperty("java.home")) : analysisJdkHome;
+ }
+
+ private MojoExecutionException analysisJdkTooOld(int requiredRelease, int actualRelease) {
+ return new MojoExecutionException("Module-info analysis requires Java " + requiredRelease
+ + " platform data, but the selected JDK at " + effectiveAnalysisJdkHome() + " provides Java "
+ + actualRelease + ". Configure moduleInfo.analysisJdkToolchain with Java " + requiredRelease
+ + " or newer.");
+ }
+
+ private static String toClassName(String internalName) {
+ return internalName.replace('/', '.');
+ }
+
+ private void addServices(MergedDescriptor target, ModuleDescriptorData source, boolean project, Origin origin) {
+ for (String service : source.uses) {
+ target.uses.add(relocateClass(service));
+ }
+ for (Map.Entry> entry : source.provides.entrySet()) {
+ String service = relocateClass(entry.getKey());
+ for (String provider : entry.getValue()) {
+ String relocatedProvider = relocateClass(provider);
+ if (project || hasClass(relocatedProvider, target.release)) {
+ target.provides
+ .computeIfAbsent(service, key -> new TreeSet<>())
+ .add(relocatedProvider);
+ providerOrigins.putIfAbsent(new ServiceProvider(service, relocatedProvider), origin);
+ } else {
+ logger.warn("Omitting module provider " + relocatedProvider + " from " + origin.artifact
+ + " because the class is not present in the shaded output.");
+ }
+ }
+ }
+ }
+
+ private void validateProjectDirectives(MergedDescriptor result, ModuleDescriptorData primary, int release)
+ throws MojoExecutionException {
+ Set packages = effectivePackages(release);
+ for (String packaze : result.primaryExports.keySet()) {
+ if (!packages.contains(packaze)) {
+ throw new MojoExecutionException("Cannot create module " + result.name + ": exported package " + packaze
+ + " from the primary descriptor is absent from the shaded output at Java " + release + '.');
+ }
+ }
+ for (String packaze : result.primaryOpens.keySet()) {
+ if (!packages.contains(packaze)) {
+ throw new MojoExecutionException("Cannot create module " + result.name + ": opened package " + packaze
+ + " from the primary descriptor is absent from the shaded output at Java " + release + '.');
+ }
+ }
+ if (result.mainClass != null && !hasClass(result.mainClass, release)) {
+ throw new MojoExecutionException("Cannot create module " + result.name + ": main class " + result.mainClass
+ + " from the primary descriptor is absent from the shaded output at Java " + release + '.');
+ }
+ for (Map.Entry> provides : primary.provides.entrySet()) {
+ for (String provider : provides.getValue()) {
+ String relocated = relocateClass(provider);
+ if (!hasClass(relocated, release)) {
+ throw new MojoExecutionException("Cannot create module " + result.name + ": provider " + relocated
+ + " from the primary descriptor is absent from the shaded output at Java " + release + '.');
+ }
+ }
+ }
+ }
+
+ private void validatePrimaryBoundary(List candidates) throws MojoExecutionException {
+ MergedDescriptor first = candidates.get(0);
+ for (MergedDescriptor candidate : candidates) {
+ if (!Objects.equals(first.name, candidate.name)
+ || first.access != candidate.access
+ || !Objects.equals(first.version, candidate.version)
+ || !Objects.equals(first.mainClass, candidate.mainClass)
+ || !Objects.equals(first.targetPlatform, candidate.targetPlatform)
+ || !Objects.equals(first.resolutionFlags, candidate.resolutionFlags)
+ || !Objects.equals(first.primaryExports, candidate.primaryExports)
+ || !Objects.equals(first.primaryOpens, candidate.primaryOpens)) {
+ throw new MojoExecutionException(
+ "Primary module descriptors for " + primaryArtifact
+ + " change module identity, exports, opens, main class, target, or resolution across releases.");
+ }
+ }
+ }
+
+ private void normalizeInvariantRequirements(List candidates) {
+ Map invariant = new TreeMap<>();
+ for (MergedDescriptor candidate : candidates) {
+ for (Require requirement : candidate.requires.values()) {
+ if (!isPlatformModule(requirement.module) || requirement.isTransitive()) {
+ invariant.merge(requirement.module, requirement, Require::strongest);
+ }
+ }
+ }
+ for (MergedDescriptor candidate : candidates) {
+ for (String module : invariant.keySet()) {
+ candidate.requires.remove(module);
+ }
+ candidate.requires.putAll(invariant);
+ }
+ }
+
+ private void normalizeProvidersAndFloor(List candidates, int earliestRelease)
+ throws MojoExecutionException {
+ Map> invariantProvides = new TreeMap<>();
+ Map invariantExports = new TreeMap<>();
+ Map invariantOpens = new TreeMap<>();
+ for (MergedDescriptor candidate : candidates) {
+ mergeProvides(invariantProvides, candidate.provides);
+ for (Export export : candidate.exports.values()) {
+ mergeExport(invariantExports, export);
+ }
+ for (Open open : candidate.opens.values()) {
+ mergeOpen(invariantOpens, open);
+ }
+ }
+
+ int floor = earliestRelease;
+ List reasons = new ArrayList<>();
+ Map providerAvailability = new HashMap<>();
+ for (Map.Entry> provides : invariantProvides.entrySet()) {
+ for (String provider : provides.getValue()) {
+ int available = firstAvailableRelease(provider, candidates);
+ if (available < 0) {
+ throw new MojoExecutionException("Cannot create module " + outputModuleName + ": provider class "
+ + provider + " is absent.");
+ }
+ if (available > earliestRelease) {
+ ServiceProvider key = new ServiceProvider(provides.getKey(), provider);
+ reasons.add(FloorReason.provider(key, available, providerOrigins.get(key)));
+ }
+ providerAvailability.put(new ServiceProvider(provides.getKey(), provider), available);
+ floor = Math.max(floor, available);
+ }
+ }
+
+ for (String packaze : invariantExports.keySet()) {
+ int available = firstAvailablePackageRelease(packaze, candidates);
+ if (available < 0) {
+ throw new MojoExecutionException(
+ "Cannot export package " + packaze + " from " + outputModuleName + ": the package is absent.");
+ }
+ if (available > earliestRelease) {
+ reasons.add(FloorReason.exportedPackage(packaze, available));
+ floor = Math.max(floor, available);
+ }
+ }
+ for (String packaze : invariantOpens.keySet()) {
+ int available = firstAvailablePackageRelease(packaze, candidates);
+ if (available < 0) {
+ throw new MojoExecutionException(
+ "Cannot open package " + packaze + " from " + outputModuleName + ": the package is absent.");
+ }
+ if (available > earliestRelease) {
+ reasons.add(FloorReason.openPackage(packaze, available));
+ floor = Math.max(floor, available);
+ }
+ }
+
+ Map platformTransitiveStart = new TreeMap<>();
+ for (MergedDescriptor candidate : candidates) {
+ for (Require requirement : candidate.requires.values()) {
+ if (isPlatformModule(requirement.module) && requirement.isTransitive()) {
+ platformTransitiveStart.putIfAbsent(requirement.module, candidate.release);
+ }
+ }
+ }
+ for (Map.Entry requirement : platformTransitiveStart.entrySet()) {
+ if (requirement.getValue() > earliestRelease) {
+ reasons.add(FloorReason.requirement(
+ requirement.getKey(),
+ requirement.getValue(),
+ transitiveRequirementOrigins.get(requirement.getKey())));
+ }
+ floor = Math.max(floor, requirement.getValue());
+ }
+
+ for (Map.Entry provider : providerAvailability.entrySet()) {
+ if (provider.getValue() < floor && !hasRootClass(provider.getKey().provider)) {
+ throw new MojoExecutionException("Provider " + provider.getKey().provider + " for "
+ + provider.getKey().service + " first available in Java " + provider.getValue()
+ + " cannot be exposed while " + outputModuleName + " is automatic below its Java " + floor
+ + " modular floor: META-INF/services cannot be versioned.");
+ }
+ }
+
+ this.modularFloor = floor;
+ this.floorReasons = reasons;
+ for (MergedDescriptor candidate : candidates) {
+ candidate.provides.clear();
+ mergeProvides(candidate.provides, invariantProvides);
+ candidate.exports.clear();
+ candidate.exports.putAll(invariantExports);
+ candidate.opens.clear();
+ candidate.opens.putAll(invariantOpens);
+ }
+ }
+
+ private void bridgeAutomaticServices(
+ List candidates, int earliestRelease, ServicesResourceTransformer servicesTransformer) {
+ if (servicesTransformer == null || modularFloor <= earliestRelease) {
+ return;
+ }
+ MergedDescriptor floorDescriptor = null;
+ for (MergedDescriptor candidate : candidates) {
+ if (candidate.release >= modularFloor) {
+ floorDescriptor = candidate;
+ break;
+ }
+ }
+ if (floorDescriptor == null) {
+ return;
+ }
+ for (Map.Entry> provides : floorDescriptor.provides.entrySet()) {
+ for (String provider : provides.getValue()) {
+ if (hasRootClass(provider)) {
+ servicesTransformer.addServiceProvider(provides.getKey(), provider, floorDescriptor.timestamp);
+ }
+ }
+ }
+ }
+
+ private void logRaisedFloor(int earliestRelease) {
+ if (modularFloor <= earliestRelease) {
+ return;
+ }
+ logger.warn("Raising the module descriptor floor for " + outputModuleName + " from Java " + earliestRelease
+ + " to Java " + modularFloor + '.');
+ logger.warn("No explicit module descriptor will be effective on Java " + earliestRelease + " through "
+ + (modularFloor - 1) + "; the JAR will be the automatic module " + outputModuleName
+ + " on those releases.");
+ logger.warn("Automatic-Module-Name has been set to keep the module name stable.");
+ logger.warn("Reasons:");
+ List reasons = new ArrayList<>(floorReasons);
+ Collections.sort(reasons);
+ for (FloorReason reason : reasons) {
+ logger.warn(" - " + reason.describe());
+ }
+ }
+
+ private void writeDescriptor(JarOutputStream output, String entryName, MergedDescriptor descriptor)
+ throws IOException {
+ ClassWriter writer = new ClassWriter(0);
+ writer.visit(
+ descriptor.classVersion, descriptor.classAccess | Opcodes.ACC_MODULE, "module-info", null, null, null);
+ ModuleVisitor module = writer.visitModule(descriptor.name, descriptor.access, descriptor.version);
+ if (descriptor.mainClass != null) {
+ module.visitMainClass(toInternalName(descriptor.mainClass));
+ }
+ for (String packaze : descriptor.packages) {
+ module.visitPackage(toInternalName(packaze));
+ }
+ for (Require requirement : descriptor.requires.values()) {
+ module.visitRequire(requirement.module, requirement.access, requirement.version);
+ }
+ for (Export export : descriptor.exports.values()) {
+ module.visitExport(toInternalName(export.packaze), export.access, toArrayOrNull(export.targets));
+ }
+ for (Open open : descriptor.opens.values()) {
+ module.visitOpen(toInternalName(open.packaze), open.access, toArrayOrNull(open.targets));
+ }
+ for (String use : descriptor.uses) {
+ module.visitUse(toInternalName(use));
+ }
+ for (Map.Entry> provides : descriptor.provides.entrySet()) {
+ String[] providers = provides.getValue().stream()
+ .map(ModuleInfoProcessor::toInternalName)
+ .toArray(String[]::new);
+ module.visitProvide(toInternalName(provides.getKey()), providers);
+ }
+ module.visitEnd();
+ if (descriptor.targetPlatform != null) {
+ writer.visitAttribute(new ModuleTargetAttribute(descriptor.targetPlatform));
+ }
+ if (descriptor.resolutionFlags != null) {
+ writer.visitAttribute(new ModuleResolutionAttribute(descriptor.resolutionFlags));
+ }
+ writer.visitEnd();
+
+ JarEntry entry = new JarEntry(entryName);
+ entry.setTime(descriptor.timestamp);
+ output.putNextEntry(entry);
+ output.write(writer.toByteArray());
+ }
+
+ private SortedSet collectReleaseBreakpoints() {
+ SortedSet releases = new TreeSet<>();
+ for (ArtifactData artifact : artifacts.values()) {
+ for (DescriptorEntry descriptor : artifact.descriptorsByName.values()) {
+ if (descriptor.included && (descriptor.physicalRelease == 0 || artifact.multiRelease)) {
+ releases.add(descriptor.effectiveRelease());
+ }
+ }
+ }
+ for (Integer release : outputClasses.keySet()) {
+ releases.add(release == 0 ? FIRST_MODULE_RELEASE : release);
+ }
+ releases.removeIf(release -> release < FIRST_MODULE_RELEASE);
+ return releases;
+ }
+
+ private int findEarliestPrimaryRelease(SortedSet releases) {
+ ArtifactData primary = artifacts.get(primaryArtifact);
+ for (int release : releases) {
+ DescriptorEntry descriptor = primary.effectiveDescriptor(release);
+ if (descriptor != null && descriptor.included) {
+ return release;
+ }
+ }
+ return -1;
+ }
+
+ private boolean hasIncludedRootPrimaryDescriptor() {
+ ArtifactData primary = artifacts.get(primaryArtifact);
+ DescriptorEntry root = primary.descriptorsByName.get("module-info.class");
+ return root != null && root.included;
+ }
+
+ private SortedSet collectAllPackages(List candidates) {
+ SortedSet packages = new TreeSet<>();
+ for (MergedDescriptor candidate : candidates) {
+ if (candidate.release >= modularFloor) {
+ packages.addAll(candidate.packages);
+ }
+ }
+ return packages;
+ }
+
+ private Set effectivePackages(int release) {
+ return effectivePackages(outputPackages, release);
+ }
+
+ private Set effectiveArtifactPackages(File artifact, int release) {
+ return effectivePackages(
+ artifactOutputPackages.getOrDefault(artifact, Collections.>emptyMap()), release);
+ }
+
+ private Set effectivePackages(Map> packages, int release) {
+ Set result = new LinkedHashSet<>();
+ Set root = packages.get(0);
+ if (root != null) {
+ result.addAll(root);
+ }
+ for (Map.Entry> entry : packages.entrySet()) {
+ if (entry.getKey() > 0 && entry.getKey() <= release) {
+ result.addAll(entry.getValue());
+ }
+ }
+ return result;
+ }
+
+ private boolean hasClass(String className, int release) {
+ String internalName = toInternalName(className);
+ if (outputClasses.getOrDefault(0, Collections.emptySet()).contains(internalName)) {
+ return true;
+ }
+ for (Map.Entry> entry : outputClasses.entrySet()) {
+ if (entry.getKey() > 0
+ && entry.getKey() <= release
+ && entry.getValue().contains(internalName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasRootClass(String className) {
+ return outputClasses.getOrDefault(0, Collections.emptySet()).contains(toInternalName(className));
+ }
+
+ private int firstAvailableRelease(String className, List candidates) {
+ for (MergedDescriptor candidate : candidates) {
+ if (hasClass(className, candidate.release)) {
+ return candidate.release;
+ }
+ }
+ return -1;
+ }
+
+ private int firstAvailablePackageRelease(String packaze, List candidates) {
+ for (MergedDescriptor candidate : candidates) {
+ if (effectivePackages(candidate.release).contains(packaze)) {
+ return candidate.release;
+ }
+ }
+ return -1;
+ }
+
+ private String relocatePackage(String packaze) {
+ return relocateEntity(packaze);
+ }
+
+ private String relocateClass(String clazz) {
+ return relocateEntity(clazz);
+ }
+
+ private String relocateEntity(String name) {
+ if (name == null) {
+ return null;
+ }
+ String dotted = name.replace('/', '.');
+ String path = name.replace('.', '/');
+ for (Relocator relocator : relocators) {
+ if (relocator.canRelocateClass(dotted)) {
+ return relocator.relocateClass(dotted).replace('/', '.');
+ } else if (relocator.canRelocatePath(path)) {
+ return relocator.relocatePath(path).replace('/', '.');
+ }
+ }
+ return dotted;
+ }
+
+ private void determinePrimaryModuleName() throws MojoExecutionException {
+ if (primaryArtifact == null) {
+ return;
+ }
+ ArtifactData primary = artifacts.get(primaryArtifact);
+ if (primary == null || primary.descriptorsByName.isEmpty()) {
+ return;
+ }
+ Set names = new TreeSet<>();
+ for (DescriptorEntry descriptor : primary.descriptorsByName.values()) {
+ if (descriptor.physicalRelease == 0 || primary.multiRelease) {
+ names.add(descriptor.descriptor.name);
+ }
+ }
+ if (names.isEmpty()) {
+ return;
+ }
+ if (names.size() != 1) {
+ throw new MojoExecutionException(
+ "Primary artifact " + primaryArtifact + " has conflicting module names " + names + '.');
+ }
+ primaryModuleName = names.iterator().next();
+ if (primary.automaticModuleNameExplicit
+ && primary.automaticModuleName != null
+ && !primaryModuleName.equals(primary.automaticModuleName)) {
+ throw new MojoExecutionException("Primary artifact " + primaryArtifact + " declares Automatic-Module-Name "
+ + primary.automaticModuleName + " but its descriptor declares " + primaryModuleName + '.');
+ }
+ }
+
+ private void determineOutputModuleName() throws MojoExecutionException {
+ if (primaryModuleName == null) {
+ return;
+ }
+ String configuredModuleName = configuration.getModuleName();
+ if (configuredModuleName == null) {
+ outputModuleName = primaryModuleName;
+ } else if (!ModuleInfoConfiguration.isValidModuleName(configuredModuleName)) {
+ throw new MojoExecutionException(
+ "Invalid moduleInfo.moduleName '" + configuredModuleName + "': expected a qualified Java name.");
+ } else {
+ outputModuleName = configuredModuleName;
+ }
+ }
+
+ private boolean isAmalgamatedModuleName(String moduleName) {
+ return primaryModuleName.equals(moduleName) || outputModuleName.equals(moduleName);
+ }
+
+ private boolean primaryRootDescriptorRequiresVersionedOutput() {
+ if (primaryArtifact == null) {
+ return false;
+ }
+ ArtifactData primary = artifacts.get(primaryArtifact);
+ if (primary == null) {
+ return false;
+ }
+ DescriptorEntry root = primary.descriptorsByName.get("module-info.class");
+ return root != null && root.effectiveRelease() > FIRST_MODULE_RELEASE;
+ }
+
+ private void warnDroppedAttributes(ModuleDescriptorData descriptor, Origin origin) {
+ for (String attribute : descriptor.attributes) {
+ String warningKey = origin.describe() + ':' + attribute;
+ if (!warnedDroppedAttributes.add(warningKey)) {
+ continue;
+ }
+ if ("ModuleHashes".equals(attribute)) {
+ logger.warn("Dropping ModuleHashes from " + origin.describe()
+ + " because shading changes the module contents and invalidates the recorded hashes.");
+ } else {
+ logger.warn("Dropping unsupported module descriptor attribute " + attribute + " from "
+ + origin.describe() + " because it cannot be safely rewritten.");
+ }
+ }
+ }
+
+ private static String deriveAutomaticModuleName(String filename) {
+ String name = filename;
+ if (name.toLowerCase().endsWith(".jar")) {
+ name = name.substring(0, name.length() - 4);
+ }
+ Matcher version = Pattern.compile("-(\\d+(?:[.]|$))").matcher(name);
+ if (version.find()) {
+ name = name.substring(0, version.start());
+ }
+ return name.replaceAll("[^A-Za-z0-9]", ".").replaceAll("[.]+", ".").replaceAll("^[.]|[.]$", "");
+ }
+
+ private static boolean isPlatformModule(String module) {
+ return module.startsWith("java.") || module.startsWith("jdk.");
+ }
+
+ private static int classRelease(int classVersion) {
+ int major = classVersion & 0xFFFF;
+ return Math.max(FIRST_MODULE_RELEASE, major - 44);
+ }
+
+ private static String toInternalName(String name) {
+ return name == null ? null : name.replace('.', '/');
+ }
+
+ private static String[] toArrayOrNull(SortedSet values) {
+ return values == null ? null : values.toArray(new String[0]);
+ }
+
+ private static SortedSet asSortedSet(String[] values) {
+ return values == null ? null : new TreeSet<>(Arrays.asList(values));
+ }
+
+ private static SortedSet removeEmbeddedTargets(SortedSet targets, Set embedded) {
+ if (targets == null) {
+ return new TreeSet<>();
+ }
+ SortedSet result = new TreeSet<>(targets);
+ result.removeAll(embedded);
+ return result;
+ }
+
+ private static SortedSet targetsOrNull(SortedSet original, SortedSet filtered) {
+ return original == null ? null : filtered;
+ }
+
+ private static void mergeProvides(
+ Map> target, Map> source) {
+ for (Map.Entry> entry : source.entrySet()) {
+ target.computeIfAbsent(entry.getKey(), key -> new TreeSet<>()).addAll(entry.getValue());
+ }
+ }
+
+ private static final class ArtifactData {
+ private final File file;
+ private final Map descriptorsByName = new LinkedHashMap<>();
+ private final Map serviceConfigurations = new LinkedHashMap<>();
+ private final Map> classes = new TreeMap<>();
+ private boolean multiRelease;
+ private String automaticModuleName;
+ private boolean automaticModuleNameExplicit;
+
+ private ArtifactData(File file) {
+ this.file = file;
+ }
+
+ private void readManifest(Manifest manifest) {
+ if (manifest == null) {
+ return;
+ }
+ Attributes attributes = manifest.getMainAttributes();
+ multiRelease = "true".equalsIgnoreCase(attributes.getValue("Multi-Release"));
+ automaticModuleName = attributes.getValue("Automatic-Module-Name");
+ automaticModuleNameExplicit = automaticModuleName != null;
+ }
+
+ private void addDescriptor(String name, ModuleDescriptorData descriptor, long time) {
+ int release = 0;
+ Matcher matcher = VERSIONED_MODULE_INFO.matcher(name);
+ if (matcher.matches()) {
+ release = Integer.parseInt(matcher.group(1));
+ }
+ descriptorsByName.put(name, new DescriptorEntry(name, release, descriptor, time));
+ }
+
+ private void addServiceConfiguration(ServiceConfiguration configuration) {
+ serviceConfigurations.put(configuration.name, configuration);
+ }
+
+ private void addClass(String name) {
+ Matcher matcher = VERSIONED_CLASS.matcher(name);
+ int release = 0;
+ String className;
+ if (matcher.matches()) {
+ release = Integer.parseInt(matcher.group(1));
+ className = matcher.group(2);
+ } else {
+ className = name.substring(0, name.length() - ".class".length());
+ }
+ classes.computeIfAbsent(release, key -> new LinkedHashSet<>()).add(className);
+ }
+
+ private boolean hasClass(String className, int release) {
+ if (classes.getOrDefault(0, Collections.emptySet()).contains(className)) {
+ return true;
+ }
+ if (!multiRelease) {
+ return false;
+ }
+ for (Map.Entry> entry : classes.entrySet()) {
+ if (entry.getKey() > 0
+ && entry.getKey() <= release
+ && entry.getValue().contains(className)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private DescriptorEntry effectiveDescriptor(int release) {
+ return effectiveDescriptor(release, true);
+ }
+
+ private DescriptorEntry effectiveDescriptor(int release, boolean includedOnly) {
+ DescriptorEntry result = null;
+ for (DescriptorEntry descriptor : descriptorsByName.values()) {
+ if (includedOnly && !descriptor.included) {
+ continue;
+ }
+ if (descriptor.physicalRelease > 0 && !multiRelease) {
+ continue;
+ }
+ if (descriptor.effectiveRelease() <= release
+ && (result == null || descriptor.physicalRelease > result.physicalRelease)) {
+ result = descriptor;
+ }
+ }
+ return result;
+ }
+
+ private String effectiveOutputModuleName(int release) {
+ DescriptorEntry descriptor = effectiveDescriptor(release);
+ return descriptor == null ? automaticModuleName : descriptor.descriptor.name;
+ }
+
+ private String configuredModuleName(int release) {
+ DescriptorEntry descriptor = effectiveDescriptor(release, false);
+ return descriptor == null ? automaticModuleName : descriptor.descriptor.name;
+ }
+ }
+
+ private static final class ServiceConfiguration {
+ private final String name;
+ private final String service;
+ private final SortedSet providers;
+ private boolean included;
+
+ private ServiceConfiguration(String name, String service, SortedSet providers) {
+ this.name = name;
+ this.service = service;
+ this.providers = providers;
+ }
+ }
+
+ private static final class RetainedClass {
+ private final ArtifactData origin;
+ private final String className;
+ private final int effectiveRelease;
+ private final Set references;
+ private final Set serviceUses;
+ private final SortedSet unresolvedServiceUses;
+
+ private RetainedClass(
+ ArtifactData origin,
+ String className,
+ int effectiveRelease,
+ Set references,
+ Set serviceUses,
+ SortedSet unresolvedServiceUses) {
+ this.origin = origin;
+ this.className = className;
+ this.effectiveRelease = effectiveRelease;
+ this.references = references;
+ this.serviceUses = serviceUses;
+ this.unresolvedServiceUses = unresolvedServiceUses;
+ }
+ }
+
+ private static final class DescriptorEntry {
+ private final String name;
+ private final int physicalRelease;
+ private final ModuleDescriptorData descriptor;
+ private final long time;
+ private boolean included;
+
+ private DescriptorEntry(String name, int physicalRelease, ModuleDescriptorData descriptor, long time) {
+ this.name = name;
+ this.physicalRelease = physicalRelease;
+ this.descriptor = descriptor;
+ this.time = time;
+ }
+
+ private int effectiveRelease() {
+ return Math.max(
+ physicalRelease == 0 ? FIRST_MODULE_RELEASE : physicalRelease,
+ classRelease(descriptor.classVersion));
+ }
+ }
+
+ private static final class ModuleDescriptorData {
+ private int classVersion;
+ private int classAccess;
+ private String name;
+ private int access;
+ private String version;
+ private String mainClass;
+ private String targetPlatform;
+ private Integer resolutionFlags;
+ private final Map requires = new TreeMap<>();
+ private final Map exports = new TreeMap<>();
+ private final Map opens = new TreeMap<>();
+ private final SortedSet uses = new TreeSet<>();
+ private final Map> provides = new TreeMap<>();
+ private final SortedSet packages = new TreeSet<>();
+ private final SortedSet attributes = new TreeSet<>();
+ }
+
+ private static final class MergedDescriptor {
+ private final int release;
+ private final long timestamp;
+ private int classVersion;
+ private int classAccess;
+ private String name;
+ private int access;
+ private String version;
+ private String mainClass;
+ private String targetPlatform;
+ private Integer resolutionFlags;
+ private final Map requires = new TreeMap<>();
+ private final Map primaryExports = new TreeMap<>();
+ private final Map primaryOpens = new TreeMap<>();
+ private final Map exports = new TreeMap<>();
+ private final Map opens = new TreeMap<>();
+ private final SortedSet uses = new TreeSet<>();
+ private final Map> provides = new TreeMap<>();
+ private final SortedSet packages = new TreeSet<>();
+
+ private MergedDescriptor(int release, DescriptorEntry primary) {
+ this.release = release;
+ this.timestamp = primary.time;
+ }
+
+ private boolean sameModuleSemantics(MergedDescriptor other) {
+ return other != null
+ && Objects.equals(name, other.name)
+ && access == other.access
+ && Objects.equals(version, other.version)
+ && Objects.equals(mainClass, other.mainClass)
+ && Objects.equals(targetPlatform, other.targetPlatform)
+ && Objects.equals(resolutionFlags, other.resolutionFlags)
+ && Objects.equals(requires, other.requires)
+ && Objects.equals(exports, other.exports)
+ && Objects.equals(opens, other.opens)
+ && Objects.equals(uses, other.uses)
+ && Objects.equals(provides, other.provides)
+ && Objects.equals(packages, other.packages);
+ }
+ }
+
+ private static final class ModuleTargetAttribute extends Attribute {
+ private String targetPlatform;
+
+ private ModuleTargetAttribute() {
+ this(null);
+ }
+
+ private ModuleTargetAttribute(String targetPlatform) {
+ super("ModuleTarget");
+ this.targetPlatform = targetPlatform;
+ }
+
+ @Override
+ protected Attribute read(
+ ClassReader classReader,
+ int offset,
+ int length,
+ char[] charBuffer,
+ int codeAttributeOffset,
+ Label[] labels) {
+ return new ModuleTargetAttribute(classReader.readUTF8(offset, charBuffer));
+ }
+
+ @Override
+ protected ByteVector write(ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
+ return new ByteVector().putShort(classWriter.newUTF8(targetPlatform));
+ }
+ }
+
+ private static final class ModuleResolutionAttribute extends Attribute {
+ private int resolutionFlags;
+
+ private ModuleResolutionAttribute() {
+ this(0);
+ }
+
+ private ModuleResolutionAttribute(int resolutionFlags) {
+ super("ModuleResolution");
+ this.resolutionFlags = resolutionFlags;
+ }
+
+ @Override
+ protected Attribute read(
+ ClassReader classReader,
+ int offset,
+ int length,
+ char[] charBuffer,
+ int codeAttributeOffset,
+ Label[] labels) {
+ return new ModuleResolutionAttribute(classReader.readUnsignedShort(offset));
+ }
+
+ @Override
+ protected ByteVector write(ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
+ return new ByteVector().putShort(resolutionFlags);
+ }
+ }
+
+ private static final class Require {
+ private final String module;
+ private final int access;
+ private final String version;
+
+ private Require(String module, int access, String version) {
+ this.module = module;
+ this.access = access;
+ this.version = version;
+ }
+
+ private boolean isTransitive() {
+ return (access & Opcodes.ACC_TRANSITIVE) != 0;
+ }
+
+ private static Require strongest(Require left, Require right) {
+ int access = (left.access | right.access) & ~Opcodes.ACC_STATIC_PHASE;
+ if ((left.access & Opcodes.ACC_STATIC_PHASE) != 0 && (right.access & Opcodes.ACC_STATIC_PHASE) != 0) {
+ access |= Opcodes.ACC_STATIC_PHASE;
+ }
+ String version = left.version != null ? left.version : right.version;
+ return new Require(left.module, access, version);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof Require)) {
+ return false;
+ }
+ Require other = (Require) object;
+ return access == other.access
+ && Objects.equals(module, other.module)
+ && Objects.equals(version, other.version);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(module, access, version);
+ }
+ }
+
+ private abstract static class PackageDirective {
+ protected final String packaze;
+ protected final int access;
+ protected final SortedSet targets;
+
+ private PackageDirective(String packaze, int access, SortedSet targets) {
+ this.packaze = packaze;
+ this.access = access;
+ this.targets = targets;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object == null || object.getClass() != getClass()) {
+ return false;
+ }
+ PackageDirective other = (PackageDirective) object;
+ return access == other.access
+ && Objects.equals(packaze, other.packaze)
+ && Objects.equals(targets, other.targets);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packaze, access, targets);
+ }
+ }
+
+ private static final class Export extends PackageDirective {
+ private Export(String packaze, int access, SortedSet targets) {
+ super(packaze, access, targets);
+ }
+ }
+
+ private static final class Open extends PackageDirective {
+ private Open(String packaze, int access, SortedSet targets) {
+ super(packaze, access, targets);
+ }
+ }
+
+ private static final class ServiceProvider {
+ private final String service;
+ private final String provider;
+
+ private ServiceProvider(String service, String provider) {
+ this.service = service;
+ this.provider = provider;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof ServiceProvider)) {
+ return false;
+ }
+ ServiceProvider other = (ServiceProvider) object;
+ return Objects.equals(service, other.service) && Objects.equals(provider, other.provider);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(service, provider);
+ }
+ }
+
+ private static final class Origin {
+ private final File artifact;
+ private final String entry;
+
+ private Origin(File artifact, DescriptorEntry descriptor) {
+ this(artifact, descriptor.name);
+ }
+
+ private Origin(File artifact, String entry) {
+ this.artifact = artifact;
+ this.entry = entry;
+ }
+
+ private String describe() {
+ return artifact + "!" + entry;
+ }
+ }
+
+ private static final class FloorReason implements Comparable {
+ private final String description;
+
+ private FloorReason(String description) {
+ this.description = description;
+ }
+
+ private static FloorReason provider(ServiceProvider provider, int release, Origin origin) {
+ return new FloorReason("provider " + provider.provider + " for " + provider.service + " from "
+ + describe(origin) + " is not present before Java " + release);
+ }
+
+ private static FloorReason requirement(String module, int release, Origin origin) {
+ return new FloorReason("transitive platform requirement " + module + " from " + describe(origin)
+ + " cannot be represented before Java " + release);
+ }
+
+ private static FloorReason exportedPackage(String packaze, int release) {
+ return new FloorReason("exported package " + packaze + " is not present before Java " + release);
+ }
+
+ private static FloorReason openPackage(String packaze, int release) {
+ return new FloorReason("open package " + packaze + " is not present before Java " + release);
+ }
+
+ private static String describe(Origin origin) {
+ return origin == null ? "an embedded descriptor" : origin.describe();
+ }
+
+ private String describe() {
+ return description;
+ }
+
+ @Override
+ public int compareTo(FloorReason other) {
+ return description.compareTo(other.description);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/ModuleInfoPublicBoundary.java b/src/main/java/org/apache/maven/plugins/shade/ModuleInfoPublicBoundary.java
new file mode 100644
index 00000000..3c0cfb68
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/ModuleInfoPublicBoundary.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.shade;
+
+import java.util.Locale;
+
+/**
+ * Controls which embedded package boundaries are retained in a merged module descriptor.
+ */
+public enum ModuleInfoPublicBoundary {
+ /** Retain only the primary artifact's exports and opens. */
+ PRIMARY,
+
+ /** Merge the effective exports and opens of embedded explicit and automatic modules. */
+ MERGE;
+
+ /**
+ * Parses a public boundary mode without regard to case.
+ *
+ * @param value public boundary mode
+ * @return parsed public boundary mode
+ * @throws IllegalArgumentException if the value is not a supported mode
+ */
+ public static ModuleInfoPublicBoundary fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("Module info public boundary must not be null.");
+ }
+ try {
+ return valueOf(value.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Unknown module info public boundary '" + value + "'. Expected one of: primary, merge.");
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/PlatformModuleIndex.java b/src/main/java/org/apache/maven/plugins/shade/PlatformModuleIndex.java
new file mode 100644
index 00000000..5ce397f4
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/PlatformModuleIndex.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.shade;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.objectweb.asm.ClassReader;
+
+/**
+ * Release-aware ownership index for classes in a selected JDK.
+ */
+final class PlatformModuleIndex {
+ private static final int FIRST_MODULE_RELEASE = 9;
+
+ private final File jdkHome;
+
+ private final Map>> classOwners = new HashMap<>();
+
+ private final Map> modules = new HashMap<>();
+
+ private final int release;
+
+ PlatformModuleIndex(File jdkHome) throws MojoExecutionException {
+ this.jdkHome = normalizeJdkHome(jdkHome);
+ this.release = readCurrentRelease();
+ readHistoricalReleases();
+ readCurrentReleaseClasses();
+ }
+
+ int getRelease() {
+ return release;
+ }
+
+ SortedSet findOwners(String className, int targetRelease) {
+ Map> releaseOwners = classOwners.get(targetRelease);
+ if (releaseOwners == null) {
+ return new TreeSet<>();
+ }
+ Set owners = releaseOwners.get(className);
+ return owners == null ? new TreeSet() : new TreeSet<>(owners);
+ }
+
+ boolean hasModule(String module, int targetRelease) {
+ return modules.getOrDefault(targetRelease, Collections.emptySet())
+ .contains(module);
+ }
+
+ private int readCurrentRelease() throws MojoExecutionException {
+ File javaBase = new File(new File(jdkHome, "jmods"), "java.base.jmod");
+ if (!javaBase.isFile()) {
+ throw invalidJdk("missing jmods/java.base.jmod");
+ }
+ try (JarFile jmod = new JarFile(javaBase)) {
+ JarEntry descriptor = jmod.getJarEntry("classes/module-info.class");
+ if (descriptor == null) {
+ throw invalidJdk("jmods/java.base.jmod has no module descriptor");
+ }
+ try (InputStream input = jmod.getInputStream(descriptor)) {
+ return classRelease(new ClassReader(input).readShort(6));
+ }
+ } catch (IOException | IllegalArgumentException e) {
+ throw new MojoExecutionException("Cannot read the module-info analysis JDK at " + jdkHome, e);
+ }
+ }
+
+ private void readHistoricalReleases() throws MojoExecutionException {
+ File ctSym = new File(new File(jdkHome, "lib"), "ct.sym");
+ if (!ctSym.isFile()) {
+ if (release > FIRST_MODULE_RELEASE) {
+ throw invalidJdk("missing lib/ct.sym");
+ }
+ return;
+ }
+ try (JarFile archive = new JarFile(ctSym)) {
+ Enumeration entries = archive.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ if (entry.isDirectory()) {
+ continue;
+ }
+ String[] parts = entry.getName().split("/", 3);
+ if (parts.length < 3) {
+ continue;
+ }
+ String releaseSet = parts[0];
+ String module = parts[1];
+ String path = parts[2];
+ if ("module-info.sig".equals(path)) {
+ forEachRelease(
+ releaseSet,
+ value -> modules.computeIfAbsent(value, key -> new TreeSet<>())
+ .add(module));
+ } else if (path.endsWith(".sig")) {
+ String className = path.substring(0, path.length() - ".sig".length());
+ forEachRelease(releaseSet, value -> addOwner(value, className, module));
+ }
+ }
+ } catch (IOException e) {
+ throw new MojoExecutionException("Cannot read " + ctSym + " for module-info analysis", e);
+ }
+ }
+
+ private void readCurrentReleaseClasses() throws MojoExecutionException {
+ File jmods = new File(jdkHome, "jmods");
+ File[] files = jmods.listFiles((directory, name) -> name.endsWith(".jmod"));
+ if (files == null || files.length == 0) {
+ throw invalidJdk("the jmods directory is empty");
+ }
+ for (File file : files) {
+ String module = file.getName().substring(0, file.getName().length() - ".jmod".length());
+ modules.computeIfAbsent(release, key -> new TreeSet<>()).add(module);
+ try (JarFile jmod = new JarFile(file)) {
+ Enumeration entries = jmod.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (!entry.isDirectory()
+ && name.startsWith("classes/")
+ && name.endsWith(".class")
+ && !"classes/module-info.class".equals(name)) {
+ addOwner(
+ release,
+ name.substring("classes/".length(), name.length() - ".class".length()),
+ module);
+ }
+ }
+ } catch (IOException e) {
+ throw new MojoExecutionException("Cannot read " + file + " for module-info analysis", e);
+ }
+ }
+ }
+
+ private void forEachRelease(String releaseSet, ReleaseConsumer consumer) {
+ for (int index = 0; index < releaseSet.length(); index++) {
+ int value = Character.digit(releaseSet.charAt(index), 36);
+ if (value >= FIRST_MODULE_RELEASE && value < release) {
+ consumer.accept(value);
+ }
+ }
+ }
+
+ private void addOwner(int targetRelease, String className, String module) {
+ classOwners
+ .computeIfAbsent(targetRelease, key -> new HashMap<>())
+ .computeIfAbsent(className, key -> new TreeSet<>())
+ .add(module);
+ modules.computeIfAbsent(targetRelease, key -> new TreeSet<>()).add(module);
+ }
+
+ private MojoExecutionException invalidJdk(String reason) {
+ return new MojoExecutionException("Invalid module-info analysis JDK at " + jdkHome + ": " + reason
+ + ". Configure moduleInfo.analysisJdkToolchain with a complete modular JDK.");
+ }
+
+ private static File normalizeJdkHome(File configured) {
+ File home = configured == null ? new File(System.getProperty("java.home")) : configured;
+ if (!new File(home, "jmods").isDirectory()
+ && home.getParentFile() != null
+ && new File(home.getParentFile(), "jmods").isDirectory()) {
+ return home.getParentFile();
+ }
+ return home;
+ }
+
+ private static int classRelease(int classVersion) {
+ return (classVersion & 0xFFFF) - 44;
+ }
+
+ private interface ReleaseConsumer {
+ void accept(int release);
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/ShadeRequest.java b/src/main/java/org/apache/maven/plugins/shade/ShadeRequest.java
index c7781452..b014653d 100644
--- a/src/main/java/org/apache/maven/plugins/shade/ShadeRequest.java
+++ b/src/main/java/org/apache/maven/plugins/shade/ShadeRequest.java
@@ -19,6 +19,7 @@
package org.apache.maven.plugins.shade;
import java.io.File;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -34,6 +35,8 @@ public class ShadeRequest {
private Set jars;
+ private File primaryArtifact;
+
private File uberJar;
private List filters;
@@ -44,6 +47,14 @@ public class ShadeRequest {
private boolean shadeSourcesContent;
+ private ModuleInfoMode moduleInfoMode = ModuleInfoMode.DISCARD;
+
+ private ModuleInfoConfiguration moduleInfoConfiguration = new ModuleInfoConfiguration();
+
+ private File moduleInfoAnalysisJdkHome;
+
+ private Set dependencyAnalysisArtifacts = Collections.emptySet();
+
public Set getJars() {
return jars;
}
@@ -57,6 +68,19 @@ public void setJars(Set jars) {
this.jars = jars;
}
+ public File getPrimaryArtifact() {
+ return primaryArtifact;
+ }
+
+ /**
+ * Sets the artifact whose module descriptor defines the identity and public boundary of the shaded module.
+ *
+ * @param primaryArtifact the primary artifact, or {@code null} when the output has no primary artifact
+ */
+ public void setPrimaryArtifact(File primaryArtifact) {
+ this.primaryArtifact = primaryArtifact;
+ }
+
public File getUberJar() {
return uberJar;
}
@@ -123,4 +147,58 @@ public boolean isShadeSourcesContent() {
public void setShadeSourcesContent(boolean shadeSourcesContent) {
this.shadeSourcesContent = shadeSourcesContent;
}
+
+ public ModuleInfoMode getModuleInfoMode() {
+ return moduleInfoMode;
+ }
+
+ /**
+ * Sets how module descriptors are handled.
+ *
+ * @param moduleInfoMode module descriptor handling mode
+ */
+ public void setModuleInfoMode(ModuleInfoMode moduleInfoMode) {
+ this.moduleInfoMode = moduleInfoMode == null ? ModuleInfoMode.DISCARD : moduleInfoMode;
+ }
+
+ public ModuleInfoConfiguration getModuleInfoConfiguration() {
+ return moduleInfoConfiguration;
+ }
+
+ /**
+ * Sets the choices used while constructing a merged descriptor.
+ *
+ * @param moduleInfoConfiguration module descriptor configuration
+ */
+ public void setModuleInfoConfiguration(ModuleInfoConfiguration moduleInfoConfiguration) {
+ this.moduleInfoConfiguration =
+ moduleInfoConfiguration == null ? new ModuleInfoConfiguration() : moduleInfoConfiguration;
+ }
+
+ public File getModuleInfoAnalysisJdkHome() {
+ return moduleInfoAnalysisJdkHome;
+ }
+
+ /**
+ * Sets the JDK home used to resolve platform-module ownership for automatic-module bytecode.
+ *
+ * @param moduleInfoAnalysisJdkHome analysis JDK home, or {@code null} to use the running JDK
+ */
+ public void setModuleInfoAnalysisJdkHome(File moduleInfoAnalysisJdkHome) {
+ this.moduleInfoAnalysisJdkHome = moduleInfoAnalysisJdkHome;
+ }
+
+ public Set getDependencyAnalysisArtifacts() {
+ return dependencyAnalysisArtifacts;
+ }
+
+ /**
+ * Sets the complete binary dependency hull used to resolve module ownership during automatic-module analysis.
+ *
+ * @param dependencyAnalysisArtifacts resolved binary artifacts, including artifacts excluded from shading
+ */
+ public void setDependencyAnalysisArtifacts(Set dependencyAnalysisArtifacts) {
+ this.dependencyAnalysisArtifacts =
+ dependencyAnalysisArtifacts == null ? Collections.emptySet() : dependencyAnalysisArtifacts;
+ }
}
diff --git a/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java b/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java
index 09e3956f..edaca412 100644
--- a/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java
+++ b/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java
@@ -51,6 +51,8 @@
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.plugins.shade.ModuleInfoConfiguration;
+import org.apache.maven.plugins.shade.ModuleInfoMode;
import org.apache.maven.plugins.shade.ShadeRequest;
import org.apache.maven.plugins.shade.Shader;
import org.apache.maven.plugins.shade.filter.Filter;
@@ -69,6 +71,8 @@
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.project.ProjectBuildingResult;
+import org.apache.maven.toolchain.Toolchain;
+import org.apache.maven.toolchain.ToolchainManager;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.WriterFactory;
import org.eclipse.aether.DefaultRepositorySystemSession;
@@ -331,6 +335,27 @@ public class ShadeMojo extends AbstractMojo {
@Parameter(property = "shadeSourcesContent", defaultValue = "false")
private boolean shadeSourcesContent;
+ /**
+ * Controls how compiled Java module descriptors are handled. The {@code discard} mode preserves the historical
+ * behavior. The {@code merge} mode retains the primary artifact's module identity by default and its public
+ * boundary while incorporating requirements and service declarations from embedded artifacts. The output module
+ * name can be changed with {@code moduleInfo.moduleName}.
+ *
+ * @see Merging Java
+ * Module Descriptors
+ * @since 3.7.0
+ */
+ @Parameter(property = "shade.moduleInfoMode", defaultValue = "discard")
+ private String moduleInfoMode = "discard";
+
+ /**
+ * Configures how the merged descriptor represents the amalgamated shaded contents.
+ *
+ * @since 3.7.0
+ */
+ @Parameter
+ private ModuleInfoConfiguration moduleInfo = new ModuleInfoConfiguration();
+
/**
* When true, dependencies will be stripped down on the class level to only the transitive hull required for the
* artifact. See also {@link #entryPoints}, if you wish to further optimize JAR minimization.
@@ -463,6 +488,9 @@ public class ShadeMojo extends AbstractMojo {
@Inject
private Map shaders;
+ @Inject
+ private ToolchainManager toolchainManager;
+
/**
* @throws MojoExecutionException in case of an error.
*/
@@ -481,6 +509,7 @@ public void execute() throws MojoExecutionException {
Set sourceArtifacts = new LinkedHashSet<>();
Set testArtifacts = new LinkedHashSet<>();
Set testSourceArtifacts = new LinkedHashSet<>();
+ Set dependencyAnalysisArtifacts = new LinkedHashSet<>();
ArtifactSelector artifactSelector = new ArtifactSelector(project.getArtifact(), artifactSet, shadedGroupFilter);
@@ -502,6 +531,7 @@ public void execute() throws MojoExecutionException {
+ " that is not a file (does not exist or is not a file)");
}
artifacts.add(extraJar);
+ dependencyAnalysisArtifacts.add(extraJar);
}
}
@@ -528,7 +558,13 @@ public void execute() throws MojoExecutionException {
}
List processedArtifacts = processArtifactSelectors(
- artifacts, artifactIds, sourceArtifacts, testArtifacts, testSourceArtifacts, artifactSelector);
+ artifacts,
+ artifactIds,
+ sourceArtifacts,
+ testArtifacts,
+ testSourceArtifacts,
+ dependencyAnalysisArtifacts,
+ artifactSelector);
File outputJar = (outputFile != null) ? outputFile : shadedArtifactFileWithClassifier();
File sourcesJar = shadedSourceArtifactFileWithClassifier();
@@ -553,8 +589,15 @@ public void execute() throws MojoExecutionException {
}
}
- ShadeRequest shadeRequest =
- shadeRequest("jar", artifacts, outputJar, filters, relocators, resourceTransformers);
+ ShadeRequest shadeRequest = shadeRequest(
+ "jar",
+ artifacts,
+ project.getArtifact().getFile(),
+ outputJar,
+ filters,
+ relocators,
+ resourceTransformers,
+ dependencyAnalysisArtifacts);
shader.shade(shadeRequest);
@@ -566,8 +609,16 @@ public void execute() throws MojoExecutionException {
}
if (shadeTestJar) {
- ShadeRequest shadeTestRequest =
- shadeRequest("test-jar", testArtifacts, testJar, filters, relocators, resourceTransformers);
+ File primaryTestArtifact = shadedTestArtifactFile();
+ ShadeRequest shadeTestRequest = shadeRequest(
+ "test-jar",
+ testArtifacts,
+ testArtifacts.contains(primaryTestArtifact) ? primaryTestArtifact : null,
+ testJar,
+ filters,
+ relocators,
+ resourceTransformers,
+ dependencyAnalysisArtifacts);
shader.shade(shadeTestRequest);
}
@@ -695,19 +746,31 @@ private void createErrorOutput() {
getLog().error("- You removed the configuration of the maven-jar-plugin that produces the main artifact.");
}
+ @SuppressWarnings("checkstyle:ParameterNumber")
private ShadeRequest shadeRequest(
String shade,
Set artifacts,
+ File primaryArtifact,
File outputJar,
List filters,
List relocators,
- List resourceTransformers) {
+ List resourceTransformers,
+ Set dependencyAnalysisArtifacts)
+ throws MojoExecutionException {
ShadeRequest shadeRequest = new ShadeRequest();
shadeRequest.setJars(artifacts);
+ shadeRequest.setPrimaryArtifact(primaryArtifact);
shadeRequest.setUberJar(outputJar);
shadeRequest.setFilters(filters);
shadeRequest.setRelocators(relocators);
shadeRequest.setResourceTransformers(toResourceTransformers(shade, resourceTransformers));
+ ModuleInfoMode parsedModuleInfoMode = ModuleInfoMode.fromString(moduleInfoMode);
+ shadeRequest.setModuleInfoMode(parsedModuleInfoMode);
+ shadeRequest.setModuleInfoConfiguration(moduleInfo);
+ if (parsedModuleInfoMode == ModuleInfoMode.MERGE && primaryArtifact != null) {
+ shadeRequest.setModuleInfoAnalysisJdkHome(resolveModuleInfoAnalysisJdkHome());
+ }
+ shadeRequest.setDependencyAnalysisArtifacts(dependencyAnalysisArtifacts);
return shadeRequest;
}
@@ -717,13 +780,67 @@ private ShadeRequest createShadeSourcesRequest(
File testJar,
List filters,
List relocators,
- List resourceTransformers) {
- ShadeRequest shadeSourcesRequest =
- shadeRequest(shade, testArtifacts, testJar, filters, relocators, resourceTransformers);
+ List resourceTransformers)
+ throws MojoExecutionException {
+ ShadeRequest shadeSourcesRequest = shadeRequest(
+ shade,
+ testArtifacts,
+ null,
+ testJar,
+ filters,
+ relocators,
+ resourceTransformers,
+ Collections.emptySet());
+ shadeSourcesRequest.setModuleInfoMode(ModuleInfoMode.DISCARD);
shadeSourcesRequest.setShadeSourcesContent(shadeSourcesContent);
return shadeSourcesRequest;
}
+ private File resolveModuleInfoAnalysisJdkHome() throws MojoExecutionException {
+ Toolchain toolchain = null;
+ Map requirements = moduleInfo.getAnalysisJdkToolchain();
+ if (!requirements.isEmpty()) {
+ List matches = toolchainManager.getToolchains(session, "jdk", requirements);
+ if (matches == null || matches.isEmpty()) {
+ throw new MojoExecutionException(
+ "No JDK toolchain matches moduleInfo.analysisJdkToolchain " + requirements + '.');
+ }
+ toolchain = matches.get(0);
+ } else {
+ toolchain = toolchainManager.getToolchainFromBuildContext("jdk", session);
+ }
+
+ if (toolchain == null) {
+ return normalizeJdkHome(new File(System.getProperty("java.home")));
+ }
+
+ String javac = toolchain.findTool("javac");
+ if (javac == null) {
+ throw new MojoExecutionException("The selected module-info analysis JDK toolchain has no javac tool.");
+ }
+ File executable = new File(javac);
+ File bin = executable.getParentFile();
+ if (bin == null || bin.getParentFile() == null) {
+ throw new MojoExecutionException(
+ "Cannot determine the JDK home from the selected javac executable " + executable + '.');
+ }
+ return normalizeJdkHome(bin.getParentFile());
+ }
+
+ private File normalizeJdkHome(File home) throws MojoExecutionException {
+ File candidate = home;
+ if (!new File(candidate, "jmods").isDirectory()
+ && candidate.getParentFile() != null
+ && new File(candidate.getParentFile(), "jmods").isDirectory()) {
+ candidate = candidate.getParentFile();
+ }
+ try {
+ return candidate.getCanonicalFile();
+ } catch (IOException e) {
+ throw new MojoExecutionException("Cannot resolve module-info analysis JDK home " + candidate, e);
+ }
+ }
+
private void setupHintedShader() throws MojoExecutionException {
if (shaderHint != null) {
shader = shaders.get(shaderHint);
@@ -741,6 +858,7 @@ private List processArtifactSelectors(
Set sourceArtifacts,
Set testArtifacts,
Set testSourceArtifacts,
+ Set dependencyAnalysisArtifacts,
ArtifactSelector artifactSelector)
throws MojoExecutionException {
@@ -774,6 +892,15 @@ private List processArtifactSelectors(
}
processedArtifacts.addAll(project.getArtifacts());
+ for (Artifact artifact : processedArtifacts) {
+ if (!"pom".equals(artifact.getType())
+ && artifact.getFile() != null
+ && (artifact.getArtifactHandler() == null
+ || artifact.getArtifactHandler().isAddedToClasspath())) {
+ dependencyAnalysisArtifacts.add(artifact.getFile());
+ }
+ }
+
// for loop over COPY; as we add to the list in this loop
for (Artifact artifact : new ArrayList<>(processedArtifacts)) {
if (!artifactSelector.isSelected(artifact)) {
diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java b/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java
index b9d63b37..0762f6da 100644
--- a/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java
+++ b/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java
@@ -59,6 +59,10 @@ public class ManifestResourceTransformer extends AbstractCompatibilityTransforme
private String shade;
+ private boolean forceMultiRelease;
+
+ private String forceAutomaticModuleName;
+
public void setMainClass(String mainClass) {
this.mainClass = mainClass;
}
@@ -71,6 +75,54 @@ public void setAdditionalAttributes(List additionalAttributes) {
this.additionalAttributes = additionalAttributes;
}
+ /**
+ * Forces the output manifest to retain multi-release processing semantics.
+ *
+ * @param forceMultiRelease whether {@code Multi-Release: true} must be written
+ */
+ public void setForceMultiRelease(boolean forceMultiRelease) {
+ this.forceMultiRelease = forceMultiRelease;
+ }
+
+ /**
+ * @return whether configured manifest entries explicitly disable multi-release processing
+ */
+ public boolean isMultiReleaseExplicitlyDisabled() {
+ if (manifestEntries == null) {
+ return false;
+ }
+ for (Map.Entry entry : manifestEntries.entrySet()) {
+ if ("Multi-Release".equalsIgnoreCase(entry.getKey())) {
+ return "false".equalsIgnoreCase(String.valueOf(entry.getValue()));
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return whether configured manifest entries explicitly enable multi-release processing
+ */
+ public boolean isMultiReleaseExplicitlyEnabled() {
+ if (manifestEntries == null) {
+ return false;
+ }
+ for (Map.Entry entry : manifestEntries.entrySet()) {
+ if ("Multi-Release".equalsIgnoreCase(entry.getKey())) {
+ return "true".equalsIgnoreCase(String.valueOf(entry.getValue()));
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Forces a stable automatic module name for releases where no explicit descriptor is effective.
+ *
+ * @param moduleName module name
+ */
+ public void setForceAutomaticModuleName(String moduleName) {
+ this.forceAutomaticModuleName = moduleName;
+ }
+
@Override
public boolean canTransformResource(String resource) {
return JarFile.MANIFEST_NAME.equalsIgnoreCase(resource);
@@ -122,31 +174,42 @@ public boolean hasTransformedResource() {
@Override
public void modifyOutputStream(JarOutputStream jos) throws IOException {
- // If we didn't find a manifest, then let's create one.
- if (manifest == null) {
- manifest = new Manifest();
- }
-
- Attributes attributes = manifest.getMainAttributes();
+ try {
+ Manifest outputManifest = manifest == null ? new Manifest() : new Manifest(manifest);
+ Attributes attributes = outputManifest.getMainAttributes();
+ if (attributes.getValue(Attributes.Name.MANIFEST_VERSION) == null) {
+ attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ }
- if (mainClass != null) {
- attributes.put(Attributes.Name.MAIN_CLASS, mainClass);
- }
+ if (mainClass != null) {
+ attributes.put(Attributes.Name.MAIN_CLASS, mainClass);
+ }
- if (manifestEntries != null) {
- for (Map.Entry entry : manifestEntries.entrySet()) {
- if (entry.getValue() == null) {
- attributes.remove(new Attributes.Name(entry.getKey()));
- } else {
- attributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
+ if (manifestEntries != null) {
+ for (Map.Entry entry : manifestEntries.entrySet()) {
+ if (entry.getValue() == null) {
+ attributes.remove(new Attributes.Name(entry.getKey()));
+ } else {
+ attributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
+ }
}
}
- }
- JarEntry jarEntry = new JarEntry(JarFile.MANIFEST_NAME);
- jarEntry.setTime(time);
- jos.putNextEntry(jarEntry);
- manifest.write(jos);
+ if (forceMultiRelease) {
+ attributes.putValue("Multi-Release", "true");
+ }
+ if (forceAutomaticModuleName != null) {
+ attributes.putValue("Automatic-Module-Name", forceAutomaticModuleName);
+ }
+
+ JarEntry jarEntry = new JarEntry(JarFile.MANIFEST_NAME);
+ jarEntry.setTime(time);
+ jos.putNextEntry(jarEntry);
+ outputManifest.write(jos);
+ } finally {
+ forceMultiRelease = false;
+ forceAutomaticModuleName = null;
+ }
}
private String relocate(String originalValue, List relocators) {
diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/ServicesResourceTransformer.java b/src/main/java/org/apache/maven/plugins/shade/resource/ServicesResourceTransformer.java
index ec8f521a..8c317089 100644
--- a/src/main/java/org/apache/maven/plugins/shade/resource/ServicesResourceTransformer.java
+++ b/src/main/java/org/apache/maven/plugins/shade/resource/ServicesResourceTransformer.java
@@ -47,6 +47,21 @@ public class ServicesResourceTransformer extends AbstractCompatibilityTransforme
private long time = Long.MIN_VALUE;
+ /**
+ * Adds an already relocated provider to a service descriptor.
+ *
+ * @param service service type
+ * @param provider provider implementation
+ * @param time entry timestamp
+ */
+ public void addServiceProvider(String service, String provider, long time) {
+ String resource = SERVICES_PATH + '/' + service;
+ serviceEntries.computeIfAbsent(resource, key -> new LinkedHashSet<>()).add(provider);
+ if (time > this.time) {
+ this.time = time;
+ }
+ }
+
@Override
public boolean canTransformResource(String resource) {
return resource.startsWith(SERVICES_PATH);
diff --git a/src/site/markdown/examples/module-info-merging.md.vm b/src/site/markdown/examples/module-info-merging.md.vm
new file mode 100644
index 00000000..2530d0f9
--- /dev/null
+++ b/src/site/markdown/examples/module-info-merging.md.vm
@@ -0,0 +1,203 @@
+---
+title: Merging Java Module Descriptors
+author:
+ - The Apache Maven Team
+date: 2026-07-30
+---
+
+
+
+# Merging Java Module Descriptors
+
+By default, the plugin discards every root and versioned `module-info.class` from the inputs. This preserves the
+historical behavior: an unmodified descriptor describes the original artifact rather than the contents of the
+shaded JAR.
+
+Modular projects can opt into descriptor merging:
+
+```xml
+
+ ...
+
+
+