desruisseaux commented on code in PR #508: URL: https://github.com/apache/maven-jar-plugin/pull/508#discussion_r3844265707
########## src/main/java/org/apache/maven/plugins/jar/MetadataFiles.java: ########## @@ -0,0 +1,250 @@ +/* + * 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.jar; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import org.apache.maven.api.ProducedArtifact; +import org.apache.maven.api.Project; +import org.apache.maven.shared.archiver.MavenArchiveConfiguration; + +/** + * Temporary metadata files generated by Maven before inclusion in the archive. + * Those files are created in a temporary {@code META-INF} directory when first needed. + * Those files are deleted after the build, unless the build failed or Maven was run in verbose mode. + */ +final class MetadataFiles implements Closeable { + /** + * The {@value} directory. + * This is part of <abbr>JAR</abbr> file specification. + */ + static final String META_INF = "META-INF"; + + /** + * The {@value} file. + * This is part of <abbr>JAR</abbr> file specification. + */ + static final String MANIFEST = "MANIFEST.MF"; + + /** + * The subdirectory where to add Maven-specific files. + */ + static final String MAVEN_DIR = "maven"; + + /** + * The project for which to write metadata files. + */ + private final Project project; + + /** + * The output directory (usually {@code ${baseDir}/target/}). + */ + private final Path buildDir; + + /** + * All files and directories in the order that they were created. + * The first element of this list shall be the root temporary directory created by this class. + */ + private final List<Path> filesToDelete; + + /** + * The <abbr>POM</abbr> file to attach to the artifact. This is initially the project <abbr>POM</abbr>, + * but will be replaced by a new file generated by {@link ForModule} if module hierarchy is used. + * That file may be copied verbatim in the {@code META-INF/maven/} directory of the <abbr>JAR</abbr>. + */ + private Path attachedPOM; + + /** + * Creates an initially empty set of temporary metadata files. + * + * @param project the project for which to write metadata files + * @param buildDir the (usually) {@code ${baseDir}/target/} directory + */ + MetadataFiles(Project project, Path buildDir) { + this.project = project; + this.buildDir = buildDir; + filesToDelete = new ArrayList<>(); + attachedPOM = project.getPomPath(); + } + + /** + * Derives a <abbr>POM</abbr> as the intersection of the given {@code model} and {@code archive}. + * + * @param context the tool executor which is generating all archives + * @param archive the archive for which to generate a <abbr>POM</abbr> + * @param manifest manifest to use for deriving project name, or {@code null} if none + * @throws IOException if an error occurred while reading the {@code module-info.class} file + * or while writing the <abbr>POM</abbr> file + */ + void deriveModulePOM(ToolExecutor context, Archive archive, Manifest manifest) throws IOException { + var pom = context.pomDerivation.new ForModule(archive, manifest); + pom.writeModulePOM(); + attachedPOM = pom.pomFile; + archive.pomFile = pom.pomFile; + } + + /** + * Adds the given manifest in a temporary file. + * The file will be deleted when {@link #close()} is invoked. + * + * @param manifest the manifest to write + * @return the temporary manifest file + * @throws IOException if an error occurred while writing the file + */ + public Path addManifest(final Manifest manifest) throws IOException { + Path file = baseDirectory().resolve(MANIFEST); + try (OutputStream out = Files.newOutputStream(file)) { + filesToDelete.add(file); + manifest.write(out); + } + return file; + } + + /** + * {@return the root temporary directory for the files created by this class} + * The directory is created the first time that this method is invoked. + * + * @throws IOException if an error occurred while creating the temporary directory + */ + private Path baseDirectory() throws IOException { + if (filesToDelete.isEmpty()) { + filesToDelete.add(Files.createTempDirectory(buildDir, "classes-")); + } + return filesToDelete.get(0); + } + + /** + * Creates a new directory and adds it to the list of files to delete after the build. + * + * @param dir the existing directory where to create a sub-directory + * @param path path to the subdirectory to create + * @return the new directory + * @throws IOException if an error occurred while creating the subdirectory + */ + private Path createDirectories(Path dir, String... path) throws IOException { + for (String subdir : path) { + dir = Files.createDirectory(dir.resolve(subdir)); + filesToDelete.add(dir); + } + return dir; + } + + /** + * Writes the {@code pom.xml} and {@code pom.properties} files. + * This method returns the base temporary directory followed by files that the "jar" tool will need to add + * + * @param archive archive configuration + * @param reproducible whether to enforce reproducible build + * @return arguments for the "jar" tool + * @throws IOException if an error occurred while writing the files + */ + public List<Path> addPOM(final MavenArchiveConfiguration archive, final boolean reproducible) throws IOException { + final String groupId = project.getGroupId(); + final String artifactId = project.getArtifactId(); + final String version; + final ProducedArtifact pom = project.getPomArtifact(); + if (pom.isSnapshot()) { + version = pom.getVersion().toString(); + } else { + version = project.getVersion(); + } + final Path baseDir = baseDirectory(); + final Path mavenDir = createDirectories(baseDir, META_INF, MAVEN_DIR, groupId, artifactId); + final Path pomFile = linkOrCopy(attachedPOM, mavenDir.resolve("pom.xml")); + filesToDelete.add(pomFile); // Add soon for deleting this file even if an exception is thrown below. + /* + * Subset of above "pom.xml" file but written as a properties file. + * If reproducible build is enabled, we will need to reformat after + * writing for ensuring a deterministic order of entries. + */ + final var properties = new Properties(); + Path propertiesFile = archive.getPomPropertiesFile(); + if (propertiesFile != null) { + try (InputStream in = Files.newInputStream(propertiesFile)) { + properties.load(in); + } + } + properties.setProperty("groupId", groupId); + properties.setProperty("artifactId", artifactId); + properties.setProperty("version", version); + propertiesFile = mavenDir.resolve("pom.properties"); + try (BufferedWriter out = Files.newBufferedWriter(propertiesFile)) { + filesToDelete.add(propertiesFile); // Add soon for deleting this file even if an exception is thrown below. + properties.store(out, "Subset of pom.xml"); + } + if (reproducible) { + // The encoding can be either UTF-8 or ISO-8859-1, as any non ASCII character + // is transformed into a \\uxxxx sequence anyway. + Files.writeString( + propertiesFile, + Files.lines(propertiesFile) + .filter(line -> !line.startsWith("#")) + .sorted() + .collect(Collectors.joining("\n", "", "\n"))); // system independent new line. + } + return List.of(baseDir, Path.of(META_INF, MAVEN_DIR)); + } + + /** + * Creates a link to the given source if supported, or copies the file otherwise. + * + * @param source the source file to link or copy + * @param target the file to create + * @return the target file which should be deleted after the build + */ + private static Path linkOrCopy(final Path source, final Path target) throws IOException { + try { + return Files.createLink(target, source); + } catch (UnsupportedOperationException e) { + return Files.copy(source, target); + } + } + + /** + * Cancels the deletion of files. The files will stay present after the build. + * This is desired for allowing user to execute {@code jar} on the command-line, + * for example when the build failed. + */ + public void cancelFileDeletion() { + filesToDelete.clear(); + } + + /** + * Deletes all temporary files and directories created by this class. + * + * @throws IOException if an error occurred while deleting a file or directory + */ + @Override + public void close() throws IOException { + for (int i = filesToDelete.size(); --i >= 0; ) { + Files.delete(filesToDelete.get(i)); Review Comment: Not really needed. These temporary files are in the build directory, so they are deleted when `mvn clean` is deleted even if the above code failed. Furthermore, the plugin intentionally left those files undeleted when the `jar` tool failed in order to allow users to debug. Stopping at the first failed attempt to delete a file would be consistent with this policy. I will rather add a comment explaining that in the javadoc of this method. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
