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. passDirectives. size() == 0 ) {
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' ) {
@@ -47,15 +76,52 @@ class MuzzlePlugin implements Plugin<Project> {
4776 project. tasks. compileMuzzle. dependsOn(project. tasks. compileScala)
4877 }
4978 }
50-
79+ // TODO: consider:
80+ // project.tasks.withType(ScalaCompile) { Task scalaTask ->
81+ // project.tasks.compileMuzzle.dependsOn(scalaTask)
82+ // }
5183 project. tasks. muzzle. dependsOn(project. tasks. compileMuzzle)
5284 project. tasks. printReferences. dependsOn(project. tasks. compileMuzzle)
85+
86+ def hasRelevantTask = project. gradle. startParameter. taskNames. any { taskName ->
87+ // removing leading ':' if present
88+ taskName = taskName. replaceFirst(' ^:' , ' ' )
89+ String muzzleTaskPath = project. path. replaceFirst(' ^:' , ' ' )
90+ return ' muzzle' == taskName || " ${ muzzleTaskPath} :muzzle" == taskName
91+ }
92+ if (! hasRelevantTask) {
93+ // Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly run.
94+ return
95+ }
96+
97+ final RepositorySystem system = newRepositorySystem()
98+ final RepositorySystemSession session = newRepositorySystemSession(system)
99+
100+ project. afterEvaluate {
101+ // use runAfter to set up task finalizers in version order
102+ Task runAfter = project. tasks. muzzle
103+
104+ for (MuzzleDirective pass : project. muzzle. passDirectives) {
105+ project. getLogger(). info(" configured pass directive: ${ pass.group} :${ pass.module} :${ pass.versions} " )
106+
107+ muzzleDirectiveToArtifacts(pass, system, session). collect() { Artifact singleVersion ->
108+ runAfter = addMuzzleTask(true , singleVersion, project, runAfter, bootstrapProject, toolingProject)
109+ }
110+ }
111+ for (MuzzleDirective fail : project. muzzle. failDirectives) {
112+ project. getLogger(). info(" configured fail directive: ${ fail.group} :${ fail.module} :${ fail.versions} " )
113+
114+ muzzleDirectiveToArtifacts(fail , system, session). collect() { Artifact singleVersion ->
115+ runAfter = addMuzzleTask(false , singleVersion, project, runAfter, bootstrapProject, toolingProject)
116+ }
117+ }
118+ }
53119 }
54120
55121 /**
56122 * Create a classloader with core agent classes and project instrumentation on the classpath.
57123 */
58- private ClassLoader createDDClassloader (Project project , Project toolingProject ) {
124+ private static ClassLoader createDDClassloader (Project project , Project toolingProject ) {
59125 project. getLogger(). info(" Creating dd classpath for: " + project. getName())
60126 Set<URL > ddUrls = new HashSet<> ()
61127 for (File f : toolingProject. sourceSets. main. runtimeClasspath. getFiles()) {
@@ -71,11 +137,11 @@ class MuzzlePlugin implements Plugin<Project> {
71137 }
72138
73139 /**
74- * Create a classloader with user/library classes on the classpath.
140+ * Create a classloader with all compile-time dependencies on the classpath
75141 */
76- private ClassLoader createUserClassLoader (Project project , Project bootstrapProject ) {
142+ private static ClassLoader createCompileDepsClassLoader (Project project , Project bootstrapProject ) {
77143 List<URL > userUrls = new ArrayList<> ()
78- project. getLogger(). info(" Creating user classpath for: " + project. getName())
144+ project. getLogger(). info(" Creating compile-time classpath for: " + project. getName())
79145 for (File f : project. configurations. compileOnly. getFiles()) {
80146 project. getLogger(). info(' --' + f)
81147 userUrls. add(f. toURI(). toURL())
@@ -86,4 +152,157 @@ class MuzzlePlugin implements Plugin<Project> {
86152 }
87153 return new URLClassLoader (userUrls. toArray(new URL [0 ]), (ClassLoader ) null )
88154 }
155+
156+ /**
157+ * Create a classloader with dependencies for a single muzzle task.
158+ */
159+ private static ClassLoader createClassLoaderForTask (Project project , Project bootstrapProject , String muzzleTaskName ) {
160+ final List<URL > userUrls = new ArrayList<> ()
161+
162+ project. getLogger(). info(" Creating task classpath" )
163+ project. configurations. getByName(muzzleTaskName). resolvedConfiguration. files. each { File jarFile ->
164+ project. getLogger(). info(" -- Added to instrumentation classpath: $jarFile " )
165+ userUrls. add(jarFile. toURI(). toURL())
166+ }
167+
168+ for (File f : bootstrapProject. sourceSets. main. runtimeClasspath. getFiles()) {
169+ project. getLogger(). info(" -- Added to instrumentation bootstrap classpath: $f " )
170+ userUrls. add(f. toURI(). toURL())
171+ }
172+ return new URLClassLoader (userUrls. toArray(new URL [0 ]), (ClassLoader ) null )
173+ }
174+
175+ /**
176+ * Convert a muzzle directive to a list of artifacts
177+ */
178+ private static List<Artifact > muzzleDirectiveToArtifacts (MuzzleDirective muzzleDirective , RepositorySystem system , RepositorySystemSession session ) {
179+ final Artifact directiveArtifact = new DefaultArtifact (muzzleDirective. group, muzzleDirective. module, " jar" , muzzleDirective. versions)
180+
181+ final VersionRangeRequest rangeRequest = new VersionRangeRequest ()
182+ rangeRequest. setRepositories(MUZZLE_REPOS )
183+ rangeRequest. setArtifact(directiveArtifact)
184+ final VersionRangeResult rangeResult = system. resolveVersionRange(session, rangeRequest)
185+
186+ final List<Artifact > allVersionArtifacts = filterVersion(rangeResult. versions). collect { version ->
187+ new DefaultArtifact (muzzleDirective. group, muzzleDirective. module, " jar" , version. toString())
188+ }
189+
190+ return allVersionArtifacts
191+ }
192+
193+ /**
194+ * Configure a muzzle task to pass or fail a given version.
195+ *
196+ * @param assertPass If true, assert that muzzle validation passes
197+ * @param versionArtifact version to assert against.
198+ * @param instrumentationProject instrumentation being asserted against.
199+ * @param runAfter Task which runs before the new muzzle task.
200+ * @param bootstrapProject Agent bootstrap project.
201+ * @param toolingProject Agent tooling project.
202+ *
203+ * @return The created muzzle task.
204+ */
205+ private static Task addMuzzleTask (boolean assertPass , Artifact versionArtifact , Project instrumentationProject , Task runAfter , Project bootstrapProject , Project toolingProject ) {
206+ def taskName = " muzzle-Assert${ assertPass ? "Pass" : "Fail"} -$versionArtifact . groupId -$versionArtifact . artifactId -$versionArtifact . version "
207+ def config = instrumentationProject. configurations. create(taskName)
208+ config. dependencies. add(instrumentationProject. dependencies. create(" $versionArtifact . groupId :$versionArtifact . artifactId :$versionArtifact . version " ) {
209+ transitive = true
210+ })
211+
212+ def muzzleTask = instrumentationProject. task(taskName) {
213+ doLast {
214+ final ClassLoader userCL = createClassLoaderForTask(instrumentationProject, bootstrapProject, taskName)
215+ final ClassLoader agentCL = createDDClassloader(instrumentationProject, toolingProject)
216+ // find all instrumenters, get muzzle, and assert
217+ Method assertionMethod = agentCL. loadClass(' datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin' )
218+ .getMethod(' assertInstrumentationMuzzled' , ClassLoader . class, boolean . class)
219+ assertionMethod. invoke(null , userCL, assertPass)
220+ }
221+ }
222+ runAfter. finalizedBy(muzzleTask)
223+ return muzzleTask
224+ }
225+
226+ /**
227+ * Create muzzle's repository system
228+ */
229+ private static RepositorySystem newRepositorySystem () {
230+ DefaultServiceLocator locator = MavenRepositorySystemUtils . newServiceLocator()
231+ locator. addService(RepositoryConnectorFactory . class, BasicRepositoryConnectorFactory . class)
232+ locator. addService(TransporterFactory . class, HttpTransporterFactory . class)
233+
234+ return locator. getService(RepositorySystem . class)
235+ }
236+
237+ /**
238+ * Create muzzle's repository system session
239+ */
240+ private static RepositorySystemSession newRepositorySystemSession (RepositorySystem system ) {
241+ DefaultRepositorySystemSession session = MavenRepositorySystemUtils . newSession()
242+
243+ def tempDir = File . createTempDir()
244+ tempDir. deleteOnExit()
245+ LocalRepository localRepo = new LocalRepository (tempDir)
246+ session. setLocalRepositoryManager(system. newLocalRepositoryManager(session, localRepo))
247+
248+ return session
249+ }
250+
251+ /**
252+ * Filter out snapshot-type builds from versions list.
253+ */
254+ private static filterVersion (List<Version > list ) {
255+ list. removeIf {
256+ def version = it. toString(). toLowerCase()
257+ return version. contains(" rc" ) ||
258+ version. contains(" .cr" ) ||
259+ version. contains(" alpha" ) ||
260+ version. contains(" beta" ) ||
261+ version. contains(" -b" ) ||
262+ version. contains(" .m" ) ||
263+ version. contains(" -dev" ) ||
264+ version. contains(" public_draft" )
265+ }
266+ return list
267+ }
268+ }
269+
270+ // plugin extension classes
271+
272+ /**
273+ * A pass or fail directive for a single dependency.
274+ */
275+ class MuzzleDirective {
276+ String group
277+ String module
278+ String versions
279+ }
280+
281+ /**
282+ * Muzzle extension containing all pass and fail directives.
283+ */
284+ class MuzzleExtension {
285+ // TODO: merge pass and fail directives into single collection
286+ final List<MuzzleDirective > passDirectives
287+ final List<MuzzleDirective > failDirectives
288+ private final ObjectFactory objectFactory
289+
290+ @javax.inject.Inject
291+ MuzzleExtension (final ObjectFactory objectFactory ) {
292+ this . objectFactory = objectFactory
293+ passDirectives = new ArrayList<> ()
294+ failDirectives = new ArrayList<> ()
295+ }
296+
297+ void pass (Action<? super MuzzleDirective > action ) {
298+ final MuzzleDirective pass = objectFactory. newInstance(MuzzleDirective )
299+ action. execute(pass)
300+ passDirectives. add(pass)
301+ }
302+
303+ void fail (Action<? super MuzzleDirective > action ) {
304+ final MuzzleDirective fail = objectFactory. newInstance(MuzzleDirective )
305+ action. execute(fail )
306+ failDirectives. add(fail )
307+ }
89308}
0 commit comments