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}