diff --git a/src/it/multirelease-on-classpath/pom.xml b/src/it/multirelease-on-classpath/pom.xml index 3a47eb970..2ca77ca44 100644 --- a/src/it/multirelease-on-classpath/pom.xml +++ b/src/it/multirelease-on-classpath/pom.xml @@ -23,7 +23,7 @@ multirelease-on-classpath 1.0-SNAPSHOT jar - Mulirelease in Maven 4 + Multirelease in Maven 4 diff --git a/src/it/multirelease-with-modules/invoker.properties b/src/it/multirelease-with-modules/invoker.properties new file mode 100644 index 000000000..b015adb31 --- /dev/null +++ b/src/it/multirelease-with-modules/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 compile -Dmaven.compiler.preview=true +invoker.buildResult = success diff --git a/src/it/multirelease-with-modules/pom.xml b/src/it/multirelease-with-modules/pom.xml new file mode 100644 index 000000000..a33f0a4ff --- /dev/null +++ b/src/it/multirelease-with-modules/pom.xml @@ -0,0 +1,65 @@ + + + + 4.1.0 + org.apache.maven.plugins + multirelease-with-modules + 1.0-SNAPSHOT + jar + Multirelease with modules + + + + + org.apache.maven.plugins + maven-compiler-plugin + @project.version@ + + + + + + + + + + + foo.bar + src/foo.bar/main/java + 15 + + + foo.bar + src/foo.bar/main/java_16 + 16 + + + foo.bar.more + src/foo.bar.more/main/java + 15 + + + foo.bar.more + src/foo.bar.more/main/java_16 + 16 + + + + diff --git a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java new file mode 100644 index 000000000..778a3a4af --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java @@ -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. + */ +module foo.bar.more {} diff --git a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java new file mode 100644 index 000000000..d64f30a78 --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.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 more; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class MainFile { + public static void main(String[] args) { + System.out.println("MainFile of more"); + } +} diff --git a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java new file mode 100644 index 000000000..54e29b3c2 --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.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 more; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class OtherFile { + public static void main(String[] args) { + System.out.println("OtherFile of more"); + } +} diff --git a/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java new file mode 100644 index 000000000..4b21485ed --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java @@ -0,0 +1,30 @@ +/* + * 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 more; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class OtherFile { + public static void main(String[] args) { + System.out.println("OtherFile of more on Java 16"); + MainFile.main(args); // Verify that we have access to the base version. + } +} diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java new file mode 100644 index 000000000..502f2780e --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.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 foo; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class MainFile { + public static void main(String[] args) { + System.out.println("MainFile"); + } +} diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java new file mode 100644 index 000000000..472210e14 --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.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 foo; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class OtherFile { + public static void main(String[] args) { + System.out.println("OtherFile"); + } +} diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java new file mode 100644 index 000000000..ab5f90091 --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.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 foo; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class YetAnotherFile { + public static void main(String[] args) { + System.out.println("YetAnotherFile"); + } +} diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java new file mode 100644 index 000000000..38f61c0e8 --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java @@ -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. + */ +module foo.bar {} diff --git a/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java new file mode 100644 index 000000000..cbfa0b982 --- /dev/null +++ b/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java @@ -0,0 +1,34 @@ +/* + * 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 foo; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class OtherFile { + public static void main(String[] args) { + System.out.println("OtherFile on Java 16"); + MainFile.main(args); // Verify that we have access to the base version. + } + + static void requireJava16() { + System.out.println("Method available only on Java 16+"); + } +} diff --git a/src/it/multirelease-with-modules/verify.groovy b/src/it/multirelease-with-modules/verify.groovy new file mode 100644 index 000000000..a4d068ae5 --- /dev/null +++ b/src/it/multirelease-with-modules/verify.groovy @@ -0,0 +1,47 @@ +/* + * 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 baseVersion = 59; // Java 15 +def nextVersion = 60; // Java 16 + +assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar/foo/MainFile.class")) +assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar/foo/OtherFile.class")) +assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar/foo/YetAnotherFile.class")) +assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar.more/more/MainFile.class")) +assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar.more/more/OtherFile.class")) +assert nextVersion == getMajor(new File( basedir, "target/classes/META-INF/versions/16/foo.bar/foo/OtherFile.class")) +assert nextVersion == getMajor(new File( basedir, "target/classes/META-INF/versions/16/foo.bar.more/more/OtherFile.class")) + +int getMajor(File file) +{ + assert file.exists() + def dis = new DataInputStream(new FileInputStream(file)) + final String firstFourBytes = Integer.toHexString(dis.readUnsignedShort()) + Integer.toHexString(dis.readUnsignedShort()) + if (!firstFourBytes.equalsIgnoreCase("cafebabe")) + { + throw new IllegalArgumentException(dataSourceName + " is not a Java .class file.") + } + final int minorVersion = dis.readUnsignedShort() + final int majorVersion = dis.readUnsignedShort() + + dis.close() + return majorVersion +} diff --git a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java index c2f7a50fe..76e945a2f 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -40,6 +40,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -98,6 +99,14 @@ * @since 2.0 */ public abstract class AbstractCompilerMojo implements Mojo { + /** + * Whether feature previews are enabled. + * To enable the preview, the following option must be provided to Maven: + * + *
-Dmaven.compiler.preview=true
+ */ + static final boolean PREVIEW_ENABLED = Boolean.getBoolean("maven.compiler.preview"); + /** * Whether to support legacy (and often deprecated) behavior. * This is currently hard-coded to {@code true} for compatibility reason. @@ -1311,6 +1320,7 @@ public Options parseParameters(final OptionChecker compiler) { * @throws IOException if an input file cannot be read * @throws MojoException if the compilation failed */ + @SuppressWarnings("UseSpecificCatch") private void compile(final JavaCompiler compiler, final Options configuration) throws IOException { final ToolExecutor executor = createExecutor(null); if (!executor.applyIncrementalBuild(this, configuration)) { @@ -1353,7 +1363,7 @@ private void compile(final JavaCompiler compiler, final Options configuration) t if (!success || verbose || logger.isDebugEnabled()) { IOException suppressed = null; try { - writeDebugFile(executor, configuration); + writeDebugFile(executor, configuration, success); if (success && tipForCommandLineCompilation != null) { logger.debug(tipForCommandLineCompilation); tipForCommandLineCompilation = null; @@ -1536,6 +1546,7 @@ final DependencyResolverResult resolveDependencies(boolean hasModuleDeclaration) * {@code processor}, {@code classpath-processor} or {@code modular-processor}. */ @Deprecated(since = "4.0.0") + @SuppressWarnings("UseSpecificCatch") final void resolveProcessorPathEntries(Map> addTo) throws MojoException { List dependencies = annotationProcessorPaths; if (dependencies != null && !dependencies.isEmpty()) { @@ -1699,11 +1710,13 @@ private void writePlugin(MessageBuilder mb, String option, String value) { * * @param executor the executor that compiled the classes * @param configuration options provided to the compiler + * @param showBaseVersion whether the tip shown to user suggests the base Java release instead of the last one * @throws IOException if an error occurred while writing the debug file */ - private void writeDebugFile(final ToolExecutor executor, final Options configuration) throws IOException { - final Path path = getDebugFilePath(); - if (path == null) { + private void writeDebugFile(final ToolExecutor executor, final Options configuration, final boolean showBaseVersion) + throws IOException { + final Path debugFilePath = getDebugFilePath(); + if (debugFilePath == null) { logger.warn("The parameter should not be empty."); return; } @@ -1719,40 +1732,77 @@ private void writeDebugFile(final ToolExecutor executor, final Options configura .append(chdir); } commandLine.append(System.lineSeparator()).append(" ").append(executable != null ? executable : compilerId); - try (BufferedWriter out = Files.newBufferedWriter(path)) { - configuration.format(commandLine, out); - for (Map.Entry> entry : executor.dependencies.entrySet()) { - List files = entry.getValue(); - files = files.stream().map(this::relativize).toList(); - String separator = ""; - for (String element : entry.getKey().option(files)) { - out.write(separator); - out.write(element); - separator = " "; + Path pathForRelease = debugFilePath; + /* + * The following loop will iterate over all groups of source files compiled for the same Java release, + * starting with the base release. If the project is not a multi-release project, it iterates only once. + * If the compilation failed, the loop will stop after the first Java release for which an error occurred. + */ + final int count = executor.sourcesForDebugFile.size(); + final int indexToShow = showBaseVersion ? 0 : count - 1; + for (int i = 0; i < count; i++) { + final SourcesForRelease sources = executor.sourcesForDebugFile.get(i); + if (i != 0) { + String version = sources.outputForRelease.getFileName().toString(); + String filename = debugFilePath.getFileName().toString(); + int s = filename.lastIndexOf('.'); + if (s >= 0) { + filename = filename.substring(0, s) + '-' + version + filename.substring(s); + } else { + filename = filename + '-' + version; + } + pathForRelease = debugFilePath.resolveSibling(filename); + } + /* + * Write the `javac.args` or `javac-.args` file where `` is the targeted Java release. + * The `-J` options need to be on the command line rather than in the file, and therefore can be written + * only once. + */ + try (BufferedWriter out = Files.newBufferedWriter(pathForRelease)) { + configuration.setRelease(sources.getReleaseString()); + configuration.format((i == indexToShow) ? commandLine : null, out); + for (Map.Entry> entry : sources.dependencySnapshot.entrySet()) { + writeOption(out, entry.getKey(), entry.getValue()); + } + for (Map.Entry> root : sources.roots.entrySet()) { + String moduleName = root.getKey(); + writeOption(out, SourcePathType.valueOf(moduleName), root.getValue()); } + out.write("-d \""); + out.write(relativize(sources.outputForRelease).toString()); + out.write('"'); out.newLine(); + for (final Path file : sources.files) { + out.write('"'); + out.write(relativize(file).toString()); + out.write('"'); + out.newLine(); + } } - out.write("-d \""); - out.write(relativize(getOutputDirectory()).toString()); - out.write('"'); - out.newLine(); - try { - executor.getSourceFiles().forEach((file) -> { - try { - out.write('"'); - out.write(relativize(file).toString()); - out.write('"'); - out.newLine(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } catch (UncheckedIOException e) { - throw e.getCause(); + } + Path path = relativize(showBaseVersion ? debugFilePath : pathForRelease); + tipForCommandLineCompilation = commandLine.append(" @").append(path).toString(); + } + + /** + * Writes the paths for the given Java compiler option. + * + * @param out where to write + * @param type the type of path to write as a compiler option + * @param files the paths associated to the specified option + * @throws IOException in an error occurred while writing to the output + */ + private void writeOption(BufferedWriter out, PathType type, Collection files) throws IOException { + if (!files.isEmpty()) { + files = files.stream().map(this::relativize).toList(); + String separator = ""; + for (String element : type.option(files)) { + out.write(separator); + out.write(element); + separator = " "; } + out.newLine(); } - tipForCommandLineCompilation = - commandLine.append(" @").append(relativize(path)).toString(); } /** diff --git a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java index 5e3083ba3..e0a8ee574 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java +++ b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java @@ -624,9 +624,8 @@ String inputFileTreeChanges() throws IOException { * Each given root can be either a regular file (typically a JAR file) or a directory. * Directories are scanned recursively. * - * @param directories files or directories to scan + * @param dependencies files or directories to scan * @param fileExtensions extensions of the file to check (usually "jar" and "class") - * @param changeTime the time at which a file is considered as changed * @return {@code null} if the project does not need to be rebuilt, otherwise a message saying why to rebuild * @throws IOException if an error occurred while scanning the directories * diff --git a/src/main/java/org/apache/maven/plugin/compiler/Options.java b/src/main/java/org/apache/maven/plugin/compiler/Options.java index 47b5b61ae..58b778cd2 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/Options.java +++ b/src/main/java/org/apache/maven/plugin/compiler/Options.java @@ -387,10 +387,12 @@ void format(final StringBuilder commandLine, final Appendable out) throws IOExce continue; } if (option.startsWith("-J")) { - if (commandLine.length() != 0) { - commandLine.append(' '); + if (commandLine != null) { + if (commandLine.length() != 0) { + commandLine.append(' '); + } + commandLine.append(option); } - commandLine.append(option); continue; } if (hasOptions) { diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java b/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java index 1e986a5f3..547920f98 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java @@ -20,6 +20,7 @@ import java.io.File; import java.nio.file.Path; +import java.util.Objects; import java.util.Optional; import java.util.StringJoiner; @@ -106,6 +107,22 @@ public String[] option(Iterable paths) { return new String[] {option().get(), joiner.toString()}; } + /** + * {@return a hash code value based on the module name}. + */ + @Override + public int hashCode() { + return Objects.hashCode(moduleName) + 17; + } + + /** + * {@return whether the given object represents the same source path as this object}. + */ + @Override + public boolean equals(Object obj) { + return (obj instanceof SourcePathType) && Objects.equals(moduleName, ((SourcePathType) obj).moduleName); + } + /** * {@return a string representation for debugging purposes}. */ diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java index 017cb9597..077838143 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java @@ -30,6 +30,8 @@ import java.util.Map; import java.util.Set; +import org.apache.maven.api.PathType; + /** * Source files for a specific Java release. Instances of {@code SourcesForRelease} are created from * a list of {@link SourceFile} after the sources have been filtered according include and exclude filters. @@ -74,6 +76,19 @@ final class SourcesForRelease implements Closeable { */ private SourceDirectory lastDirectoryAdded; + /** + * Snapshot of {@link ToolExecutor#dependencies}. + * This information is saved in case a {@code target/javac.args} debug file needs to be written. + */ + Map> dependencySnapshot; + + /** + * The output directory for the release. This is either the base output directory or a sub-directory + * in {@code META-INF/versions/}. This field is not used by this class, but made available for making + * easier to write the {@code target/javac.args} debug file. + */ + Path outputForRelease; + /** * Creates an initially empty instance for the given Java release. * diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java index 4177459b4..0b67e6f8b 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java @@ -34,7 +34,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.EnumMap; import java.util.EnumSet; @@ -45,7 +44,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; import org.apache.maven.api.JavaPathType; import org.apache.maven.api.PathType; @@ -130,17 +128,17 @@ public class ToolExecutor { * The path type can be the class-path, module-path, annotation processor path, patched path, etc. * Some path types include a module name. * + *

Modifications during the build of multi-release project

+ * When building a multi-release project, values associated to {@code --class-path}, {@code --module-path} + * or {@code --patch-module} options are modified every time that {@code ToolExecutor} compiles for a new + * Java release. The output directories for the previous Java releases are inserted as the first elements + * of their lists, or new entries are created if no list existed previously for an option. + * * @see #dependencies(PathType) + * @see #prependDependency(PathType, Path) */ protected final Map> dependencies; - /** - * The classpath given to the compiler. Stored for making possible to prepend the paths - * of the compilation results of previous versions in a multi-version JAR file. - * This list needs to be modifiable. - */ - private List classpath; - /** * The destination directory (or class output directory) for class files. * This directory will be given to the {@code -d} Java compiler option @@ -188,6 +186,13 @@ public class ToolExecutor { */ protected final Log logger; + /** + * The sources to write in the {@code target/javac.args} debug files. + * This list contains only the sources for which the compiler has been executed, successfully or not. + * If a compilation error occurred, the last element in the list contains the sources where the error occurred. + */ + final List sourcesForDebugFile; + /** * Creates a new task by taking a snapshot of the current configuration of the given MOJO. * This constructor creates the {@linkplain #outputDirectory output directory} if it does not already exist. @@ -206,8 +211,8 @@ protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener(); + sourcesForDebugFile = new ArrayList<>(); /* * Get the source files and whether they include or are assumed to include `module-info.java`. * Note that we perform this step after processing compiler arguments, because this block may @@ -254,26 +260,21 @@ protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener e.setValue(new ArrayList<>(e.getValue()))); + copyDependencyValues(); } mojo.resolveProcessorPathEntries(dependencies); } /** - * {@return the source files to compile}. + * Copies all values of the dependency map in unmodifiable lists. + * This is used for creating a snapshot of the current state of the dependency map. */ - public Stream getSourceFiles() { - return sourceFiles.stream().map((s) -> s.file); + private void copyDependencyValues() { + dependencies.entrySet().forEach((entry) -> entry.setValue(List.copyOf(entry.getValue()))); } /** @@ -366,7 +367,17 @@ public boolean applyIncrementalBuild(final AbstractCompilerMojo mojo, final Opti * @param pathType type of path for which to get the dependencies */ protected List dependencies(PathType pathType) { - return dependencies.computeIfAbsent(pathType, (key) -> new ArrayList<>()); + return dependencies.compute(pathType, (key, paths) -> { + if (paths == null) { + return new ArrayList<>(); + } else if (paths instanceof ArrayList) { + return paths; + } else { + var copy = new ArrayList(paths.size() + 4); // Anticipate the addition of new elements. + copy.addAll(paths); + return copy; + } + }); } /** @@ -389,8 +400,6 @@ private void setDependencyPaths(final StandardJavaFileManager fileManager) throw if (location.isPresent()) { // Cannot use `Optional.ifPresent(…)` because of checked IOException. var value = location.get(); if (value == StandardLocation.CLASS_PATH) { - classpath = new ArrayList<>(paths); // Need a modifiable list. - paths = classpath; if (isPartialBuild && !hasModuleDeclaration) { /* * From https://docs.oracle.com/en/java/javase/24/docs/specs/man/javac.html: @@ -399,9 +408,11 @@ private void setDependencyPaths(final StandardJavaFileManager fileManager) throw * When not compiling for modules, for backwards compatibility, the directory is not * automatically checked for previously compiled classes, and so it is recommended to * specify the class output directory as one of the locations on the user class path, - * using the --class-path option or one of its alternate forms." + * using the --class-path option or one of its alternate forms." */ + paths = new ArrayList<>(paths); paths.add(outputDirectory); + entry.setValue(paths); } } fileManager.setLocationFromPaths(value, paths); @@ -414,14 +425,7 @@ private void setDependencyPaths(final StandardJavaFileManager fileManager) throw */ Optional location = type.rawType().location(); if (location.isPresent()) { - try { - fileManager.setLocationForModule(location.get(), type.moduleName(), paths); - } catch (UnsupportedOperationException e) { // Happen with `PATCH_MODULE_PATH`. - var it = Arrays.asList(type.option(paths)).iterator(); - if (!fileManager.handleOption(it.next(), it) || it.hasNext()) { - throw new CompilationFailureException("Cannot handle " + type, e); - } - } + fileManager.setLocationForModule(location.get(), type.moduleName(), paths); continue; } } @@ -436,6 +440,24 @@ private void setDependencyPaths(final StandardJavaFileManager fileManager) throw } } + /** + * Inserts the given path as the first element of the list of paths of the given type. + * The main purpose of this method is during the build of a multi-release project, + * for adding the output directory of the code targeting the previous Java release + * before to compile the code targeting the next Java release. In this context, + * the {@code type} argument usually identifies a {@code --class-path}, + * {@code --module-path} or {@code --patch-module} option. + * + * @param pathType type of path for which to add an element + * @param first the path to put first + * @return the new paths for the given type, as a modifiable list + */ + protected List prependDependency(final PathType pathType, final Path first) { + List paths = dependencies(pathType); + paths.add(0, first); + return paths; + } + /** * Ensures that the given value is non-null, replacing null values by the latest version. */ @@ -487,6 +509,24 @@ private Collection groupByReleaseAndModule() { return result.values(); } + /** + * Creates the file manager which will be used by the compiler. + * This method does not configure the locations (sources, dependencies, etc.). + * Locations will be set by {@link #compile(JavaCompiler, Options, Writer)} on the + * file manager returned by this method. + * + * @param compiler the compiler + * @param workaround whether to apply {@link WorkaroundForPatchModule} + * @return the file manager to use + */ + private StandardJavaFileManager createFileManager(JavaCompiler compiler, boolean workaround) { + StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, LOCALE, encoding); + if (workaround && WorkaroundForPatchModule.ENABLED) { + fileManager = new WorkaroundForPatchModule(fileManager); + } + return fileManager; + } + /** * Runs the compilation task. * @@ -498,12 +538,13 @@ private Collection groupByReleaseAndModule() { * @throws MojoException if the compilation failed for a reason identified by this method * @throws RuntimeException if any other kind of error occurred */ - @SuppressWarnings("checkstyle:MagicNumber") + @SuppressWarnings("checkstyle:MethodLength") public boolean compile(final JavaCompiler compiler, final Options configuration, final Writer otherOutput) throws IOException { /* * Announce what the compiler is about to do. */ + sourcesForDebugFile.clear(); if (sourceFiles.isEmpty()) { String message = "No sources to compile."; try { @@ -530,7 +571,7 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, * disposal in order to reuse its cache. */ boolean success = true; - try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, LOCALE, encoding)) { + try (StandardJavaFileManager fileManager = createFileManager(compiler, hasModuleDeclaration)) { setDependencyPaths(fileManager); if (!generatedSourceDirectories.isEmpty()) { fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, generatedSourceDirectories); @@ -548,13 +589,18 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, Path outputForRelease = null; boolean isClasspathProject = false; boolean isModularProject = false; + String defaultModuleName = null; configuration.setRelease(unit.getReleaseString()); for (final Map.Entry> root : unit.roots.entrySet()) { - final String moduleName = inferModuleNameIfMissing(root.getKey()); + final String declaredModuleName = root.getKey(); + final String moduleName = inferModuleNameIfMissing(declaredModuleName); if (moduleName.isEmpty()) { isClasspathProject = true; } else { isModularProject = true; + if (declaredModuleName.isEmpty()) { // Modular project using package source hierarchy. + defaultModuleName = moduleName; + } } if (isClasspathProject & isModularProject) { throw new CompilationFailureException("Mix of modular and non-modular sources."); @@ -567,40 +613,50 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, } outputForRelease = outputDirectory; // Modified below if compiling a non-base release. if (isVersioned) { + outputForRelease = Files.createDirectories( + SourceDirectory.outputDirectoryForReleases(outputForRelease, unit.release)); if (isClasspathProject) { /* * For a non-modular project, this block is executed at most once par compilation unit. * Add the paths to the classes compiled for previous versions. */ - if (classpath == null) { - classpath = new ArrayList<>(); - } - classpath.add(0, latestOutputDirectory); + List classpath = prependDependency(JavaPathType.CLASSES, latestOutputDirectory); fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath); - outputForRelease = Files.createDirectories( - SourceDirectory.outputDirectoryForReleases(outputForRelease, unit.release)); + } else if (!AbstractCompilerMojo.PREVIEW_ENABLED) { + throw new CompilationFailureException("Multi-release in a modular project " + + "is a preview feature. For enabling this feature, add the " + + "-Dmaven.compiler.preview=true option to the mvn command."); } else { /* * For a modular project, this block can be executed an arbitrary number of times * (once per module). - * TODO: need to provide --patch-module. Difficulty is that we can specify only once. */ - throw new UnsupportedOperationException( - "Multi-versions of a modular project is not yet implemented."); - } - } else { - /* - * This addition is for allowing AbstractCompilerMojo.writeDebugFile(…) to show those paths. - * It has no effect on the compilation performed in this method, because the dependencies - * have already been set by the call to `setDependencyPaths(fileManager)`. - */ - if (!sourcePaths.isEmpty()) { - dependencies.put(SourcePathType.valueOf(moduleName), List.copyOf(sourcePaths)); + Path latestOutputForModule = latestOutputDirectory.resolve(moduleName); + JavaPathType.Modular pathType = JavaPathType.patchModule(moduleName); + List paths = prependDependency(pathType, latestOutputForModule); + fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName, paths); } } } + /* + * At this point, we finished to set the source paths. We have also modified the class-path or + * patched the modules with the output directories of codes compiled for lower Java releases. + * The `defaultModuleName` is an adjustment done when the project is a Java module, but still + * organized in a package source hierarchy instead of a module source hierarchy. Updating the + * `unit.roots` map is not needed for this class, but done in case a `target/javac.args` file + * will be written after the compilation. + */ + if (defaultModuleName != null) { + Set paths = unit.roots.remove(""); + if (paths != null) { + unit.roots.put(defaultModuleName, paths); + } + } + copyDependencyValues(); + unit.dependencySnapshot = new LinkedHashMap<>(dependencies); fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(outputForRelease)); latestOutputDirectory = outputForRelease; + unit.outputForRelease = outputForRelease; /* * Compile the source files now. The following loop should be executed exactly once. * It may be executed twice when compiling test classes overwriting the `module-info`, @@ -610,8 +666,25 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, JavaCompiler.CompilationTask task; for (CompilationTaskSources c : toCompilationTasks(unit)) { Iterable sources = fileManager.getJavaFileObjectsFromPaths(c.files); - task = compiler.getTask(otherOutput, fileManager, listener, configuration.options, null, sources); + StandardJavaFileManager workaround = fileManager; + boolean workaroundNeedsClose = false; + // Check flag separately to clearly indicate this entire block is a workaround hack. + if (WorkaroundForPatchModule.ENABLED) { + if (workaround instanceof WorkaroundForPatchModule wp) { + workaround = wp.getFileManagerIfUsable(); + if (workaround == null) { + workaround = createFileManager(compiler, false); + wp.copyTo(workaround); + workaroundNeedsClose = true; + } + } + } + task = compiler.getTask(otherOutput, workaround, listener, configuration.options, null, sources); success = c.compile(task); + if (workaroundNeedsClose) { + workaround.close(); + } + sourcesForDebugFile.add(unit); if (!success) { break compile; } diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java index 6be8b699e..8e4ba652c 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java @@ -199,7 +199,7 @@ class ToolExecutorForTest extends ToolExecutor { } } } - dependencies(pathType).add(0, mainOutputDirectory); + prependDependency(pathType, mainOutputDirectory); } } diff --git a/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java b/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java new file mode 100644 index 000000000..e659a84fd --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java @@ -0,0 +1,237 @@ +/* + * 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.plugin.compiler; + +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.maven.api.JavaPathType; + +/** + * Workaround for a {@code javax.tools} method which seems not yet supported on all compilers. + * At least with OpenJDK 24, an {@link UnsupportedOperationException} may occur during the call to + * {@code fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName, paths)}. + * The workaround is to format the paths in a {@code --patch-module} option instead. + * The problem is that we can specify this option only once per file manager instance. + * + *

We may remove this workaround in a future version of the Maven Compiler Plugin + * if the {@code UnsupportedOperationException} is fixed in a future Java release. + * For checking if this workaround is still necessary, set {@link #ENABLED} to {@code false} + * and run the JUnit tests.

+ * + * @author Martin Desruisseaux + */ +final class WorkaroundForPatchModule extends ForwardingJavaFileManager + implements StandardJavaFileManager { + /** + * Set this flag to {@code false} for testing if this workaround is still necessary. + */ + static final boolean ENABLED = true; + + /** + * All locations that have been successfully specified to the file manager through programmatic API. + * This set excludes the {@code PATCH_MODULE_PATH} locations which were defined using the workaround + * described in class Javadoc. + */ + private final Set definedLocations; + + /** + * The locations that we had to define by formatting a {@code --patch-module} option. + * Keys are module names and values are the paths for the associated module. + */ + private final Map> patchesAsOption; + + /** + * Whether the caller needs to create a new file manager. + * It happens when we have been unable to set a {@code --patch-module} option on the current file manager. + */ + private boolean needsNewFileManager; + + /** + * Creates a new workaround for the given file manager. + */ + WorkaroundForPatchModule(final StandardJavaFileManager fileManager) { + super(fileManager); + definedLocations = new HashSet<>(); + patchesAsOption = new HashMap<>(); + } + + /** + * {@return the original file manager, or {@code null} if the caller needs to create a new one}. + * The returned value is {@code null} when we have been unable to set a {@code --patch-module} + * option on the current file manager. In such case, the caller should create a new file manager + * and configure it with {@link #copyTo(StandardJavaFileManager)}. + */ + StandardJavaFileManager getFileManagerIfUsable() { + return needsNewFileManager ? null : fileManager; + } + + /** + * Copies the locations defined in this file manager to the given file manager. + * + * @param target where to copy the locations + * @throws IOException if a location cannot be set on the target file manager + */ + void copyTo(final StandardJavaFileManager target) throws IOException { + for (JavaFileManager.Location location : definedLocations) { + target.setLocation(location, fileManager.getLocation(location)); + } + for (Map.Entry> entry : patchesAsOption.entrySet()) { + Collection paths = entry.getValue(); + String moduleName = entry.getKey(); + try { + target.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName, paths); + } catch (UnsupportedOperationException e) { + specifyAsOption(target, JavaPathType.patchModule(moduleName), paths, e); + } + } + } + + /** + * Sets a module path by asking the file manager to parse an option formatted by this method. + * Invoked when a module path cannot be specified through the API + * This is the workaround described in class Javadoc. + * + * @param fileManager the file manager on which an attempt to set the location has been made and failed + * @param type the type of path together with the module name + * @param paths the paths to set + * @param cause the exception that occurred when invoking the standard API + * @throws CompilationFailureException if this workaround doesn't work neither + */ + private static void specifyAsOption( + StandardJavaFileManager fileManager, + JavaPathType.Modular type, + Collection paths, + UnsupportedOperationException cause) + throws IOException { + + var it = Arrays.asList(type.option(paths)).iterator(); + if (!fileManager.handleOption(it.next(), it) || it.hasNext()) { + throw new CompilationFailureException("Cannot handle " + type, cause); + } + } + + /** + * Adds the given module path to the file manager. + * If we cannot do that using the programmatic API, formats as a command-line option. + */ + @Override + public void setLocationForModule( + JavaFileManager.Location location, String moduleName, Collection paths) throws IOException { + + if (paths.isEmpty()) { + return; + } + final boolean isPatch = (location == StandardLocation.PATCH_MODULE_PATH); + if (isPatch && patchesAsOption.replace(moduleName, paths) != null) { + /* + * The patch was already specified by formatting the `--patch-module` option. + * We cannot do that again, because that option can appear only once per module. + */ + needsNewFileManager = true; + return; + } + try { + fileManager.setLocationForModule(location, moduleName, paths); + } catch (UnsupportedOperationException e) { + if (isPatch) { + specifyAsOption(fileManager, JavaPathType.patchModule(moduleName), paths, e); + patchesAsOption.put(moduleName, paths); + return; + } + throw e; + } + definedLocations.add(fileManager.getLocationForModule(location, moduleName)); + } + + /** + * Adds the given path to the file manager. + */ + @Override + public void setLocationFromPaths(JavaFileManager.Location location, Collection paths) + throws IOException { + fileManager.setLocationFromPaths(location, paths); + definedLocations.add(location); + } + + @Override + public void setLocation(Location location, Iterable files) throws IOException { + fileManager.setLocation(location, files); + definedLocations.add(location); + } + + @Override + public Iterable getLocation(Location location) { + return fileManager.getLocation(location); + } + + @Override + public Iterable getLocationAsPaths(Location location) { + return fileManager.getLocationAsPaths(location); + } + + @Override + public Iterable getJavaFileObjects(String... names) { + return fileManager.getJavaFileObjects(names); + } + + @Override + public Iterable getJavaFileObjects(File... files) { + return fileManager.getJavaFileObjects(files); + } + + @Override + public Iterable getJavaFileObjects(Path... paths) { + return fileManager.getJavaFileObjects(paths); + } + + @Override + public Iterable getJavaFileObjectsFromStrings(Iterable names) { + return fileManager.getJavaFileObjectsFromStrings(names); + } + + @Override + public Iterable getJavaFileObjectsFromFiles(Iterable files) { + return fileManager.getJavaFileObjectsFromFiles(files); + } + + @Override + public Iterable getJavaFileObjectsFromPaths(Collection paths) { + return fileManager.getJavaFileObjectsFromPaths(paths); + } + + @Override + public Path asPath(FileObject file) { + return fileManager.asPath(file); + } +}