001/* 002 * Copyright 2017-2022 original authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.micronaut.maven.aot; 017 018import io.micronaut.maven.core.MojoUtils; 019import io.micronaut.maven.services.CompilerService; 020import io.micronaut.maven.services.DependencyResolutionService; 021import io.micronaut.maven.services.ExecutorService; 022import org.apache.maven.execution.MavenSession; 023import org.apache.maven.plugin.MojoExecutionException; 024import org.apache.maven.plugins.annotations.Parameter; 025import org.apache.maven.project.MavenProject; 026import org.apache.maven.shared.invoker.InvocationResult; 027import org.apache.maven.shared.invoker.MavenInvocationException; 028import org.apache.maven.toolchain.ToolchainManager; 029import org.codehaus.plexus.util.StringUtils; 030import org.codehaus.plexus.util.xml.Xpp3Dom; 031import org.eclipse.aether.artifact.Artifact; 032import org.eclipse.aether.artifact.DefaultArtifact; 033import org.eclipse.aether.resolution.DependencyResolutionException; 034import org.eclipse.aether.util.artifact.JavaScopes; 035 036import javax.inject.Inject; 037import java.io.File; 038import java.io.IOException; 039import java.nio.charset.StandardCharsets; 040import java.nio.file.Files; 041import java.util.ArrayList; 042import java.util.Arrays; 043import java.util.Collections; 044import java.util.List; 045import java.util.Optional; 046import java.util.stream.Collectors; 047import java.util.stream.Stream; 048 049import static io.micronaut.maven.aot.Constants.MICRONAUT_AOT_ARTIFACT_ID_PREFIX; 050import static io.micronaut.maven.aot.Constants.MICRONAUT_AOT_GROUP_ID; 051import static io.micronaut.maven.aot.Constants.MICRONAUT_AOT_MAIN_CLASS; 052import static io.micronaut.maven.aot.Constants.MICRONAUT_AOT_PACKAGE_NAME; 053import static io.micronaut.maven.services.DependencyResolutionService.toClasspath; 054import static org.twdata.maven.mojoexecutor.MojoExecutor.configuration; 055import static org.twdata.maven.mojoexecutor.MojoExecutor.element; 056 057/** 058 * Base class for Micronaut AOT mojos. 059 * 060 * @author Álvaro Sánchez-Mariscal 061 * @since 3.2.0 062 */ 063public abstract class AbstractMicronautAotCliMojo extends AbstractMicronautAotMojo { 064 065 public static final String EXEC_MAVEN_PLUGIN_GROUP = "org.codehaus.mojo"; 066 public static final String EXEC_MAVEN_PLUGIN_ARTIFACT = "exec-maven-plugin"; 067 public static final String EXEC_MAVEN_PLUGIN_VERSION_PROPERTY = "exec-maven-plugin.version"; 068 public static final String DEFAULT_EXEC_MAVEN_PLUGIN_VERSION = "3.1.0"; 069 070 private static final String[] AOT_MODULES = { 071 "api", 072 "cli", 073 "std-optimizers" 074 }; 075 076 /** 077 * Package name to use for generated sources. 078 */ 079 @Parameter(property = MICRONAUT_AOT_PACKAGE_NAME) 080 protected String packageName; 081 082 private final ExecutorService executorService; 083 private final DependencyResolutionService dependencyResolutionService; 084 private final MavenSession mavenSession; 085 private final ToolchainManager toolchainManager; 086 087 @Parameter 088 private List<org.apache.maven.model.Dependency> aotDependencies; 089 090 /** 091 * Additional JVM arguments to pass to the AOT compiler (eg: <code>--enable-preview</code>). 092 * 093 * @since 4.0.2 094 */ 095 @Parameter(property = "micronaut.aot.jvmArgs") 096 private List<String> aotJvmArgs; 097 098 @Inject 099 protected AbstractMicronautAotCliMojo(CompilerService compilerService, 100 ExecutorService executorService, 101 MavenProject mavenProject, 102 DependencyResolutionService dependencyResolutionService, 103 MavenSession mavenSession, 104 ToolchainManager toolchainManager) { 105 super(compilerService, mavenProject); 106 this.executorService = executorService; 107 this.dependencyResolutionService = dependencyResolutionService; 108 this.mavenSession = mavenSession; 109 this.toolchainManager = toolchainManager; 110 } 111 112 protected abstract List<String> getExtraArgs() throws MojoExecutionException; 113 114 @Override 115 protected void doExecute() throws MojoExecutionException, DependencyResolutionException { 116 if (StringUtils.isEmpty(packageName)) { 117 throw new MojoExecutionException(MICRONAUT_AOT_PACKAGE_NAME + " is not set, and is required if AOT is enabled"); 118 } 119 try { 120 getLog().info("Packaging project"); 121 compilerService.compileProject(); 122 InvocationResult packagingResult = compilerService.packageProject(); 123 if (packagingResult.getExitCode() != 0) { 124 getLog().error("Error when packaging the project: ", packagingResult.getExecutionException()); 125 } else { 126 executeAot(); 127 } 128 } catch (MavenInvocationException e) { 129 getLog().error("Error when packaging project", e); 130 } 131 } 132 133 static List<String> buildJavaCommandArguments(List<String> jvmArgs, 134 String aotClasspath, 135 String classpath, 136 String packageName, 137 String runtime, 138 List<String> extraArgs) { 139 Stream<String> mainArgs = Stream.of( 140 "-classpath", 141 aotClasspath, 142 MICRONAUT_AOT_MAIN_CLASS, 143 "--classpath=" + classpath, 144 "--package=" + packageName, 145 "--runtime=" + runtime 146 ); 147 return Stream.concat(Stream.concat(jvmArgs.stream(), mainArgs), extraArgs.stream()).toList(); 148 } 149 150 static File writeJavaArgumentFile(File directory, List<String> arguments) throws MojoExecutionException { 151 try { 152 File argumentFile = File.createTempFile("micronaut-aot-", ".args", directory); 153 Files.write(argumentFile.toPath(), renderJavaArgumentFile(arguments), StandardCharsets.UTF_8); 154 return argumentFile; 155 } catch (IOException e) { 156 throw new MojoExecutionException("Unable to create Java argument file for Micronaut AOT execution", e); 157 } 158 } 159 160 static List<String> renderJavaArgumentFile(List<String> arguments) { 161 return arguments.stream().map(AbstractMicronautAotCliMojo::escapeJavaArgumentFileArgument).toList(); 162 } 163 164 static String escapeJavaArgumentFileArgument(String argument) { 165 if (argument.isEmpty()) { 166 return "\"\""; 167 } 168 String escaped = argument.replace("\\", "\\\\") 169 .replace("\"", "\\\""); 170 if (escaped.chars().anyMatch(Character::isWhitespace) || escaped.indexOf('#') >= 0) { 171 return "\"" + escaped + "\""; 172 } 173 return escaped; 174 } 175 176 private void executeAot() throws DependencyResolutionException, MojoExecutionException { 177 getLog().info("Executing Micronaut AOT analysis"); 178 AotJavaExecution execution = createJavaExecution(); 179 boolean executionSucceeded = false; 180 try { 181 executorService.executeGoal( 182 EXEC_MAVEN_PLUGIN_GROUP, 183 EXEC_MAVEN_PLUGIN_ARTIFACT, 184 mavenProject.getProperties().getProperty(EXEC_MAVEN_PLUGIN_VERSION_PROPERTY, DEFAULT_EXEC_MAVEN_PLUGIN_VERSION), 185 "exec", 186 execution.config 187 ); 188 executionSucceeded = true; 189 } catch (MojoExecutionException e) { 190 getLog().error("Error when executing Micronaut AOT: " + e.getMessage()); 191 getLog().error("Command line was: java @" + execution.argumentFile.getAbsolutePath()); 192 getLog().error("Micronaut AOT argument file retained at: " + execution.argumentFile.getAbsolutePath()); 193 throw e; 194 } finally { 195 if (executionSucceeded) { 196 deleteArgumentFile(execution.argumentFile); 197 } 198 } 199 } 200 201 private AotJavaExecution createJavaExecution() throws DependencyResolutionException, MojoExecutionException { 202 List<String> aotClasspath = resolveAotClasspath(); 203 List<String> aotPluginsClasspath = resolveAotPluginsClasspath(); 204 List<String> applicationClasspath = resolveApplicationClasspath(); 205 206 ArrayList<String> classpath = new ArrayList<>(aotPluginsClasspath.size() + applicationClasspath.size()); 207 classpath.addAll(aotClasspath); 208 classpath.addAll(aotPluginsClasspath); 209 classpath.addAll(applicationClasspath); 210 211 if (aotExclusions != null && !aotExclusions.isEmpty()) { 212 getLog().info("Using exclusions for the AOT classpath: " + 213 aotExclusions.stream().map(exclusion -> exclusion.getGroupId() + ":" + exclusion.getArtifactId()).collect(Collectors.joining(", "))); 214 getLog().info("Resulting AOT classpath: " + String.join(", ", classpath)); 215 } 216 217 List<String> commandArguments = buildJavaCommandArguments( 218 Optional.ofNullable(aotJvmArgs).orElse(List.of()), 219 String.join(File.pathSeparator, aotClasspath), 220 String.join(File.pathSeparator, classpath), 221 packageName, 222 runtime, 223 getExtraArgs() 224 ); 225 File argumentFile = writeJavaArgumentFile(getBaseOutputDirectory(), commandArguments); 226 String javaExecutable = MojoUtils.findJavaExecutable(toolchainManager, mavenSession); 227 Xpp3Dom config = configuration( 228 element("executable", javaExecutable), 229 element("arguments", element("argument", "@" + argumentFile.getAbsolutePath())) 230 ); 231 return new AotJavaExecution(config, argumentFile); 232 } 233 234 private void deleteArgumentFile(File argumentFile) { 235 try { 236 Files.deleteIfExists(argumentFile.toPath()); 237 } catch (IOException e) { 238 getLog().warn("Unable to delete temporary Micronaut AOT argument file " + argumentFile.getAbsolutePath(), e); 239 } 240 } 241 242 private List<String> resolveApplicationClasspath() { 243 String projectJar = new File(mavenProject.getBuild().getDirectory(), mavenProject.getBuild().getFinalName() + ".jar").getAbsolutePath(); 244 ArrayList<String> result = new ArrayList<>(); 245 result.add(projectJar); 246 String classpath = compilerService.buildClasspath( 247 compilerService.resolveDependencies(mavenProject, JavaScopes.RUNTIME).stream() 248 .filter(dependency -> isDependencyIncluded(dependency.getArtifact())) 249 .toList() 250 ); 251 result.addAll(Arrays.asList(classpath.split(File.pathSeparator))); 252 return result; 253 } 254 255 private List<String> resolveAotClasspath() throws DependencyResolutionException { 256 Stream<Artifact> aotArtifacts = Arrays.stream(AOT_MODULES) 257 .map(module -> new DefaultArtifact(MICRONAUT_AOT_GROUP_ID + ":" + MICRONAUT_AOT_ARTIFACT_ID_PREFIX + module + ":" + micronautAotVersion)); 258 return toClasspath( 259 dependencyResolutionService.artifactResultsFor(aotArtifacts, false).stream() 260 .filter(result -> isDependencyIncluded(result.getArtifact())) 261 .toList() 262 ); 263 } 264 265 private List<String> resolveAotPluginsClasspath() throws DependencyResolutionException { 266 if (aotDependencies == null || aotDependencies.isEmpty()) { 267 return Collections.emptyList(); 268 } 269 Stream<Artifact> aotPlugins = aotDependencies.stream() 270 .map(dependency -> new DefaultArtifact(dependency.getGroupId(), dependency.getArtifactId(), dependency.getType(), dependency.getVersion())); 271 return toClasspath( 272 dependencyResolutionService.artifactResultsFor(aotPlugins, false).stream() 273 .filter(result -> isDependencyIncluded(result.getArtifact())) 274 .toList() 275 ); 276 } 277 278 private boolean isDependencyIncluded(Artifact dependency) { 279 if (aotExclusions == null) { 280 return true; 281 } 282 return aotExclusions.stream() 283 .noneMatch(exclusion -> exclusion.getGroupId().equals(dependency.getGroupId()) && exclusion.getArtifactId().equals(dependency.getArtifactId())); 284 } 285 286 private record AotJavaExecution(Xpp3Dom config, File argumentFile) { 287 } 288}