Skip to content

Commit

Permalink
Option to serialize ApplicationModel in quarkus:generate-code-tests f…
Browse files Browse the repository at this point in the history
…or QuarkusTests to re-use it instead of initializing the resolver and re-resolving the model.
  • Loading branch information
Alexey Loubyansky committed Dec 6, 2024
1 parent 74457f7 commit 8931ff6
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.maven;

import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.List;
Expand All @@ -16,6 +17,7 @@
import io.quarkus.bootstrap.app.CuratedApplication;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.bootstrap.util.BootstrapUtils;
import io.quarkus.maven.dependency.ArtifactCoords;
import io.quarkus.paths.PathCollection;
import io.quarkus.paths.PathList;
Expand All @@ -31,8 +33,11 @@ public class GenerateCodeMojo extends QuarkusBootstrapMojo {
* Skip the execution of this mojo
*/
@Parameter(defaultValue = "false", property = "quarkus.generate-code.skip", alias = "quarkus.prepare.skip")
private boolean skipSourceGeneration = false;
boolean skipSourceGeneration = false;

/**
* Application launch mode for which to generate the source code.
*/
@Parameter(defaultValue = "NORMAL", property = "launchMode")
String mode;

Expand All @@ -57,7 +62,7 @@ protected void doExecute() throws MojoExecutionException, MojoFailureException {

void generateCode(PathCollection sourceParents,
Consumer<Path> sourceRegistrar,
boolean test) throws MojoFailureException, MojoExecutionException {
boolean test) throws MojoExecutionException {

final LaunchMode launchMode;
if (test) {
Expand Down Expand Up @@ -97,13 +102,31 @@ void generateCode(PathCollection sourceParents,
if (deploymentClassLoader != null) {
deploymentClassLoader.close();
}
// in case of test mode, we can't share the bootstrapped app with the testing plugins, so we are closing it right away
// In case of the test mode, we can't share the application model with the test plugins, so we are closing it right away,
// but we are serializing the application model so the test plugins can deserialize it from disk instead of re-initializing
// the resolver and re-resolving it as part of the test bootstrap
if (test && curatedApplication != null) {
curatedApplication.close();
var appModel = curatedApplication.getApplicationModel();
closeApplication(LaunchMode.TEST);
if (isSerializeTestModel()) {
final int workspaceId = getWorkspaceId();
if (workspaceId != 0) {
try {
BootstrapUtils.writeAppModelWithWorkspaceId(appModel, workspaceId, BootstrapUtils
.getSerializedTestAppModelPath(Path.of(mavenProject().getBuild().getDirectory())));
} catch (IOException e) {
getLog().warn("Failed to serialize application model", e);
}
}
}
}
}
}

protected boolean isSerializeTestModel() {
return false;
}

protected PathCollection getParentDirs(List<String> sourceDirs) {
if (sourceDirs.size() == 1) {
return PathList.of(Path.of(sourceDirs.get(0)).getParent());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,29 @@
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;

import io.quarkus.bootstrap.app.CuratedApplication;
import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.builder.Json;
import io.quarkus.maven.dependency.ResolvedDependency;
import io.quarkus.runtime.LaunchMode;

@Mojo(name = "generate-code-tests", defaultPhase = LifecyclePhase.GENERATE_TEST_SOURCES, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, threadSafe = true)
public class GenerateCodeTestsMojo extends GenerateCodeMojo {

/**
* A switch that enables or disables serialization of an {@link ApplicationModel} to a file for tests.
* Deserializing an application model when bootstrapping Quarkus tests has a performance advantage in that
* the tests will not have to initialize a Maven resolver and re-resolve the application model, which may save,
* depending on a project, ~80-95% of time on {@link ApplicationModel} resolution.
* <p>
* Serialization of the test model is enabled by default.
*/
@Parameter(property = "quarkus.generate-code.serialize-test-model", defaultValue = "true")
boolean serializeTestModel;

@Override
protected void doExecute() throws MojoExecutionException, MojoFailureException {
generateCode(getParentDirs(mavenProject().getTestCompileSourceRoots()),
Expand All @@ -32,6 +46,11 @@ protected void doExecute() throws MojoExecutionException, MojoFailureException {
}
}

@Override
protected boolean isSerializeTestModel() {
return serializeTestModel;
}

private boolean isTestWithNativeAgent() {
String value = System.getProperty("quarkus.test.integration-test-profile");
if ("test-with-native-agent".equals(value)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,20 @@ protected CuratedApplication bootstrapApplication(LaunchMode mode) throws MojoEx
return bootstrapProvider.bootstrapApplication(this, mode);
}

protected void closeApplication(LaunchMode mode) {
bootstrapProvider.closeApplication(this, mode);
}

/**
* Workspace ID associated with a given bootstrap mojo.
* If the returned value is {@code 0}, a workspace was not associated with the bootstrap mojo.
*
* @return workspace ID associated with a given bootstrap mojo
*/
protected int getWorkspaceId() {
return bootstrapProvider.getWorkspaceId(this);
}

protected CuratedApplication bootstrapApplication(LaunchMode mode, Consumer<QuarkusBootstrap.Builder> builderCustomizer)
throws MojoExecutionException {
return bootstrapProvider.bootstrapApplication(this, mode, builderCustomizer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver;
import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver;
import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver;
import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject;
import io.quarkus.maven.components.ManifestSection;
import io.quarkus.maven.components.QuarkusWorkspaceProvider;
import io.quarkus.maven.dependency.ArtifactCoords;
Expand Down Expand Up @@ -138,6 +139,21 @@ public CuratedApplication bootstrapApplication(QuarkusBootstrapMojo mojo, Launch
return bootstrapper(mojo).bootstrapApplication(mojo, mode, builderCustomizer);
}

public void closeApplication(QuarkusBootstrapMojo mojo, LaunchMode mode) {
bootstrapper(mojo).closeApplication(mode);
}

/**
* Workspace ID associated with a given bootstrap mojo.
* If the returned value is {@code 0}, a workspace was not associated with the bootstrap mojo.
*
* @param mojo bootstrap mojo
* @return workspace ID associated with a given bootstrap mojo
*/
public int getWorkspaceId(QuarkusBootstrapMojo mojo) {
return bootstrapper(mojo).workspaceId;
}

public ApplicationModel getResolvedApplicationModel(ArtifactKey projectId, LaunchMode mode, String bootstrapId) {
if (appBootstrapProviders.size() == 0) {
return null;
Expand Down Expand Up @@ -180,14 +196,15 @@ private static boolean isWorkspaceDiscovery(QuarkusBootstrapMojo mojo) {

public class QuarkusMavenAppBootstrap implements Closeable {

private int workspaceId;
private CuratedApplication prodApp;
private CuratedApplication devApp;
private CuratedApplication testApp;

private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, LaunchMode mode) {
try {
if (mode == LaunchMode.DEVELOPMENT || mode == LaunchMode.TEST || isWorkspaceDiscovery(mojo)) {
return workspaceProvider.createArtifactResolver(
var resolver = workspaceProvider.createArtifactResolver(
BootstrapMavenContext.config()
// it's important to pass user settings in case the process was not launched using the original mvn script
// for example using org.codehaus.plexus.classworlds.launcher.Launcher
Expand All @@ -199,6 +216,11 @@ private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, Launch
.setRemoteRepositories(mojo.remoteRepositories())
.setEffectiveModelBuilder(BootstrapMavenContextConfig
.getEffectiveModelBuilderProperty(mojo.mavenProject().getProperties())));
final LocalProject currentProject = resolver.getMavenContext().getCurrentProject();
if (currentProject != null && workspaceId == 0) {
workspaceId = currentProject.getWorkspace().getId();
}
return resolver;
}
// PROD packaging mode with workspace discovery disabled
return MavenArtifactResolver.builder()
Expand Down Expand Up @@ -376,6 +398,23 @@ protected CuratedApplication bootstrapApplication(QuarkusBootstrapMojo mojo, Lau
return prodApp == null ? prodApp = doBootstrap(mojo, mode, builderCustomizer) : prodApp;
}

protected void closeApplication(LaunchMode mode) {
if (mode == LaunchMode.DEVELOPMENT) {
if (devApp != null) {
devApp.close();
devApp = null;
}
} else if (mode == LaunchMode.TEST) {
if (testApp != null) {
testApp.close();
testApp = null;
}
} else if (prodApp != null) {
prodApp.close();
prodApp = null;
}
}

protected ArtifactCoords managingProject(QuarkusBootstrapMojo mojo) {
if (mojo.appArtifactCoords() == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@
import java.nio.file.Path;
import java.util.regex.Pattern;

import org.jboss.logging.Logger;

import io.quarkus.bootstrap.BootstrapConstants;
import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.bootstrap.resolver.AppModelResolverException;
import io.quarkus.maven.dependency.ArtifactKey;
import io.quarkus.maven.dependency.DependencyFlags;
import io.quarkus.maven.dependency.GACT;
import io.quarkus.maven.dependency.ResolvedDependency;

public class BootstrapUtils {

private static final Logger log = Logger.getLogger(BootstrapUtils.class);

private static final int CP_CACHE_FORMAT_ID = 2;

private static Pattern splitByWs;

public static String[] splitByWhitespace(String s) {
Expand Down Expand Up @@ -81,7 +89,90 @@ public static ApplicationModel deserializeQuarkusModel(Path modelPath) throws Ap
throw new AppModelResolverException("Unable to locate quarkus model");
}

/**
* Returns a location where a serialized {@link ApplicationModel} would be found for dev mode.
*
* @param projectBuildDir project build directory
* @return file of a serialized application model for dev mode
*/
public static Path resolveSerializedAppModelPath(Path projectBuildDir) {
return projectBuildDir.resolve("quarkus").resolve("bootstrap").resolve("dev-app-model.dat");
return getBootstrapBuildDir(projectBuildDir).resolve("dev-app-model.dat");
}

/**
* Returns a location where a serialized {@link ApplicationModel} would be found for test mode.
*
* @param projectBuildDir project build directory
* @return file of a serialized application model for test mode
*/
public static Path getSerializedTestAppModelPath(Path projectBuildDir) {
return getBootstrapBuildDir(projectBuildDir).resolve("test-app-model.dat");
}

private static Path getBootstrapBuildDir(Path projectBuildDir) {
return projectBuildDir.resolve("quarkus").resolve("bootstrap");
}

/**
* Serializes an {@link ApplicationModel} along with the workspace ID for which it was resolved.
* The serialization format will be different from the one used by {@link #resolveSerializedAppModelPath(Path)}
* and {@link #getSerializedTestAppModelPath(Path)}.
*
* @param appModel application model to serialize
* @param workspaceId workspace ID
* @param file target file
* @throws IOException in case of an IO failure
*/
public static void writeAppModelWithWorkspaceId(ApplicationModel appModel, int workspaceId, Path file) throws IOException {
Files.createDirectories(file.getParent());
try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(file))) {
out.writeInt(CP_CACHE_FORMAT_ID);
out.writeInt(workspaceId);
out.writeObject(appModel);
}
log.debugf("Serialized application model to %s", file);
}

/**
* Deserializes an {@link ApplicationModel} from a file.
* <p>
* The implementation will check whether the serialization format of the file matches the expected one.
* If it does not, the method will return null even if the file exists.
* <p>
* The implementation will compare the deserialized workspace ID to the argument {@code workspaceId}
* and if they don't match the method will return null.
* <p>
* Once the {@link ApplicationModel} was deserialized, the dependency paths will be checked for existence.
* If a dependency path does not exist, the method will throw an exception.
*
* @param file serialized application model file
* @param workspaceId expected workspace ID
* @return deserialized application model
* @throws ClassNotFoundException in case a required class could not be loaded
* @throws IOException in case of an IO failure
*/
public static ApplicationModel readAppModelWithWorkspaceId(Path file, int workspaceId)
throws ClassNotFoundException, IOException {
try (ObjectInputStream reader = new ObjectInputStream(Files.newInputStream(file))) {
if (reader.readInt() == CP_CACHE_FORMAT_ID) {
if (reader.readInt() == workspaceId) {
final ApplicationModel appModel = (ApplicationModel) reader.readObject();
log.debugf("Loaded application model %s from %s", appModel, file);
for (ResolvedDependency d : appModel.getDependencies(DependencyFlags.DEPLOYMENT_CP)) {
for (Path p : d.getResolvedPaths()) {
if (!Files.exists(p)) {
throw new IOException("Cached artifact does not exist: " + p);
}
}
}
return appModel;
} else {
log.debugf("Application model saved in %s has a different workspace ID", file);
}
} else {
log.debugf("Unsupported application model serialization format in %s", file);
}
}
return null;
}
}
Loading

0 comments on commit 8931ff6

Please sign in to comment.