Skip to content

Commit 37d8e62

Browse files
committed
feat(manifest): cover Android Gradle Plugin variants in --facts
Adds an android-library fixture (AGP 8.7.3, compileSdk 34) and the minimum machinery the init script needs to scrape AGP-flavored classpaths without blowing up. Two changes to socket-facts.init.gradle: - Skip configurations matching *AndroidTest* (instrumented tests). Their resolution needs device-vs-host target attributes the init script doesn't set, and they fail before producing useful data. - Wrap per-configuration resolution in try/catch. AGP unit-test classpaths (releaseUnitTestCompileClasspath etc.) pull in the project's own debugApiElements, which exposes multiple variants (android-classes-jar, r-class-jar, android-lint, ...); without consumer-side build-type attributes we hit "variant ambiguity" errors. We log "[socket-facts] skipping <cfg>: ..." and continue so other classpaths still produce output. Production (release + debug compile/runtime) variants resolve fine. The e2e test skips the Android case when neither ANDROID_HOME nor ANDROID_SDK_ROOT is set — same auto-skip posture as the rest of the gradle suite. Asserts that androidx.annotation:annotation is captured as a direct dep, confirming AGP variant configs are being walked. Still pending: principled discovery via androidComponents.onVariants (AGP 7+) or android.libraryVariants — current name-pattern matching catches Android variant configs by suffix and gets the job done, but isn't AGP-aware in the strict sense.
1 parent 2753566 commit 37d8e62

6 files changed

Lines changed: 117 additions & 19 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ test/fixtures/commands/manifest/gradle-facts/**/.gradle/
2626
test/fixtures/commands/manifest/gradle-facts/**/build/
2727
test/fixtures/commands/manifest/gradle-facts/**/.socket.facts.json
2828
test/fixtures/commands/manifest/gradle-facts/**/pom.xml
29+
test/fixtures/commands/manifest/gradle-facts/**/local.properties
2930

