Skip to content

Conversation

igordmn
Copy link
Collaborator

@igordmn igordmn commented Aug 15, 2025

Fixes https://youtrack.jetbrains.com/issue/CMP-8050/Desktop-runRelease-crash-when-upgrade-to-CMP-1.8.0-rc01

./gradlew runRelease throws an error:

Exception in thread "main" kotlinx.a.g: Serializer for class 'LoginRoute' is not found

in this code:

import androidx.compose.ui.window.singleWindowApplication
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.serialization.Serializable

fun main() = singleWindowApplication {
    NavHost(
        navController = rememberNavController(),
        startDestination = LoginRoute()
    ) {
        composable<LoginRoute> {}
    }
}

@Serializable
data class LoginRoute(val id: Long? = null)

with obfuscation enabled:

compose.desktop {
    application {
        mainClass = "MainKt"

        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "org.jetbrains.nav_cupcake"
            packageVersion = "1.0.0"
        }

        buildTypes.release.proguard {
            obfuscate.set(true)
        }
    }
}

It crashes because:

  • we use @InternalSerializationApi kotlinx.serialization.serializer which is used by Navigation
  • it uses reflection underneath. Looks like it tries to find serializer 2 ways:
    • Looking at the field static final LoginRoute.Companion.serializer
    • Looking at the inner class LoginRoute.Companion and its name
  • The the default rules:
    • [no obfuscation] work by the second way, but doesn't work by the first
    • [with obfuscation] don't work by both ways

The new rule support the first for both obfuscated/non-obfuscated code

Details

Old rules

Without obfuscation

// LoginRoute.class
public final class LoginRoute {
  public static final Companion Companion = new Companion((byte)0);
  ...
  public static final class Companion {
    private Companion() {}
  }
}

// LoginRoute$Companion.class
public final class Companion {
  private Companion() {}
}

With obfuscation

// LoginRoute.class
@Serializable
public final class LoginRoute {
  public static final a Companion = new a((byte)0);
  ...
}

// a.class
public final class a {
  private a() {}
}

With new rule

Without obfuscation

// LoginRoute.class
@Serializable
public final class LoginRoute {
  public static final Companion Companion = new Companion((byte)0);
  ...
  public static final class Companion {
    private Companion() {}
    
    public final KSerializer<LoginRoute> serializer() {
      return (KSerializer<LoginRoute>)LoginRoute.$serializer.INSTANCE;
    }
  }
}

// LoginRoute$Companion.class
public final class Companion {
  private Companion() {}
  
  public final KSerializer<LoginRoute> serializer() {
    return (KSerializer<LoginRoute>)LoginRoute.$serializer.INSTANCE;
  }
}

With obfuscation

// LoginRoute.class
@Serializable
public final class LoginRoute {
  public static final a Companion = new a((byte)0);
  ...
}

// a.class
public final class a {
  private a() {}
  
  public final b serializer() {
    return (b)LoginRoute$$serializer.INSTANCE;
  }
}

Testing

  • new test
  • snippet above doesn't produce the error anymore

Release Notes

Fixes - Desktop

Fix runRelease task when navigation and obfuscate.set(true) are used

Fixes https://youtrack.jetbrains.com/issue/CMP-8050/Desktop-runRelease-crash-when-upgrade-to-CMP-1.8.0-rc01

