1+ import org.apache.maven.repository.internal.MavenRepositorySystemUtils
2+ import org.eclipse.aether.DefaultRepositorySystemSession
3+ import org.eclipse.aether.RepositorySystem
4+ import org.eclipse.aether.RepositorySystemSession
5+ import org.eclipse.aether.artifact.Artifact
6+ import org.eclipse.aether.artifact.DefaultArtifact
7+ import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory
8+ import org.eclipse.aether.impl.DefaultServiceLocator
9+ import org.eclipse.aether.repository.LocalRepository
10+ import org.eclipse.aether.repository.RemoteRepository
11+ import org.eclipse.aether.resolution.VersionRangeRequest
12+ import org.eclipse.aether.resolution.VersionRangeResult
13+ import org.eclipse.aether.spi.connector.RepositoryConnectorFactory
14+ import org.eclipse.aether.spi.connector.transport.TransporterFactory
15+ import org.eclipse.aether.transport.http.HttpTransporterFactory
16+ import org.eclipse.aether.version.Version
17+ import org.gradle.api.Action
118import org.gradle.api.Plugin
219import org.gradle.api.Project
20+ import org.gradle.api.Task
21+ import org.gradle.api.model.ObjectFactory
322
423import java.lang.reflect.Method
524
625/**
7- * muzzle task plugin which runs muzzle validation against an instrumentation's compile-time dependencies.
8- *
9- * <p />TODO: merge this with version scan
26+ * muzzle task plugin which runs muzzle validation against a range of dependencies.
1027 */
1128class MuzzlePlugin implements Plugin<Project > {
29+ /**
30+ * Remote repositories used to query version ranges and fetch dependencies
31+ */
32+ private static final List<RemoteRepository > MUZZLE_REPOS
33+ static {
34+ RemoteRepository central = new RemoteRepository.Builder (" central" , " default" , " http://central.maven.org/maven2/" ). build()
35+ MUZZLE_REPOS = new ArrayList<RemoteRepository > (Arrays . asList(central))
36+ }
37+
1238 @Override
1339 void apply (Project project ) {
1440 def bootstrapProject = project. rootProject. getChildProjects(). get(' dd-java-agent' ). getChildProjects(). get(' agent-bootstrap' )
1541 def toolingProject = project. rootProject. getChildProjects(). get(' dd-java-agent' ). getChildProjects(). get(' agent-tooling' )
16- project. extensions. create(" muzzle" , MuzzleExtension )
17- def compileMuzzle = project. task(' compileMuzzle' ) {
18- // not adding user and group to hide this from `gradle tasks`
19- }
42+ project. extensions. create(" muzzle" , MuzzleExtension , project. objects)
43+
44+ // compileMuzzle compiles all projects required to run muzzle validation.
45+ // Not adding group and description to keep this task from showing in `gradle tasks`.
46+ def compileMuzzle = project. task(' compileMuzzle' )
2047 def muzzle = project. task(' muzzle' ) {
2148 group = ' Muzzle'
2249 description = " Run instrumentation muzzle on compile time dependencies"
2350 doLast {
24- final ClassLoader userCL = createUserClassLoader(project, bootstrapProject)
25- final ClassLoader agentCL = createDDClassloader(project, toolingProject)
26- // find all instrumenters, get muzzle, and assert
27- Method assertionMethod = agentCL. loadClass(' datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin' )
28- .getMethod(' assertInstrumentationNotMuzzled' , ClassLoader . class)
29- assertionMethod. invoke(null , userCL)
51+ if (! project. muzzle. directives. any { it. assertPass }) {
52+ project. getLogger(). info(' No muzzle pass directives configured. Asserting pass against instrumentation compile-time dependencies' )
53+ final ClassLoader userCL = createCompileDepsClassLoader(project, bootstrapProject)
54+ final ClassLoader agentCL = createDDClassloader(project, toolingProject)
55+ Method assertionMethod = agentCL. loadClass(' datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin' )
56+ .getMethod(' assertInstrumentationMuzzled' , ClassLoader . class, boolean . class)
57+ assertionMethod. invoke(null , userCL, true )
58+ }
3059 }
3160 }
3261 def printReferences = project. task(' printReferences' ) {
@@ -43,19 +72,52 @@ class MuzzlePlugin implements Plugin<Project> {
4372 project. tasks. compileMuzzle. dependsOn(toolingProject. tasks. compileJava)
4473 project. afterEvaluate {
4574 project. tasks. compileMuzzle. dependsOn(project. tasks. compileJava)
46- if (project. tasks. getNames(). contains(" compileScala" )) {
75+ if (project. tasks. getNames(). contains(' compileScala' )) {
4776 project. tasks. compileMuzzle. dependsOn(project. tasks. compileScala)
4877 }
4978 }
50-
5179 project. tasks. muzzle. dependsOn(project. tasks. compileMuzzle)
5280 project. tasks. printReferences. dependsOn(project. tasks. compileMuzzle)
81+
82+ def hasRelevantTask = project. gradle. startParameter. taskNames. any { taskName ->
83+ // removing leading ':' if present
84+ taskName = taskName. replaceFirst(' ^:' , ' ' )
85+ String muzzleTaskPath = project. path. replaceFirst(' ^:' , ' ' )
86+ return ' muzzle' == taskName || " ${ muzzleTaskPath} :muzzle" == taskName
87+ }
88+ if (! hasRelevantTask) {
89+ // Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly run.
90+ return
91+ }
92+
93+ final RepositorySystem system = newRepositorySystem()
94+ final RepositorySystemSession session = newRepositorySystemSession(system)
95+
96+ project. afterEvaluate {
97+ // use runAfter to set up task finalizers in version order
98+ Task runAfter = project. tasks. muzzle
99+
100+ for (MuzzleDirective muzzleDirective : project. muzzle. directives) {
101+ project. getLogger(). info(" configured ${ muzzleDirective.assertPass ? 'pass' : 'fail'} directive: ${ muzzleDirective.group} :${ muzzleDirective.module} :${ muzzleDirective.versions} " )
102+
103+ muzzleDirectiveToArtifacts(muzzleDirective, system, session). collect() { Artifact singleVersion ->
104+ runAfter = addMuzzleTask(muzzleDirective, singleVersion, project, runAfter, bootstrapProject, toolingProject)
105+ }
106+ if (muzzleDirective. assertInverse) {
107+ inverseOf(muzzleDirective, system, session). collect() { MuzzleDirective inverseDirective ->
108+ muzzleDirectiveToArtifacts(inverseDirective, system, session). collect() { Artifact singleVersion ->
109+ runAfter = addMuzzleTask(inverseDirective, singleVersion, project, runAfter, bootstrapProject, toolingProject)
110+ }
111+ }
112+ }
113+ }
114+ }
53115 }
54116
55117 /**
56118 * Create a classloader with core agent classes and project instrumentation on the classpath.
57119 */
58- private ClassLoader createDDClassloader (Project project , Project toolingProject ) {
120+ private static ClassLoader createDDClassloader (Project project , Project toolingProject ) {
59121 project. getLogger(). info(" Creating dd classpath for: " + project. getName())
60122 Set<URL > ddUrls = new HashSet<> ()
61123 for (File f : toolingProject. sourceSets. main. runtimeClasspath. getFiles()) {
@@ -71,11 +133,11 @@ class MuzzlePlugin implements Plugin<Project> {
71133 }
72134
73135 /**
74- * Create a classloader with user/library classes on the classpath.
136+ * Create a classloader with all compile-time dependencies on the classpath
75137 */
76- private ClassLoader createUserClassLoader (Project project , Project bootstrapProject ) {
138+ private static ClassLoader createCompileDepsClassLoader (Project project , Project bootstrapProject ) {
77139 List<URL > userUrls = new ArrayList<> ()
78- project. getLogger(). info(" Creating user classpath for: " + project. getName())
140+ project. getLogger(). info(" Creating compile-time classpath for: " + project. getName())
79141 for (File f : project. configurations. compileOnly. getFiles()) {
80142 project. getLogger(). info(' --' + f)
81143 userUrls. add(f. toURI(). toURL())
@@ -86,4 +148,201 @@ class MuzzlePlugin implements Plugin<Project> {
86148 }
87149 return new URLClassLoader (userUrls. toArray(new URL [0 ]), (ClassLoader ) null )
88150 }
151+
152+ /**
153+ * Create a classloader with dependencies for a single muzzle task.
154+ */
155+ private static ClassLoader createClassLoaderForTask (Project project , Project bootstrapProject , String muzzleTaskName ) {
156+ final List<URL > userUrls = new ArrayList<> ()
157+
158+ project. getLogger(). info(" Creating task classpath" )
159+ project. configurations. getByName(muzzleTaskName). resolvedConfiguration. files. each { File jarFile ->
160+ project. getLogger(). info(" -- Added to instrumentation classpath: $jarFile " )
161+ userUrls. add(jarFile. toURI(). toURL())
162+ }
163+
164+ for (File f : bootstrapProject. sourceSets. main. runtimeClasspath. getFiles()) {
165+ project. getLogger(). info(" -- Added to instrumentation bootstrap classpath: $f " )
166+ userUrls. add(f. toURI(). toURL())
167+ }
168+ return new URLClassLoader (userUrls. toArray(new URL [0 ]), (ClassLoader ) null )
169+ }
170+
171+ /**
172+ * Convert a muzzle directive to a list of artifacts
173+ */
174+ private static List<Artifact > muzzleDirectiveToArtifacts (MuzzleDirective muzzleDirective , RepositorySystem system , RepositorySystemSession session ) {
175+ final Artifact directiveArtifact = new DefaultArtifact (muzzleDirective. group, muzzleDirective. module, " jar" , muzzleDirective. versions)
176+
177+ final VersionRangeRequest rangeRequest = new VersionRangeRequest ()
178+ rangeRequest. setRepositories(MUZZLE_REPOS )
179+ rangeRequest. setArtifact(directiveArtifact)
180+ final VersionRangeResult rangeResult = system. resolveVersionRange(session, rangeRequest)
181+
182+ final List<Artifact > allVersionArtifacts = filterVersion(rangeResult. versions). collect { version ->
183+ new DefaultArtifact (muzzleDirective. group, muzzleDirective. module, " jar" , version. toString())
184+ }
185+
186+ return allVersionArtifacts
187+ }
188+
189+ /**
190+ * Create a list of muzzle directives which assert the opposite of the given MuzzleDirective.
191+ */
192+ private static List<MuzzleDirective > inverseOf (MuzzleDirective muzzleDirective , RepositorySystem system , RepositorySystemSession session ) {
193+ List<MuzzleDirective > inverseDirectives = new ArrayList<> ()
194+
195+ final Artifact allVerisonsArtifact = new DefaultArtifact (muzzleDirective. group, muzzleDirective. module, " jar" , " [,)" )
196+ final Artifact directiveArtifact = new DefaultArtifact (muzzleDirective. group, muzzleDirective. module, " jar" , muzzleDirective. versions)
197+
198+
199+ final VersionRangeRequest allRangeRequest = new VersionRangeRequest ()
200+ allRangeRequest. setRepositories(MUZZLE_REPOS )
201+ allRangeRequest. setArtifact(allVerisonsArtifact)
202+ final VersionRangeResult allRangeResult = system. resolveVersionRange(session, allRangeRequest)
203+
204+ final VersionRangeRequest rangeRequest = new VersionRangeRequest ()
205+ rangeRequest. setRepositories(MUZZLE_REPOS )
206+ rangeRequest. setArtifact(directiveArtifact)
207+ final VersionRangeResult rangeResult = system. resolveVersionRange(session, rangeRequest)
208+
209+ filterVersion(allRangeResult. versions). collect { version ->
210+ if (! rangeResult. versions. contains(version)) {
211+ final MuzzleDirective inverseDirective = new MuzzleDirective ()
212+ inverseDirective. group = muzzleDirective. group
213+ inverseDirective. module = muzzleDirective. module
214+ inverseDirective. versions = " $version "
215+ inverseDirective. assertPass = ! muzzleDirective. assertPass
216+ inverseDirectives. add(inverseDirective)
217+ }
218+ }
219+
220+ return inverseDirectives
221+ }
222+
223+
224+ /**
225+ * Configure a muzzle task to pass or fail a given version.
226+ *
227+ * @param assertPass If true, assert that muzzle validation passes
228+ * @param versionArtifact version to assert against.
229+ * @param instrumentationProject instrumentation being asserted against.
230+ * @param runAfter Task which runs before the new muzzle task.
231+ * @param bootstrapProject Agent bootstrap project.
232+ * @param toolingProject Agent tooling project.
233+ *
234+ * @return The created muzzle task.
235+ */
236+ private static Task addMuzzleTask (MuzzleDirective muzzleDirective , Artifact versionArtifact , Project instrumentationProject , Task runAfter , Project bootstrapProject , Project toolingProject ) {
237+ def taskName = " muzzle-Assert${ muzzleDirective.assertPass ? "Pass" : "Fail"} -$versionArtifact . groupId -$versionArtifact . artifactId -$versionArtifact . version "
238+ def config = instrumentationProject. configurations. create(taskName)
239+ config. dependencies. add(instrumentationProject. dependencies. create(" $versionArtifact . groupId :$versionArtifact . artifactId :$versionArtifact . version " ) {
240+ transitive = true
241+ })
242+ for (String additionalDependency : muzzleDirective. additionalDependencies) {
243+ config. dependencies. add(instrumentationProject. dependencies. create(additionalDependency) {
244+ transitive = true
245+ })
246+ }
247+
248+ def muzzleTask = instrumentationProject. task(taskName) {
249+ doLast {
250+ final ClassLoader userCL = createClassLoaderForTask(instrumentationProject, bootstrapProject, taskName)
251+ final ClassLoader agentCL = createDDClassloader(instrumentationProject, toolingProject)
252+ // find all instrumenters, get muzzle, and assert
253+ Method assertionMethod = agentCL. loadClass(' datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin' )
254+ .getMethod(' assertInstrumentationMuzzled' , ClassLoader . class, boolean . class)
255+ assertionMethod. invoke(null , userCL, muzzleDirective. assertPass)
256+ }
257+ }
258+ runAfter. finalizedBy(muzzleTask)
259+ return muzzleTask
260+ }
261+
262+ /**
263+ * Create muzzle's repository system
264+ */
265+ private static RepositorySystem newRepositorySystem () {
266+ DefaultServiceLocator locator = MavenRepositorySystemUtils . newServiceLocator()
267+ locator. addService(RepositoryConnectorFactory . class, BasicRepositoryConnectorFactory . class)
268+ locator. addService(TransporterFactory . class, HttpTransporterFactory . class)
269+
270+ return locator. getService(RepositorySystem . class)
271+ }
272+
273+ /**
274+ * Create muzzle's repository system session
275+ */
276+ private static RepositorySystemSession newRepositorySystemSession (RepositorySystem system ) {
277+ DefaultRepositorySystemSession session = MavenRepositorySystemUtils . newSession()
278+
279+ def tempDir = File . createTempDir()
280+ tempDir. deleteOnExit()
281+ LocalRepository localRepo = new LocalRepository (tempDir)
282+ session. setLocalRepositoryManager(system. newLocalRepositoryManager(session, localRepo))
283+
284+ return session
285+ }
286+
287+ /**
288+ * Filter out snapshot-type builds from versions list.
289+ */
290+ private static filterVersion (List<Version > list ) {
291+ list. removeIf {
292+ def version = it. toString(). toLowerCase()
293+ return version. contains(" rc" ) ||
294+ version. contains(" .cr" ) ||
295+ version. contains(" alpha" ) ||
296+ version. contains(" beta" ) ||
297+ version. contains(" -b" ) ||
298+ version. contains(" .m" ) ||
299+ version. contains(" -dev" ) ||
300+ version. contains(" public_draft" )
301+ }
302+ return list
303+ }
304+ }
305+
306+ // plugin extension classes
307+
308+ /**
309+ * A pass or fail directive for a single dependency.
310+ */
311+ class MuzzleDirective {
312+ String group
313+ String module
314+ String versions
315+ List<String > additionalDependencies = new ArrayList<> ()
316+ boolean assertPass
317+ boolean assertInverse = false
318+ void extraDependency (String compileString ) {
319+ additionalDependencies. add(compileString)
320+ }
321+ }
322+
323+ /**
324+ * Muzzle extension containing all pass and fail directives.
325+ */
326+ class MuzzleExtension {
327+ final List<MuzzleDirective > directives = new ArrayList<> ()
328+ private final ObjectFactory objectFactory
329+
330+ @javax.inject.Inject
331+ MuzzleExtension (final ObjectFactory objectFactory ) {
332+ this . objectFactory = objectFactory
333+ }
334+
335+ void pass (Action<? super MuzzleDirective > action ) {
336+ final MuzzleDirective pass = objectFactory. newInstance(MuzzleDirective )
337+ action. execute(pass)
338+ pass. assertPass = true
339+ directives. add(pass)
340+ }
341+
342+ void fail (Action<? super MuzzleDirective > action ) {
343+ final MuzzleDirective fail = objectFactory. newInstance(MuzzleDirective )
344+ action. execute(fail )
345+ fail . assertPass = false
346+ directives. add(fail )
347+ }
89348}
0 commit comments