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.MojoUtils;
020import io.micronaut.maven.services.CompilerService;
021import io.micronaut.maven.services.DependencyResolutionService;
022import io.micronaut.maven.services.ExecutorService;
023
024import org.apache.maven.execution.MavenSession;
025import org.apache.maven.plugin.MojoExecutionException;
026import org.apache.maven.plugins.annotations.Parameter;
027import org.apache.maven.project.MavenProject;
028import org.apache.maven.shared.invoker.InvocationResult;
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            InvocationResult packagingResult = compilerService.packageProject();
120            if (packagingResult.getExitCode() != 0) {
121                getLog().error("Error when packaging the project: ", packagingResult.getExecutionException());
122            } else {
123                executeAot();
124            }
125        } catch (MavenInvocationException e) {
126            getLog().error("Error when packaging project", e);
127        }
128    }
129
130    private void executeAot() throws DependencyResolutionException, MojoExecutionException {
131        getLog().info("Executing Micronaut AOT analysis");
132        Xpp3Dom config = createExecPluginConfig();
133
134        try {
135            executorService.executeGoal(
136                EXEC_MAVEN_PLUGIN_GROUP,
137                EXEC_MAVEN_PLUGIN_ARTIFACT,
138                mavenProject.getProperties().getProperty(EXEC_MAVEN_PLUGIN_VERSION_PROPERTY, DEFAULT_EXEC_MAVEN_PLUGIN_VERSION),
139                "exec",
140                config
141            );
142        } catch (MojoExecutionException e) {
143            getLog().error("Error when executing Micronaut AOT: " + e.getMessage());
144            String commandLine = Arrays.stream(config.getChild("arguments").getChildren())
145                .map(Xpp3Dom::getValue)
146                .collect(Collectors.joining(" "));
147            getLog().error("Command line was: java " + commandLine);
148            throw e;
149        }
150
151    }
152
153    private Xpp3Dom createExecPluginConfig() throws DependencyResolutionException, MojoExecutionException {
154        List<String> aotClasspath = resolveAotClasspath();
155        List<String> aotPluginsClasspath = resolveAotPluginsClasspath();
156        List<String> applicationClasspath = resolveApplicationClasspath();
157
158        var classpath = new ArrayList<String>(aotPluginsClasspath.size() + applicationClasspath.size());
159        classpath.addAll(aotClasspath);
160        classpath.addAll(aotPluginsClasspath);
161        classpath.addAll(applicationClasspath);
162
163        if (!CollectionUtils.isEmpty(aotExclusions)) {
164            getLog().info("Using exclusions for the AOT classpath: " +
165                    aotExclusions.stream().map(v -> v.getGroupId() + ":" + v.getArtifactId()).collect(Collectors.joining(", ")));
166            getLog().info("Resulting AOT classpath: " + String.join(", ", classpath));
167        }
168
169        Stream<String> jvmArgs = Optional.ofNullable(aotJvmArgs).orElse(List.of()).stream();
170        Stream<String> mainArgs = Stream.of(
171            "-classpath",
172            String.join(File.pathSeparator, aotClasspath),
173            MICRONAUT_AOT_MAIN_CLASS,
174
175            // CLI args
176            "--classpath=" + String.join(File.pathSeparator, classpath),
177            "--package=" + packageName,
178            "--runtime=" + runtime
179        );
180        MojoExecutor.Element[] runnerArgs = Stream.concat(Stream.concat(jvmArgs, mainArgs), getExtraArgs().stream())
181            .map(arg -> element("argument", arg))
182            .toArray(MojoExecutor.Element[]::new);
183        
184        String javaExecutable = MojoUtils.findJavaExecutable(toolchainManager, mavenSession);
185
186        return configuration(
187            element("executable", javaExecutable),
188            element("arguments", runnerArgs)
189        );
190    }
191
192    private List<String> resolveApplicationClasspath() {
193        String projectJar = new File(mavenProject.getBuild().getDirectory(), mavenProject.getBuild().getFinalName() + ".jar")
194            .getAbsolutePath();
195        var result = new ArrayList<String>();
196        result.add(projectJar);
197        String classpath = compilerService.buildClasspath(compilerService.resolveDependencies(mavenProject, JavaScopes.RUNTIME)
198                .stream().filter(dep -> this.isDependencyIncluded(dep.getArtifact())).toList()
199        );
200        result.addAll(Arrays.asList(classpath.split(File.pathSeparator)));
201        return result;
202    }
203
204    private List<String> resolveAotClasspath() throws DependencyResolutionException {
205        Stream<Artifact> aotArtifacts = Arrays.stream(AOT_MODULES)
206            .map(m -> new DefaultArtifact(MICRONAUT_AOT_GROUP_ID + ":" + MICRONAUT_AOT_ARTIFACT_ID_PREFIX + m + ":" + micronautAotVersion));
207        return toClasspath(dependencyResolutionService.artifactResultsFor(aotArtifacts, false)
208                .stream().filter(r -> this.isDependencyIncluded(r.getArtifact())).toList()
209        );
210    }
211
212    private List<String> resolveAotPluginsClasspath() throws DependencyResolutionException {
213        if (aotDependencies != null && !aotDependencies.isEmpty()) {
214            Stream<Artifact> aotPlugins = aotDependencies.stream()
215                .map(d -> new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getType(), d.getVersion()));
216            return toClasspath(dependencyResolutionService.artifactResultsFor(aotPlugins, false)
217                    .stream().filter(r -> this.isDependencyIncluded(r.getArtifact())).toList()
218            );
219        } else {
220            return Collections.emptyList();
221        }
222    }
223
224    private boolean isDependencyIncluded(Artifact dependency) {
225        if (aotExclusions == null) {
226            return true;
227        }
228        return aotExclusions.stream()
229                .noneMatch(e -> e.getGroupId().equals(dependency.getGroupId()) && e.getArtifactId().equals(dependency.getArtifactId()));
230    }
231
232}