From a7daf1405c04d25fd23d05f9363f02a1a3d6bcf1 Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Mon, 25 Aug 2025 20:23:19 +0300 Subject: [PATCH 1/3] Prepare quarkus tests for gradle9 --- .../quarkus2-plugin/build.gradle.kts | 22 + .../quarkus2plugin/Quarkus2Plugin.java | 20 + .../ConditionalDependenciesEnabler.java | 245 +++++++ .../GradleApplicationModelBuilder.java | 674 ++++++++++++++++++ .../quarkus/gradle/tooling/ToolingUtils.java | 108 +++ .../tooling/dependency/DependencyUtils.java | 212 ++++++ .../quarkus2-testing/build.gradle.kts | 47 +- .../quarkus3-plugin/build.gradle.kts | 22 + .../quarkus3plugin/Quarkus3Plugin.java | 20 + .../ConditionalDependenciesEnabler.java | 245 +++++++ .../GradleApplicationModelBuilder.java | 674 ++++++++++++++++++ .../quarkus/gradle/tooling/ToolingUtils.java | 108 +++ .../tooling/dependency/DependencyUtils.java | 212 ++++++ .../quarkus3-testing/build.gradle.kts | 46 +- settings.gradle.kts | 2 + 15 files changed, 2643 insertions(+), 14 deletions(-) create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/build.gradle.kts create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/opentelemetry/instrumentation/quarkus2plugin/Quarkus2Plugin.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/build.gradle.kts create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/opentelemetry/instrumentation/quarkus3plugin/Quarkus3Plugin.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/build.gradle.kts b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/build.gradle.kts new file mode 100644 index 000000000000..96daf6507b26 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + `java-gradle-plugin` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation(gradleApi()) + implementation("io.quarkus:quarkus-gradle-model:2.16.7.Final") +} + +gradlePlugin { + plugins { + create("quarkus2Plugin") { + id = "io.opentelemetry.instrumentation.quarkus2" + implementationClass = "io.opentelemetry.instrumentation.quarkus2plugin.Quarkus2Plugin" + } + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/opentelemetry/instrumentation/quarkus2plugin/Quarkus2Plugin.java b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/opentelemetry/instrumentation/quarkus2plugin/Quarkus2Plugin.java new file mode 100644 index 000000000000..8ca0a40f5631 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/opentelemetry/instrumentation/quarkus2plugin/Quarkus2Plugin.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.quarkus2plugin; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +@SuppressWarnings("unused") +public class Quarkus2Plugin implements Plugin { + + @Override + public void apply(Project project) { + // we use this plugin with apply false and call its classes directly from the build script + throw new IllegalStateException("this plugin is not meant to be applied"); + } +} + diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java new file mode 100644 index 000000000000..01690e33986a --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java @@ -0,0 +1,245 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Quarkus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.quarkus.gradle.dependency; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedArtifact; + +import io.quarkus.gradle.tooling.dependency.DependencyUtils; +import io.quarkus.gradle.tooling.dependency.ExtensionDependency; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.GACT; +import io.quarkus.runtime.LaunchMode; + +public class ConditionalDependenciesEnabler { + + /** + * Links dependencies to extensions + */ + private final Map> featureVariants = new HashMap<>(); + /** + * Despite its name, only contains extensions which have no conditional dependencies, or have + * resolved their conditional dependencies. + */ + private final Map allExtensions = new HashMap<>(); + private final Project project; + private final Configuration enforcedPlatforms; + private final Set existingArtifacts = new HashSet<>(); + private final List unsatisfiedConditionalDeps = new ArrayList<>(); + + public ConditionalDependenciesEnabler(Project project, LaunchMode mode, + Configuration platforms) { + this.project = project; + this.enforcedPlatforms = platforms; + + // Get runtimeClasspath (quarkusProdBaseRuntimeClasspathConfiguration to be exact) + Configuration baseRuntimeConfig = project.getConfigurations() + .getByName(ApplicationDeploymentClasspathBuilder.getBaseRuntimeConfigName(mode)); + + if (!baseRuntimeConfig.getIncoming().getDependencies().isEmpty()) { + // Gather all extensions from the full resolved dependency tree + collectConditionalDependencies(baseRuntimeConfig.getResolvedConfiguration().getResolvedArtifacts()); + // If there are any extensions which had unresolved conditional dependencies: + while (!unsatisfiedConditionalDeps.isEmpty()) { + boolean satisfiedConditionalDeps = false; + final int originalUnsatisfiedCount = unsatisfiedConditionalDeps.size(); + int i = 0; + // Go through each unsatisfied/unresolved dependency once: + while (i < unsatisfiedConditionalDeps.size()) { + final Dependency conditionalDep = unsatisfiedConditionalDeps.get(i); + // Try to resolve it with the latest evolved graph available + if (resolveConditionalDependency(conditionalDep)) { + // Mark the resolution as a success so we know the graph evolved + satisfiedConditionalDeps = true; + unsatisfiedConditionalDeps.remove(i); + } else { + // No resolution (yet) or graph evolution; move on to the next + ++i; + } + } + // If we didn't resolve any dependencies and the graph did not evolve, give up. + if (!satisfiedConditionalDeps && unsatisfiedConditionalDeps.size() == originalUnsatisfiedCount) { + break; + } + } + reset(); + } + + } + + public Collection getAllExtensions() { + return allExtensions.values(); + } + + private void reset() { + featureVariants.clear(); + existingArtifacts.clear(); + unsatisfiedConditionalDeps.clear(); + } + + private void collectConditionalDependencies(Set runtimeArtifacts) { + // For every artifact in the dependency graph: + for (ResolvedArtifact artifact : runtimeArtifacts) { + // Add to master list of artifacts: + existingArtifacts.add(getKey(artifact)); + ExtensionDependency extension = DependencyUtils.getExtensionInfoOrNull(project, artifact); + // If this artifact represents an extension: + if (extension != null) { + // Add to master list of accepted extensions: + allExtensions.put(extension.getExtensionId(), extension); + for (Dependency conditionalDep : extension.getConditionalDependencies()) { + // If the dependency is not present yet in the graph, queue it for resolution later + if (!exists(conditionalDep)) { + queueConditionalDependency(extension, conditionalDep); + } + } + } + } + } + + private boolean resolveConditionalDependency(Dependency conditionalDep) { + + final Configuration conditionalDeps = createConditionalDependenciesConfiguration(project, conditionalDep); + Set resolvedArtifacts = conditionalDeps.getResolvedConfiguration().getResolvedArtifacts(); + + boolean satisfied = false; + // Resolved artifacts don't have great linking back to the original artifact, so I think + // this loop is trying to find the artifact that represents the original conditional + // dependency + for (ResolvedArtifact artifact : resolvedArtifacts) { + if (conditionalDep.getName().equals(artifact.getName()) + && conditionalDep.getVersion().equals(artifact.getModuleVersion().getId().getVersion()) + && artifact.getModuleVersion().getId().getGroup().equals(conditionalDep.getGroup())) { + // Once the dependency is found, reload the extension info from within + final ExtensionDependency extensionDependency = DependencyUtils.getExtensionInfoOrNull(project, artifact); + // Now check if this conditional dependency is resolved given the latest graph evolution + if (extensionDependency != null && (extensionDependency.getDependencyConditions().isEmpty() + || exist(extensionDependency.getDependencyConditions()))) { + satisfied = true; + enableConditionalDependency(extensionDependency.getExtensionId()); + break; + } + } + } + + // No resolution (yet); give up. + if (!satisfied) { + return false; + } + + // The conditional dependency resolved! Let's now add all of /its/ dependencies + for (ResolvedArtifact artifact : resolvedArtifacts) { + // First add the artifact to the master list + existingArtifacts.add(getKey(artifact)); + ExtensionDependency extensionDependency = DependencyUtils.getExtensionInfoOrNull(project, artifact); + if (extensionDependency == null) { + continue; + } + // If this artifact represents an extension, mark this one as a conditional extension + extensionDependency.setConditional(true); + // Add to the master list of accepted extensions + allExtensions.put(extensionDependency.getExtensionId(), extensionDependency); + for (Dependency cd : extensionDependency.getConditionalDependencies()) { + // Add any unsatisfied/unresolved conditional dependencies of this dependency to the queue + if (!exists(cd)) { + queueConditionalDependency(extensionDependency, cd); + } + } + } + return satisfied; + } + + private void queueConditionalDependency(ExtensionDependency extension, Dependency conditionalDep) { + // 1. Add to master list of unresolved/unsatisfied dependencies + // 2. Add map entry to link dependency to extension + featureVariants.computeIfAbsent(getFeatureKey(conditionalDep), k -> { + unsatisfiedConditionalDeps.add(conditionalDep); + return new HashSet<>(); + }).add(extension); + } + + private Configuration createConditionalDependenciesConfiguration(Project project, Dependency conditionalDep) { + /* + Configuration conditionalDepConfiguration = project.getConfigurations() + .detachedConfiguration() + .extendsFrom(enforcedPlatforms); + */ + Configuration conditionalDepConfiguration = project.getConfigurations().detachedConfiguration(); + enforcedPlatforms.getExcludeRules().forEach(rule -> { + conditionalDepConfiguration.exclude(Map.of( + "group", rule.getGroup(), + "module", rule.getModule())); + }); + enforcedPlatforms.getAllDependencies().forEach(dependency -> { + conditionalDepConfiguration.getDependencies().add(dependency); + }); + conditionalDepConfiguration.getDependencies().add(conditionalDep); + return conditionalDepConfiguration; + } + + private void enableConditionalDependency(ModuleVersionIdentifier dependency) { + final Set extensions = featureVariants.remove(getFeatureKey(dependency)); + if (extensions == null) { + return; + } + extensions.forEach(e -> e.importConditionalDependency(project.getDependencies(), dependency)); + } + + private boolean exist(List dependencies) { + return existingArtifacts.containsAll(dependencies); + } + + private boolean exists(Dependency dependency) { + return existingArtifacts + .contains(ArtifactKey.of(dependency.getGroup(), dependency.getName(), null, ArtifactCoords.TYPE_JAR)); + } + + public boolean exists(ExtensionDependency dependency) { + return existingArtifacts + .contains(ArtifactKey.of(dependency.getGroup(), dependency.getName(), null, ArtifactCoords.TYPE_JAR)); + } + + private static GACT getFeatureKey(ModuleVersionIdentifier version) { + return new GACT(version.getGroup(), version.getName()); + } + + private static GACT getFeatureKey(Dependency version) { + return new GACT(version.getGroup(), version.getName()); + } + + private static ArtifactKey getKey(ResolvedArtifact a) { + return ArtifactKey.of(a.getModuleVersion().getId().getGroup(), a.getName(), a.getClassifier(), a.getType()); + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java new file mode 100644 index 000000000000..5f2b35b16019 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java @@ -0,0 +1,674 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Quarkus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.quarkus.gradle.tooling; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.ResolvedConfiguration; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTree; +import org.gradle.api.initialization.IncludedBuild; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskCollection; +import org.gradle.api.tasks.compile.AbstractCompile; +import org.gradle.api.tasks.testing.Test; +import org.gradle.internal.composite.IncludedBuildInternal; +import org.gradle.language.jvm.tasks.ProcessResources; +import org.gradle.tooling.provider.model.ParameterizedToolingModelBuilder; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.model.ApplicationModelBuilder; +import io.quarkus.bootstrap.model.CapabilityContract; +import io.quarkus.bootstrap.model.PlatformImports; +import io.quarkus.bootstrap.model.gradle.ModelParameter; +import io.quarkus.bootstrap.model.gradle.impl.ModelParameterImpl; +import io.quarkus.bootstrap.workspace.ArtifactSources; +import io.quarkus.bootstrap.workspace.DefaultArtifactSources; +import io.quarkus.bootstrap.workspace.DefaultSourceDir; +import io.quarkus.bootstrap.workspace.DefaultWorkspaceModule; +import io.quarkus.bootstrap.workspace.SourceDir; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.fs.util.ZipUtils; +import io.quarkus.gradle.dependency.ApplicationDeploymentClasspathBuilder; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactDependency; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.GACT; +import io.quarkus.maven.dependency.GACTV; +import io.quarkus.maven.dependency.GAV; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; +import io.quarkus.paths.PathCollection; +import io.quarkus.paths.PathList; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.util.HashUtil; + +public class GradleApplicationModelBuilder implements ParameterizedToolingModelBuilder { + + private static final String MAIN_RESOURCES_OUTPUT = "build/resources/main"; + private static final String CLASSES_OUTPUT = "build/classes"; + + /* @formatter:off */ + private static final byte COLLECT_TOP_EXTENSION_RUNTIME_NODES = 0b001; + private static final byte COLLECT_DIRECT_DEPS = 0b010; + private static final byte COLLECT_RELOADABLE_MODULES = 0b100; + /* @formatter:on */ + + @Override + public boolean canBuild(String modelName) { + return modelName.equals(ApplicationModel.class.getName()); + } + + @Override + public Class getParameterType() { + return ModelParameter.class; + } + + @Override + public Object buildAll(String modelName, Project project) { + final ModelParameterImpl modelParameter = new ModelParameterImpl(); + modelParameter.setMode(LaunchMode.DEVELOPMENT.toString()); + return buildAll(modelName, modelParameter, project); + } + + @Override + public Object buildAll(String modelName, ModelParameter parameter, Project project) { + final LaunchMode mode = LaunchMode.valueOf(parameter.getMode()); + + final ApplicationDeploymentClasspathBuilder classpathBuilder = new ApplicationDeploymentClasspathBuilder(project, + mode); + final Configuration classpathConfig = classpathBuilder.getRuntimeConfiguration(); + final Configuration deploymentConfig = classpathBuilder.getDeploymentConfiguration(); + final PlatformImports platformImports = classpathBuilder.getPlatformImports(); + + boolean workspaceDiscovery = LaunchMode.DEVELOPMENT.equals(mode) || LaunchMode.TEST.equals(mode) + || Boolean.parseBoolean(System.getProperty(BootstrapConstants.QUARKUS_BOOTSTRAP_WORKSPACE_DISCOVERY)); + if (!workspaceDiscovery) { + Object o = project.getProperties().get(BootstrapConstants.QUARKUS_BOOTSTRAP_WORKSPACE_DISCOVERY); + if (o != null) { + workspaceDiscovery = Boolean.parseBoolean(o.toString()); + } + } + + final ResolvedDependency appArtifact = getProjectArtifact(project, workspaceDiscovery); + final ApplicationModelBuilder modelBuilder = new ApplicationModelBuilder() + .setAppArtifact(appArtifact) + .addReloadableWorkspaceModule(appArtifact.getKey()) + .setPlatformImports(platformImports); + + collectDependencies(classpathConfig.getResolvedConfiguration(), classpathConfig.getIncoming(), workspaceDiscovery, + project, modelBuilder, appArtifact.getWorkspaceModule().mutable()); + collectExtensionDependencies(project, deploymentConfig, modelBuilder); + + return modelBuilder.build(); + } + + public static ResolvedDependency getProjectArtifact(Project project, boolean workspaceDiscovery) { + final ResolvedDependencyBuilder appArtifact = ResolvedDependencyBuilder.newInstance() + .setGroupId(project.getGroup().toString()) + .setArtifactId(project.getName()) + .setVersion(project.getVersion().toString()); + + final SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + final WorkspaceModule.Mutable mainModule = WorkspaceModule.builder() + .setModuleId(new GAV(appArtifact.getGroupId(), appArtifact.getArtifactId(), appArtifact.getVersion())) + .setModuleDir(project.getProjectDir().toPath()) + .setBuildDir(project.getBuildDir().toPath()) + .setBuildFile(project.getBuildFile().toPath()); + + initProjectModule(project, mainModule, sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME), ArtifactSources.MAIN); + if (workspaceDiscovery) { + final TaskCollection testTasks = project.getTasks().withType(Test.class); + if (!testTasks.isEmpty()) { + final Map sourceSetsByClassesDir = new HashMap<>(); + sourceSets.forEach(s -> { + s.getOutput().getClassesDirs().forEach(d -> { + if (d.exists()) { + sourceSetsByClassesDir.put(d, s); + } + }); + }); + testTasks.forEach(t -> { + if (t.getEnabled()) { + t.getTestClassesDirs().forEach(d -> { + if (d.exists()) { + final SourceSet sourceSet = sourceSetsByClassesDir.remove(d); + if (sourceSet != null) { + initProjectModule(project, mainModule, sourceSet, + sourceSet.getName().equals(SourceSet.TEST_SOURCE_SET_NAME) + ? ArtifactSources.TEST + : sourceSet.getName()); + } + } + }); + } + }); + } + } + + final PathList.Builder paths = PathList.builder(); + collectDestinationDirs(mainModule.getMainSources().getSourceDirs(), paths); + collectDestinationDirs(mainModule.getMainSources().getResourceDirs(), paths); + + return appArtifact.setWorkspaceModule(mainModule).setResolvedPaths(paths.build()).build(); + } + + private static void collectDestinationDirs(Collection sources, final PathList.Builder paths) { + for (SourceDir src : sources) { + final Path path = src.getOutputDir(); + if (paths.contains(path) || !Files.exists(path)) { + continue; + } + paths.add(path); + } + } + + private void collectExtensionDependencies(Project project, Configuration deploymentConfiguration, + ApplicationModelBuilder modelBuilder) { + final ResolvedConfiguration rc = deploymentConfiguration.getResolvedConfiguration(); + for (ResolvedArtifact a : rc.getResolvedArtifacts()) { + if (a.getId().getComponentIdentifier() instanceof ProjectComponentIdentifier) { + ProjectComponentIdentifier projectComponentIdentifier = (ProjectComponentIdentifier) a.getId() + .getComponentIdentifier(); + var includedBuild = ToolingUtils.includedBuild(project, projectComponentIdentifier); + Project projectDep = null; + if (includedBuild != null) { + projectDep = ToolingUtils.includedBuildProject((IncludedBuildInternal) includedBuild, + projectComponentIdentifier); + } else { + projectDep = project.getRootProject().findProject(projectComponentIdentifier.getProjectPath()); + } + Objects.requireNonNull(projectDep, "project " + projectComponentIdentifier.getProjectPath() + " should exist"); + SourceSetContainer sourceSets = projectDep.getExtensions().getByType(SourceSetContainer.class); + + SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + ResolvedDependencyBuilder dep = modelBuilder.getDependency( + toAppDependenciesKey(a.getModuleVersion().getId().getGroup(), a.getName(), a.getClassifier())); + if (dep == null) { + dep = toDependency(a, mainSourceSet); + modelBuilder.addDependency(dep); + } + dep.setDeploymentCp(); + dep.clearFlag(DependencyFlags.RELOADABLE); + } else if (isDependency(a)) { + ResolvedDependencyBuilder dep = modelBuilder.getDependency( + toAppDependenciesKey(a.getModuleVersion().getId().getGroup(), a.getName(), a.getClassifier())); + if (dep == null) { + dep = toDependency(a); + modelBuilder.addDependency(dep); + } + dep.setDeploymentCp(); + dep.clearFlag(DependencyFlags.RELOADABLE); + } + } + } + + private void collectDependencies(ResolvedConfiguration configuration, ResolvableDependencies dependencies, + boolean workspaceDiscovery, Project project, ApplicationModelBuilder modelBuilder, + WorkspaceModule.Mutable wsModule) { + + final Set resolvedArtifacts = configuration.getResolvedArtifacts(); + // if the number of artifacts is less than the number of files then probably + // the project includes direct file dependencies + // final Set artifactFiles = resolvedArtifacts.size() < configuration.getFiles().size() + final Set artifactFiles = resolvedArtifacts.size() < dependencies.getFiles().getFiles().size() + ? new HashSet<>(resolvedArtifacts.size()) + : null; + + configuration.getFirstLevelModuleDependencies() + .forEach(d -> { + collectDependencies(d, workspaceDiscovery, project, artifactFiles, new HashSet<>(), + modelBuilder, + wsModule, + (byte) (COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS | COLLECT_RELOADABLE_MODULES)); + }); + + if (artifactFiles != null) { + // detect FS paths that aren't provided by the resolved artifacts + // for (File f : configuration.getFiles()) { + for (File f : dependencies.getFiles().getFiles()) { + if (artifactFiles.contains(f) || !f.exists()) { + continue; + } + // here we are trying to represent a direct FS path dependency + // as an artifact dependency + // SHA1 hash is used to avoid long file names in the lib dir + final String parentPath = f.getParent(); + final String group = HashUtil.sha1(parentPath == null ? f.getName() : parentPath); + String name = f.getName(); + String type = ArtifactCoords.TYPE_JAR; + if (!f.isDirectory()) { + final int dot = f.getName().lastIndexOf('.'); + if (dot > 0) { + name = f.getName().substring(0, dot); + type = f.getName().substring(dot + 1); + } + } + // hash could be a better way to represent the version + final String version = String.valueOf(f.lastModified()); + final ResolvedDependencyBuilder artifactBuilder = ResolvedDependencyBuilder.newInstance() + .setGroupId(group) + .setArtifactId(name) + .setType(type) + .setVersion(version) + .setResolvedPath(f.toPath()) + .setDirect(true) + .setRuntimeCp(); + processQuarkusDependency(artifactBuilder, modelBuilder); + modelBuilder.addDependency(artifactBuilder); + } + } + } + + private void collectDependencies(org.gradle.api.artifacts.ResolvedDependency resolvedDep, boolean workspaceDiscovery, + Project project, Set artifactFiles, Set processedModules, ApplicationModelBuilder modelBuilder, + WorkspaceModule.Mutable parentModule, + byte flags) { + WorkspaceModule.Mutable projectModule = null; + for (ResolvedArtifact a : resolvedDep.getModuleArtifacts()) { + final ArtifactKey artifactKey = toAppDependenciesKey(a.getModuleVersion().getId().getGroup(), a.getName(), + a.getClassifier()); + if (!isDependency(a) || modelBuilder.getDependency(artifactKey) != null) { + continue; + } + final ArtifactCoords depCoords = toArtifactCoords(a); + final ResolvedDependencyBuilder depBuilder = ResolvedDependencyBuilder.newInstance() + .setCoords(depCoords) + .setRuntimeCp(); + if (isFlagOn(flags, COLLECT_DIRECT_DEPS)) { + depBuilder.setDirect(true); + flags = clearFlag(flags, COLLECT_DIRECT_DEPS); + } + if (parentModule != null) { + parentModule.addDependency(new ArtifactDependency(depCoords)); + } + + PathCollection paths = null; + if (workspaceDiscovery && a.getId().getComponentIdentifier() instanceof ProjectComponentIdentifier) { + + Project projectDep = project.getRootProject().findProject( + ((ProjectComponentIdentifier) a.getId().getComponentIdentifier()).getProjectPath()); + SourceSetContainer sourceSets = projectDep == null ? null + : projectDep.getExtensions().findByType(SourceSetContainer.class); + + final String classifier = a.getClassifier(); + if (classifier == null || classifier.isEmpty()) { + final IncludedBuild includedBuild = ToolingUtils.includedBuild(project.getRootProject(), + (ProjectComponentIdentifier) a.getId().getComponentIdentifier()); + if (includedBuild != null) { + final PathList.Builder pathBuilder = PathList.builder(); + + if (includedBuild instanceof IncludedBuildInternal) { + projectDep = ToolingUtils.includedBuildProject((IncludedBuildInternal) includedBuild, + (ProjectComponentIdentifier) a.getId().getComponentIdentifier()); + } + if (projectDep != null) { + projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, + pathBuilder, SourceSet.MAIN_SOURCE_SET_NAME, false); + addSubstitutedProject(pathBuilder, projectDep.getProjectDir()); + } else { + addSubstitutedProject(pathBuilder, includedBuild.getProjectDir()); + } + paths = pathBuilder.build(); + } else if (sourceSets != null) { + final PathList.Builder pathBuilder = PathList.builder(); + projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, + pathBuilder, SourceSet.MAIN_SOURCE_SET_NAME, false); + paths = pathBuilder.build(); + } + } else if (sourceSets != null) { + if (SourceSet.TEST_SOURCE_SET_NAME.equals(classifier)) { + final PathList.Builder pathBuilder = PathList.builder(); + projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, + pathBuilder, SourceSet.TEST_SOURCE_SET_NAME, true); + paths = pathBuilder.build(); + } else if ("test-fixtures".equals(classifier)) { + final PathList.Builder pathBuilder = PathList.builder(); + projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, + pathBuilder, "testFixtures", true); + paths = pathBuilder.build(); + } + } + } + + depBuilder.setResolvedPaths(paths == null ? PathList.of(a.getFile().toPath()) : paths) + .setWorkspaceModule(projectModule); + if (processQuarkusDependency(depBuilder, modelBuilder)) { + if (isFlagOn(flags, COLLECT_TOP_EXTENSION_RUNTIME_NODES)) { + depBuilder.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); + flags = clearFlag(flags, COLLECT_TOP_EXTENSION_RUNTIME_NODES); + } + flags = clearFlag(flags, COLLECT_RELOADABLE_MODULES); + } + if (!isFlagOn(flags, COLLECT_RELOADABLE_MODULES)) { + depBuilder.clearFlag(DependencyFlags.RELOADABLE); + } + modelBuilder.addDependency(depBuilder); + + if (artifactFiles != null) { + artifactFiles.add(a.getFile()); + } + } + + processedModules.add(ArtifactKey.ga(resolvedDep.getModuleGroup(), resolvedDep.getModuleName())); + for (org.gradle.api.artifacts.ResolvedDependency child : resolvedDep.getChildren()) { + if (!processedModules.contains(new GACT(child.getModuleGroup(), child.getModuleName()))) { + collectDependencies(child, workspaceDiscovery, project, artifactFiles, processedModules, + modelBuilder, projectModule, flags); + } + } + } + + private static String toNonNullClassifier(String resolvedClassifier) { + return resolvedClassifier == null ? ArtifactCoords.DEFAULT_CLASSIFIER : resolvedClassifier; + } + + private WorkspaceModule.Mutable initProjectModuleAndBuildPaths(final Project project, + ResolvedArtifact resolvedArtifact, ApplicationModelBuilder appModel, final ResolvedDependencyBuilder appDep, + PathList.Builder buildPaths, String sourceName, boolean test) { + + appDep.setWorkspaceModule().setReloadable(); + + final WorkspaceModule.Mutable projectModule = appModel.getOrCreateProjectModule( + new GAV(resolvedArtifact.getModuleVersion().getId().getGroup(), resolvedArtifact.getName(), + resolvedArtifact.getModuleVersion().getId().getVersion()), + project.getProjectDir(), + project.getBuildDir()) + .setBuildFile(project.getBuildFile().toPath()); + + final String classifier = toNonNullClassifier(resolvedArtifact.getClassifier()); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + initProjectModule(project, projectModule, sourceSets.findByName(sourceName), classifier); + + collectDestinationDirs(projectModule.getSources(classifier).getSourceDirs(), buildPaths); + collectDestinationDirs(projectModule.getSources(classifier).getResourceDirs(), buildPaths); + + appModel.addReloadableWorkspaceModule( + ArtifactKey.of(resolvedArtifact.getModuleVersion().getId().getGroup(), resolvedArtifact.getName(), classifier, + ArtifactCoords.TYPE_JAR)); + return projectModule; + } + + private boolean processQuarkusDependency(ResolvedDependencyBuilder artifactBuilder, ApplicationModelBuilder modelBuilder) { + for (Path artifactPath : artifactBuilder.getResolvedPaths()) { + if (!Files.exists(artifactPath) || !artifactBuilder.getType().equals(ArtifactCoords.TYPE_JAR)) { + break; + } + if (Files.isDirectory(artifactPath)) { + return processQuarkusDir(artifactBuilder, artifactPath.resolve(BootstrapConstants.META_INF), modelBuilder); + } else { + try (FileSystem artifactFs = ZipUtils.newFileSystem(artifactPath)) { + return processQuarkusDir(artifactBuilder, artifactFs.getPath(BootstrapConstants.META_INF), modelBuilder); + } catch (IOException e) { + throw new RuntimeException("Failed to process " + artifactPath, e); + } + } + } + return false; + } + + private static boolean processQuarkusDir(ResolvedDependencyBuilder artifactBuilder, Path quarkusDir, + ApplicationModelBuilder modelBuilder) { + if (!Files.exists(quarkusDir)) { + return false; + } + final Path quarkusDescr = quarkusDir.resolve(BootstrapConstants.DESCRIPTOR_FILE_NAME); + if (!Files.exists(quarkusDescr)) { + return false; + } + final Properties extProps = readDescriptor(quarkusDescr); + if (extProps == null) { + return false; + } + artifactBuilder.setRuntimeExtensionArtifact(); + final String extensionCoords = artifactBuilder.toGACTVString(); + modelBuilder.handleExtensionProperties(extProps, extensionCoords); + + final String providesCapabilities = extProps.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); + if (providesCapabilities != null) { + modelBuilder + .addExtensionCapabilities(CapabilityContract.of(extensionCoords, providesCapabilities, null)); + } + return true; + } + + private static Properties readDescriptor(final Path path) { + final Properties rtProps; + if (!Files.exists(path)) { + // not a platform artifact + return null; + } + rtProps = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(path)) { + rtProps.load(reader); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load extension description " + path, e); + } + return rtProps; + } + + private static void initProjectModule(Project project, WorkspaceModule.Mutable module, SourceSet sourceSet, + String classifier) { + + if (sourceSet == null) { + return; + } + + final FileCollection allClassesDirs = sourceSet.getOutput().getClassesDirs(); + // some plugins do not add source directories to source sets and they may be missing from sourceSet.getAllJava() + // see https://github.com/quarkusio/quarkus/issues/20755 + + final List sourceDirs = new ArrayList<>(1); + project.getTasks().withType(AbstractCompile.class, t -> { + if (!t.getEnabled()) { + return; + } + final FileTree source = t.getSource(); + if (source.isEmpty()) { + return; + } + final File destDir = t.getDestinationDirectory().getAsFile().get(); + if (!allClassesDirs.contains(destDir)) { + return; + } + source.visit(a -> { + // we are looking for the root dirs containing sources + if (a.getRelativePath().getSegments().length == 1) { + final File srcDir = a.getFile().getParentFile(); + sourceDirs.add(new DefaultSourceDir(srcDir.toPath(), destDir.toPath(), Map.of("compiler", t.getName()))); + } + }); + }); + + // This "try/catch" is needed because of the way the "quarkus-cli" Gradle tests work. Without it, the tests fail. + /* + try { + Class.forName("org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile"); + project.getTasks().withType(KotlinJvmCompile.class, t -> { + if (!t.getEnabled()) { + return; + } + final FileTree source = t.getSources().getAsFileTree(); + if (source.isEmpty()) { + return; + } + final File destDir = t.getDestinationDirectory().getAsFile().get(); + if (!allClassesDirs.contains(destDir)) { + return; + } + source.visit(a -> { + // we are looking for the root dirs containing sources + if (a.getRelativePath().getSegments().length == 1) { + final File srcDir = a.getFile().getParentFile(); + sourceDirs + .add(new DefaultSourceDir(srcDir.toPath(), destDir.toPath(), Map.of("compiler", t.getName()))); + } + }); + }); + } catch (ClassNotFoundException e) { + // ignore + } + */ + + final LinkedHashMap resourceDirs = new LinkedHashMap<>(1); + final File resourcesOutputDir = sourceSet.getOutput().getResourcesDir(); + project.getTasks().withType(ProcessResources.class, t -> { + if (!t.getEnabled()) { + return; + } + final FileCollection source = t.getSource(); + if (source.isEmpty()) { + return; + } + if (!t.getDestinationDir().equals(resourcesOutputDir)) { + return; + } + final Path destDir = t.getDestinationDir().toPath(); + source.getAsFileTree().visit(a -> { + // we are looking for the root dirs containing sources + if (a.getRelativePath().getSegments().length == 1) { + final File srcDir = a.getFile().getParentFile(); + resourceDirs.put(srcDir, destDir); + } + }); + }); + // there could be a task generating resources + if (resourcesOutputDir.exists() && resourceDirs.isEmpty()) { + sourceSet.getResources().getSrcDirs() + .forEach(srcDir -> resourceDirs.put(srcDir, resourcesOutputDir.toPath())); + } + final List resources = new ArrayList<>(resourceDirs.size()); + for (Map.Entry e : resourceDirs.entrySet()) { + resources.add(new DefaultSourceDir(e.getKey().toPath(), e.getValue())); + } + module.addArtifactSources(new DefaultArtifactSources(classifier, sourceDirs, resources)); + } + + private void addSubstitutedProject(PathList.Builder paths, File projectFile) { + File mainResourceDirectory = new File(projectFile, MAIN_RESOURCES_OUTPUT); + if (mainResourceDirectory.exists()) { + paths.add(mainResourceDirectory.toPath()); + } + File classesOutput = new File(projectFile, CLASSES_OUTPUT); + File[] languageDirectories = classesOutput.listFiles(); + if (languageDirectories != null) { + for (File languageDirectory : languageDirectories) { + if (languageDirectory.isDirectory()) { + for (File sourceSet : languageDirectory.listFiles()) { + if (sourceSet.isDirectory() && sourceSet.getName().equals(SourceSet.MAIN_SOURCE_SET_NAME)) { + paths.add(sourceSet.toPath()); + } + } + } + } + } + } + + private static boolean isFlagOn(byte walkingFlags, byte flag) { + return (walkingFlags & flag) > 0; + } + + private static byte clearFlag(byte flags, byte flag) { + if ((flags & flag) > 0) { + flags ^= flag; + } + return flags; + } + + private static boolean isDependency(ResolvedArtifact a) { + return ArtifactCoords.TYPE_JAR.equalsIgnoreCase(a.getExtension()) || "exe".equalsIgnoreCase(a.getExtension()) || + a.getFile().isDirectory(); + } + + /** + * Creates an instance of Dependency and associates it with the ResolvedArtifact's path + */ + static ResolvedDependencyBuilder toDependency(ResolvedArtifact a, int... flags) { + return toDependency(a, PathList.of(a.getFile().toPath()), null, flags); + } + + static ResolvedDependencyBuilder toDependency(ResolvedArtifact a, SourceSet s) { + PathList.Builder resolvedPathBuilder = PathList.builder(); + + for (File classesDir : s.getOutput().getClassesDirs()) { + if (classesDir.exists()) { + resolvedPathBuilder.add(classesDir.toPath()); + } + } + File resourceDir = s.getOutput().getResourcesDir(); + if (resourceDir != null && resourceDir.exists()) { + resolvedPathBuilder.add(resourceDir.toPath()); + } + + return ResolvedDependencyBuilder + .newInstance() + .setResolvedPaths(resolvedPathBuilder.build()) + .setCoords(toArtifactCoords(a)); + } + + static ResolvedDependencyBuilder toDependency(ResolvedArtifact a, PathCollection paths, DefaultWorkspaceModule module, + int... flags) { + int allFlags = 0; + for (int f : flags) { + allFlags |= f; + } + return ResolvedDependencyBuilder.newInstance() + .setCoords(toArtifactCoords(a)) + .setResolvedPaths(paths) + .setWorkspaceModule(module) + .setFlags(allFlags); + } + + private static ArtifactCoords toArtifactCoords(ResolvedArtifact a) { + final String[] split = a.getModuleVersion().toString().split(":"); + return new GACTV(split[0], split[1], a.getClassifier(), a.getType(), split.length > 2 ? split[2] : null); + } + + private static ArtifactKey toAppDependenciesKey(String groupId, String artifactId, String classifier) { + return new GACT(groupId, artifactId, classifier, ArtifactCoords.TYPE_JAR); + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java new file mode 100644 index 000000000000..f4cddb7bdc13 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Quarkus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.quarkus.gradle.tooling; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.attributes.Category; +import org.gradle.api.initialization.IncludedBuild; +import org.gradle.internal.composite.IncludedBuildInternal; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.model.gradle.ModelParameter; +import io.quarkus.bootstrap.model.gradle.impl.ModelParameterImpl; +import io.quarkus.runtime.LaunchMode; + +public class ToolingUtils { + + private static final String DEPLOYMENT_CONFIGURATION_SUFFIX = "Deployment"; + private static final String PLATFORM_CONFIGURATION_SUFFIX = "Platform"; + public static final String DEV_MODE_CONFIGURATION_NAME = "quarkusDev"; + + public static String toDeploymentConfigurationName(String baseConfigurationName) { + return baseConfigurationName + DEPLOYMENT_CONFIGURATION_SUFFIX; + } + + public static String toPlatformConfigurationName(String baseConfigurationName) { + return baseConfigurationName + PLATFORM_CONFIGURATION_SUFFIX; + } + + public static boolean isEnforcedPlatform(ModuleDependency module) { + final Category category = module.getAttributes().getAttribute(Category.CATEGORY_ATTRIBUTE); + return category != null && (Category.ENFORCED_PLATFORM.equals(category.getName()) + || Category.REGULAR_PLATFORM.equals(category.getName())); + } + + public static IncludedBuild includedBuild(final Project project, + final ProjectComponentIdentifier projectComponentIdentifier) { +/* + final String name = projectComponentIdentifier.getBuild().getName(); + for (IncludedBuild ib : project.getRootProject().getGradle().getIncludedBuilds()) { + if (ib.getName().equals(name)) { + return ib; + } + } +*/ + final String buildPath = projectComponentIdentifier.getBuild().getBuildPath(); + for (IncludedBuild ib : project.getRootProject().getGradle().getIncludedBuilds()) { + if (((IncludedBuildInternal) ib).getTarget().getBuildIdentifier().getBuildPath().equals(buildPath)) { + return ib; + } + } + return null; + } + + public static Project includedBuildProject(IncludedBuildInternal includedBuild, + final ProjectComponentIdentifier componentIdentifier) { + return includedBuild.getTarget().getMutableModel().getRootProject().findProject( + componentIdentifier.getProjectPath()); + } + + public static Path serializeAppModel(ApplicationModel appModel, Task context, boolean test) throws IOException { + final Path serializedModel = context.getTemporaryDir().toPath() + .resolve("quarkus-app" + (test ? "-test" : "") + "-model.dat"); + try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(serializedModel))) { + out.writeObject(appModel); + } + return serializedModel; + } + + public static ApplicationModel create(Project project, LaunchMode mode) { + final ModelParameter params = new ModelParameterImpl(); + params.setMode(mode.toString()); + return create(project, params); + } + + public static ApplicationModel create(Project project, ModelParameter params) { + return (ApplicationModel) new GradleApplicationModelBuilder().buildAll(ApplicationModel.class.getName(), params, + project); + } + +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java new file mode 100644 index 000000000000..68008babd8b6 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-plugin/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java @@ -0,0 +1,212 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Quarkus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.quarkus.gradle.tooling.dependency; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.capabilities.Capability; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.internal.composite.IncludedBuildInternal; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.util.BootstrapUtils; +import io.quarkus.fs.util.ZipUtils; +import io.quarkus.gradle.tooling.ToolingUtils; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.GACTV; + +public class DependencyUtils { + + private static final String COPY_CONFIGURATION_NAME = "quarkusDependency"; + private static final String TEST_FIXTURE_SUFFIX = "-test-fixtures"; + + public static Configuration duplicateConfiguration(Project project, Configuration toDuplicate) { + Configuration configurationCopy = project.getConfigurations().findByName(COPY_CONFIGURATION_NAME); + if (configurationCopy != null) { + project.getConfigurations().remove(configurationCopy); + } + return duplicateConfiguration(project, COPY_CONFIGURATION_NAME, toDuplicate); + } + + public static Configuration duplicateConfiguration(Project project, String name, Configuration toDuplicate) { + final Configuration configurationCopy = project.getConfigurations().create(name); + configurationCopy.getDependencies().addAll(toDuplicate.getAllDependencies()); + configurationCopy.getDependencyConstraints().addAll(toDuplicate.getAllDependencyConstraints()); + return configurationCopy; + } + + public static boolean isTestFixtureDependency(Dependency dependency) { + if (!(dependency instanceof ModuleDependency)) { + return false; + } + ModuleDependency module = (ModuleDependency) dependency; + for (Capability requestedCapability : module.getRequestedCapabilities()) { + if (requestedCapability.getName().endsWith(TEST_FIXTURE_SUFFIX)) { + return true; + } + } + return false; + } + + public static String asDependencyNotation(Dependency dependency) { + return String.join(":", dependency.getGroup(), dependency.getName(), dependency.getVersion()); + } + + public static String asDependencyNotation(ArtifactCoords artifactCoords) { + return String.join(":", artifactCoords.getGroupId(), artifactCoords.getArtifactId(), artifactCoords.getVersion()); + } + + public static ExtensionDependency getExtensionInfoOrNull(Project project, ResolvedArtifact artifact) { + ModuleVersionIdentifier artifactId = artifact.getModuleVersion().getId(); + File artifactFile = artifact.getFile(); + + if (artifact.getId().getComponentIdentifier() instanceof ProjectComponentIdentifier) { + ProjectComponentIdentifier componentIdentifier = ((ProjectComponentIdentifier) artifact.getId() + .getComponentIdentifier()); + Project projectDep = project.getRootProject().findProject( + componentIdentifier.getProjectPath()); + SourceSetContainer sourceSets = projectDep == null ? null + : projectDep.getExtensions().findByType(SourceSetContainer.class); + final String classifier = artifact.getClassifier(); + boolean isIncludedBuild = false; + /* + if ((!componentIdentifier.getBuild().isCurrentBuild() || sourceSets == null) + && (classifier == null || classifier.isEmpty())) { + var includedBuild = ToolingUtils.includedBuild(project, componentIdentifier); + if (includedBuild instanceof IncludedBuildInternal) { + projectDep = ToolingUtils.includedBuildProject((IncludedBuildInternal) includedBuild, componentIdentifier); + sourceSets = projectDep == null ? null : projectDep.getExtensions().findByType(SourceSetContainer.class); + isIncludedBuild = true; + } + } + */ + if (sourceSets != null) { + SourceSet mainSourceSet = sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME); + if (mainSourceSet == null) { + return null; + } + File resourcesDir = mainSourceSet.getOutput().getResourcesDir(); + Path descriptorPath = resourcesDir.toPath().resolve(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId, projectDep, isIncludedBuild); + } + } + } + + if (!artifactFile.exists()) { + return null; + } + if (artifactFile.isDirectory()) { + Path descriptorPath = artifactFile.toPath().resolve(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId, null, false); + } + } else if (ArtifactCoords.TYPE_JAR.equals(artifact.getExtension())) { + try (FileSystem artifactFs = ZipUtils.newFileSystem(artifactFile.toPath())) { + Path descriptorPath = artifactFs.getPath(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId, null, false); + } + } catch (IOException e) { + throw new GradleException("Failed to read " + artifactFile, e); + } + } + return null; + } + + private static ExtensionDependency loadExtensionInfo(Project project, Path descriptorPath, + ModuleVersionIdentifier exentionId, Project extensionProject, boolean isIncludedBuild) { + final Properties extensionProperties = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(descriptorPath)) { + extensionProperties.load(reader); + } catch (IOException e) { + throw new GradleException("Failed to load " + descriptorPath, e); + } + ArtifactCoords deploymentModule = GACTV + .fromString(extensionProperties.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT)); + final List conditionalDependencies; + if (extensionProperties.containsKey(BootstrapConstants.CONDITIONAL_DEPENDENCIES)) { + final String[] deps = BootstrapUtils + .splitByWhitespace(extensionProperties.getProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES)); + conditionalDependencies = new ArrayList<>(deps.length); + for (String conditionalDep : deps) { + conditionalDependencies.add(create(project.getDependencies(), conditionalDep)); + } + } else { + conditionalDependencies = Collections.emptyList(); + } + + final ArtifactKey[] constraints = BootstrapUtils + .parseDependencyCondition(extensionProperties.getProperty(BootstrapConstants.DEPENDENCY_CONDITION)); + if (isIncludedBuild) { + return new IncludedBuildExtensionDependency(extensionProject, exentionId, deploymentModule, conditionalDependencies, + constraints == null ? Collections.emptyList() : Arrays.asList(constraints)); + } + if (extensionProject != null) { + return new LocalExtensionDependency(extensionProject, exentionId, deploymentModule, conditionalDependencies, + constraints == null ? Collections.emptyList() : Arrays.asList(constraints)); + } + return new ExtensionDependency(exentionId, deploymentModule, conditionalDependencies, + constraints == null ? Collections.emptyList() : Arrays.asList(constraints)); + } + + public static Dependency create(DependencyHandler dependencies, String conditionalDependency) { + final ArtifactCoords dependencyCoords = GACTV.fromString(conditionalDependency); + return dependencies.create(String.join(":", dependencyCoords.getGroupId(), dependencyCoords.getArtifactId(), + dependencyCoords.getVersion())); + } + + public static void addLocalDeploymentDependency(String deploymentConfigurationName, LocalExtensionDependency extension, + DependencyHandler dependencies) { + dependencies.add(deploymentConfigurationName, + dependencies.project(Collections.singletonMap("path", extension.findDeploymentModulePath()))); + } + + public static void requireDeploymentDependency(String deploymentConfigurationName, ExtensionDependency extension, + DependencyHandler dependencies) { + dependencies.add(deploymentConfigurationName, + extension.getDeploymentModule().getGroupId() + ":" + extension.getDeploymentModule().getArtifactId() + ":" + + extension.getDeploymentModule().getVersion()); + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/build.gradle.kts b/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/build.gradle.kts index ff63fb854165..5de45394ba34 100644 --- a/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/build.gradle.kts +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/build.gradle.kts @@ -1,7 +1,14 @@ +import io.quarkus.bootstrap.model.ApplicationModel +import io.quarkus.bootstrap.model.gradle.impl.ModelParameterImpl +import io.quarkus.bootstrap.util.BootstrapUtils +import io.quarkus.gradle.tooling.GradleApplicationModelBuilder +import io.quarkus.runtime.LaunchMode +import kotlin.io.path.notExists + plugins { id("otel.javaagent-testing") - id("io.quarkus") version "2.16.7.Final" + id("io.opentelemetry.instrumentation.quarkus2") apply false } otelJava { @@ -22,15 +29,37 @@ dependencies { testImplementation("io.quarkus:quarkus-junit5") } -tasks.named("compileJava").configure { - dependsOn(tasks.named("compileQuarkusGeneratedSourcesJava")) +tasks.register("integrationTestClasses") {} + +val quarkusTestBaseRuntimeClasspathConfiguration by configurations.creating { + extendsFrom(configurations["testRuntimeClasspath"]) } -tasks.named("sourcesJar").configure { - dependsOn(tasks.named("compileQuarkusGeneratedSourcesJava")) + +val quarkusTestCompileOnlyConfiguration by configurations.creating { } -tasks.named("checkstyleTest").configure { - dependsOn(tasks.named("compileQuarkusGeneratedSourcesJava")) + +val testModelPath = layout.buildDirectory.file("quarkus-app-test-model.dat").get().asFile.toPath() + +val buildModel = tasks.register("buildModel") { + if (testModelPath.notExists()) { + doLast { + val modelParameter = ModelParameterImpl() + modelParameter.mode = LaunchMode.TEST.toString() + val model = GradleApplicationModelBuilder().buildAll( + ApplicationModel::class.java.getName(), + modelParameter, + project + ) + BootstrapUtils.serializeAppModel(model as ApplicationModel?, testModelPath) + } + } + outputs.file(testModelPath) } -tasks.named("compileTestJava").configure { - dependsOn(tasks.named("compileQuarkusTestGeneratedSourcesJava")) + +tasks { + test { + dependsOn(buildModel) + + systemProperty("quarkus-internal-test.serialized-app-model.path", testModelPath.toString()) + } } diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/build.gradle.kts b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/build.gradle.kts new file mode 100644 index 000000000000..9fd5a36fcc32 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + `java-gradle-plugin` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation(gradleApi()) + implementation("io.quarkus:quarkus-gradle-model:3.0.0.Final") +} + +gradlePlugin { + plugins { + create("quarkus3Plugin") { + id = "io.opentelemetry.instrumentation.quarkus3" + implementationClass = "io.opentelemetry.instrumentation.quarkus3plugin.Quarkus3Plugin" + } + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/opentelemetry/instrumentation/quarkus3plugin/Quarkus3Plugin.java b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/opentelemetry/instrumentation/quarkus3plugin/Quarkus3Plugin.java new file mode 100644 index 000000000000..ea717f0dc466 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/opentelemetry/instrumentation/quarkus3plugin/Quarkus3Plugin.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.quarkus3plugin; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +@SuppressWarnings("unused") +public class Quarkus3Plugin implements Plugin { + + @Override + public void apply(Project project) { + // we use this plugin with apply false and call its classes directly from the build script + throw new IllegalStateException("this plugin is not meant to be applied"); + } +} + diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java new file mode 100644 index 000000000000..01690e33986a --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java @@ -0,0 +1,245 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Quarkus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.quarkus.gradle.dependency; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedArtifact; + +import io.quarkus.gradle.tooling.dependency.DependencyUtils; +import io.quarkus.gradle.tooling.dependency.ExtensionDependency; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.GACT; +import io.quarkus.runtime.LaunchMode; + +public class ConditionalDependenciesEnabler { + + /** + * Links dependencies to extensions + */ + private final Map> featureVariants = new HashMap<>(); + /** + * Despite its name, only contains extensions which have no conditional dependencies, or have + * resolved their conditional dependencies. + */ + private final Map allExtensions = new HashMap<>(); + private final Project project; + private final Configuration enforcedPlatforms; + private final Set existingArtifacts = new HashSet<>(); + private final List unsatisfiedConditionalDeps = new ArrayList<>(); + + public ConditionalDependenciesEnabler(Project project, LaunchMode mode, + Configuration platforms) { + this.project = project; + this.enforcedPlatforms = platforms; + + // Get runtimeClasspath (quarkusProdBaseRuntimeClasspathConfiguration to be exact) + Configuration baseRuntimeConfig = project.getConfigurations() + .getByName(ApplicationDeploymentClasspathBuilder.getBaseRuntimeConfigName(mode)); + + if (!baseRuntimeConfig.getIncoming().getDependencies().isEmpty()) { + // Gather all extensions from the full resolved dependency tree + collectConditionalDependencies(baseRuntimeConfig.getResolvedConfiguration().getResolvedArtifacts()); + // If there are any extensions which had unresolved conditional dependencies: + while (!unsatisfiedConditionalDeps.isEmpty()) { + boolean satisfiedConditionalDeps = false; + final int originalUnsatisfiedCount = unsatisfiedConditionalDeps.size(); + int i = 0; + // Go through each unsatisfied/unresolved dependency once: + while (i < unsatisfiedConditionalDeps.size()) { + final Dependency conditionalDep = unsatisfiedConditionalDeps.get(i); + // Try to resolve it with the latest evolved graph available + if (resolveConditionalDependency(conditionalDep)) { + // Mark the resolution as a success so we know the graph evolved + satisfiedConditionalDeps = true; + unsatisfiedConditionalDeps.remove(i); + } else { + // No resolution (yet) or graph evolution; move on to the next + ++i; + } + } + // If we didn't resolve any dependencies and the graph did not evolve, give up. + if (!satisfiedConditionalDeps && unsatisfiedConditionalDeps.size() == originalUnsatisfiedCount) { + break; + } + } + reset(); + } + + } + + public Collection getAllExtensions() { + return allExtensions.values(); + } + + private void reset() { + featureVariants.clear(); + existingArtifacts.clear(); + unsatisfiedConditionalDeps.clear(); + } + + private void collectConditionalDependencies(Set runtimeArtifacts) { + // For every artifact in the dependency graph: + for (ResolvedArtifact artifact : runtimeArtifacts) { + // Add to master list of artifacts: + existingArtifacts.add(getKey(artifact)); + ExtensionDependency extension = DependencyUtils.getExtensionInfoOrNull(project, artifact); + // If this artifact represents an extension: + if (extension != null) { + // Add to master list of accepted extensions: + allExtensions.put(extension.getExtensionId(), extension); + for (Dependency conditionalDep : extension.getConditionalDependencies()) { + // If the dependency is not present yet in the graph, queue it for resolution later + if (!exists(conditionalDep)) { + queueConditionalDependency(extension, conditionalDep); + } + } + } + } + } + + private boolean resolveConditionalDependency(Dependency conditionalDep) { + + final Configuration conditionalDeps = createConditionalDependenciesConfiguration(project, conditionalDep); + Set resolvedArtifacts = conditionalDeps.getResolvedConfiguration().getResolvedArtifacts(); + + boolean satisfied = false; + // Resolved artifacts don't have great linking back to the original artifact, so I think + // this loop is trying to find the artifact that represents the original conditional + // dependency + for (ResolvedArtifact artifact : resolvedArtifacts) { + if (conditionalDep.getName().equals(artifact.getName()) + && conditionalDep.getVersion().equals(artifact.getModuleVersion().getId().getVersion()) + && artifact.getModuleVersion().getId().getGroup().equals(conditionalDep.getGroup())) { + // Once the dependency is found, reload the extension info from within + final ExtensionDependency extensionDependency = DependencyUtils.getExtensionInfoOrNull(project, artifact); + // Now check if this conditional dependency is resolved given the latest graph evolution + if (extensionDependency != null && (extensionDependency.getDependencyConditions().isEmpty() + || exist(extensionDependency.getDependencyConditions()))) { + satisfied = true; + enableConditionalDependency(extensionDependency.getExtensionId()); + break; + } + } + } + + // No resolution (yet); give up. + if (!satisfied) { + return false; + } + + // The conditional dependency resolved! Let's now add all of /its/ dependencies + for (ResolvedArtifact artifact : resolvedArtifacts) { + // First add the artifact to the master list + existingArtifacts.add(getKey(artifact)); + ExtensionDependency extensionDependency = DependencyUtils.getExtensionInfoOrNull(project, artifact); + if (extensionDependency == null) { + continue; + } + // If this artifact represents an extension, mark this one as a conditional extension + extensionDependency.setConditional(true); + // Add to the master list of accepted extensions + allExtensions.put(extensionDependency.getExtensionId(), extensionDependency); + for (Dependency cd : extensionDependency.getConditionalDependencies()) { + // Add any unsatisfied/unresolved conditional dependencies of this dependency to the queue + if (!exists(cd)) { + queueConditionalDependency(extensionDependency, cd); + } + } + } + return satisfied; + } + + private void queueConditionalDependency(ExtensionDependency extension, Dependency conditionalDep) { + // 1. Add to master list of unresolved/unsatisfied dependencies + // 2. Add map entry to link dependency to extension + featureVariants.computeIfAbsent(getFeatureKey(conditionalDep), k -> { + unsatisfiedConditionalDeps.add(conditionalDep); + return new HashSet<>(); + }).add(extension); + } + + private Configuration createConditionalDependenciesConfiguration(Project project, Dependency conditionalDep) { + /* + Configuration conditionalDepConfiguration = project.getConfigurations() + .detachedConfiguration() + .extendsFrom(enforcedPlatforms); + */ + Configuration conditionalDepConfiguration = project.getConfigurations().detachedConfiguration(); + enforcedPlatforms.getExcludeRules().forEach(rule -> { + conditionalDepConfiguration.exclude(Map.of( + "group", rule.getGroup(), + "module", rule.getModule())); + }); + enforcedPlatforms.getAllDependencies().forEach(dependency -> { + conditionalDepConfiguration.getDependencies().add(dependency); + }); + conditionalDepConfiguration.getDependencies().add(conditionalDep); + return conditionalDepConfiguration; + } + + private void enableConditionalDependency(ModuleVersionIdentifier dependency) { + final Set extensions = featureVariants.remove(getFeatureKey(dependency)); + if (extensions == null) { + return; + } + extensions.forEach(e -> e.importConditionalDependency(project.getDependencies(), dependency)); + } + + private boolean exist(List dependencies) { + return existingArtifacts.containsAll(dependencies); + } + + private boolean exists(Dependency dependency) { + return existingArtifacts + .contains(ArtifactKey.of(dependency.getGroup(), dependency.getName(), null, ArtifactCoords.TYPE_JAR)); + } + + public boolean exists(ExtensionDependency dependency) { + return existingArtifacts + .contains(ArtifactKey.of(dependency.getGroup(), dependency.getName(), null, ArtifactCoords.TYPE_JAR)); + } + + private static GACT getFeatureKey(ModuleVersionIdentifier version) { + return new GACT(version.getGroup(), version.getName()); + } + + private static GACT getFeatureKey(Dependency version) { + return new GACT(version.getGroup(), version.getName()); + } + + private static ArtifactKey getKey(ResolvedArtifact a) { + return ArtifactKey.of(a.getModuleVersion().getId().getGroup(), a.getName(), a.getClassifier(), a.getType()); + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java new file mode 100644 index 000000000000..5f2b35b16019 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java @@ -0,0 +1,674 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Quarkus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.quarkus.gradle.tooling; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.ResolvedConfiguration; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTree; +import org.gradle.api.initialization.IncludedBuild; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskCollection; +import org.gradle.api.tasks.compile.AbstractCompile; +import org.gradle.api.tasks.testing.Test; +import org.gradle.internal.composite.IncludedBuildInternal; +import org.gradle.language.jvm.tasks.ProcessResources; +import org.gradle.tooling.provider.model.ParameterizedToolingModelBuilder; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.model.ApplicationModelBuilder; +import io.quarkus.bootstrap.model.CapabilityContract; +import io.quarkus.bootstrap.model.PlatformImports; +import io.quarkus.bootstrap.model.gradle.ModelParameter; +import io.quarkus.bootstrap.model.gradle.impl.ModelParameterImpl; +import io.quarkus.bootstrap.workspace.ArtifactSources; +import io.quarkus.bootstrap.workspace.DefaultArtifactSources; +import io.quarkus.bootstrap.workspace.DefaultSourceDir; +import io.quarkus.bootstrap.workspace.DefaultWorkspaceModule; +import io.quarkus.bootstrap.workspace.SourceDir; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.fs.util.ZipUtils; +import io.quarkus.gradle.dependency.ApplicationDeploymentClasspathBuilder; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactDependency; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.GACT; +import io.quarkus.maven.dependency.GACTV; +import io.quarkus.maven.dependency.GAV; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; +import io.quarkus.paths.PathCollection; +import io.quarkus.paths.PathList; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.util.HashUtil; + +public class GradleApplicationModelBuilder implements ParameterizedToolingModelBuilder { + + private static final String MAIN_RESOURCES_OUTPUT = "build/resources/main"; + private static final String CLASSES_OUTPUT = "build/classes"; + + /* @formatter:off */ + private static final byte COLLECT_TOP_EXTENSION_RUNTIME_NODES = 0b001; + private static final byte COLLECT_DIRECT_DEPS = 0b010; + private static final byte COLLECT_RELOADABLE_MODULES = 0b100; + /* @formatter:on */ + + @Override + public boolean canBuild(String modelName) { + return modelName.equals(ApplicationModel.class.getName()); + } + + @Override + public Class getParameterType() { + return ModelParameter.class; + } + + @Override + public Object buildAll(String modelName, Project project) { + final ModelParameterImpl modelParameter = new ModelParameterImpl(); + modelParameter.setMode(LaunchMode.DEVELOPMENT.toString()); + return buildAll(modelName, modelParameter, project); + } + + @Override + public Object buildAll(String modelName, ModelParameter parameter, Project project) { + final LaunchMode mode = LaunchMode.valueOf(parameter.getMode()); + + final ApplicationDeploymentClasspathBuilder classpathBuilder = new ApplicationDeploymentClasspathBuilder(project, + mode); + final Configuration classpathConfig = classpathBuilder.getRuntimeConfiguration(); + final Configuration deploymentConfig = classpathBuilder.getDeploymentConfiguration(); + final PlatformImports platformImports = classpathBuilder.getPlatformImports(); + + boolean workspaceDiscovery = LaunchMode.DEVELOPMENT.equals(mode) || LaunchMode.TEST.equals(mode) + || Boolean.parseBoolean(System.getProperty(BootstrapConstants.QUARKUS_BOOTSTRAP_WORKSPACE_DISCOVERY)); + if (!workspaceDiscovery) { + Object o = project.getProperties().get(BootstrapConstants.QUARKUS_BOOTSTRAP_WORKSPACE_DISCOVERY); + if (o != null) { + workspaceDiscovery = Boolean.parseBoolean(o.toString()); + } + } + + final ResolvedDependency appArtifact = getProjectArtifact(project, workspaceDiscovery); + final ApplicationModelBuilder modelBuilder = new ApplicationModelBuilder() + .setAppArtifact(appArtifact) + .addReloadableWorkspaceModule(appArtifact.getKey()) + .setPlatformImports(platformImports); + + collectDependencies(classpathConfig.getResolvedConfiguration(), classpathConfig.getIncoming(), workspaceDiscovery, + project, modelBuilder, appArtifact.getWorkspaceModule().mutable()); + collectExtensionDependencies(project, deploymentConfig, modelBuilder); + + return modelBuilder.build(); + } + + public static ResolvedDependency getProjectArtifact(Project project, boolean workspaceDiscovery) { + final ResolvedDependencyBuilder appArtifact = ResolvedDependencyBuilder.newInstance() + .setGroupId(project.getGroup().toString()) + .setArtifactId(project.getName()) + .setVersion(project.getVersion().toString()); + + final SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + final WorkspaceModule.Mutable mainModule = WorkspaceModule.builder() + .setModuleId(new GAV(appArtifact.getGroupId(), appArtifact.getArtifactId(), appArtifact.getVersion())) + .setModuleDir(project.getProjectDir().toPath()) + .setBuildDir(project.getBuildDir().toPath()) + .setBuildFile(project.getBuildFile().toPath()); + + initProjectModule(project, mainModule, sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME), ArtifactSources.MAIN); + if (workspaceDiscovery) { + final TaskCollection testTasks = project.getTasks().withType(Test.class); + if (!testTasks.isEmpty()) { + final Map sourceSetsByClassesDir = new HashMap<>(); + sourceSets.forEach(s -> { + s.getOutput().getClassesDirs().forEach(d -> { + if (d.exists()) { + sourceSetsByClassesDir.put(d, s); + } + }); + }); + testTasks.forEach(t -> { + if (t.getEnabled()) { + t.getTestClassesDirs().forEach(d -> { + if (d.exists()) { + final SourceSet sourceSet = sourceSetsByClassesDir.remove(d); + if (sourceSet != null) { + initProjectModule(project, mainModule, sourceSet, + sourceSet.getName().equals(SourceSet.TEST_SOURCE_SET_NAME) + ? ArtifactSources.TEST + : sourceSet.getName()); + } + } + }); + } + }); + } + } + + final PathList.Builder paths = PathList.builder(); + collectDestinationDirs(mainModule.getMainSources().getSourceDirs(), paths); + collectDestinationDirs(mainModule.getMainSources().getResourceDirs(), paths); + + return appArtifact.setWorkspaceModule(mainModule).setResolvedPaths(paths.build()).build(); + } + + private static void collectDestinationDirs(Collection sources, final PathList.Builder paths) { + for (SourceDir src : sources) { + final Path path = src.getOutputDir(); + if (paths.contains(path) || !Files.exists(path)) { + continue; + } + paths.add(path); + } + } + + private void collectExtensionDependencies(Project project, Configuration deploymentConfiguration, + ApplicationModelBuilder modelBuilder) { + final ResolvedConfiguration rc = deploymentConfiguration.getResolvedConfiguration(); + for (ResolvedArtifact a : rc.getResolvedArtifacts()) { + if (a.getId().getComponentIdentifier() instanceof ProjectComponentIdentifier) { + ProjectComponentIdentifier projectComponentIdentifier = (ProjectComponentIdentifier) a.getId() + .getComponentIdentifier(); + var includedBuild = ToolingUtils.includedBuild(project, projectComponentIdentifier); + Project projectDep = null; + if (includedBuild != null) { + projectDep = ToolingUtils.includedBuildProject((IncludedBuildInternal) includedBuild, + projectComponentIdentifier); + } else { + projectDep = project.getRootProject().findProject(projectComponentIdentifier.getProjectPath()); + } + Objects.requireNonNull(projectDep, "project " + projectComponentIdentifier.getProjectPath() + " should exist"); + SourceSetContainer sourceSets = projectDep.getExtensions().getByType(SourceSetContainer.class); + + SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + ResolvedDependencyBuilder dep = modelBuilder.getDependency( + toAppDependenciesKey(a.getModuleVersion().getId().getGroup(), a.getName(), a.getClassifier())); + if (dep == null) { + dep = toDependency(a, mainSourceSet); + modelBuilder.addDependency(dep); + } + dep.setDeploymentCp(); + dep.clearFlag(DependencyFlags.RELOADABLE); + } else if (isDependency(a)) { + ResolvedDependencyBuilder dep = modelBuilder.getDependency( + toAppDependenciesKey(a.getModuleVersion().getId().getGroup(), a.getName(), a.getClassifier())); + if (dep == null) { + dep = toDependency(a); + modelBuilder.addDependency(dep); + } + dep.setDeploymentCp(); + dep.clearFlag(DependencyFlags.RELOADABLE); + } + } + } + + private void collectDependencies(ResolvedConfiguration configuration, ResolvableDependencies dependencies, + boolean workspaceDiscovery, Project project, ApplicationModelBuilder modelBuilder, + WorkspaceModule.Mutable wsModule) { + + final Set resolvedArtifacts = configuration.getResolvedArtifacts(); + // if the number of artifacts is less than the number of files then probably + // the project includes direct file dependencies + // final Set artifactFiles = resolvedArtifacts.size() < configuration.getFiles().size() + final Set artifactFiles = resolvedArtifacts.size() < dependencies.getFiles().getFiles().size() + ? new HashSet<>(resolvedArtifacts.size()) + : null; + + configuration.getFirstLevelModuleDependencies() + .forEach(d -> { + collectDependencies(d, workspaceDiscovery, project, artifactFiles, new HashSet<>(), + modelBuilder, + wsModule, + (byte) (COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS | COLLECT_RELOADABLE_MODULES)); + }); + + if (artifactFiles != null) { + // detect FS paths that aren't provided by the resolved artifacts + // for (File f : configuration.getFiles()) { + for (File f : dependencies.getFiles().getFiles()) { + if (artifactFiles.contains(f) || !f.exists()) { + continue; + } + // here we are trying to represent a direct FS path dependency + // as an artifact dependency + // SHA1 hash is used to avoid long file names in the lib dir + final String parentPath = f.getParent(); + final String group = HashUtil.sha1(parentPath == null ? f.getName() : parentPath); + String name = f.getName(); + String type = ArtifactCoords.TYPE_JAR; + if (!f.isDirectory()) { + final int dot = f.getName().lastIndexOf('.'); + if (dot > 0) { + name = f.getName().substring(0, dot); + type = f.getName().substring(dot + 1); + } + } + // hash could be a better way to represent the version + final String version = String.valueOf(f.lastModified()); + final ResolvedDependencyBuilder artifactBuilder = ResolvedDependencyBuilder.newInstance() + .setGroupId(group) + .setArtifactId(name) + .setType(type) + .setVersion(version) + .setResolvedPath(f.toPath()) + .setDirect(true) + .setRuntimeCp(); + processQuarkusDependency(artifactBuilder, modelBuilder); + modelBuilder.addDependency(artifactBuilder); + } + } + } + + private void collectDependencies(org.gradle.api.artifacts.ResolvedDependency resolvedDep, boolean workspaceDiscovery, + Project project, Set artifactFiles, Set processedModules, ApplicationModelBuilder modelBuilder, + WorkspaceModule.Mutable parentModule, + byte flags) { + WorkspaceModule.Mutable projectModule = null; + for (ResolvedArtifact a : resolvedDep.getModuleArtifacts()) { + final ArtifactKey artifactKey = toAppDependenciesKey(a.getModuleVersion().getId().getGroup(), a.getName(), + a.getClassifier()); + if (!isDependency(a) || modelBuilder.getDependency(artifactKey) != null) { + continue; + } + final ArtifactCoords depCoords = toArtifactCoords(a); + final ResolvedDependencyBuilder depBuilder = ResolvedDependencyBuilder.newInstance() + .setCoords(depCoords) + .setRuntimeCp(); + if (isFlagOn(flags, COLLECT_DIRECT_DEPS)) { + depBuilder.setDirect(true); + flags = clearFlag(flags, COLLECT_DIRECT_DEPS); + } + if (parentModule != null) { + parentModule.addDependency(new ArtifactDependency(depCoords)); + } + + PathCollection paths = null; + if (workspaceDiscovery && a.getId().getComponentIdentifier() instanceof ProjectComponentIdentifier) { + + Project projectDep = project.getRootProject().findProject( + ((ProjectComponentIdentifier) a.getId().getComponentIdentifier()).getProjectPath()); + SourceSetContainer sourceSets = projectDep == null ? null + : projectDep.getExtensions().findByType(SourceSetContainer.class); + + final String classifier = a.getClassifier(); + if (classifier == null || classifier.isEmpty()) { + final IncludedBuild includedBuild = ToolingUtils.includedBuild(project.getRootProject(), + (ProjectComponentIdentifier) a.getId().getComponentIdentifier()); + if (includedBuild != null) { + final PathList.Builder pathBuilder = PathList.builder(); + + if (includedBuild instanceof IncludedBuildInternal) { + projectDep = ToolingUtils.includedBuildProject((IncludedBuildInternal) includedBuild, + (ProjectComponentIdentifier) a.getId().getComponentIdentifier()); + } + if (projectDep != null) { + projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, + pathBuilder, SourceSet.MAIN_SOURCE_SET_NAME, false); + addSubstitutedProject(pathBuilder, projectDep.getProjectDir()); + } else { + addSubstitutedProject(pathBuilder, includedBuild.getProjectDir()); + } + paths = pathBuilder.build(); + } else if (sourceSets != null) { + final PathList.Builder pathBuilder = PathList.builder(); + projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, + pathBuilder, SourceSet.MAIN_SOURCE_SET_NAME, false); + paths = pathBuilder.build(); + } + } else if (sourceSets != null) { + if (SourceSet.TEST_SOURCE_SET_NAME.equals(classifier)) { + final PathList.Builder pathBuilder = PathList.builder(); + projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, + pathBuilder, SourceSet.TEST_SOURCE_SET_NAME, true); + paths = pathBuilder.build(); + } else if ("test-fixtures".equals(classifier)) { + final PathList.Builder pathBuilder = PathList.builder(); + projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, + pathBuilder, "testFixtures", true); + paths = pathBuilder.build(); + } + } + } + + depBuilder.setResolvedPaths(paths == null ? PathList.of(a.getFile().toPath()) : paths) + .setWorkspaceModule(projectModule); + if (processQuarkusDependency(depBuilder, modelBuilder)) { + if (isFlagOn(flags, COLLECT_TOP_EXTENSION_RUNTIME_NODES)) { + depBuilder.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); + flags = clearFlag(flags, COLLECT_TOP_EXTENSION_RUNTIME_NODES); + } + flags = clearFlag(flags, COLLECT_RELOADABLE_MODULES); + } + if (!isFlagOn(flags, COLLECT_RELOADABLE_MODULES)) { + depBuilder.clearFlag(DependencyFlags.RELOADABLE); + } + modelBuilder.addDependency(depBuilder); + + if (artifactFiles != null) { + artifactFiles.add(a.getFile()); + } + } + + processedModules.add(ArtifactKey.ga(resolvedDep.getModuleGroup(), resolvedDep.getModuleName())); + for (org.gradle.api.artifacts.ResolvedDependency child : resolvedDep.getChildren()) { + if (!processedModules.contains(new GACT(child.getModuleGroup(), child.getModuleName()))) { + collectDependencies(child, workspaceDiscovery, project, artifactFiles, processedModules, + modelBuilder, projectModule, flags); + } + } + } + + private static String toNonNullClassifier(String resolvedClassifier) { + return resolvedClassifier == null ? ArtifactCoords.DEFAULT_CLASSIFIER : resolvedClassifier; + } + + private WorkspaceModule.Mutable initProjectModuleAndBuildPaths(final Project project, + ResolvedArtifact resolvedArtifact, ApplicationModelBuilder appModel, final ResolvedDependencyBuilder appDep, + PathList.Builder buildPaths, String sourceName, boolean test) { + + appDep.setWorkspaceModule().setReloadable(); + + final WorkspaceModule.Mutable projectModule = appModel.getOrCreateProjectModule( + new GAV(resolvedArtifact.getModuleVersion().getId().getGroup(), resolvedArtifact.getName(), + resolvedArtifact.getModuleVersion().getId().getVersion()), + project.getProjectDir(), + project.getBuildDir()) + .setBuildFile(project.getBuildFile().toPath()); + + final String classifier = toNonNullClassifier(resolvedArtifact.getClassifier()); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + initProjectModule(project, projectModule, sourceSets.findByName(sourceName), classifier); + + collectDestinationDirs(projectModule.getSources(classifier).getSourceDirs(), buildPaths); + collectDestinationDirs(projectModule.getSources(classifier).getResourceDirs(), buildPaths); + + appModel.addReloadableWorkspaceModule( + ArtifactKey.of(resolvedArtifact.getModuleVersion().getId().getGroup(), resolvedArtifact.getName(), classifier, + ArtifactCoords.TYPE_JAR)); + return projectModule; + } + + private boolean processQuarkusDependency(ResolvedDependencyBuilder artifactBuilder, ApplicationModelBuilder modelBuilder) { + for (Path artifactPath : artifactBuilder.getResolvedPaths()) { + if (!Files.exists(artifactPath) || !artifactBuilder.getType().equals(ArtifactCoords.TYPE_JAR)) { + break; + } + if (Files.isDirectory(artifactPath)) { + return processQuarkusDir(artifactBuilder, artifactPath.resolve(BootstrapConstants.META_INF), modelBuilder); + } else { + try (FileSystem artifactFs = ZipUtils.newFileSystem(artifactPath)) { + return processQuarkusDir(artifactBuilder, artifactFs.getPath(BootstrapConstants.META_INF), modelBuilder); + } catch (IOException e) { + throw new RuntimeException("Failed to process " + artifactPath, e); + } + } + } + return false; + } + + private static boolean processQuarkusDir(ResolvedDependencyBuilder artifactBuilder, Path quarkusDir, + ApplicationModelBuilder modelBuilder) { + if (!Files.exists(quarkusDir)) { + return false; + } + final Path quarkusDescr = quarkusDir.resolve(BootstrapConstants.DESCRIPTOR_FILE_NAME); + if (!Files.exists(quarkusDescr)) { + return false; + } + final Properties extProps = readDescriptor(quarkusDescr); + if (extProps == null) { + return false; + } + artifactBuilder.setRuntimeExtensionArtifact(); + final String extensionCoords = artifactBuilder.toGACTVString(); + modelBuilder.handleExtensionProperties(extProps, extensionCoords); + + final String providesCapabilities = extProps.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); + if (providesCapabilities != null) { + modelBuilder + .addExtensionCapabilities(CapabilityContract.of(extensionCoords, providesCapabilities, null)); + } + return true; + } + + private static Properties readDescriptor(final Path path) { + final Properties rtProps; + if (!Files.exists(path)) { + // not a platform artifact + return null; + } + rtProps = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(path)) { + rtProps.load(reader); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load extension description " + path, e); + } + return rtProps; + } + + private static void initProjectModule(Project project, WorkspaceModule.Mutable module, SourceSet sourceSet, + String classifier) { + + if (sourceSet == null) { + return; + } + + final FileCollection allClassesDirs = sourceSet.getOutput().getClassesDirs(); + // some plugins do not add source directories to source sets and they may be missing from sourceSet.getAllJava() + // see https://github.com/quarkusio/quarkus/issues/20755 + + final List sourceDirs = new ArrayList<>(1); + project.getTasks().withType(AbstractCompile.class, t -> { + if (!t.getEnabled()) { + return; + } + final FileTree source = t.getSource(); + if (source.isEmpty()) { + return; + } + final File destDir = t.getDestinationDirectory().getAsFile().get(); + if (!allClassesDirs.contains(destDir)) { + return; + } + source.visit(a -> { + // we are looking for the root dirs containing sources + if (a.getRelativePath().getSegments().length == 1) { + final File srcDir = a.getFile().getParentFile(); + sourceDirs.add(new DefaultSourceDir(srcDir.toPath(), destDir.toPath(), Map.of("compiler", t.getName()))); + } + }); + }); + + // This "try/catch" is needed because of the way the "quarkus-cli" Gradle tests work. Without it, the tests fail. + /* + try { + Class.forName("org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile"); + project.getTasks().withType(KotlinJvmCompile.class, t -> { + if (!t.getEnabled()) { + return; + } + final FileTree source = t.getSources().getAsFileTree(); + if (source.isEmpty()) { + return; + } + final File destDir = t.getDestinationDirectory().getAsFile().get(); + if (!allClassesDirs.contains(destDir)) { + return; + } + source.visit(a -> { + // we are looking for the root dirs containing sources + if (a.getRelativePath().getSegments().length == 1) { + final File srcDir = a.getFile().getParentFile(); + sourceDirs + .add(new DefaultSourceDir(srcDir.toPath(), destDir.toPath(), Map.of("compiler", t.getName()))); + } + }); + }); + } catch (ClassNotFoundException e) { + // ignore + } + */ + + final LinkedHashMap resourceDirs = new LinkedHashMap<>(1); + final File resourcesOutputDir = sourceSet.getOutput().getResourcesDir(); + project.getTasks().withType(ProcessResources.class, t -> { + if (!t.getEnabled()) { + return; + } + final FileCollection source = t.getSource(); + if (source.isEmpty()) { + return; + } + if (!t.getDestinationDir().equals(resourcesOutputDir)) { + return; + } + final Path destDir = t.getDestinationDir().toPath(); + source.getAsFileTree().visit(a -> { + // we are looking for the root dirs containing sources + if (a.getRelativePath().getSegments().length == 1) { + final File srcDir = a.getFile().getParentFile(); + resourceDirs.put(srcDir, destDir); + } + }); + }); + // there could be a task generating resources + if (resourcesOutputDir.exists() && resourceDirs.isEmpty()) { + sourceSet.getResources().getSrcDirs() + .forEach(srcDir -> resourceDirs.put(srcDir, resourcesOutputDir.toPath())); + } + final List resources = new ArrayList<>(resourceDirs.size()); + for (Map.Entry e : resourceDirs.entrySet()) { + resources.add(new DefaultSourceDir(e.getKey().toPath(), e.getValue())); + } + module.addArtifactSources(new DefaultArtifactSources(classifier, sourceDirs, resources)); + } + + private void addSubstitutedProject(PathList.Builder paths, File projectFile) { + File mainResourceDirectory = new File(projectFile, MAIN_RESOURCES_OUTPUT); + if (mainResourceDirectory.exists()) { + paths.add(mainResourceDirectory.toPath()); + } + File classesOutput = new File(projectFile, CLASSES_OUTPUT); + File[] languageDirectories = classesOutput.listFiles(); + if (languageDirectories != null) { + for (File languageDirectory : languageDirectories) { + if (languageDirectory.isDirectory()) { + for (File sourceSet : languageDirectory.listFiles()) { + if (sourceSet.isDirectory() && sourceSet.getName().equals(SourceSet.MAIN_SOURCE_SET_NAME)) { + paths.add(sourceSet.toPath()); + } + } + } + } + } + } + + private static boolean isFlagOn(byte walkingFlags, byte flag) { + return (walkingFlags & flag) > 0; + } + + private static byte clearFlag(byte flags, byte flag) { + if ((flags & flag) > 0) { + flags ^= flag; + } + return flags; + } + + private static boolean isDependency(ResolvedArtifact a) { + return ArtifactCoords.TYPE_JAR.equalsIgnoreCase(a.getExtension()) || "exe".equalsIgnoreCase(a.getExtension()) || + a.getFile().isDirectory(); + } + + /** + * Creates an instance of Dependency and associates it with the ResolvedArtifact's path + */ + static ResolvedDependencyBuilder toDependency(ResolvedArtifact a, int... flags) { + return toDependency(a, PathList.of(a.getFile().toPath()), null, flags); + } + + static ResolvedDependencyBuilder toDependency(ResolvedArtifact a, SourceSet s) { + PathList.Builder resolvedPathBuilder = PathList.builder(); + + for (File classesDir : s.getOutput().getClassesDirs()) { + if (classesDir.exists()) { + resolvedPathBuilder.add(classesDir.toPath()); + } + } + File resourceDir = s.getOutput().getResourcesDir(); + if (resourceDir != null && resourceDir.exists()) { + resolvedPathBuilder.add(resourceDir.toPath()); + } + + return ResolvedDependencyBuilder + .newInstance() + .setResolvedPaths(resolvedPathBuilder.build()) + .setCoords(toArtifactCoords(a)); + } + + static ResolvedDependencyBuilder toDependency(ResolvedArtifact a, PathCollection paths, DefaultWorkspaceModule module, + int... flags) { + int allFlags = 0; + for (int f : flags) { + allFlags |= f; + } + return ResolvedDependencyBuilder.newInstance() + .setCoords(toArtifactCoords(a)) + .setResolvedPaths(paths) + .setWorkspaceModule(module) + .setFlags(allFlags); + } + + private static ArtifactCoords toArtifactCoords(ResolvedArtifact a) { + final String[] split = a.getModuleVersion().toString().split(":"); + return new GACTV(split[0], split[1], a.getClassifier(), a.getType(), split.length > 2 ? split[2] : null); + } + + private static ArtifactKey toAppDependenciesKey(String groupId, String artifactId, String classifier) { + return new GACT(groupId, artifactId, classifier, ArtifactCoords.TYPE_JAR); + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java new file mode 100644 index 000000000000..6993416b2d71 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Quarkus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.quarkus.gradle.tooling; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.attributes.Category; +import org.gradle.api.initialization.IncludedBuild; +import org.gradle.internal.composite.IncludedBuildInternal; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.model.gradle.ModelParameter; +import io.quarkus.bootstrap.model.gradle.impl.ModelParameterImpl; +import io.quarkus.runtime.LaunchMode; + +public class ToolingUtils { + + private static final String DEPLOYMENT_CONFIGURATION_SUFFIX = "Deployment"; + private static final String PLATFORM_CONFIGURATION_SUFFIX = "Platform"; + public static final String DEV_MODE_CONFIGURATION_NAME = "quarkusDev"; + + public static String toDeploymentConfigurationName(String baseConfigurationName) { + return baseConfigurationName + DEPLOYMENT_CONFIGURATION_SUFFIX; + } + + public static String toPlatformConfigurationName(String baseConfigurationName) { + return baseConfigurationName + PLATFORM_CONFIGURATION_SUFFIX; + } + + public static boolean isEnforcedPlatform(ModuleDependency module) { + final Category category = module.getAttributes().getAttribute(Category.CATEGORY_ATTRIBUTE); + return category != null && (Category.ENFORCED_PLATFORM.equals(category.getName()) + || Category.REGULAR_PLATFORM.equals(category.getName())); + } + + public static IncludedBuild includedBuild(final Project project, + final ProjectComponentIdentifier projectComponentIdentifier) { + /* + final String name = projectComponentIdentifier.getBuild().getName(); + for (IncludedBuild ib : project.getRootProject().getGradle().getIncludedBuilds()) { + if (ib.getName().equals(name)) { + return ib; + } + } + */ + final String buildPath = projectComponentIdentifier.getBuild().getBuildPath(); + for (IncludedBuild ib : project.getRootProject().getGradle().getIncludedBuilds()) { + if (((IncludedBuildInternal) ib).getTarget().getBuildIdentifier().getBuildPath().equals(buildPath)) { + return ib; + } + } + return null; + } + + public static Project includedBuildProject(IncludedBuildInternal includedBuild, + final ProjectComponentIdentifier componentIdentifier) { + return includedBuild.getTarget().getMutableModel().getRootProject().findProject( + componentIdentifier.getProjectPath()); + } + + public static Path serializeAppModel(ApplicationModel appModel, Task context, boolean test) throws IOException { + final Path serializedModel = context.getTemporaryDir().toPath() + .resolve("quarkus-app" + (test ? "-test" : "") + "-model.dat"); + try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(serializedModel))) { + out.writeObject(appModel); + } + return serializedModel; + } + + public static ApplicationModel create(Project project, LaunchMode mode) { + final ModelParameter params = new ModelParameterImpl(); + params.setMode(mode.toString()); + return create(project, params); + } + + public static ApplicationModel create(Project project, ModelParameter params) { + return (ApplicationModel) new GradleApplicationModelBuilder().buildAll(ApplicationModel.class.getName(), params, + project); + } + +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java new file mode 100644 index 000000000000..68008babd8b6 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-plugin/src/main/java/io/quarkus/gradle/tooling/dependency/DependencyUtils.java @@ -0,0 +1,212 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Quarkus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.quarkus.gradle.tooling.dependency; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.capabilities.Capability; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.internal.composite.IncludedBuildInternal; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.util.BootstrapUtils; +import io.quarkus.fs.util.ZipUtils; +import io.quarkus.gradle.tooling.ToolingUtils; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.GACTV; + +public class DependencyUtils { + + private static final String COPY_CONFIGURATION_NAME = "quarkusDependency"; + private static final String TEST_FIXTURE_SUFFIX = "-test-fixtures"; + + public static Configuration duplicateConfiguration(Project project, Configuration toDuplicate) { + Configuration configurationCopy = project.getConfigurations().findByName(COPY_CONFIGURATION_NAME); + if (configurationCopy != null) { + project.getConfigurations().remove(configurationCopy); + } + return duplicateConfiguration(project, COPY_CONFIGURATION_NAME, toDuplicate); + } + + public static Configuration duplicateConfiguration(Project project, String name, Configuration toDuplicate) { + final Configuration configurationCopy = project.getConfigurations().create(name); + configurationCopy.getDependencies().addAll(toDuplicate.getAllDependencies()); + configurationCopy.getDependencyConstraints().addAll(toDuplicate.getAllDependencyConstraints()); + return configurationCopy; + } + + public static boolean isTestFixtureDependency(Dependency dependency) { + if (!(dependency instanceof ModuleDependency)) { + return false; + } + ModuleDependency module = (ModuleDependency) dependency; + for (Capability requestedCapability : module.getRequestedCapabilities()) { + if (requestedCapability.getName().endsWith(TEST_FIXTURE_SUFFIX)) { + return true; + } + } + return false; + } + + public static String asDependencyNotation(Dependency dependency) { + return String.join(":", dependency.getGroup(), dependency.getName(), dependency.getVersion()); + } + + public static String asDependencyNotation(ArtifactCoords artifactCoords) { + return String.join(":", artifactCoords.getGroupId(), artifactCoords.getArtifactId(), artifactCoords.getVersion()); + } + + public static ExtensionDependency getExtensionInfoOrNull(Project project, ResolvedArtifact artifact) { + ModuleVersionIdentifier artifactId = artifact.getModuleVersion().getId(); + File artifactFile = artifact.getFile(); + + if (artifact.getId().getComponentIdentifier() instanceof ProjectComponentIdentifier) { + ProjectComponentIdentifier componentIdentifier = ((ProjectComponentIdentifier) artifact.getId() + .getComponentIdentifier()); + Project projectDep = project.getRootProject().findProject( + componentIdentifier.getProjectPath()); + SourceSetContainer sourceSets = projectDep == null ? null + : projectDep.getExtensions().findByType(SourceSetContainer.class); + final String classifier = artifact.getClassifier(); + boolean isIncludedBuild = false; + /* + if ((!componentIdentifier.getBuild().isCurrentBuild() || sourceSets == null) + && (classifier == null || classifier.isEmpty())) { + var includedBuild = ToolingUtils.includedBuild(project, componentIdentifier); + if (includedBuild instanceof IncludedBuildInternal) { + projectDep = ToolingUtils.includedBuildProject((IncludedBuildInternal) includedBuild, componentIdentifier); + sourceSets = projectDep == null ? null : projectDep.getExtensions().findByType(SourceSetContainer.class); + isIncludedBuild = true; + } + } + */ + if (sourceSets != null) { + SourceSet mainSourceSet = sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME); + if (mainSourceSet == null) { + return null; + } + File resourcesDir = mainSourceSet.getOutput().getResourcesDir(); + Path descriptorPath = resourcesDir.toPath().resolve(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId, projectDep, isIncludedBuild); + } + } + } + + if (!artifactFile.exists()) { + return null; + } + if (artifactFile.isDirectory()) { + Path descriptorPath = artifactFile.toPath().resolve(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId, null, false); + } + } else if (ArtifactCoords.TYPE_JAR.equals(artifact.getExtension())) { + try (FileSystem artifactFs = ZipUtils.newFileSystem(artifactFile.toPath())) { + Path descriptorPath = artifactFs.getPath(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId, null, false); + } + } catch (IOException e) { + throw new GradleException("Failed to read " + artifactFile, e); + } + } + return null; + } + + private static ExtensionDependency loadExtensionInfo(Project project, Path descriptorPath, + ModuleVersionIdentifier exentionId, Project extensionProject, boolean isIncludedBuild) { + final Properties extensionProperties = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(descriptorPath)) { + extensionProperties.load(reader); + } catch (IOException e) { + throw new GradleException("Failed to load " + descriptorPath, e); + } + ArtifactCoords deploymentModule = GACTV + .fromString(extensionProperties.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT)); + final List conditionalDependencies; + if (extensionProperties.containsKey(BootstrapConstants.CONDITIONAL_DEPENDENCIES)) { + final String[] deps = BootstrapUtils + .splitByWhitespace(extensionProperties.getProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES)); + conditionalDependencies = new ArrayList<>(deps.length); + for (String conditionalDep : deps) { + conditionalDependencies.add(create(project.getDependencies(), conditionalDep)); + } + } else { + conditionalDependencies = Collections.emptyList(); + } + + final ArtifactKey[] constraints = BootstrapUtils + .parseDependencyCondition(extensionProperties.getProperty(BootstrapConstants.DEPENDENCY_CONDITION)); + if (isIncludedBuild) { + return new IncludedBuildExtensionDependency(extensionProject, exentionId, deploymentModule, conditionalDependencies, + constraints == null ? Collections.emptyList() : Arrays.asList(constraints)); + } + if (extensionProject != null) { + return new LocalExtensionDependency(extensionProject, exentionId, deploymentModule, conditionalDependencies, + constraints == null ? Collections.emptyList() : Arrays.asList(constraints)); + } + return new ExtensionDependency(exentionId, deploymentModule, conditionalDependencies, + constraints == null ? Collections.emptyList() : Arrays.asList(constraints)); + } + + public static Dependency create(DependencyHandler dependencies, String conditionalDependency) { + final ArtifactCoords dependencyCoords = GACTV.fromString(conditionalDependency); + return dependencies.create(String.join(":", dependencyCoords.getGroupId(), dependencyCoords.getArtifactId(), + dependencyCoords.getVersion())); + } + + public static void addLocalDeploymentDependency(String deploymentConfigurationName, LocalExtensionDependency extension, + DependencyHandler dependencies) { + dependencies.add(deploymentConfigurationName, + dependencies.project(Collections.singletonMap("path", extension.findDeploymentModulePath()))); + } + + public static void requireDeploymentDependency(String deploymentConfigurationName, ExtensionDependency extension, + DependencyHandler dependencies) { + dependencies.add(deploymentConfigurationName, + extension.getDeploymentModule().getGroupId() + ":" + extension.getDeploymentModule().getArtifactId() + ":" + + extension.getDeploymentModule().getVersion()); + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/build.gradle.kts b/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/build.gradle.kts index afd93f30ed41..e8ad601f0b7f 100644 --- a/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/build.gradle.kts +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/build.gradle.kts @@ -1,7 +1,15 @@ +import io.quarkus.bootstrap.model.ApplicationModel +import io.quarkus.bootstrap.model.gradle.impl.ModelParameterImpl +import io.quarkus.bootstrap.util.BootstrapUtils +import io.quarkus.gradle.tooling.GradleApplicationModelBuilder +import io.quarkus.runtime.LaunchMode +import kotlin.io.path.notExists +import kotlin.jvm.java + plugins { id("otel.javaagent-testing") - id("io.quarkus") version "3.0.0.Final" + id("io.opentelemetry.instrumentation.quarkus3") apply false } otelJava { @@ -28,9 +36,37 @@ dependencies { testImplementation("io.quarkus:quarkus-junit5") } -tasks.named("compileJava").configure { - dependsOn(tasks.named("compileQuarkusGeneratedSourcesJava")) +tasks.register("integrationTestClasses") {} + +val quarkusTestBaseRuntimeClasspathConfiguration by configurations.creating { + extendsFrom(configurations["testRuntimeClasspath"]) } -tasks.named("sourcesJar").configure { - dependsOn(tasks.named("compileQuarkusGeneratedSourcesJava")) + +val quarkusTestCompileOnlyConfiguration by configurations.creating { +} + +val testModelPath = layout.buildDirectory.file("quarkus-app-test-model.dat").get().asFile.toPath() + +val buildModel = tasks.register("buildModel") { + if (testModelPath.notExists()) { + doLast { + val modelParameter = ModelParameterImpl() + modelParameter.mode = LaunchMode.TEST.toString() + val model = GradleApplicationModelBuilder().buildAll( + ApplicationModel::class.java.getName(), + modelParameter, + project + ) + BootstrapUtils.serializeAppModel(model as ApplicationModel?, testModelPath) + } + } + outputs.file(testModelPath) +} + +tasks { + test { + dependsOn(buildModel) + + systemProperty("quarkus-internal-test.serialized-app-model.path", testModelPath.toString()) + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 79e5362815d4..f9a578be4e8a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -475,7 +475,9 @@ include(":instrumentation:pulsar:pulsar-2.8:javaagent") include(":instrumentation:pulsar:pulsar-2.8:javaagent-unit-tests") include(":instrumentation:quarkus-resteasy-reactive:common-testing") include(":instrumentation:quarkus-resteasy-reactive:javaagent") +includeBuild("instrumentation/quarkus-resteasy-reactive/quarkus2-plugin") include(":instrumentation:quarkus-resteasy-reactive:quarkus2-testing") +includeBuild("instrumentation/quarkus-resteasy-reactive/quarkus3-plugin") include(":instrumentation:quarkus-resteasy-reactive:quarkus3-testing") include(":instrumentation:quartz-2.0:javaagent") include(":instrumentation:quartz-2.0:library") From 0fe60900d8af9b83d4344d29e77a7b8d0c737931 Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Mon, 25 Aug 2025 20:34:32 +0300 Subject: [PATCH 2/3] exclude quarkus plugins --- .github/workflows/build-common.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-common.yml b/.github/workflows/build-common.yml index fce47bc963c0..618e0d650ae8 100644 --- a/.github/workflows/build-common.yml +++ b/.github/workflows/build-common.yml @@ -133,6 +133,7 @@ jobs: set +e grep '^ implementation(".*:.*:[0-9].*")\|^ api(".*:.*:[0-9].*")' \ --include=\*.kts \ + --exclude-dir=quarkus\*-plugin \ -r instrumentation \ | grep -v testing/build.gradle.kts \ | grep -v com.azure:azure-core-tracing-opentelemetry \ From d15d372780e9fd4befbc508c59d45ae1ed9b6dfe Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Mon, 25 Aug 2025 20:46:04 +0300 Subject: [PATCH 3/3] remove quarkus tasks --- .github/workflows/codeql.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 11d2259245c2..ada9f745228d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -75,8 +75,7 @@ jobs: # --no-build-cache is required for codeql to analyze all modules # --no-daemon is required for codeql to observe the compilation # (see https://docs.github.com/en/code-security/codeql-cli/getting-started-with-the-codeql-cli/preparing-your-code-for-codeql-analysis#specifying-build-commands) - # quarkus tasks are disabled because they often cause the build to fail (see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13284) - run: ./gradlew assemble -x javadoc -x :instrumentation:quarkus-resteasy-reactive:quarkus3-testing:quarkusGenerateCodeDev -x :instrumentation:quarkus-resteasy-reactive:quarkus2-testing:quarkusGenerateCodeDev --no-build-cache --no-daemon + run: ./gradlew assemble -x javadoc --no-build-cache --no-daemon - name: Perform CodeQL analysis uses: github/codeql-action/analyze@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11