Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/cli/AndroidLintCliInvoker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ class AndroidLintCliInvoker(
const val ERRNO_INTERNAL_CONTINUE = 100

fun createUsingJars(
parentClassloader: ClassLoader = this::class.java.classLoader,
// Parent on the platform classloader (JDK classes only) so lint loads its entire runtime —
// crucially kotlin-stdlib AND kotlin-reflect — from its own deploy jar. Parenting on the
// worker's classloader instead lets the worker's kotlin-stdlib shadow lint's, and since the
// worker has no kotlin-reflect, Kotlin reflection inside lint detectors (e.g. the partial
// analysis "fx" checks) silently fails. This mirrors AGP's AndroidLintWorkAction.
parentClassloader: ClassLoader = ClassLoader.getPlatformClassLoader(),
vararg jars: Path,
): AndroidLintCliInvoker {
require(jars.isNotEmpty()) {
Expand Down
13 changes: 11 additions & 2 deletions src/cli/AndroidLintCliInvokerCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import java.nio.file.Path
* while a request against the old jar is still running, so the old classloader is only disposed
* once its last lease is released.
*/
internal class AndroidLintCliInvokerCache {
internal class AndroidLintCliInvokerCache(
// The classloader lint's deploy jar is parented on. Defaults to the platform classloader so lint
// is fully isolated from the worker's classpath. Tests override this to resolve stub lint classes
// from the test classpath.
private val parentClassloader: ClassLoader = ClassLoader.getPlatformClassLoader(),
) {
private class Entry(
val key: String,
val invoker: AndroidLintCliInvoker,
Expand Down Expand Up @@ -44,7 +49,11 @@ internal class AndroidLintCliInvokerCache {
}
current = null
}
val invoker = AndroidLintCliInvoker.createUsingJars(jars = jars.toTypedArray())
val invoker =
AndroidLintCliInvoker.createUsingJars(
parentClassloader = parentClassloader,
jars = jars.toTypedArray(),
)
createdCount += 1
current = Entry(key = key, invoker = invoker, activeLeases = 1)
return invoker
Expand Down
8 changes: 4 additions & 4 deletions tests/src/cli/AndroidLintActionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class AndroidLintActionTest {

@Test
fun `sequential work requests reuse a single cached lint classloader`() {
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val executor = AndroidLintAction.AndroidLintExecutor(cache)
val jar = writeStubLintJar()

Expand All @@ -43,7 +43,7 @@ class AndroidLintActionTest {
fun `lint issue exit codes do not fail the work request or evict the cache`() {
// By design the action always succeeds and produces a report; enforcement happens in the
// android_lint_test validator. A lint exit code must not kill or degrade the worker.
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val executor = AndroidLintAction.AndroidLintExecutor(cache)
val jar = writeStubLintJar()

Expand All @@ -61,7 +61,7 @@ class AndroidLintActionTest {

@Test
fun `lint runs with an invocation-scoped user home`() {
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val executor = AndroidLintAction.AndroidLintExecutor(cache)
val jar = writeStubLintJar()

Expand All @@ -75,7 +75,7 @@ class AndroidLintActionTest {

@Test
fun `lint receives sdk home from android home`() {
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val executor = AndroidLintAction.AndroidLintExecutor(cache)
val jar = writeStubLintJar()

Expand Down
10 changes: 5 additions & 5 deletions tests/src/cli/AndroidLintCliInvokerCacheTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class AndroidLintCliInvokerCacheTest {

@Test
fun `acquiring twice with an unchanged jar reuses the invoker`() {
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val jar = writeJar("lint.jar", entryCount = 1)

val first = cache.acquire(listOf(jar))
Expand All @@ -44,7 +44,7 @@ class AndroidLintCliInvokerCacheTest {

@Test
fun `a changed jar evicts the cached invoker and disposes it`() {
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val jar = writeJar("lint.jar", entryCount = 1)

val first = cache.acquire(listOf(jar))
Expand All @@ -62,7 +62,7 @@ class AndroidLintCliInvokerCacheTest {

@Test
fun `eviction defers disposal until the last lease is released`() {
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val jar = writeJar("lint.jar", entryCount = 1)

val first = cache.acquire(listOf(jar))
Expand All @@ -83,7 +83,7 @@ class AndroidLintCliInvokerCacheTest {

@Test
fun `invoke creates a fresh Main per call and applies checkDependencies per call`() {
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val jar = writeJar("lint.jar", entryCount = 1)
val invoker = cache.acquire(listOf(jar))

Expand All @@ -103,7 +103,7 @@ class AndroidLintCliInvokerCacheTest {

@Test
fun `concurrent invocations share one classloader with independent Main instances`() {
val cache = AndroidLintCliInvokerCache()
val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)
val jar = writeJar("lint.jar", entryCount = 1)
// Both runs must be in flight at the same time before either completes.
Main.runBarrier = CyclicBarrier(2)
Expand Down
Loading