3031
/.claude/*
3132
!/.claude/agents/

src/commands/manifest/socket-facts-init-gradle.e2e.test.mts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,37 @@ describeOrSkip('socket-facts.init.gradle', () => {
181181
})
182182
})
183183

184+
describe('android-library fixture', () => {
185+
const fixture = path.join(fixturesRoot, 'android-library')
186+
const output = path.join(fixture, '.socket.facts.json')
187+
188+
const androidSdk =
189+
process.env['ANDROID_HOME'] || process.env['ANDROID_SDK_ROOT']
190+
const androidDescribeOrSkip = androidSdk ? describe : describe.skip
191+
192+
androidDescribeOrSkip('with ANDROID_HOME set', () => {
193+
it('resolves Android variant classpaths (debug + release)', async () => {
194+
clean(output)
195+
await runFacts(fixture)
196+
expect(existsSync(output)).toBe(true)
197+
const facts = readFacts(output)
198+
// The androidx.annotation dep is declared as `implementation` and
199+
// should appear via debug/release runtime classpaths. Its
200+
// qualifiers.ext should be 'jar' or 'aar' (annotation 1.7.1 ships
201+
// both — Android uses the aar via variant resolution).
202+
const annotation = findById(
203+
facts,
204+
c => c.namespace === 'androidx.annotation' && c.name === 'annotation',
205+
)
206+
expect(
207+
annotation.length,
208+
`androidx.annotation:annotation present (got ${facts.components.length} components total)`,
209+
).toBeGreaterThan(0)
210+
expect(annotation.some(c => c.direct === true)).toBe(true)
211+
})
212+
})
213+
})
214+
184215
describe('multi-module-java fixture', () => {
185216
const fixture = path.join(fixturesRoot, 'multi-module-java')
186217
const rootOut = path.join(fixture, '.socket.facts.json')

src/commands/manifest/socket-facts.init.gradle

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -128,18 +128,26 @@ allprojects { project ->
128128
// `testCompileClasspath`, `testRuntimeClasspath`), Kotlin Gradle Plugin
129129
// (`jvmMainCompileClasspath`, `linuxX64MainRuntimeClasspath`, ...) and
130130
// AGP per-variant (`debugCompileClasspath`, `releaseRuntimeClasspath`,
131-
// `debugUnitTestRuntimeClasspath`, ...). A future revision should
132-
// switch to Gradle data-model discovery (source sets, KMP compilations,
133-
// AGP variants), but until we have fixtures covering each of those, a
134-
// name-based whitelist gives us broad coverage with one code path.
131+
// `debugUnitTestRuntimeClasspath`, ...).
132+
//
133+
// We exclude AGP's instrumented-test classpaths (`*AndroidTest*`)
134+
// because their variant resolution requires consumer attributes
135+
// (target SDK, device/host runtime) that an init-script-driven
136+
// resolution doesn't set, and they produce ambiguity errors at
137+
// resolution time. Unit-test classpaths (`*UnitTest*`) resolve fine.
138+
// A future revision should switch to Gradle data-model discovery
139+
// (source sets, KMP compilations, AGP variants).
135140
def isClasspath = { String name ->
136141
def lower = name.toLowerCase()
137142
lower.endsWith('compileclasspath') || lower.endsWith('runtimeclasspath')
138143
}
144+
def isAndroidInstrumentedTest = { String name ->
145+
name.toLowerCase().contains('androidtest')
146+
}
139147
def isTestClasspath = { String name -> name.toLowerCase().contains('test') }
140148

141149
def targetConfigs = project.configurations.findAll {
142-
it.canBeResolved && isClasspath(it.name)
150+
it.canBeResolved && isClasspath(it.name) && !isAndroidInstrumentedTest(it.name)
143151
}
144152

145153
// The project being scanned is the SBOM target itself, not one of its
@@ -149,20 +157,29 @@ allprojects { project ->
149157
// first-level edges so consumers don't need a synthetic root node.
150158
targetConfigs.each { cfg ->
151159
def isProd = !isTestClasspath(cfg.name)
152-
def lenient = cfg.resolvedConfiguration.lenientConfiguration
153-
def cache = [:]
154-
lenient.firstLevelModuleDependencies.each { dep ->
155-
directIds.addAll(visit(dep, isProd, cache))
156-
}
157-
lenient.unresolvedModuleDependencies.each { dep ->
158-
def coord = [
159-
groupId : dep.selector.group ?: '',
160-
artifactId: dep.selector.name,
161-
version : dep.selector.version ?: '',
162-
classifier: '',
163-
ext : '',
164-
]
165-
directIds.add(upsertNode(coord, isProd))
160+
// Per-configuration try/catch: AGP-style configurations can fail with
161+
// "variant ambiguity" when resolved from an init-script context that
162+
// doesn't carry the consumer attributes AGP sets internally. We log
163+
// and continue so a single ambiguous config doesn't sink the whole
164+
// facts file — the other classpaths still produce useful output.
165+
try {
166+
def lenient = cfg.resolvedConfiguration.lenientConfiguration
167+
def cache = [:]
168+
lenient.firstLevelModuleDependencies.each { dep ->
169+
directIds.addAll(visit(dep, isProd, cache))
170+
}
171+
lenient.unresolvedModuleDependencies.each { dep ->
172+
def coord = [
173+
groupId : dep.selector.group ?: '',
174+
artifactId: dep.selector.name,
175+
version : dep.selector.version ?: '',
176+
classifier: '',
177+
ext : '',
178+
]
179+
directIds.add(upsertNode(coord, isProd))
180+
}
181+
} catch (Exception e) {
182+
println "[socket-facts] skipping ${cfg.name}: ${e.message?.readLines()?.first()}"
166183
}
167184
}
168185

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Minimal AGP fixture for socket-facts.init.gradle. Exercises Android's
2+
// per-variant compile/runtime classpath configurations (debugCompileClasspath,
3+
// releaseRuntimeClasspath, debugUnitTestRuntimeClasspath, ...) that the Java
4+
// SourceSetContainer doesn't surface.
5+
//
6+
// Requires:
7+
// - JDK 17+
8+
// - Gradle 8.7+ (matched to the AGP version below)
9+
// - An Android SDK reachable via ANDROID_HOME / ANDROID_SDK_ROOT or
10+
// local.properties (gitignored).
11+
plugins {
12+
id 'com.android.library' version '8.7.3'
13+
}
14+
15+
android {
16+
namespace 'com.example.socket.androidlib'
17+
compileSdk 34
18+
19+
defaultConfig {
20+
minSdk 24
21+
}
22+
23+
compileOptions {
24+
sourceCompatibility JavaVersion.VERSION_17
25+
targetCompatibility JavaVersion.VERSION_17
26+
}
27+
}
28+
29+
dependencies {
30+
implementation 'androidx.annotation:annotation:1.7.1'
31+
testImplementation 'junit:junit:4.13.2'
32+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
android.useAndroidX=true
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
pluginManagement {
2+
repositories {
3+
google()
4+
mavenCentral()
5+
gradlePluginPortal()
6+
}
7+
}
8+
9+
dependencyResolutionManagement {
10+
repositories {
11+
google()
12+
mavenCentral()
13+
}
14+
}
15+
16+
rootProject.name = 'android-library'

0 commit comments

Comments
 (0)