# KGP silently ships empty Compose resources when a dependency's `*.kotlin_resources.zip` fails to download (lenient artifact view)

> **Status: REPRODUCED** on KGP 2.4.10 via a targeted cache manipulation + credential swap (no code
> change). Build stays `BUILD SUCCESSFUL`; the app **crashes at runtime** due to missing resources.
> See [Reproduction (confirmed)](#reproduction-confirmed).

> **Note on placeholders:** private identifiers are anonymized. `com.example.sdk:*` and
> `com.example.uikit:*` are private, token-authenticated dependencies that publish Compose
> multiplatform resources; `<private-maven-repo>` is a credential-protected Maven repository;
> `<app>` is the application module and `<lib-module>` a library module in the same build. Example
> versions (`1.2.3`, `4.5.6`) are placeholders. JetBrains-owned class/task names are real.

## TL;DR

The Kotlin Gradle Plugin resolves **multiplatform/Compose resources from dependencies** through a
**lenient** `ArtifactView`. If the resource artifact (`*.kotlin_resources.zip`) of a dependency
**fails to download** (in our case HTTP **401 Unauthorized** from a private Maven repo because an
access token had expired), the failure is **silently swallowed**: the artifact is dropped from the
resolved file collection, the resource-aggregation tasks succeed, and **the build is GREEN** — but
the produced app is **missing those resources at runtime**.

Compile artifacts (`.klib`) are resolved **strictly**, so the same expired token makes *compilation*
fail loudly. This asymmetry means that when the `.klib`s are already cached but the resource zips are
not, an expired token yields a **successful build that produces a broken app**.

## Environment

| Item                         | Value                                                                                 |
|------------------------------|---------------------------------------------------------------------------------------|
| Kotlin / KGP                 | **2.4.10** (`kotlin-gradle-plugin-2.4.10-gradle813.jar`)                              |
| Compose Multiplatform plugin | 1.9.3                                                                                 |
| Gradle                       | 9.3.0                                                                                 |
| AGP                          | 8.13.2                                                                                |
| OS                           | macOS (Apple Silicon)                                                                 |
| Targets                      | `iosArm64`, `iosSimulatorArm64`                                                       |
| Private repo                 | Token-authenticated Maven repository (`<private-maven-repo>`)                         |
| Credentials                  | Maven `~/.m2/settings.xml` (server credentials; `net.linguica.maven-settings` plugin) |
| Affected deps (examples)     | `com.example.sdk:*`, `com.example.uikit:*` (private; publish Compose resources)       |

## Symptom

- The iOS app built successfully but **crashed at runtime** because it **could not find Compose
  resources**.
- **No code change** fixed it — updating the (expired) repository **access token** did.
- All local Gradle builds reported `BUILD SUCCESSFUL`; nothing pointed at resources.

## Root cause (confirmed in KGP 2.4.10 bytecode)

`org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.KotlinTargetResourcesResolution#resourceArchives`
builds the "resources from dependencies" file collection like this (reconstructed from bytecode):

```kotlin
compileDependencyConfiguration.incoming
    .artifactView {
        withVariantReselection()                       // reselect the resources variant
        attributes { configureResourcesPublicationAttributes(...) } // Usage = kotlin-multiplatformresources
        isLenient = true                               // <-- swallows resolution/download failures
    }
    .files
```

Disassembly (the decisive bytes):

```
// KotlinTargetResourcesResolution$resourceArchives$1.execute(ArtifactView$ViewConfiguration)
 1: invokeinterface ArtifactView$ViewConfiguration.withVariantReselection()
22: invokeinterface ArtifactView$ViewConfiguration.attributes(Action)
29: iconst_1                                           // push  true
30: invokeinterface ArtifactView$ViewConfiguration.setLenient:(Z)V   // setLenient(TRUE)
```

The constant pool of `resourceArchives` even preserves the source literal:
`"...nt = true\n        }.files"` (tail of `... isLenient = true }.files`).

That `FileCollection` is wired into
`ResolveResourcesFromDependenciesTask.getArchivesFromDependencies()`, whose `copyResources()` simply
unzips whatever archives are present into the output dir. Because the view is **lenient**, a dependency
whose resource zip **failed to download** contributes **nothing** — with **no error** — so the output
is silently empty/partial.

### Why the asymmetry with `.klib`

Both `.klib` and resources are resolved from the **same** `compileDependencyConfiguration`, but:

| Consumer                                               | Resolution                       | On 401                       |
|--------------------------------------------------------|----------------------------------|------------------------------|
| `compileKotlin<Target>` (klibs)                        | **strict**                       | **hard fail**                |
| `<Target>ResolveResourcesFromDependencies` (resources) | **lenient** (`isLenient = true`) | **silently dropped → empty** |

We believe the leniency exists so that dependencies **without** a resources variant are tolerated.
The problem is that it **does not distinguish** "dependency legitimately has no resources" from
"resource artifact exists but failed to download (401/403/5xx/network)". Both are treated as "skip".

## Evidence

### 1. The token failure is real (loud on klibs)

From a build with an expired token (older checkout of the same project, older dependency versions):

```
> Task :<lib-module>:compileKotlinIosSimulatorArm64 FAILED
> Could not resolve all files for configuration ':<lib-module>:iosSimulatorArm64CompileKlibraries'.
   > Could not download renderer-iosSimulatorArm64Main-1.2.3.klib (com.example.sdk:renderer-iossimulatorarm64:1.2.3)
      > Could not GET 'https://<private-maven-repo>/com/example/sdk/renderer-iossimulatorarm64/1.2.3/renderer-iossimulatorarm64-1.2.3.klib'. Received status code 401 from server: Unauthorized
   > Could not download ui-kit-iosSimulatorArm64Main-4.5.6.klib (com.example.uikit:ui-kit-iossimulatorarm64:4.5.6)
      > ... /com/example/uikit/ui-kit-iossimulatorarm64/4.5.6/ui-kit-iossimulatorarm64-4.5.6.klib ... 401 Unauthorized
BUILD FAILED in 5m 27s
```

(Many `Could not GET ... .klib ... 401` entries in that run.) The build **aborted at the klib/compile
stage**, i.e. **before** any resource-resolution task ran — which is why no resource GET appears in
that log.

### 2. After the token was fixed, the resource chain runs and succeeds

```
> Task :<app>:iosSimulatorArm64ResolveResourcesFromDependencies
> Task :<app>:iosSimulatorArm64AggregateResources
> Task :<app>:syncComposeResourcesForIos
BUILD SUCCESSFUL in 54s
```

### 3. Resource artifacts are genuine remote downloads (module cache)

The Gradle **module download cache** (`caches/modules-2/files-2.1/...`, populated only by fetching
from a repository) contains the resource zips for the same coordinates whose `.klib` 401'd:

```
com.example.sdk/renderer-iossimulatorarm64/1.2.3/.../renderer-1.2.3.kotlin_resources.zip
com.example.sdk/core-iossimulatorarm64/1.2.3/.../core-1.2.3.kotlin_resources.zip
com.example.sdk/webview-iossimulatorarm64/1.2.3/.../webview-1.2.3.kotlin_resources.zip
com.example.uikit/ui-kit-iossimulatorarm64/4.5.6/.../ui-kit-4.5.6.kotlin_resources.zip
```

So the resource zip for e.g. `renderer-iossimulatorarm64:1.2.3` is fetched from the **same feed** as
its `.klib`; with an expired token that GET returns 401 too — but leniently swallowed.

### 4. Reproduced locally on KGP 2.4.10 — build stays GREEN, app crashes

With the dependency resource zips removed from the cache (klibs kept) and an **expired** token in
`~/.m2/settings.xml`, the app build reported `BUILD SUCCESSFUL` but the app **crashed at runtime**
(missing resources). Restoring a **valid** token and rebuilding — **no code change** — produced a
working app. Full steps in [Reproduction (confirmed)](#reproduction-confirmed).

## Trigger conditions (the state needed to activate the bug)

All of the following at once:

1. A dependency's **compile `.klib`s are already cached** (from earlier compile/test/IDE-sync runs),
   **but** its **resources variant (`*.kotlin_resources.zip`) is not yet cached**.
2. The **credential** for the private repo (access token) is **expired/invalid**.
3. A build runs that triggers the **resource chain**
   (`...ResolveResourcesFromDependencies` → `AggregateResources` → `syncComposeResourcesForIos`),
   i.e. an actual **app/framework build** (not a compile-/test-only invocation).

Result: the resource-zip GET → **401** → lenient view drops it → `copyResources()` unzips nothing →
**BUILD SUCCESSFUL** with **missing resources** in the app.

The "klibs cached but resources not" state (condition 1) is easy to reach through ordinary
workflows: any earlier task that resolves compile klibs but not the resource variant
(unit tests, `compileKotlin*`, IDE import, or a build that stopped before the resource stage — exactly
like the failing log above, which died at klib compile and never reached resource resolution).

## Reproduction (confirmed)

Reproduced deterministically **without clearing the whole cache** — only the dependency **resource**
artifacts were removed, with a targeted command (klibs left untouched):

1. **Soft-remove only the `*.kotlin_resources.zip` artifacts** for the resolved versions, leaving
   `.klib`s in place — reversible (rename to `.bak`). Substitute your private coordinates/versions:
   ```bash
   SDK_GROUP=com.example.sdk           # module-cache dir uses the literal group id
   DS_GROUP=com.example.uikit
   SDK_VER=1.2.3
   DS_VER=4.5.6

   find ~/.gradle/caches/modules-2/files-2.1 \
     \( -path "*/$SDK_GROUP/*/$SDK_VER/*" -o -path "*/$DS_GROUP/*/$DS_VER/*" \) \
     -name '*.kotlin_resources.zip' -print -exec mv {} {}.bak \;
   ```
2. **Put an expired token** in `~/.m2/settings.xml` (the build reads repo credentials from Maven settings).
3. **Build & run the app** → `BUILD SUCCESSFUL`, but the app **crashes at runtime** (missing resources).
4. **Restore a valid token** in `~/.m2/settings.xml`.
5. **Rebuild & run** (no code change) → build succeeds and the app **works** (resources present).


### What the reproduction proves

- **Resource download failure is silently ignored.** Step 3 yields a GREEN build whose app is missing
  resources (runtime crash) — no error, no warning surfaced.
- **Recoverable without clearing caches or changing code.** Restoring a valid token and rebuilding
  (step 5) was sufficient to produce the correct resources and a working app — the empty output from
  step 3 was not permanently stuck. 

## Suggested resolution

The lenient resources artifact view conflates two very different situations:

- **Dependency has no resources variant** → skipping is correct.
- **Resource artifact exists but failed to download** (401/403/5xx/network) → should **not** be
  silently ignored.

Requested behavior:
- **Fail** the resource resolution on a genuine transport/resolution error, while still tolerating a
  genuinely-absent resources variant

Silently producing an app with missing resources on an auth failure is the core problem.

## Appendix — how the code evidence was obtained

```bash
# KGP jar (Kotlin 2.4.10)
KGP=~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-gradle-plugin/2.4.10/*/kotlin-gradle-plugin-2.4.10-gradle813.jar
unzip -oq "$KGP" 'org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/*'
javap -p -c 'org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/KotlinTargetResourcesResolution$resourceArchives$1.class'
# -> shows: iconst_1 ; invokeinterface ArtifactView$ViewConfiguration.setLenient:(Z)V
```

