Skip to content

Commit aefb09c

Browse files
committed
Support transitive archrules
archRules variants now include code and dependencies form the main source set the runner plugin now resolves archrules variants in a reified configuration for each source set, rather than using artifactViews fixes #24
1 parent c405eec commit aefb09c

File tree

8 files changed

+615
-88
lines changed

8 files changed

+615
-88
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ When authoring rules about the usage of your own library code, it is recommended
6565
same project as the library code. The ArchRules plugin will publish the rules in a separate Jar, and the Runner plugin
6666
will select that jar for running rules, but these rule classes will not end up in the runtime classpath.
6767

68+
You may also create a "standalone" rules library which contains only `archRules` sources, and not `main` sources. These are useful when you want to write rules a bout libraries you do not control. They can be applied to downstream project's `archRules` configuration so that the Runner plugin will run these rules against any source set.
69+
70+
#### Dependencies
71+
72+
You may declare dependencies on the `archRulesImplementation` configuration. This is useful for 2 use cases:
73+
1) using a dependency as helper for your rule code
74+
2) transitively depending on a standalone rules library so that its rules are run in any project which runs the current project's rules
75+
6876
### Testing Rules
6977

7078
The ArchRules Library plugin creates a test suite called `archRulesTest`. You can write unit tests for your rules in the `archRulesTest` source set.

nebula-archrules-gradle-plugin/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ testing {
3939
suites{
4040
named<JvmTestSuite>("test"){
4141
useJUnitJupiter()
42+
targets.all {
43+
testTask.configure {
44+
maxParallelForks = 2
45+
}
46+
}
4247
}
4348
}
4449
}

nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,43 @@ class ArchrulesLibraryPlugin : Plugin<Project> {
2828
project.pluginManager.withPlugin("java") {
2929
val javaExt = project.extensions.getByType<JavaPluginExtension>()
3030
val archRulesSourceSet = javaExt.sourceSets.create("archRules")
31+
project.configurations.named(archRulesSourceSet.implementationConfigurationName).configure {
32+
extendsFrom(project.configurations.getByName(javaExt.sourceSets.getByName("main").implementationConfigurationName))
33+
}
34+
project.configurations.named(archRulesSourceSet.runtimeClasspathConfigurationName).configure {
35+
attributes {
36+
attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES))
37+
}
38+
}
39+
project.configurations.named(archRulesSourceSet.compileClasspathConfigurationName).configure {
40+
attributes {
41+
attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES))
42+
}
43+
}
3144
project.dependencies.add(
3245
archRulesSourceSet.implementationConfigurationName,
3346
"com.netflix.nebula:nebula-archrules-core:$version"
3447
)
35-
val generateServicesTask = project.tasks.register<GenerateServicesRegistryTask>("generateServicesRegistry"){
36-
archRuleServicesFile.set(
37-
project.layout.buildDirectory.file(
38-
"resources/archRules/META-INF/services/com.netflix.nebula.archrules.core.ArchRulesService"
39-
).map { it.asFile }
40-
)
41-
ruleSourceClasses.setFrom(archRulesSourceSet.output)
42-
dependsOn(archRulesSourceSet.classesTaskName)
43-
}
44-
project.tasks.named(archRulesSourceSet.classesTaskName){
48+
val generateServicesTask =
49+
project.tasks.register<GenerateServicesRegistryTask>("generateServicesRegistry") {
50+
archRuleServicesFile.set(
51+
project.layout.buildDirectory.file(
52+
"resources/archRules/META-INF/services/com.netflix.nebula.archrules.core.ArchRulesService"
53+
).map { it.asFile }
54+
)
55+
ruleSourceClasses.setFrom(archRulesSourceSet.output)
56+
dependsOn(archRulesSourceSet.classesTaskName)
57+
}
58+
project.tasks.named(archRulesSourceSet.classesTaskName) {
4559
finalizedBy(generateServicesTask)
4660
}
47-
project.tasks.named("processArchRulesResources"){
61+
project.tasks.named("processArchRulesResources") {
4862
finalizedBy(generateServicesTask)
4963
}
5064
val jarTask = project.tasks.register<Jar>("archRulesJar") {
5165
description = "Assembles a jar archive containing the classes of the arch rules."
5266
group = "build"
53-
from(archRulesSourceSet.output)
67+
from(archRulesSourceSet.output, javaExt.sourceSets.getByName("main").output)
5468
archiveClassifier.set("arch-rules")
5569
dependsOn(generateServicesTask)
5670
}
@@ -61,19 +75,33 @@ class ArchrulesLibraryPlugin : Plugin<Project> {
6175
register("archRulesTest", JvmTestSuite::class.java) {
6276
useJUnitJupiter()
6377
dependencies {
64-
implementation(project())
65-
implementation(archRulesSourceSet.runtimeClasspath)
78+
implementation(archRulesSourceSet.output)
6679
implementation("com.netflix.nebula:nebula-archrules-core:$version")
6780
}
6881
javaExt.sourceSets.named("archRulesTest").configure {
69-
project.tasks.named(compileJavaTaskName){
82+
project.tasks.named(compileJavaTaskName) {
7083
dependsOn(generateServicesTask)
7184
}
7285
project.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
7386
project.tasks.named(getCompileTaskName("kotlin")) {
7487
dependsOn(generateServicesTask)
7588
}
7689
}
90+
project.configurations.named(implementationConfigurationName) {
91+
extendsFrom(project.configurations.getByName(javaExt.sourceSets.getByName("main").implementationConfigurationName))
92+
}
93+
project.configurations.named(runtimeClasspathConfigurationName).configure {
94+
extendsFrom(project.configurations.getByName(archRulesSourceSet.runtimeClasspathConfigurationName))
95+
attributes {
96+
attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES))
97+
}
98+
}
99+
project.configurations.named(compileClasspathConfigurationName).configure {
100+
extendsFrom(project.configurations.getByName(archRulesSourceSet.compileClasspathConfigurationName))
101+
attributes {
102+
attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES))
103+
}
104+
}
77105
}
78106
}
79107
}

nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,35 +60,16 @@ class ArchrulesRunnerPlugin : Plugin<Project> {
6060

6161
fun Project.configureCheckTaskForSourceSet(sourceSet: SourceSet) {
6262
val archRulesReportDir = project.layout.buildDirectory.dir("reports/archrules")
63+
val sourceSetArchRulesRuntime = configurations.resolvable(sourceSet.name + "ArchRulesRuntime") {
64+
extendsFrom(configurations.getByName(sourceSet.runtimeClasspathConfigurationName))
65+
attributes {
66+
attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named<ArchRuleAttribute>(ARCH_RULES))
67+
}
68+
}
6369
tasks.register<CheckRulesTask>("checkArchRules" + sourceSet.name.capitalized()) {
6470
description = "Checks ArchRules on ${sourceSet.name}"
65-
val artifactView =
66-
project.configurations.getByName(sourceSet.runtimeClasspathConfigurationName)
67-
.incoming
68-
.artifactView {
69-
withVariantReselection()
70-
attributes {
71-
attribute(
72-
ArchRuleAttribute.ARCH_RULES_ATTRIBUTE,
73-
project.objects.named<ArchRuleAttribute>(ARCH_RULES)
74-
)
75-
attribute(
76-
Usage.USAGE_ATTRIBUTE,
77-
project.objects.named<Usage>(Usage.JAVA_RUNTIME)
78-
)
79-
attribute(
80-
Category.CATEGORY_ATTRIBUTE,
81-
project.objects.named<Category>(Category.LIBRARY)
82-
)
83-
attribute(
84-
Bundling.BUNDLING_ATTRIBUTE,
85-
project.objects.named(Bundling.EXTERNAL)
86-
)
87-
}
88-
lenient(false)
89-
}
9071
rulesClasspath.setFrom(
91-
artifactView.files,
72+
sourceSetArchRulesRuntime,
9273
project.configurations.getByName("archRules")
9374
)
9475
dataFile.set(archRulesReportDir.map {

nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.netflix.nebula.archrules.gradle
22

33
import nebula.test.dsl.*
44
import nebula.test.dsl.TestKitAssertions.assertThat
5+
import net.javacrumbs.jsonunit.assertj.JsonAssertions.json
56
import net.javacrumbs.jsonunit.assertj.assertThatJson
67
import org.gradle.testfixtures.ProjectBuilder
78
import org.gradle.testkit.runner.TaskOutcome
@@ -77,7 +78,8 @@ class ArchrulesLibraryPluginTest {
7778
.hasNoMutableStateWarnings()
7879
.hasNoDeprecationWarnings()
7980

80-
val serviceFile = projectDir.resolve("build/resources/archRules/META-INF/services/com.netflix.nebula.archrules.core.ArchRulesService")
81+
val serviceFile =
82+
projectDir.resolve("build/resources/archRules/META-INF/services/com.netflix.nebula.archrules.core.ArchRulesService")
8183
assertThat(serviceFile)
8284
.`as`("service file is created")
8385
.exists()
@@ -117,7 +119,7 @@ class ArchrulesLibraryPluginTest {
117119
}
118120

119121
@Test
120-
fun `plugin produces proper outgoingVariants`() {
122+
fun `main dependencies are included in archRules`() {
121123
val runner = testProject(projectDir) {
122124
properties {
123125
gradleCache(true)
@@ -138,6 +140,7 @@ class ArchrulesLibraryPluginTest {
138140
mavenCentral()
139141
}
140142
declareMavenPublication()
143+
dependencies("""implementation("com.google.guava:guava:33.5.0-jre")""")
141144
src {
142145
main {
143146
exampleLibraryClass()
@@ -149,15 +152,45 @@ class ArchrulesLibraryPluginTest {
149152
}
150153
}
151154

152-
val result = runner.run("outgoingVariants", "-Pversion=0.0.1")
153-
assertThat(result.output)
154-
.contains("Variant archRulesRuntimeElements")
155-
.contains("Variant testResultsElementsForArchRulesTest")
156-
.doesNotContain("Variant archRulesApiElements")
155+
val result = runner.run(
156+
"build",
157+
"archRulesJar",
158+
"generateMetadataFileForMavenPublication", // to test publication metadata without actually publishing,
159+
"-Pversion=0.0.1"
160+
)
161+
162+
assertThat(result)
163+
.hasNoMutableStateWarnings()
164+
.hasNoDeprecationWarnings()
165+
166+
val moduleMetadata = projectDir.resolve("build/publications/maven/module.json")
167+
assertThat(moduleMetadata)
168+
.`as`("Gradle Module Metadata is created")
169+
.exists()
170+
171+
val moduleMetadataJson = moduleMetadata.readText()
172+
173+
assertThatJson(moduleMetadataJson)
174+
.inPath("$.variants[?(@.name=='archRulesRuntimeElements')].dependencies[1]")
175+
.isArray
176+
.contains(
177+
json(
178+
//language=json
179+
"""
180+
{
181+
"group": "com.google.guava",
182+
"module": "guava",
183+
"version": {
184+
"requires": "33.5.0-jre"
185+
}
186+
}
187+
"""
188+
)
189+
)
157190
}
158191

159192
@Test
160-
fun `plugin sets up tests for rules`() {
193+
fun `plugin produces proper outgoingVariants`() {
161194
val runner = testProject(projectDir) {
162195
properties {
163196
gradleCache(true)
@@ -167,48 +200,54 @@ class ArchrulesLibraryPluginTest {
167200
}
168201
rootProject {
169202
group("com.example")
203+
// a library that contains production code and rules to go along with it
170204
plugins {
171205
id("java-library")
172206
id("com.netflix.nebula.archrules.library")
207+
id("maven-publish")
173208
}
174209
repositories {
175210
maven("https://netflixoss.jfrog.io/artifactory/gradle-plugins")
176211
mavenCentral()
177212
}
213+
declareMavenPublication()
178214
src {
179215
main {
180216
exampleLibraryClass()
181217
}
182218
sourceSet("archRules") {
183219
exampleDeprecatedArchRule()
184220
}
185-
sourceSet("archRulesTest") {
186-
exampleTestForArchRule()
187-
}
188221
}
189222
}
190223
}
191224

192-
val result = runner.run("check")
193-
194-
assertThat(result.task(":archRulesTest"))
195-
.`as`("archRules test task runs")
196-
.hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE)
197-
assertThat(result)
198-
.hasNoMutableStateWarnings()
199-
.hasNoDeprecationWarnings()
225+
val result = runner.run("outgoingVariants", "-Pversion=0.0.1")
226+
assertThat(result.output)
227+
.contains("Variant archRulesRuntimeElements")
228+
.contains("Variant testResultsElementsForArchRulesTest")
229+
.doesNotContain("Variant archRulesApiElements")
230+
.contains("- com.netflix.nebula.archrules = arch-rules")
231+
val indexOfArchRulesVariant = result.output.indexOf("- com.netflix.nebula.archrules = arch-rules")
232+
assertThat(indexOfArchRulesVariant)
233+
.`as`("archRulesRuntimeElements has arch-rules variant")
234+
.isGreaterThan(0)
235+
.isBetween(
236+
result.output.indexOf("Variant archRulesRuntimeElements"),
237+
result.output.substring(indexOfArchRulesVariant).indexOf("Variant") + indexOfArchRulesVariant
238+
)
200239
}
201240

202241
@Test
203-
fun `plugin sets up tests for rules with dependencies`() {
242+
fun `plugin sets up tests for rules`() {
204243
val runner = testProject(projectDir) {
205244
properties {
206245
gradleCache(true)
207246
}
208247
settings {
209248
name("library-with-rules")
210249
}
211-
subProject("rules") {
250+
rootProject {
212251
group("com.example")
213252
plugins {
214253
id("java-library")
@@ -218,43 +257,23 @@ class ArchrulesLibraryPluginTest {
218257
maven("https://netflixoss.jfrog.io/artifactory/gradle-plugins")
219258
mavenCentral()
220259
}
221-
dependencies(
222-
"""archRulesImplementation(project(":helper"))""",
223-
"""archRulesTestImplementation("org.jspecify:jspecify:1.0.0")"""
224-
)
225260
src {
226261
main {
227262
exampleLibraryClass()
228263
}
229264
sourceSet("archRules") {
230-
exampleNullabilityArchRule() // rules that uses a helper from a dependency
265+
exampleDeprecatedArchRule()
231266
}
232267
sourceSet("archRulesTest") {
233-
exampleTestForNullabilityArchRule()
234-
}
235-
}
236-
}
237-
subProject("helper") {
238-
group("com.example")
239-
plugins {
240-
id("java-library")
241-
}
242-
repositories {
243-
maven("https://netflixoss.jfrog.io/artifactory/gradle-plugins")
244-
mavenCentral()
245-
}
246-
dependencies("""implementation("com.tngtech.archunit:archunit:1.4.1")""")
247-
src {
248-
main {
249-
exampleHelperClass()
268+
exampleTestForArchRule()
250269
}
251270
}
252271
}
253272
}
254273

255-
val result = runner.run("check", "--stacktrace")
274+
val result = runner.run("check")
256275

257-
assertThat(result.task(":rules:archRulesTest"))
276+
assertThat(result.task(":archRulesTest"))
258277
.`as`("archRules test task runs")
259278
.hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE)
260279
assertThat(result)

nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ internal class IntegrationTest {
4444
id("java")
4545
id("com.netflix.nebula.archrules.runner")
4646
}
47+
repositories {
48+
maven("https://netflixoss.jfrog.io/artifactory/gradle-plugins")
49+
mavenCentral()
50+
}
4751
dependencies(
4852
"""implementation(project(":library-with-rules"))"""
4953
)

nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/TestKitDslExtensions.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,16 @@ import org.junit.jupiter.api.Test;
167167
import org.junit.jupiter.api.Assertions;
168168
169169
public class LibraryArchRulesTest {
170+
@Deprecated
171+
static void deprecatedMethod(){
172+
}
170173
static class PassingCode {
171174
public void aMethod() {
172-
LibraryClass.newApi();
173175
}
174176
}
175177
static class FailingCode {
176178
public void aMethod() {
177-
LibraryClass.deprecatedApi();
179+
deprecatedMethod();
178180
}
179181
}
180182

0 commit comments

Comments
 (0)