diff --git a/src/main/java/org/springframework/data/release/build/BuildConfiguration.java b/src/main/java/org/springframework/data/release/build/BuildConfiguration.java index ba6ec6d..08e1b0b 100644 --- a/src/main/java/org/springframework/data/release/build/BuildConfiguration.java +++ b/src/main/java/org/springframework/data/release/build/BuildConfiguration.java @@ -49,4 +49,5 @@ class BuildConfiguration { return new XBProjector(config, Flags.TO_STRING_RENDERS_XML); } + } diff --git a/src/main/java/org/springframework/data/release/build/CommandLine.java b/src/main/java/org/springframework/data/release/build/CommandLine.java index d61af23..5800e64 100644 --- a/src/main/java/org/springframework/data/release/build/CommandLine.java +++ b/src/main/java/org/springframework/data/release/build/CommandLine.java @@ -20,7 +20,11 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Value; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.function.Supplier; @@ -36,7 +40,7 @@ import org.springframework.util.Assert; * @author Oliver Gierke */ @Value -class CommandLine { +public class CommandLine { @NonNull List goals; @NonNull List arguments; diff --git a/src/main/java/org/springframework/data/release/build/MavenRuntime.java b/src/main/java/org/springframework/data/release/build/MavenRuntime.java index 2cf76b7..153a8b9 100644 --- a/src/main/java/org/springframework/data/release/build/MavenRuntime.java +++ b/src/main/java/org/springframework/data/release/build/MavenRuntime.java @@ -17,59 +17,32 @@ package org.springframework.data.release.build; import static org.springframework.data.release.build.CommandLine.Argument.*; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import java.io.Closeable; import java.io.File; -import java.io.FileFilter; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Properties; -import java.util.function.Consumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import org.apache.commons.io.filefilter.PrefixFileFilter; -import org.apache.maven.shared.invoker.DefaultInvocationRequest; import org.apache.maven.shared.invoker.DefaultInvoker; -import org.apache.maven.shared.invoker.InvocationRequest; import org.apache.maven.shared.invoker.InvocationResult; import org.apache.maven.shared.invoker.Invoker; -import org.apache.maven.shared.invoker.MavenInvocationException; -import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.data.release.io.JavaRuntimes; import org.springframework.data.release.io.Workspace; import org.springframework.data.release.model.JavaVersion; import org.springframework.data.release.model.Named; import org.springframework.data.release.model.SupportedProject; import org.springframework.data.release.utils.Logger; -import org.springframework.lang.Nullable; -import org.springframework.shell.support.util.StringUtils; -import org.springframework.stereotype.Component; /** * @author Oliver Gierke * @author Mark Paluch */ @Slf4j -@Component -public class MavenRuntime { +public class MavenRuntime extends MavenRuntimeSupport { - private static final Pattern versionPattern = Pattern.compile("Apache Maven ((\\d\\.?)+) \\(.*\\)"); private final Workspace workspace; private final Logger logger; private final MavenProperties properties; - private final JavaRuntimes.JdkInstallation jdk; /** * Creates a new {@link MavenRuntime} for the given {@link Workspace} and Maven home. @@ -78,82 +51,24 @@ public class MavenRuntime { * @param logger must not be {@literal null}. * @param properties must not be {@literal null}. */ - @Autowired - public MavenRuntime(Workspace workspace, Logger logger, MavenProperties properties) { - this(workspace, logger, properties, JavaVersion.VERSION_1_8); + public MavenRuntime(Workspace workspace, Logger logger, MavenRuntimes.MavenInstallation mavenInstallation, + MavenProperties properties) { + this(workspace, logger, mavenInstallation.getHome(), properties, JavaVersion.VERSION_1_8); } - private MavenRuntime(Workspace workspace, Logger logger, MavenProperties properties, + private MavenRuntime(Workspace workspace, Logger logger, File mavenHome, MavenProperties properties, JavaVersion requiredJavaVersion) { + super(mavenHome, properties.getLocalRepository(), + JavaRuntimes.Selector.from(requiredJavaVersion).notGraalVM().getRequiredJdkInstallation()); this.workspace = workspace; this.logger = logger; this.properties = properties; - this.jdk = JavaRuntimes.Selector.from(requiredJavaVersion).notGraalVM().getRequiredJdkInstallation(); - logger.log("Maven", "Using" + jdk + " as default Java Runtime"); + logger.log("Maven", "Using " + getJdk() + " as default Java Runtime"); } public MavenRuntime withJavaVersion(JavaVersion javaVersion) { - return new MavenRuntime(workspace, logger, properties, javaVersion); - } - - @SneakyThrows - public String getVersion() throws IllegalStateException { - - String version = detectBuildPropertiesVersion(); - - return version != null ? version : runVersionCommand(); - } - - @Nullable - @SneakyThrows - private String detectBuildPropertiesVersion() { - - File libs = new File(properties.getMavenHome(), "lib"); - File[] files = libs.listFiles((FileFilter) new PrefixFileFilter("maven-core-")); - - if (files == null || files.length != 1) { - return null; - } - - try (ZipFile zipFile = new ZipFile(files[0])) { - - ZipEntry entry = zipFile.getEntry("org/apache/maven/messages/build.properties"); - - if (entry == null) { - return null; - } - - Properties properties = new Properties(); - try (InputStream inputStream = zipFile.getInputStream(entry)) { - properties.load(inputStream); - } - - return properties.getProperty("version"); - } - } - - private String runVersionCommand() throws MavenInvocationException { - - StringBuilder builder = new StringBuilder(); - Invoker invoker = new DefaultInvoker(); - invoker.setMavenHome(properties.getMavenHome()); - invoker.setErrorHandler(builder::append); - invoker.setOutputHandler(builder::append); - - doWithMaven(invoker, mvn -> { - mvn.setShowVersion(true); - mvn.setGoals(Collections.emptyList()); - }); - - Matcher matcher = versionPattern.matcher(builder); - boolean foundVersion = matcher.find(); - - if (!foundVersion) { - throw new IllegalStateException("Cannot determine Maven Version: " + builder); - } - - return matcher.group(1); + return new MavenRuntime(workspace, logger, getMavenHome(), properties, javaVersion); } public MavenInvocationResult execute(SupportedProject project, CommandLine arguments) { @@ -163,14 +78,14 @@ public class MavenRuntime { try (MavenLogger mavenLogger = getLogger(project, arguments.getGoals())) { Invoker invoker = new DefaultInvoker(); - invoker.setMavenHome(properties.getMavenHome()); + invoker.setMavenHome(getMavenHome()); invoker.setOutputHandler(mavenLogger::info); invoker.setErrorHandler(mavenLogger::warn); InvocationResult result = doWithMaven(invoker, mvn -> { mvn.setBaseDirectory(workspace.getProjectDirectory(project)); - mavenLogger.info(String.format("Java Home: %s", jdk)); + mavenLogger.info(String.format("Java Home: %s", getJavaHome())); mavenLogger.info(String.format("Executing: mvn %s", arguments)); CommandLine disabledGradleBuildCache = arguments.and(arg("gradle.cache.local.enabled=false")) @@ -198,31 +113,8 @@ public class MavenRuntime { } } - private InvocationResult doWithMaven(Invoker invoker, Consumer mvn) - throws MavenInvocationException { - - File localRepository = properties.getLocalRepository(); - - if (localRepository != null) { - invoker.setLocalRepositoryDirectory(localRepository); - } - - File javaHome = getJavaHome(); - InvocationRequest request = new DefaultInvocationRequest(); - request.setJavaHome(javaHome); - request.setShellEnvironmentInherited(true); - request.setBatchMode(true); - - mvn.accept(request); - - return invoker.execute(request); - } - - private File getJavaHome() { - return jdk.getHome().getAbsoluteFile(); - } - - private MavenLogger getLogger(Named project, List goals) { + @Override + MavenLogger getLogger(Named project, List goals) { if (this.properties.isConsoleLogger()) { return new SlfLogger(log, project); @@ -231,114 +123,4 @@ public class MavenRuntime { return new FileLogger(log, project, this.workspace.getLogsDirectory(), goals); } - public static class MavenInvocationResult { - - private final List log = new ArrayList<>(); - - public List getLog() { - return log; - } - } - - /** - * Maven Logging Forwarder. - */ - interface MavenLogger extends Closeable { - - void info(String message); - - void warn(String message); - - List getLines(); - } - - @RequiredArgsConstructor - static class SlfLogger implements MavenLogger { - - private final org.slf4j.Logger logger; - private final String logPrefix; - private final List contents; - - SlfLogger(org.slf4j.Logger logger, Named project) { - this.logger = logger; - this.logPrefix = StringUtils.padRight(project.getName(), 10); - this.contents = new ArrayList<>(); - } - - @Override - public void info(String message) { - String msg = logPrefix + ": " + message; - contents.add(msg); - logger.info(msg); - } - - @Override - public void warn(String message) { - String msg = logPrefix + ": " + message; - contents.add(msg); - logger.warn(msg); - } - - @Override - public void close() throws IOException { - // no-op - } - - @Override - public List getLines() { - return contents; - } - } - - static class FileLogger implements MavenLogger { - - private final PrintWriter printWriter; - private final FileOutputStream outputStream; - private final List contents = new ArrayList<>(); - - FileLogger(org.slf4j.Logger logger, Named project, File logsDirectory, List goals) { - - if (!logsDirectory.exists()) { - logsDirectory.mkdirs(); - } - - String goalNames = goals.stream().map(CommandLine.Goal::getGoal).collect(Collectors.joining("-")); - - String filename = String.format("mvn-%s-%s.log", project.getName(), goalNames).replace(':', '.'); - - try { - File file = new File(logsDirectory, filename); - logger.info("Routing Maven output to " + file.getCanonicalPath()); - outputStream = new FileOutputStream(file, true); - } catch (IOException e) { - throw new RuntimeException(e); - } - - printWriter = new PrintWriter(outputStream, true); - } - - @Override - public void info(String message) { - printWriter.println(message); - contents.add(message); - } - - @Override - public void warn(String message) { - printWriter.println(message); - contents.add(message); - } - - @Override - public void close() throws IOException { - printWriter.close(); - outputStream.close(); - } - - @Override - public List getLines() { - return contents; - } - } - } diff --git a/src/main/java/org/springframework/data/release/build/MavenRuntimeSupport.java b/src/main/java/org/springframework/data/release/build/MavenRuntimeSupport.java new file mode 100644 index 0000000..3eb22a3 --- /dev/null +++ b/src/main/java/org/springframework/data/release/build/MavenRuntimeSupport.java @@ -0,0 +1,281 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.data.release.build; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.Closeable; +import java.io.File; +import java.io.FileFilter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.apache.commons.io.filefilter.PrefixFileFilter; +import org.apache.maven.shared.invoker.DefaultInvocationRequest; +import org.apache.maven.shared.invoker.DefaultInvoker; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.Invoker; +import org.apache.maven.shared.invoker.MavenInvocationException; + +import org.springframework.data.release.io.JavaRuntimes; +import org.springframework.data.release.model.Named; +import org.springframework.lang.Nullable; +import org.springframework.shell.support.util.StringUtils; + +/** + * @author Oliver Gierke + * @author Mark Paluch + */ +@Slf4j +public class MavenRuntimeSupport { + + private static final Pattern versionPattern = Pattern.compile("Apache Maven ((\\d\\.?)+) \\(.*\\)"); + private final File mavenHome; + private final @Nullable File localRepository; + private final JavaRuntimes.JdkInstallation jdk; + + /** + * Creates a new {@link MavenRuntimeSupport} + * + * @param mavenHome + * @param localRepository + * @param jdk + */ + public MavenRuntimeSupport(File mavenHome, @Nullable File localRepository, JavaRuntimes.JdkInstallation jdk) { + this.mavenHome = mavenHome; + this.localRepository = localRepository; + this.jdk = jdk; + } + + JavaRuntimes.JdkInstallation getJdk() { + return jdk; + } + + File getMavenHome() { + return mavenHome; + } + + @SneakyThrows + public String getVersion() throws IllegalStateException { + + String version = detectBuildPropertiesVersion(); + + return version != null ? version : runVersionCommand(); + } + + @Nullable + @SneakyThrows + private String detectBuildPropertiesVersion() { + + File libs = new File(mavenHome, "lib"); + File[] files = libs.listFiles((FileFilter) new PrefixFileFilter("maven-core-")); + + if (files == null || files.length != 1) { + return null; + } + + try (ZipFile zipFile = new ZipFile(files[0])) { + + ZipEntry entry = zipFile.getEntry("org/apache/maven/messages/build.properties"); + + if (entry == null) { + return null; + } + + Properties properties = new Properties(); + try (InputStream inputStream = zipFile.getInputStream(entry)) { + properties.load(inputStream); + } + + return properties.getProperty("version"); + } + } + + private String runVersionCommand() throws MavenInvocationException { + + StringBuilder builder = new StringBuilder(); + Invoker invoker = new DefaultInvoker(); + invoker.setMavenHome(mavenHome); + invoker.setErrorHandler(builder::append); + invoker.setOutputHandler(builder::append); + + doWithMaven(invoker, mvn -> { + mvn.setShowVersion(true); + mvn.setGoals(Collections.emptyList()); + }); + + Matcher matcher = versionPattern.matcher(builder); + boolean foundVersion = matcher.find(); + + if (!foundVersion) { + throw new IllegalStateException("Cannot determine Maven Version: " + builder); + } + + return matcher.group(1); + } + + protected InvocationResult doWithMaven(Invoker invoker, Consumer mvn) + throws MavenInvocationException { + + if (this.localRepository != null) { + invoker.setLocalRepositoryDirectory(this.localRepository); + } + + File javaHome = getJavaHome(); + InvocationRequest request = new DefaultInvocationRequest(); + request.setJavaHome(javaHome); + request.setShellEnvironmentInherited(true); + request.setBatchMode(true); + + mvn.accept(request); + + return invoker.execute(request); + } + + File getJavaHome() { + return jdk.getHome().getAbsoluteFile(); + } + + MavenLogger getLogger(Named project, List goals) { + return new SlfLogger(log, project); + } + + public static class MavenInvocationResult { + + private final List log = new ArrayList<>(); + + public List getLog() { + return log; + } + } + + /** + * Maven Logging Forwarder. + */ + interface MavenLogger extends Closeable { + + void info(String message); + + void warn(String message); + + List getLines(); + } + + @RequiredArgsConstructor + static class SlfLogger implements MavenLogger { + + private final org.slf4j.Logger logger; + private final String logPrefix; + private final List contents; + + SlfLogger(org.slf4j.Logger logger, Named project) { + this.logger = logger; + this.logPrefix = StringUtils.padRight(project.getName(), 10); + this.contents = new ArrayList<>(); + } + + @Override + public void info(String message) { + String msg = logPrefix + ": " + message; + contents.add(msg); + logger.info(msg); + } + + @Override + public void warn(String message) { + String msg = logPrefix + ": " + message; + contents.add(msg); + logger.warn(msg); + } + + @Override + public void close() throws IOException { + // no-op + } + + @Override + public List getLines() { + return contents; + } + } + + static class FileLogger implements MavenLogger { + + private final PrintWriter printWriter; + private final FileOutputStream outputStream; + private final List contents = new ArrayList<>(); + + FileLogger(org.slf4j.Logger logger, Named project, File logsDirectory, List goals) { + + if (!logsDirectory.exists()) { + logsDirectory.mkdirs(); + } + + String goalNames = goals.stream().map(CommandLine.Goal::getGoal).collect(Collectors.joining("-")); + + String filename = String.format("mvn-%s-%s.log", project.getName(), goalNames).replace(':', '.'); + + try { + File file = new File(logsDirectory, filename); + logger.info("Routing Maven output to " + file.getCanonicalPath()); + outputStream = new FileOutputStream(file, true); + } catch (IOException e) { + throw new RuntimeException(e); + } + + printWriter = new PrintWriter(outputStream, true); + } + + @Override + public void info(String message) { + printWriter.println(message); + contents.add(message); + } + + @Override + public void warn(String message) { + printWriter.println(message); + contents.add(message); + } + + @Override + public void close() throws IOException { + printWriter.close(); + outputStream.close(); + } + + @Override + public List getLines() { + return contents; + } + } + +} diff --git a/src/main/java/org/springframework/data/release/build/MavenRuntimes.java b/src/main/java/org/springframework/data/release/build/MavenRuntimes.java new file mode 100644 index 0000000..1358b35 --- /dev/null +++ b/src/main/java/org/springframework/data/release/build/MavenRuntimes.java @@ -0,0 +1,415 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed 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.springframework.data.release.build; + +import lombok.Value; + +import java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.RegexFileFilter; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.data.release.io.JavaRuntimes; +import org.springframework.data.release.model.Version; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Utility to detect a Java runtime version. + * + * @author Mark Paluch + * @author Christoph Strobl + */ +public class MavenRuntimes { + + private static final List DETECTORS = Arrays.asList(new SDKmanJdkDetector(), + new MavenWrapperDetector(), new MavenHomeEnvironmentDetector()); + + private final List detectors; + + public MavenRuntimes(MavenDetector... detectors) { + this.detectors = new ArrayList<>(DETECTORS); + this.detectors.addAll(Arrays.asList(detectors)); + } + + public static MavenDetector detector(File mavenHome) { + return new MavenHomeJdkDetectorSupport(mavenHome); + } + + /** + * Lookup a {@link MavenInstallation} by detecting installed Maven installations and applying the {@link Predicate + * filter}. Returns the first matching one {@code null} + */ + @Nullable + public MavenInstallation findMavenInstallation(JavaRuntimes.JdkInstallation jdk, + Predicate filter) { + + List jdks = getMavenInstallations(jdk); + + return jdks.stream().filter(filter).findFirst().orElse(null); + } + + /** + * Lookup a {@link MavenInstallation} by detecting installed Maven installations and applying the {@link Predicate + * filter}. Returns the first matching one or throws {@link NoSuchElementException}. + */ + public MavenInstallation getRequiredMaven(JavaRuntimes.JdkInstallation jdk, Predicate filter, + String runtimeName, Supplier message) { + + List jdks = getMavenInstallations(jdk); + + return jdks.stream().filter(filter).findFirst().orElseThrow(() -> new NoSuchMavenRuntimeException( + String.format("%s%nAvailable Maven: %s", message.get(), jdks), jdks, runtimeName)); + } + + public List getMavenInstallations(JavaRuntimes.JdkInstallation jdk) { + return DETECTORS.stream() // + .filter(MavenDetector::isAvailable) // + .flatMap(it -> it.detect(jdk).stream()) // + .sorted() // + .collect(Collectors.toList()); + } + + static boolean isDirectory(File file) { + return file.exists() && file.isDirectory(); + } + + /** + * Maven detection strategy. + */ + public interface MavenDetector { + + /** + * @return {@code true} if the detector strategy is available. + */ + boolean isAvailable(); + + /** + * @return a list of Maven installations. + */ + List detect(JavaRuntimes.JdkInstallation jdk); + + } + + /** + * Selector to determine a {@link MavenInstallation}. + */ + public static class Selector { + + private final MavenDetector[] detectors; + private String notFoundMessage; + + private String mavenRuntimeName; + private Predicate predicate; + + private Selector(MavenDetector[] detectors) { + this.detectors = detectors; + } + + public static Selector builder(MavenDetector... detectors) { + return new Selector(detectors); + } + + public Selector version(Version mavenVersion) { + + return and(it -> it.version.equals(mavenVersion)).name("Maven version " + mavenVersion) + .message("Cannot find required Maven version " + mavenVersion); + } + + public Selector and(Predicate predicate) { + this.predicate = this.predicate == null ? predicate : this.predicate.and(predicate); + return this; + } + + public Selector message(String notFoundMessage) { + this.notFoundMessage = notFoundMessage; + return this; + } + + public Selector name(String mavenRuntimeName) { + this.mavenRuntimeName = mavenRuntimeName; + return this; + } + + public MavenInstallation getRequiredMavenInstallation(JavaRuntimes.JdkInstallation jdk) { + return new MavenRuntimes(detectors).getRequiredMaven(jdk, predicate, mavenRuntimeName, () -> notFoundMessage); + } + + } + + /** + * Detector using the SDKman utility storing Java installations in {@code ~/.sdkman/candidates/maven}. + */ + static class SDKmanJdkDetector implements MavenDetector { + + private static final File sdkManMavenHome; + + private static final Pattern CANDIDATE = Pattern.compile("(\\d+[\\.\\d+]+)"); + + static { + + if (System.getenv().containsKey("SDKMAN_CANDIDATES_DIR")) { + sdkManMavenHome = new File(System.getenv().get("SDKMAN_CANDIDATES_DIR"), "maven"); + } else if (System.getenv().containsKey("SDKMAN_DIR")) { + sdkManMavenHome = new File(System.getenv().get("SDKMAN_DIR"), "candidates/maven"); + } else { + sdkManMavenHome = new File(FileUtils.getUserDirectoryPath(), ".sdkman/candidates/maven"); + } + } + + @Override + public boolean isAvailable() { + return isDirectory(sdkManMavenHome); + } + + @Override + public List detect(JavaRuntimes.JdkInstallation jdk) { + + File[] files = sdkManMavenHome.listFiles((FileFilter) new RegexFileFilter(CANDIDATE)); + + return Arrays.stream(files).map(it -> { + + Matcher matcher = CANDIDATE.matcher(it.getName()); + if (!matcher.find()) { + throw new IllegalArgumentException("Cannot determine Maven version number from SDKman candidate name " + + it.getName() + ". This should not happen in an ideal world, check the CANDIDATE regex."); + } + + String candidateVersion = matcher.group(1); + Version version = Version.parse(candidateVersion); + + return new MavenInstallation(version, it); + + }).collect(Collectors.toList()); + } + + } + + /** + * Detector Maven wrapper installations in {@code ~/.m2/wrapper/dists}. + */ + static class MavenWrapperDetector implements MavenDetector { + + private static final Pattern CANDIDATE = Pattern.compile("apache-maven-((:?\\d+(:?\\.\\d+)*)(:?_+\\d+)?)-bin"); + + private static final String userHome = System.getProperty("user.home"); + private static final File dists = new File(userHome, ".m2/wrapper/dists"); + + @Override + public boolean isAvailable() { + return isDirectory(dists); + } + + @Override + public List detect(JavaRuntimes.JdkInstallation jdk) { + + File[] files = dists.listFiles((FileFilter) new RegexFileFilter(CANDIDATE)); + + class WrapperCandidate { + + final File home; + final File hash; + final File realHome; + final Version version; + + public WrapperCandidate(File home, File hash, Version version, File realHome) { + this.home = home; + this.hash = hash; + this.realHome = realHome; + this.version = version; + } + } + + return Arrays.stream(files).map(it -> { + + File[] hashes = it.listFiles(); + File hash = hashes != null && hashes.length == 1 ? hashes[0] : null; + + Matcher matcher = CANDIDATE.matcher(it.getName()); + if (!matcher.find()) { + throw new IllegalArgumentException("Cannot determine Maven version number from Maven Wrapper candidate name " + + it.getName() + ". This should not happen in an ideal world, check the CANDIDATE regex."); + } + String candidateVersion = matcher.group(1); + Version version = Version.parse(candidateVersion); + + return new WrapperCandidate(it, hash, version, hash != null ? new File(hash, "apache-maven-" + version) : null); + + }).filter(it -> { + + if (it.hash == null) { + return false; + } + + return isDirectory(it.realHome); + }).map(it -> new MavenInstallation(it.version, it.realHome)).collect(Collectors.toList()); + } + + } + + /** + * Detector using the {@code java.home} system property. + */ + static class MavenHomeEnvironmentDetector implements MavenDetector { + + private static final String maven_home_property = System.getProperty("maven.home"); + private static final @Nullable MavenHomeJdkDetectorSupport mavenHome = StringUtils.hasText(maven_home_property) + ? new MavenHomeJdkDetectorSupport(new File(maven_home_property)) + : null; + private static final String MAVEN_HOME_ENV = System.getenv("MAVEN_HOME"); + private static final @Nullable MavenHomeJdkDetectorSupport MAVEN_HOME = StringUtils.hasText(MAVEN_HOME_ENV) + ? new MavenHomeJdkDetectorSupport(new File(MAVEN_HOME_ENV)) + : null; + + @Override + public boolean isAvailable() { + return hasMavenHomeProperty() || hasMavenHomeEnv(); + } + + private boolean hasMavenHomeProperty() { + return StringUtils.hasText(maven_home_property) && mavenHome != null; + } + + private boolean hasMavenHomeEnv() { + return StringUtils.hasText(MAVEN_HOME_ENV) && MAVEN_HOME != null; + } + + @Override + public List detect(JavaRuntimes.JdkInstallation jdk) { + + List installations = new ArrayList<>(); + + if (hasMavenHomeProperty()) { + installations.addAll(mavenHome.detect(jdk)); + } + + if (hasMavenHomeEnv()) { + installations.addAll(MAVEN_HOME.detect(jdk)); + } + + return installations; + } + } + + /** + * Detector using a Maven Home directory. + */ + static class MavenHomeJdkDetectorSupport implements MavenDetector { + + private final File mavenHome; + + public MavenHomeJdkDetectorSupport(File mavenHome) { + this.mavenHome = mavenHome; + } + + @Override + public boolean isAvailable() { + return mavenHome != null && isDirectory(mavenHome); + } + + @Override + public List detect(JavaRuntimes.JdkInstallation jdk) { + + SimpleMavenRuntime mavenRuntime = new SimpleMavenRuntime(mavenHome, jdk); + return Collections.singletonList(new MavenInstallation(Version.parse(mavenRuntime.getVersion()), mavenHome)); + } + } + + @Value + public static class MavenInstallation implements Comparable { + + Version version; + File home; + + @Override + public int compareTo(MavenInstallation o) { + return this.version.compareTo(o.version); + } + + @Override + public String toString() { + return "Version " + version + " at " + home; + } + } + + public static class NoSuchMavenRuntimeException extends NoSuchElementException { + + private final List installations; + + private final String requiredMavenVersion; + + public NoSuchMavenRuntimeException(String message, List installations, + String requiredMavenVersion) { + super(message); + this.installations = installations; + this.requiredMavenVersion = requiredMavenVersion; + } + + public List getInstallations() { + return installations; + } + + public String getRequiredMavenVersion() { + return requiredMavenVersion; + } + } + + static class NoSuchMavenRuntimeExceptionFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, NoSuchMavenRuntimeException cause) { + + String action = " Make sure to install %s using your platform installation method or SDKman.%n%n" + + " Detected Maven Runtimes are: %n" + "%s"; + + StringBuilder detectedRuntimes = new StringBuilder(); + + for (MavenInstallation installation : cause.getInstallations()) { + detectedRuntimes.append(String.format(" - %-10s %s%n", installation.getVersion(), installation.getHome())); + } + + return new FailureAnalysis("⚠️ A required Maven version was not found: " + cause.getRequiredMavenVersion(), + String.format(action, cause.getRequiredMavenVersion(), detectedRuntimes), cause); + } + } + + static class SimpleMavenRuntime extends MavenRuntimeSupport { + + /** + * Creates a new {@link MavenRuntimeSupport} + * + * @param mavenHome + * @param jdk + */ + public SimpleMavenRuntime(File mavenHome, JavaRuntimes.JdkInstallation jdk) { + super(mavenHome, null, jdk); + } + } + +} diff --git a/src/main/java/org/springframework/data/release/cli/InvalidMavenVersionException.java b/src/main/java/org/springframework/data/release/cli/InvalidMavenVersionException.java deleted file mode 100644 index 01289ab..0000000 --- a/src/main/java/org/springframework/data/release/cli/InvalidMavenVersionException.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed 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.springframework.data.release.cli; - -import lombok.Getter; - -import java.io.File; - -/** - * @author Mark Paluch - */ -@Getter -class InvalidMavenVersionException extends IllegalStateException { - - private final String expectedVersion; - private final String actualVersion; - - private final File home; - - public InvalidMavenVersionException(String expectedVersion, String installedVersion, File home) { - super(String.format("Invalid Maven version: Expected %s, found version %s", expectedVersion, installedVersion)); - this.expectedVersion = expectedVersion; - this.actualVersion = installedVersion; - this.home = home; - } -} diff --git a/src/main/java/org/springframework/data/release/cli/JavaToolingConfiguration.java b/src/main/java/org/springframework/data/release/cli/JavaToolingConfiguration.java index 924afc4..1bda86a 100644 --- a/src/main/java/org/springframework/data/release/cli/JavaToolingConfiguration.java +++ b/src/main/java/org/springframework/data/release/cli/JavaToolingConfiguration.java @@ -15,6 +15,8 @@ */ package org.springframework.data.release.cli; +import lombok.RequiredArgsConstructor; + import java.util.Properties; import org.springframework.beans.factory.annotation.Qualifier; @@ -25,7 +27,12 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.data.release.build.MavenProperties; import org.springframework.data.release.build.MavenRuntime; +import org.springframework.data.release.build.MavenRuntimes; +import org.springframework.data.release.io.JavaRuntimes; +import org.springframework.data.release.io.Workspace; +import org.springframework.data.release.model.JavaVersion; import org.springframework.data.release.utils.Logger; +import org.springframework.util.StringUtils; /** * Configuration to verify build infrastructure. @@ -33,10 +40,14 @@ import org.springframework.data.release.utils.Logger; * @author Mark Paluch */ @Configuration +@RequiredArgsConstructor class JavaToolingConfiguration { private static final Resource javaTools = new FileSystemResource("ci/java-tools.properties"); + private final Workspace workspace; + private final Logger logger; + @Bean PropertiesFactoryBean javaTools() { @@ -51,8 +62,50 @@ class JavaToolingConfiguration { } @Bean - JavaToolingVerifier verifier(@Qualifier("javaTools") Properties javaTools, MavenRuntime mavenRuntime, - MavenProperties mavenProperties, Logger logger) { - return new JavaToolingVerifier(javaTools, mavenRuntime, mavenProperties, logger); + JavaVersions javaVersions(@Qualifier("javaTools") Properties javaTools) { + + JavaVersions javaVersions = new JavaVersions(JavaVersions.parse(javaTools)); + + logger.log("JavaTooling", "🕵️ Checking presence of JDKs %s…", + StringUtils.collectionToDelimitedString(javaVersions.getExpectedVersions(), ", ")); + + for (String jdk : javaVersions.getExpectedVersions()) { + + JavaVersion javaVersion = JavaVersion.of(jdk.trim()); + + JavaRuntimes.JdkInstallation jdkInstallation = javaVersions.getInstallation(javaVersion); + logger.log("JavaTooling", "✅ Found %s by %s", javaVersion.getName(), jdkInstallation.getImplementor()); + } + + return javaVersions; } + + @Bean + MavenVersion mavenVersion(@Qualifier("javaTools") Properties javaTools) { + return MavenVersion.parse(javaTools); + } + + @Bean + public MavenRuntime mavenRuntime(JavaVersions javaVersions, MavenVersion mavenVersion, MavenProperties properties) { + + logger.log("JavaTooling", "🕵️ Checking presence of Maven %s…", mavenVersion.getExpectedVersion()); + + String firstJdk = javaVersions.getExpectedVersions().get(0); + JavaRuntimes.JdkInstallation installation = javaVersions.getInstallation(firstJdk); + + MavenRuntimes.Selector selector; + if (properties.getMavenHome() != null) { + selector = MavenRuntimes.Selector.builder(MavenRuntimes.detector(properties.getMavenHome())); + } else { + selector = MavenRuntimes.Selector.builder(); + } + + MavenRuntimes.MavenInstallation mavenInstallation = selector.version(mavenVersion.getExpectedVersion()) + .getRequiredMavenInstallation(installation); + + logger.log("JavaTooling", "✅ Found Maven %s", mavenInstallation); + + return new MavenRuntime(workspace, logger, mavenInstallation, properties); + } + } diff --git a/src/main/java/org/springframework/data/release/cli/JavaToolingVerifier.java b/src/main/java/org/springframework/data/release/cli/JavaToolingVerifier.java deleted file mode 100644 index b7ffbd4..0000000 --- a/src/main/java/org/springframework/data/release/cli/JavaToolingVerifier.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed 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.springframework.data.release.cli; - -import lombok.RequiredArgsConstructor; - -import java.util.Properties; - -import javax.annotation.PostConstruct; - -import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; -import org.springframework.boot.diagnostics.FailureAnalysis; -import org.springframework.data.release.build.MavenProperties; -import org.springframework.data.release.build.MavenRuntime; -import org.springframework.data.release.io.JavaRuntimes.JdkInstallation; -import org.springframework.data.release.io.JavaRuntimes.Selector; -import org.springframework.data.release.model.JavaVersion; -import org.springframework.data.release.utils.Logger; -import org.springframework.util.StringUtils; - -/** - * Utility to verify early on that your build environment contains all the required Java and Maven versions. - * - * @author Mark Paluch - */ -@RequiredArgsConstructor -class JavaToolingVerifier { - - private final Properties javaTools; - - private final MavenRuntime mavenRuntime; - - private final MavenProperties mavenProperties; - - private final Logger logger; - - @PostConstruct - public void verify() { - - String jdksProperty = javaTools.getProperty("jdks"); - String[] jdks = jdksProperty.split(","); - - logger.log("JavaTooling", "🕵️ Checking presence of JDKs %s…", StringUtils.arrayToDelimitedString(jdks, ", ")); - - for (String jdk : jdks) { - - JavaVersion javaVersion = JavaVersion.of(jdk.trim()); - - JdkInstallation jdkInstallation = Selector.notGraalVM(javaVersion).getRequiredJdkInstallation(); - logger.log("JavaTooling", "✅ Found %s by %s", javaVersion.getName(), jdkInstallation.getImplementor()); - } - - String expectedMavenVersion = javaTools.getProperty("maven"); - - logger.log("JavaTooling", "🕵️ Checking presence of Maven %s…", expectedMavenVersion); - - String installedMavenVersion = mavenRuntime.getVersion(); - if (!expectedMavenVersion.equals(installedMavenVersion)) { - throw new InvalidMavenVersionException(expectedMavenVersion, installedMavenVersion, - mavenProperties.getMavenHome()); - } - - logger.log("JavaTooling", "✅ Found Maven %s", installedMavenVersion); - } - - static class InvalidMavenVersionExceptionFailureAnalyzer - extends AbstractFailureAnalyzer { - - @Override - protected FailureAnalysis analyze(Throwable rootFailure, InvalidMavenVersionException cause) { - - return new FailureAnalysis( - String.format("⚠️ The configured Maven version %s at %s does not match the required version %s.", - cause.getActualVersion(), cause.getHome(), cause.getExpectedVersion()), - String.format(" Make sure to use Maven %s or update your maven.maven-home property.", - cause.getExpectedVersion()), - cause); - } - } -} diff --git a/src/main/java/org/springframework/data/release/cli/JavaVersions.java b/src/main/java/org/springframework/data/release/cli/JavaVersions.java new file mode 100644 index 0000000..352f2b4 --- /dev/null +++ b/src/main/java/org/springframework/data/release/cli/JavaVersions.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed 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.springframework.data.release.cli; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +import org.springframework.data.release.io.JavaRuntimes; +import org.springframework.data.release.model.JavaVersion; + +/** + * Value object to encapsulate expected Java versions. + * + * @author Mark Paluch + */ +class JavaVersions { + + private final List expectedVersions; + + public JavaVersions(List expectedVersions) { + this.expectedVersions = expectedVersions; + } + + /** + * Parse the Java versions from the given {@link Properties} at the key {@code jdks}. + * + * @param properties + * @return + */ + public static List parse(Properties properties) { + + String jdksProperty = properties.getProperty("jdks"); + return Arrays.asList(jdksProperty.split(",")); + } + + /** + * Retrieve the required Java Installation for the given {@code version}. + * + * @param version + * @return + */ + public JavaRuntimes.JdkInstallation getInstallation(String version) { + return getInstallation(JavaVersion.of(version.trim())); + } + + /** + * Retrieve the required Java Installation for the given {@link JavaVersion}. + * + * @param version + * @return + */ + public JavaRuntimes.JdkInstallation getInstallation(JavaVersion version) { + return JavaRuntimes.Selector.notGraalVM(version).getRequiredJdkInstallation(); + } + + public List getExpectedVersions() { + return this.expectedVersions; + } +} diff --git a/src/main/java/org/springframework/data/release/cli/MavenVersion.java b/src/main/java/org/springframework/data/release/cli/MavenVersion.java new file mode 100644 index 0000000..39ef10d --- /dev/null +++ b/src/main/java/org/springframework/data/release/cli/MavenVersion.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed 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.springframework.data.release.cli; + +import lombok.Value; + +import java.util.Properties; + +import org.springframework.data.release.model.Version; + +/** + * Value object for a Maven version. + * + * @author Mark Paluch + */ +@Value(staticConstructor = "of") +class MavenVersion { + + String expectedVersion; + + /** + * Parse the Maven version from the given {@link Properties} at the key {@code maven}. + * + * @param properties + * @return + */ + public static MavenVersion parse(Properties properties) { + return of(properties.getProperty("maven")); + } + + public Version getExpectedVersion() { + return Version.parse(this.expectedVersion); + } + + @Override + public String toString() { + return expectedVersion; + } +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories index efca3eb..1b69efa 100644 --- a/src/main/resources/META-INF/spring.factories +++ b/src/main/resources/META-INF/spring.factories @@ -1,2 +1,3 @@ org.springframework.boot.diagnostics.FailureAnalyzer=org.springframework.data.release.io.JavaRuntimes$NoSuchJavaRuntimeExceptionFailureAnalyzer,\ -org.springframework.data.release.cli.JavaToolingVerifier.InvalidMavenVersionExceptionFailureAnalyzer +org.springframework.data.release.cli.JavaToolingVerifier.InvalidMavenVersionExceptionFailureAnalyzer,\ +org.springframework.data.release.build.MavenRuntimes$NoSuchMavenRuntimeExceptionFailureAnalyzer