diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index b9676e0f8005..ce532b4e2b15 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'groovy' id 'java-gradle-plugin' id "com.diffplug.spotless" version "5.12.4" } @@ -10,16 +9,13 @@ spotless { licenseHeaderFile rootProject.file('../gradle/enforcement/spotless.license.java'), '(package|import|public)' target 'src/**/*.java' } - groovy { - licenseHeaderFile rootProject.file('../gradle/enforcement/spotless.license.java'), '(package|import|class)' - } } gradlePlugin { plugins { create("muzzle-plugin") { id = "muzzle" - implementationClass = "MuzzlePlugin" + implementationClass = "io.opentelemetry.instrumentation.gradle.muzzle.MuzzlePlugin" } } } diff --git a/buildSrc/src/main/groovy/MuzzlePlugin.groovy b/buildSrc/src/main/groovy/MuzzlePlugin.groovy deleted file mode 100644 index e7f13ed1b62b..000000000000 --- a/buildSrc/src/main/groovy/MuzzlePlugin.groovy +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.gradle.muzzle.MuzzleDirective -import io.opentelemetry.instrumentation.gradle.muzzle.MuzzleExtension -import java.lang.reflect.Method -import java.security.SecureClassLoader -import java.util.concurrent.atomic.AtomicReference -import java.util.function.Predicate -import java.util.regex.Pattern -import org.apache.maven.repository.internal.MavenRepositorySystemUtils -import org.eclipse.aether.DefaultRepositorySystemSession -import org.eclipse.aether.RepositorySystem -import org.eclipse.aether.RepositorySystemSession -import org.eclipse.aether.artifact.Artifact -import org.eclipse.aether.artifact.DefaultArtifact -import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory -import org.eclipse.aether.impl.DefaultServiceLocator -import org.eclipse.aether.repository.LocalRepository -import org.eclipse.aether.repository.RemoteRepository -import org.eclipse.aether.resolution.VersionRangeRequest -import org.eclipse.aether.resolution.VersionRangeResult -import org.eclipse.aether.spi.connector.RepositoryConnectorFactory -import org.eclipse.aether.spi.connector.transport.TransporterFactory -import org.eclipse.aether.transport.http.HttpTransporterFactory -import org.eclipse.aether.version.Version -import org.gradle.api.GradleException -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.tasks.TaskProvider - -/** - * muzzle task plugin which runs muzzle validation against a range of dependencies. - */ -class MuzzlePlugin implements Plugin { - /** - * Select a random set of versions to test - */ - private static final int RANGE_COUNT_LIMIT = 10 - private static final AtomicReference TOOLING_LOADER = new AtomicReference<>() - - @Override - void apply(Project project) { - project.extensions.create("muzzle", MuzzleExtension, project.objects) - - // compileMuzzle compiles all projects required to run muzzle validation. - // Not adding group and description to keep this task from showing in `gradle tasks`. - def compileMuzzle = project.tasks.register('compileMuzzle') { - dependsOn(':javaagent-bootstrap:classes') - dependsOn(':javaagent-tooling:classes') - dependsOn(':javaagent-extension-api:classes') - dependsOn(project.tasks.classes) - } - - def muzzle = project.tasks.register('muzzle') { - group = 'Muzzle' - description = "Run instrumentation muzzle on compile time dependencies" - dependsOn(compileMuzzle) - } - - project.tasks.register('printMuzzleReferences') { - group = 'Muzzle' - description = "Print references created by instrumentation muzzle" - dependsOn(compileMuzzle) - doLast { - ClassLoader instrumentationCL = createInstrumentationClassloader(project) - Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil') - .getMethod('printMuzzleReferences', ClassLoader.class) - assertionMethod.invoke(null, instrumentationCL) - } - } - - def hasRelevantTask = project.gradle.startParameter.taskNames.any { taskName -> - // removing leading ':' if present - taskName = taskName.replaceFirst('^:', '') - String muzzleTaskPath = project.path.replaceFirst('^:', '') - return 'muzzle' == taskName || "${muzzleTaskPath}:muzzle" == taskName - } - if (!hasRelevantTask) { - // Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly run. - return - } - - RepositorySystem system = newRepositorySystem() - RepositorySystemSession session = newRepositorySystemSession(system) - - project.afterEvaluate { - // use runAfter to set up task finalizers in version order - TaskProvider runAfter = muzzle - - for (MuzzleDirective muzzleDirective : project.muzzle.directives.get()) { - project.getLogger().info("configured $muzzleDirective") - - if (muzzleDirective.coreJdk.get()) { - runAfter = addMuzzleTask(muzzleDirective, null, project, runAfter) - } else { - muzzleDirectiveToArtifacts(project, muzzleDirective, system, session).collect() { Artifact singleVersion -> - runAfter = addMuzzleTask(muzzleDirective, singleVersion, project, runAfter) - } - if (muzzleDirective.assertInverse.get()) { - inverseOf(project, muzzleDirective, system, session).collect() { MuzzleDirective inverseDirective -> - muzzleDirectiveToArtifacts(project, inverseDirective, system, session).collect() { Artifact singleVersion -> - runAfter = addMuzzleTask(inverseDirective, singleVersion, project, runAfter) - } - } - } - } - } - } - } - - private static ClassLoader getOrCreateToolingLoader(Project project) { - synchronized (TOOLING_LOADER) { - ClassLoader toolingLoader = TOOLING_LOADER.get() - if (toolingLoader == null) { - Set urls = new HashSet<>() - project.getLogger().info('creating classpath for auto-tooling') - for (File f : project.configurations.toolingRuntime.getFiles()) { - project.getLogger().info('--' + f) - urls.add(f.toURI().toURL()) - } - def loader = new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.platformClassLoader) - assert TOOLING_LOADER.compareAndSet(null, loader) - return TOOLING_LOADER.get() - } else { - return toolingLoader - } - } - } - - /** - * Create a classloader with core agent classes and project instrumentation on the classpath. - */ - private static ClassLoader createInstrumentationClassloader(Project project) { - project.getLogger().info("Creating instrumentation classpath for: " + project.getName()) - Set urls = new HashSet<>() - for (File f : project.sourceSets.main.runtimeClasspath.getFiles()) { - project.getLogger().info('--' + f) - urls.add(f.toURI().toURL()) - } - - return new URLClassLoader(urls.toArray(new URL[0]), getOrCreateToolingLoader(project)) - } - - /** - * Create a classloader with all compile-time dependencies on the classpath - */ - private static ClassLoader createCompileDepsClassLoader(Project project) { - List userUrls = new ArrayList<>() - project.getLogger().info("Creating compile-time classpath for: " + project.getName()) - for (File f : project.configurations.compileClasspath.getFiles()) { - project.getLogger().info('--' + f) - userUrls.add(f.toURI().toURL()) - } - for (File f : project.configurations.bootstrapRuntime.getFiles()) { - project.getLogger().info('--' + f) - userUrls.add(f.toURI().toURL()) - } - return new URLClassLoader(userUrls.toArray(new URL[0]), ClassLoader.platformClassLoader) - } - - /** - * Create a classloader with dependencies for a single muzzle task. - */ - private static ClassLoader createClassLoaderForTask(Project project, String muzzleTaskName) { - List userUrls = new ArrayList<>() - - project.getLogger().info("Creating task classpath") - project.configurations.getByName(muzzleTaskName).resolvedConfiguration.files.each { File jarFile -> - project.getLogger().info("-- Added to instrumentation classpath: $jarFile") - userUrls.add(jarFile.toURI().toURL()) - } - - for (File f : project.configurations.bootstrapRuntime.getFiles()) { - project.getLogger().info("-- Added to instrumentation bootstrap classpath: $f") - userUrls.add(f.toURI().toURL()) - } - return new URLClassLoader(userUrls.toArray(new URL[0]), ClassLoader.platformClassLoader) - } - - /** - * Convert a muzzle directive to a list of artifacts - */ - private static Set muzzleDirectiveToArtifacts(Project instrumentationProject, MuzzleDirective muzzleDirective, RepositorySystem system, RepositorySystemSession session) { - Artifact directiveArtifact = new DefaultArtifact(muzzleDirective.group.get(), muzzleDirective.module.get(), "jar", muzzleDirective.versions.get()) - - VersionRangeRequest rangeRequest = new VersionRangeRequest() - rangeRequest.setRepositories(getProjectRepositories(instrumentationProject)) - rangeRequest.setArtifact(directiveArtifact) - VersionRangeResult rangeResult = system.resolveVersionRange(session, rangeRequest) - - Set allVersionArtifacts = filterVersions(rangeResult, muzzleDirective.normalizedSkipVersions).collect { version -> - new DefaultArtifact(muzzleDirective.group.get(), muzzleDirective.module.get(), "jar", version) - }.toSet() - - if (allVersionArtifacts.isEmpty()) { - throw new GradleException("No muzzle artifacts found for $muzzleDirective") - } - - return allVersionArtifacts - } - - private static List getProjectRepositories(Project project) { - project.repositories.collect { - new RemoteRepository.Builder(it.name, "default", it.url.toString()).build() - } - } - - /** - * Create a list of muzzle directives which assert the opposite of the given MuzzleDirective. - */ - private static Set inverseOf(Project instrumentationProject, MuzzleDirective muzzleDirective, RepositorySystem system, RepositorySystemSession session) { - Set inverseDirectives = new HashSet<>() - - Artifact allVersionsArtifact = new DefaultArtifact(muzzleDirective.group.get(), muzzleDirective.module.get(), "jar", "[,)") - Artifact directiveArtifact = new DefaultArtifact(muzzleDirective.group.get(), muzzleDirective.module.get(), "jar", muzzleDirective.versions.get()) - - List repos = getProjectRepositories(instrumentationProject) - VersionRangeRequest allRangeRequest = new VersionRangeRequest() - allRangeRequest.setRepositories(repos) - allRangeRequest.setArtifact(allVersionsArtifact) - VersionRangeResult allRangeResult = system.resolveVersionRange(session, allRangeRequest) - - VersionRangeRequest rangeRequest = new VersionRangeRequest() - rangeRequest.setRepositories(repos) - rangeRequest.setArtifact(directiveArtifact) - VersionRangeResult rangeResult = system.resolveVersionRange(session, rangeRequest) - - allRangeResult.getVersions().removeAll(rangeResult.getVersions()) - - filterVersions(allRangeResult, muzzleDirective.normalizedSkipVersions).each { version -> - MuzzleDirective inverseDirective = instrumentationProject.objects.newInstance(MuzzleDirective) - inverseDirective.group = muzzleDirective.group - inverseDirective.module = muzzleDirective.module - inverseDirective.versions = version - inverseDirective.assertPass = !muzzleDirective.assertPass - inverseDirectives.add(inverseDirective) - } - - return inverseDirectives - } - - private static Set filterVersions(VersionRangeResult range, Set skipVersions) { - Set result = new HashSet<>() - - def predicate = new AcceptableVersions(range, skipVersions) - if (predicate.test(range.lowestVersion)) { - result.add(range.lowestVersion.toString()) - } - if (predicate.test(range.highestVersion)) { - result.add(range.highestVersion.toString()) - } - - List copy = new ArrayList<>(range.versions) - Collections.shuffle(copy) - while (result.size() < RANGE_COUNT_LIMIT && !copy.isEmpty()) { - Version version = copy.pop() - if (predicate.test(version)) { - result.add(version.toString()) - } - } - - return result - } - - static class AcceptableVersions implements Predicate { - private static final Pattern GIT_SHA_PATTERN = Pattern.compile('^.*-[0-9a-f]{7,}$') - - private final VersionRangeResult range - private final Collection skipVersions - - AcceptableVersions(VersionRangeResult range, Collection skipVersions) { - this.range = range - this.skipVersions = skipVersions - } - - @Override - boolean test(Version version) { - if (version == null) { - return false - } - def versionString = version.toString().toLowerCase() - if (skipVersions.contains(versionString)) { - return false - } - - def draftVersion = versionString.contains("rc") || - versionString.contains(".cr") || - versionString.contains("alpha") || - versionString.contains("beta") || - versionString.contains("-b") || - versionString.contains(".m") || - versionString.contains("-m") || - versionString.contains("-dev") || - versionString.contains("-ea") || - versionString.contains("-atlassian-") || - versionString.contains("public_draft") || - versionString.contains("snapshot") || - versionString.matches(GIT_SHA_PATTERN) - - return !draftVersion - } - } - - /** - * Configure a muzzle task to pass or fail a given version. - * - * @param assertPass If true, assert that muzzle validation passes - * @param versionArtifact version to assert against. - * @param instrumentationProject instrumentation being asserted against. - * @param runAfter Task which runs before the new muzzle task. - * - * @return The created muzzle task. - */ - private static TaskProvider addMuzzleTask(MuzzleDirective muzzleDirective, Artifact versionArtifact, Project instrumentationProject, TaskProvider runAfter) { - def taskName - if (muzzleDirective.coreJdk.get()) { - taskName = "muzzle-Assert$muzzleDirective" - } else { - taskName = "muzzle-Assert${muzzleDirective.assertPass ? "Pass" : "Fail"}-$versionArtifact.groupId-$versionArtifact.artifactId-$versionArtifact.version${!muzzleDirective.name.get().isEmpty() ? "-${muzzleDirective.getNameSlug()}" : ""}" - } - def config = instrumentationProject.configurations.create(taskName) - - if (!muzzleDirective.coreJdk.get()) { - def dep = instrumentationProject.dependencies.create("$versionArtifact.groupId:$versionArtifact.artifactId:$versionArtifact.version") { - transitive = true - } - // The following optional transitive dependencies are brought in by some legacy module such as log4j 1.x but are no - // longer bundled with the JVM and have to be excluded for the muzzle tests to be able to run. - dep.exclude group: 'com.sun.jdmk', module: 'jmxtools' - dep.exclude group: 'com.sun.jmx', module: 'jmxri' - - config.dependencies.add(dep) - } - for (String additionalDependency : muzzleDirective.additionalDependencies.get()) { - if (additionalDependency.count(":") < 2) { - // Dependency definition without version, use the artifact's version. - additionalDependency += ":${versionArtifact.version}" - } - config.dependencies.add(instrumentationProject.dependencies.create(additionalDependency) { - transitive = true - }) - } - - def muzzleTask = instrumentationProject.tasks.register(taskName) { - dependsOn(instrumentationProject.configurations.named("runtimeClasspath")) - doLast { - ClassLoader instrumentationCL = createInstrumentationClassloader(instrumentationProject) - def ccl = Thread.currentThread().contextClassLoader - def bogusLoader = new SecureClassLoader() { - @Override - String toString() { - return "bogus" - } - - } - Thread.currentThread().contextClassLoader = bogusLoader - ClassLoader userCL = createClassLoaderForTask(instrumentationProject, taskName) - try { - // find all instrumenters, get muzzle, and assert - Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil') - .getMethod('assertInstrumentationMuzzled', ClassLoader.class, ClassLoader.class, boolean.class) - assertionMethod.invoke(null, instrumentationCL, userCL, muzzleDirective.assertPass.get()) - } finally { - Thread.currentThread().contextClassLoader = ccl - } - - for (Thread thread : Thread.getThreads()) { - if (thread.contextClassLoader == bogusLoader || thread.contextClassLoader == instrumentationCL || thread.contextClassLoader == userCL) { - throw new GradleException("Task $taskName has spawned a thread: $thread with classloader $thread.contextClassLoader. This will prevent GC of dynamic muzzle classes. Aborting muzzle run.") - } - } - } - } - runAfter.configure { - finalizedBy(muzzleTask) - } - return muzzleTask - } - - /** - * Create muzzle's repository system - */ - private static RepositorySystem newRepositorySystem() { - DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator() - locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class) - locator.addService(TransporterFactory.class, HttpTransporterFactory.class) - - return locator.getService(RepositorySystem.class) - } - - - /** - * Create muzzle's repository system session - */ - private static RepositorySystemSession newRepositorySystemSession(RepositorySystem system) { - DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession() - - def tempDir = File.createTempDir() - tempDir.deleteOnExit() - LocalRepository localRepo = new LocalRepository(tempDir) - session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo)) - - return session - } -} diff --git a/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/AcceptableVersions.java b/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/AcceptableVersions.java new file mode 100644 index 000000000000..f3e5dcf81295 --- /dev/null +++ b/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/AcceptableVersions.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.muzzle; + +import java.util.Collection; +import java.util.Locale; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import org.eclipse.aether.version.Version; + +class AcceptableVersions implements Predicate { + private static final Pattern GIT_SHA_PATTERN = Pattern.compile("^.*-[0-9a-f]{7,}$"); + + private final Collection skipVersions; + + AcceptableVersions(Collection skipVersions) { + this.skipVersions = skipVersions; + } + + @Override + public boolean test(Version version) { + if (version == null) { + return false; + } + String versionString = version.toString().toLowerCase(Locale.ROOT); + if (skipVersions.contains(versionString)) { + return false; + } + + boolean draftVersion = + versionString.contains("rc") + || versionString.contains(".cr") + || versionString.contains("alpha") + || versionString.contains("beta") + || versionString.contains("-b") + || versionString.contains(".m") + || versionString.contains("-m") + || versionString.contains("-dev") + || versionString.contains("-ea") + || versionString.contains("-atlassian-") + || versionString.contains("public_draft") + || versionString.contains("snapshot") + || GIT_SHA_PATTERN.matcher(versionString).matches(); + + return !draftVersion; + } +} diff --git a/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePlugin.java b/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePlugin.java new file mode 100644 index 000000000000..2228c2c996d3 --- /dev/null +++ b/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePlugin.java @@ -0,0 +1,518 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.muzzle; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.SecureClassLoader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.impl.DefaultServiceLocator; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResolutionException; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transport.http.HttpTransporterFactory; +import org.eclipse.aether.version.Version; +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; + +public class MuzzlePlugin implements Plugin { + /** Select a random set of versions to test */ + private static final int RANGE_COUNT_LIMIT = 10; + + private static volatile ClassLoader TOOLING_LOADER; + + @Override + public void apply(Project project) { + MuzzleExtension muzzleConfig = + project.getExtensions().create("muzzle", MuzzleExtension.class, project.getObjects()); + + // compileMuzzle compiles all projects required to run muzzle validation. + // Not adding group and description to keep this task from showing in `gradle tasks`. + TaskProvider compileMuzzle = + project + .getTasks() + .register( + "compileMuzzle", + task -> { + task.dependsOn(":javaagent-bootstrap:classes"); + task.dependsOn(":javaagent-tooling:classes"); + task.dependsOn(":javaagent-extension-api:classes"); + task.dependsOn(project.getTasks().named(JavaPlugin.CLASSES_TASK_NAME)); + }); + + TaskProvider muzzle = + project + .getTasks() + .register( + "muzzle", + task -> { + task.setGroup("Muzzle"); + task.setDescription("Run instrumentation muzzle on compile time dependencies"); + task.dependsOn(compileMuzzle); + }); + + project + .getTasks() + .register( + "printMuzzleReferences", + task -> { + task.setGroup("Muzzle"); + task.setDescription("Print references created by instrumentation muzzle"); + task.dependsOn(compileMuzzle); + task.doLast( + unused -> { + ClassLoader instrumentationCL = createInstrumentationClassloader(project); + try { + Method assertionMethod = + instrumentationCL + .loadClass( + "io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil") + .getMethod("printMuzzleReferences", ClassLoader.class); + assertionMethod.invoke(null, instrumentationCL); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + }); + + boolean hasRelevantTask = + project.getGradle().getStartParameter().getTaskNames().stream() + .anyMatch( + taskName -> { + // removing leading ':' if present + if (taskName.startsWith(":")) { + taskName = taskName.substring(1); + } + String projectPath = project.getPath().substring(1); + // Either the specific muzzle task in this project or the top level, full-project + // muzzle task. + return taskName.equals(projectPath + ":muzzle") || taskName.equals("muzzle"); + }); + if (!hasRelevantTask) { + // Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly + // run. + return; + } + + RepositorySystem system = newRepositorySystem(); + RepositorySystemSession session = newRepositorySystemSession(system, project); + + project.afterEvaluate( + unused -> { + // use runAfter to set up task finalizers in version order + TaskProvider runAfter = muzzle; + + for (MuzzleDirective muzzleDirective : muzzleConfig.getDirectives().get()) { + project.getLogger().info("configured " + muzzleDirective); + + if (muzzleDirective.getCoreJdk().get()) { + runAfter = addMuzzleTask(muzzleDirective, null, project, runAfter); + } else { + for (Artifact singleVersion : + muzzleDirectiveToArtifacts(project, muzzleDirective, system, session)) { + runAfter = addMuzzleTask(muzzleDirective, singleVersion, project, runAfter); + } + if (muzzleDirective.getAssertInverse().get()) { + for (MuzzleDirective inverseDirective : + inverseOf(project, muzzleDirective, system, session)) { + for (Artifact singleVersion : + muzzleDirectiveToArtifacts(project, inverseDirective, system, session)) { + runAfter = addMuzzleTask(inverseDirective, singleVersion, project, runAfter); + } + } + } + } + } + }); + } + + /** Create a classloader with core agent classes and project instrumentation on the classpath. */ + private static ClassLoader createInstrumentationClassloader(Project project) { + project.getLogger().info("Creating instrumentation classpath for: " + project.getName()); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + FileCollection runtimeClasspath = + sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath(); + + return classpathLoader(runtimeClasspath, getOrCreateToolingLoader(project), project); + } + + private static synchronized ClassLoader getOrCreateToolingLoader(Project project) { + if (TOOLING_LOADER == null) { + project.getLogger().info("creating classpath for auto-tooling"); + FileCollection toolingRuntime = project.getConfigurations().getByName("toolingRuntime"); + TOOLING_LOADER = + classpathLoader(toolingRuntime, ClassLoader.getPlatformClassLoader(), project); + } + return TOOLING_LOADER; + } + + private static ClassLoader classpathLoader( + FileCollection classpath, ClassLoader parent, Project project) { + URL[] urls = + StreamSupport.stream(classpath.spliterator(), false) + .map( + file -> { + project.getLogger().info("--" + file); + try { + return file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }) + .toArray(URL[]::new); + return new URLClassLoader(urls, parent); + } + + /** + * Configure a muzzle task to pass or fail a given version. + * + * @param versionArtifact version to assert against. + * @param instrumentationProject instrumentation being asserted against. + * @param runAfter Task which runs before the new muzzle task. + * @return The created muzzle task. + */ + private static TaskProvider addMuzzleTask( + MuzzleDirective muzzleDirective, + Artifact versionArtifact, + Project instrumentationProject, + TaskProvider runAfter) { + final String taskName; + if (muzzleDirective.getCoreJdk().get()) { + taskName = "muzzle-Assert" + muzzleDirective; + } else { + StringBuilder sb = new StringBuilder("muzzle-Assert"); + if (muzzleDirective.getAssertPass().isPresent()) { + sb.append("Pass"); + } else { + sb.append("Fail"); + } + sb.append('-') + .append(versionArtifact.getGroupId()) + .append('-') + .append(versionArtifact.getArtifactId()) + .append('-') + .append(versionArtifact.getVersion()); + if (!muzzleDirective.getName().get().isEmpty()) { + sb.append(muzzleDirective.getNameSlug()); + } + taskName = sb.toString(); + } + Configuration config = instrumentationProject.getConfigurations().create(taskName); + + if (!muzzleDirective.getCoreJdk().get()) { + ModuleDependency dep = + (ModuleDependency) + instrumentationProject + .getDependencies() + .create( + versionArtifact.getGroupId() + + ':' + + versionArtifact.getArtifactId() + + ':' + + versionArtifact.getVersion()); + dep.setTransitive(true); + // The following optional transitive dependencies are brought in by some legacy module such as + // log4j 1.x but are no + // longer bundled with the JVM and have to be excluded for the muzzle tests to be able to run. + exclude(dep, "com.sun.jdmk", "jmxtools"); + exclude(dep, "com.sun.jmx", "jmxri"); + + config.getDependencies().add(dep); + } + for (String additionalDependency : muzzleDirective.getAdditionalDependencies().get()) { + if (countColons(additionalDependency) < 2) { + // Dependency definition without version, use the artifact's version. + additionalDependency = additionalDependency + ':' + versionArtifact.getVersion(); + } + ModuleDependency dep = + (ModuleDependency) instrumentationProject.getDependencies().create(additionalDependency); + dep.setTransitive(true); + config.getDependencies().add(dep); + } + + TaskProvider muzzleTask = + instrumentationProject + .getTasks() + .register( + taskName, + task -> { + task.dependsOn( + instrumentationProject.getConfigurations().named("runtimeClasspath")); + task.doLast( + unused -> { + ClassLoader instrumentationCL = + createInstrumentationClassloader(instrumentationProject); + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + ClassLoader bogusLoader = + new SecureClassLoader() { + @Override + public String toString() { + return "bogus"; + } + }; + Thread.currentThread().setContextClassLoader(bogusLoader); + ClassLoader userCL = + createClassLoaderForTask(instrumentationProject, taskName); + try { + // find all instrumenters, get muzzle, and assert + Method assertionMethod = + instrumentationCL + .loadClass( + "io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil") + .getMethod( + "assertInstrumentationMuzzled", + ClassLoader.class, + ClassLoader.class, + boolean.class); + assertionMethod.invoke( + null, + instrumentationCL, + userCL, + muzzleDirective.getAssertPass().get()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + Thread.currentThread().setContextClassLoader(ccl); + } + + for (Thread thread : Thread.getAllStackTraces().keySet()) { + if (thread.getContextClassLoader() == bogusLoader + || thread.getContextClassLoader() == instrumentationCL + || thread.getContextClassLoader() == userCL) { + throw new GradleException( + "Task " + + taskName + + " has spawned a thread: " + + thread + + " with classloader " + + thread.getContextClassLoader() + + ". This will prevent GC of dynamic muzzle classes. Aborting muzzle run."); + } + } + }); + }); + runAfter.configure(task -> task.finalizedBy(muzzleTask)); + return muzzleTask; + } + + /** Create a classloader with dependencies for a single muzzle task. */ + private static ClassLoader createClassLoaderForTask(Project project, String muzzleTaskName) { + ConfigurableFileCollection userUrls = project.getObjects().fileCollection(); + project.getLogger().info("Creating task classpath"); + userUrls.from( + project + .getConfigurations() + .getByName(muzzleTaskName) + .getResolvedConfiguration() + .getFiles()); + return classpathLoader( + userUrls.plus(project.getConfigurations().getByName("bootstrapRuntime")), + ClassLoader.getPlatformClassLoader(), + project); + } + + /** Convert a muzzle directive to a list of artifacts */ + private static Set muzzleDirectiveToArtifacts( + Project instrumentationProject, + MuzzleDirective muzzleDirective, + RepositorySystem system, + RepositorySystemSession session) { + Artifact directiveArtifact = + new DefaultArtifact( + muzzleDirective.getGroup().get(), + muzzleDirective.getModule().get(), + "jar", + muzzleDirective.getVersions().get()); + + VersionRangeRequest rangeRequest = new VersionRangeRequest(); + rangeRequest.setRepositories(getProjectRepositories(instrumentationProject)); + rangeRequest.setArtifact(directiveArtifact); + final VersionRangeResult rangeResult; + try { + rangeResult = system.resolveVersionRange(session, rangeRequest); + } catch (VersionRangeResolutionException e) { + throw new RuntimeException(e); + } + + Set allVersionArtifacts = + filterVersions(rangeResult, muzzleDirective.getNormalizedSkipVersions()).stream() + .map( + version -> + new DefaultArtifact( + muzzleDirective.getGroup().get(), + muzzleDirective.getModule().get(), + "jar", + version)) + .collect(Collectors.toSet()); + + if (allVersionArtifacts.isEmpty()) { + throw new GradleException("No muzzle artifacts found for " + muzzleDirective); + } + + return allVersionArtifacts; + } + + private static List getProjectRepositories(Project project) { + return project.getRepositories().stream() + .filter(MavenArtifactRepository.class::isInstance) + .map( + repo -> { + MavenArtifactRepository mavenRepo = (MavenArtifactRepository) repo; + return new RemoteRepository.Builder( + mavenRepo.getName(), "default", mavenRepo.getUrl().toString()) + .build(); + }) + .collect(Collectors.toList()); + } + + /** Create a list of muzzle directives which assert the opposite of the given MuzzleDirective. */ + private static Set inverseOf( + Project instrumentationProject, + MuzzleDirective muzzleDirective, + RepositorySystem system, + RepositorySystemSession session) { + Set inverseDirectives = new HashSet<>(); + + Artifact allVersionsArtifact = + new DefaultArtifact( + muzzleDirective.getGroup().get(), muzzleDirective.getModule().get(), "jar", "[,)"); + Artifact directiveArtifact = + new DefaultArtifact( + muzzleDirective.getGroup().get(), + muzzleDirective.getModule().get(), + "jar", + muzzleDirective.getVersions().get()); + + List repos = getProjectRepositories(instrumentationProject); + VersionRangeRequest allRangeRequest = new VersionRangeRequest(); + allRangeRequest.setRepositories(repos); + allRangeRequest.setArtifact(allVersionsArtifact); + final VersionRangeResult allRangeResult; + try { + allRangeResult = system.resolveVersionRange(session, allRangeRequest); + } catch (VersionRangeResolutionException e) { + throw new RuntimeException(e); + } + + VersionRangeRequest rangeRequest = new VersionRangeRequest(); + rangeRequest.setRepositories(repos); + rangeRequest.setArtifact(directiveArtifact); + final VersionRangeResult rangeResult; + try { + rangeResult = system.resolveVersionRange(session, rangeRequest); + } catch (VersionRangeResolutionException e) { + throw new RuntimeException(e); + } + + allRangeResult.getVersions().removeAll(rangeResult.getVersions()); + + for (String version : + filterVersions(allRangeResult, muzzleDirective.getNormalizedSkipVersions())) { + MuzzleDirective inverseDirective = + instrumentationProject.getObjects().newInstance(MuzzleDirective.class); + inverseDirective.getGroup().set(muzzleDirective.getGroup()); + inverseDirective.getModule().set(muzzleDirective.getModule()); + inverseDirective.getVersions().set(version); + inverseDirective.getAssertPass().set(!muzzleDirective.getAssertPass().get()); + inverseDirectives.add(inverseDirective); + } + + return inverseDirectives; + } + + private static Set filterVersions(VersionRangeResult range, Set skipVersions) { + Set result = new HashSet<>(); + + AcceptableVersions predicate = new AcceptableVersions(skipVersions); + if (predicate.test(range.getLowestVersion())) { + result.add(range.getLowestVersion().toString()); + } + if (predicate.test(range.getHighestVersion())) { + result.add(range.getHighestVersion().toString()); + } + + List copy = new ArrayList<>(range.getVersions()); + Collections.shuffle(copy); + for (Version version : copy) { + if (result.size() >= RANGE_COUNT_LIMIT) { + break; + } + if (predicate.test(version)) { + result.add(version.toString()); + } + } + + return result; + } + + /** Create muzzle's repository system */ + private static RepositorySystem newRepositorySystem() { + DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator(); + locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); + locator.addService(TransporterFactory.class, HttpTransporterFactory.class); + + return locator.getService(RepositorySystem.class); + } + + /** Create muzzle's repository system session */ + private static RepositorySystemSession newRepositorySystemSession( + RepositorySystem system, Project project) { + DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); + File muzzleRepo = project.file("build/muzzleRepo"); + LocalRepository localRepo = new LocalRepository(muzzleRepo); + session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo)); + return session; + } + + private static int countColons(String s) { + int count = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == ':') { + count++; + } + } + return count; + } + + private static void exclude(ModuleDependency dependency, String group, String module) { + Map exclusions = new HashMap<>(); + exclusions.put("group", group); + exclusions.put("module", module); + dependency.exclude(exclusions); + } +} diff --git a/buildSrc/src/test/java/MuzzlePluginTest.java b/buildSrc/src/test/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePluginTest.java similarity index 73% rename from buildSrc/src/test/java/MuzzlePluginTest.java rename to buildSrc/src/test/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePluginTest.java index bc5643817027..2f2a6e4c945e 100644 --- a/buildSrc/src/test/java/MuzzlePluginTest.java +++ b/buildSrc/src/test/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePluginTest.java @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.instrumentation.gradle.muzzle; + import static org.assertj.core.api.Assertions.assertThat; import java.util.Collections; -import org.eclipse.aether.resolution.VersionRangeRequest; -import org.eclipse.aether.resolution.VersionRangeResult; import org.eclipse.aether.version.Version; import org.junit.jupiter.api.Test; @@ -15,9 +15,7 @@ class MuzzlePluginTest { @Test void rangeRequest() { - MuzzlePlugin.AcceptableVersions predicate = - new MuzzlePlugin.AcceptableVersions( - new VersionRangeResult(new VersionRangeRequest()), Collections.emptyList()); + AcceptableVersions predicate = new AcceptableVersions(Collections.emptyList()); assertThat(predicate.test(new TestVersion("10.1.0-rc2+19-8e20bb26"))).isFalse(); assertThat(predicate.test(new TestVersion("2.4.5.BUILD-SNAPSHOT"))).isFalse();