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.core.util.CollectionUtils;
019import io.micronaut.maven.InvocationResultWithOutput;
020import io.micronaut.maven.MojoUtils;
021import io.micronaut.maven.services.CompilerService;
022import io.micronaut.maven.services.DependencyResolutionService;
023import io.micronaut.maven.services.ExecutorService;
024
025import org.apache.maven.execution.MavenSession;
026import org.apache.maven.plugin.MojoExecutionException;
027import org.apache.maven.plugins.annotations.Parameter;
028import org.apache.maven.project.MavenProject;
029import org.apache.maven.shared.invoker.MavenInvocationException;
030import org.apache.maven.toolchain.ToolchainManager;
031import org.codehaus.plexus.util.StringUtils;
032import org.codehaus.plexus.util.xml.Xpp3Dom;
033import org.eclipse.aether.artifact.Artifact;
034import org.eclipse.aether.artifact.DefaultArtifact;
035import org.eclipse.aether.resolution.DependencyResolutionException;
036import org.eclipse.aether.util.artifact.JavaScopes;
037import org.twdata.maven.mojoexecutor.MojoExecutor;
038
039import javax.inject.Inject;
040import java.io.File;
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 */
060public abstract class AbstractMicronautAotCliMojo extends AbstractMicronautAotMojo {
061
062    public static final String EXEC_MAVEN_PLUGIN_GROUP = "org.codehaus.mojo";
063    public static final String EXEC_MAVEN_PLUGIN_ARTIFACT = "exec-maven-plugin";
064    public static final String EXEC_MAVEN_PLUGIN_VERSION_PROPERTY = "exec-maven-plugin.version";
065    public static final String DEFAULT_EXEC_MAVEN_PLUGIN_VERSION = "3.1.0";
066
067    private static final String[] AOT_MODULES = {
068        "api",
069        "cli",
070        "std-optimizers"
071    };
072
073    /**
074     * Package name to use for generated sources.
075     */
076    @Parameter(property = MICRONAUT_AOT_PACKAGE_NAME)
077    protected String packageName;
078
079    private final ExecutorService executorService;
080
081    private final DependencyResolutionService dependencyResolutionService;
082    
083    private final MavenSession mavenSession;
084    
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    public AbstractMicronautAotCliMojo(CompilerService compilerService, ExecutorService executorService,
100                                       MavenProject mavenProject, DependencyResolutionService dependencyResolutionService,
101                                       MavenSession mavenSession, ToolchainManager toolchainManager) {
102        super(compilerService, mavenProject);
103        this.executorService = executorService;
104        this.dependencyResolutionService = dependencyResolutionService;
105        this.mavenSession = mavenSession;
106        this.toolchainManager = toolchainManager;
107    }
108
109    protected abstract List<String> getExtraArgs() throws MojoExecutionException;
110
111    @Override
112    protected void doExecute() throws MojoExecutionException, DependencyResolutionException {
113        if (StringUtils.isEmpty(packageName)) {
114            throw new MojoExecutionException(MICRONAUT_AOT_PACKAGE_NAME + " is not set, and is required if AOT is enabled");
115        }
116        try {
117            getLog().info("Packaging project");
118            compilerService.compileProject();
119            InvocationResultWithOutput packagingResult = compilerService.packageProject();
120            if (packagingResult.getExitCode() != 0) {
121                getLog().error("Error when packaging the project: ", packagingResult.getExecutionException());
122                for (String line : packagingResult.outputHandler().getOutput()) {
123                    getLog().error(line);
124                }
125            } else {
126                executeAot();
127            }
128        } catch (MavenInvocationException e) {
129            getLog().error("Error when packaging project", e);
130        }
131    }
132
133    private void executeAot() throws DependencyResolutionException, MojoExecutionException {
134        getLog().info("Executing Micronaut AOT analysis");
135        Xpp3Dom config = createExecPluginConfig();
136
137        try {
138            executorService.executeGoal(
139                EXEC_MAVEN_PLUGIN_GROUP,
140                EXEC_MAVEN_PLUGIN_ARTIFACT,
141                mavenProject.getProperties().getProperty(EXEC_MAVEN_PLUGIN_VERSION_PROPERTY, DEFAULT_EXEC_MAVEN_PLUGIN_VERSION),
142                "exec",
143                config
144            );
145        } catch (MojoExecutionException e) {
146            getLog().error("Error when executing Micronaut AOT: " + e.getMessage());
147            String commandLine = Arrays.stream(config.getChild("arguments").getChildren())
148                .map(Xpp3Dom::getValue)
149                .collect(Collectors.joining(" "));
150            getLog().error("Command line was: java " + commandLine);
151            throw e;
152        }
153
154    }
155
156    private Xpp3Dom createExecPluginConfig() throws DependencyResolutionException, MojoExecutionException {
157        List<String> aotClasspath = resolveAotClasspath();
158        List<String> aotPluginsClasspath = resolveAotPluginsClasspath();
159        List<String> applicationClasspath = resolveApplicationClasspath();
160
161        var classpath = new ArrayList<String>(aotPluginsClasspath.size() + applicationClasspath.size());
162        classpath.addAll(aotClasspath);
163        classpath.addAll(aotPluginsClasspath);
164        classpath.addAll(applicationClasspath);
165
166        if (!CollectionUtils.isEmpty(aotExclusions)) {
167            getLog().info("Using exclusions for the AOT classpath: " +
168                    aotExclusions.stream().map(v -> v.getGroupId() + ":" + v.getArtifactId()).collect(Collectors.joining(", ")));
169            getLog().info("Resulting AOT classpath: " + String.join(", ", classpath));
170        }
171
172        Stream<String> jvmArgs = Optional.ofNullable(aotJvmArgs).orElse(List.of()).stream();
173        Stream<String> mainArgs = Stream.of(
174            "-classpath",
175            String.join(File.pathSeparator, aotClasspath),
176            MICRONAUT_AOT_MAIN_CLASS,
177
178            // CLI args
179            "--classpath=" + String.join(File.pathSeparator, classpath),
180            "--package=" + packageName,
181            "--runtime=" + runtime
182        );
183        MojoExecutor.Element[] runnerArgs = Stream.concat(Stream.concat(jvmArgs, mainArgs), getExtraArgs().stream())
184            .map(arg -> element("argument", arg))
185            .toArray(MojoExecutor.Element[]::new);
186        
187        String javaExecutable = MojoUtils.findJavaExecutable(toolchainManager, mavenSession);
188
189        return configuration(
190            element("executable", javaExecutable),
191            element("arguments", runnerArgs)
192        );
193    }
194
195    private List<String> resolveApplicationClasspath() {
196        String projectJar = new File(mavenProject.getBuild().getDirectory(), mavenProject.getBuild().getFinalName() + ".jar")
197            .getAbsolutePath();
198        var result = new ArrayList<String>();
199        result.add(projectJar);
200        String classpath = compilerService.buildClasspath(compilerService.resolveDependencies(mavenProject, JavaScopes.RUNTIME)
201                .stream().filter(dep -> this.isDependencyIncluded(dep.getArtifact())).toList()
202        );
203        result.addAll(Arrays.asList(classpath.split(File.pathSeparator)));
204        return result;
205    }
206
207    private List<String> resolveAotClasspath() throws DependencyResolutionException {
208        Stream<Artifact> aotArtifacts = Arrays.stream(AOT_MODULES)
209            .map(m -> new DefaultArtifact(MICRONAUT_AOT_GROUP_ID + ":" + MICRONAUT_AOT_ARTIFACT_ID_PREFIX + m + ":" + micronautAotVersion));
210        return toClasspath(dependencyResolutionService.artifactResultsFor(aotArtifacts, false)
211                .stream().filter(r -> this.isDependencyIncluded(r.getArtifact())).toList()
212        );
213    }
214
215    private List<String> resolveAotPluginsClasspath() throws DependencyResolutionException {
216        if (aotDependencies != null && !aotDependencies.isEmpty()) {
217            Stream<Artifact> aotPlugins = aotDependencies.stream()
218                .map(d -> new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getType(), d.getVersion()));
219            return toClasspath(dependencyResolutionService.artifactResultsFor(aotPlugins, false)
220                    .stream().filter(r -> this.isDependencyIncluded(r.getArtifact())).toList()
221            );
222        } else {
223            return Collections.emptyList();
224        }
225    }
226
227    private boolean isDependencyIncluded(Artifact dependency) {
228        if (aotExclusions == null) {
229            return true;
230        }
231        return aotExclusions.stream()
232                .noneMatch(e -> e.getGroupId().equals(dependency.getGroupId()) && e.getArtifactId().equals(dependency.getArtifactId()));
233    }
234
235}