Navigation uses `@InternalSerializationApi kotlinx.serialization.serializer` inside which can be not covered by [the default](https://github.com/Kotlin/kotlinx.serialization/blob/4667a18/rules/common.pro) rules.

Without obfuscation we have class `LoginRoute$Companion.class`:
```
package fsd;

import kotlin.Metadata;
import kotlinx.serialization.KSerializer;

@metadata(mv = {2, 2, 0}, k = 1, xi = 48, d1 = {"\000\026\n\002\030\002\n\002\020\000\n\002\b\003\n\002\030\002\n\002\030\002\n\000\b\003\030\0002\0020\001B\t\b\002\006\004\b\002\020\003J\f\020\004\032\b\022\004\022\0020\0060\005\006\007"}, d2 = {"Lfsd/LoginRoute$Companion;", "", "<init>", "()V", "serializer", "Lkotlinx/serialization/KSerializer;", "Lfsd/LoginRoute;", "composeApp"})
public final class Companion {
  private Companion() {}

  public final KSerializer<LoginRoute> serializer() {
    return (KSerializer<LoginRoute>)LoginRoute.$serializer.INSTANCE;
  }
}
```
which is covered by the existing rule (`-keepclassmembers class <2>$<3>` filters `LoginRoute$Companion`):
```
-if @kotlinx.serialization.Serializable class ** {
    static **$* *;
}
-keepclassmembers class <2>$<3> {
    kotlinx.serialization.KSerializer serializer(...);
}
```

With obfuscation `LoginRoute$Companion.class` renamed to `a.class`:
```
package fsd;

import kotlin.Metadata;
import kotlinx.a.b;

@metadata(mv = {2, 2, 0}, k = 1, xi = 48, d1 = {"\000\026\n\002\030\002\n\002\020\000\n\002\b\003\n\002\030\002\n\002\030\002\n\000\b\003\030\0002\0020\001B\t\b\002\006\004\b\002\020\003J\f\020\004\032\b\022\004\022\0020\0060\005\006\007"}, d2 = {"Lfsd/LoginRoute$Companion;", "", "<init>", "()V", "serializer", "Lkotlinx/serialization/KSerializer;", "Lfsd/LoginRoute;", "composeApp"})
public final class a {
  private a() {}

  public final b serializer() {
    return (b)LoginRoute$$serializer.INSTANCE;
  }
}
```
Which is covered only by the new rule in this PR:
```
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
    kotlinx.serialization.KSerializer serializer(...);
}
```

Without it, `serializer()` is removed.

## Testing
- new test
- snippet from the issue
```
import androidx.compose.ui.window.singleWindowApplication
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.serialization.Serializable

fun main() = singleWindowApplication {
    NavHost(
        navController = rememberNavController(),
        startDestination = LoginRoute()
    ) {
        composable<LoginRoute> {}
    }
}

@serializable
data class LoginRoute(val id: Long? = null)
```
with this in `build.gradle.kts`:
```
compose.desktop {
    application {
        mainClass = "MainKt"

        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "org.jetbrains.nav_cupcake"
            packageVersion = "1.0.0"
        }

        buildTypes.release.proguard {
            obfuscate.set(true)
        }
    }
}
```

## Release Notes
### Fixes - Desktop
Fix `runRelease` task when navigation and `obfuscate.set(true)` are used
@igordmn igordmn requested review from terrakok and removed request for terrakok August 15, 2025 20:59
@igordmn igordmn marked this pull request as draft August 16, 2025 13:23
@igordmn igordmn marked this pull request as ready for review August 18, 2025 17:05
@igordmn
Copy link
Collaborator Author

igordmn commented Aug 18, 2025

Modified the reasons in description, the previous explanation wasn't correct entirely

@igordmn igordmn merged commit af7c80c into master Aug 18, 2025
13 checks passed
@igordmn igordmn deleted the igor.demin/fix-serializer-proguard branch August 18, 2025 18:47
igordmn added a commit that referenced this pull request Aug 18, 2025
…sed (#5384)

Fixes
https://youtrack.jetbrains.com/issue/CMP-8050/Desktop-runRelease-crash-when-upgrade-to-CMP-1.8.0-rc01

`./gradlew runRelease` throws an error:
```
Exception in thread "main" kotlinx.a.g: Serializer for class 'LoginRoute' is not found
```
in this code:
```
import androidx.compose.ui.window.singleWindowApplication
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.serialization.Serializable

fun main() = singleWindowApplication {
    NavHost(
        navController = rememberNavController(),
        startDestination = LoginRoute()
    ) {
        composable<LoginRoute> {}
    }
}

@serializable
data class LoginRoute(val id: Long? = null)
```
with obfuscation enabled:
```
compose.desktop {
    application {
        mainClass = "MainKt"

        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "org.jetbrains.nav_cupcake"
            packageVersion = "1.0.0"
        }

        buildTypes.release.proguard {
            obfuscate.set(true)
        }
    }
}
```

It crashes because:
- we use `@InternalSerializationApi kotlinx.serialization.serializer`
which is used by Navigation
- it uses reflection underneath. Looks like it tries to find serializer
2 ways:
  - Looking at the field `static final LoginRoute.Companion.serializer`
  - Looking at the `inner class LoginRoute.Companion` and its name
- The [the
default](https://github.com/Kotlin/kotlinx.serialization/blob/4667a18/rules/common.pro)
rules:
- [no obfuscation] work by the second way, but doesn't work by the first
  - [with obfuscation] don't work by both ways

The new rule support the first for both obfuscated/non-obfuscated code

## Details
### Old rules
#### Without obfuscation
```
// LoginRoute.class
public final class LoginRoute {
  public static final Companion Companion = new Companion((byte)0);
  ...
  public static final class Companion {
    private Companion() {}
  }
}

// LoginRoute$Companion.class
public final class Companion {
  private Companion() {}
}
```
#### With obfuscation
```
// LoginRoute.class
@serializable
public final class LoginRoute {
  public static final a Companion = new a((byte)0);
  ...
}

// a.class
public final class a {
  private a() {}
}
```
### With new rule
#### Without obfuscation
```
// LoginRoute.class
@serializable
public final class LoginRoute {
  public static final Companion Companion = new Companion((byte)0);
  ...
  public static final class Companion {
    private Companion() {}

    public final KSerializer<LoginRoute> serializer() {
      return (KSerializer<LoginRoute>)LoginRoute.$serializer.INSTANCE;
    }
  }
}

// LoginRoute$Companion.class
public final class Companion {
  private Companion() {}

  public final KSerializer<LoginRoute> serializer() {
    return (KSerializer<LoginRoute>)LoginRoute.$serializer.INSTANCE;
  }
}
```
#### With obfuscation
```
// LoginRoute.class
@serializable
public final class LoginRoute {
  public static final a Companion = new a((byte)0);
  ...
}

// a.class
public final class a {
  private a() {}

  public final b serializer() {
    return (b)LoginRoute$$serializer.INSTANCE;
  }
}
```

## Testing
- new test
- snippet above doesn't produce the error anymore

## Release Notes
### Fixes - Desktop
Fix `runRelease` task when navigation and `obfuscate.set(true)` are used

(cherry picked from commit af7c80c)
sekater added a commit that referenced this pull request Aug 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants