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
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ abstract class NucleusPlugin : Plugin<Project> {
project.dependencies.extensions.add("nucleus", Dependencies(project))
}

if (!project.buildFile.endsWith(".gradle.kts")) {
// `File.endsWith(String)` matches by path components, not by string suffix, so
// `buildFile.endsWith(".gradle.kts")` is always false and the Groovy DSL setup used to
// run for Kotlin DSL projects too — registering a `jetbrainsCompose` repositories
// extension that clashes with the Compose plugin when it is applied afterwards.
if (!project.buildFile.name.endsWith(".gradle.kts")) {
setUpGroovyDslExtensions(project)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal fun configureDesktop(
nucleusExtension: NucleusExtension,
) {
if (nucleusExtension.isJvmApplicationInitialized) {
checkNoComposeDesktopApplication(project)
val appInternal = nucleusExtension.application as JvmApplicationInternal
val defaultBuildType = appInternal.data.buildTypes.default
val appData = JvmApplicationContext(project, appInternal, defaultBuildType)
Expand Down Expand Up @@ -65,11 +66,15 @@ private fun propagateMainClassToComposeApplication(
appInternal: JvmApplicationInternal,
) {
val mainClass = appInternal.data.mainClass ?: return
val compose = project.extensions.findByName("compose") as? ExtensionAware ?: return
val desktop = compose.extensions.findByName("desktop") as? ExtensionAware ?: return
val desktop = composeDesktopExtension(project) ?: return
// Only forward when the Compose Desktop application has ALREADY been initialized
// (by an explicit `compose.desktop.application { }` block or the Hot Reload plugin).
// `desktop.application` is a lazy getter that initializes the extension as a side effect;
// triggering it here would make the Compose plugin register its own packaging tasks, which
// collide with Nucleus's identically-named ones. See [isComposeJvmApplicationInitialized].
if (!isComposeJvmApplicationInitialized(desktop)) return
runCatching {
// `desktop.application` is lazy (org.jetbrains.compose.desktop.DesktopExtension).
// Accessing it via reflection avoids a compile-time dependency on the Compose plugin.
// Accessing via reflection avoids a compile-time dependency on the Compose plugin.
val applicationGetter = desktop.javaClass.getMethod("getApplication")
val composeApplication = applicationGetter.invoke(desktop) ?: return
val mainClassSetter = composeApplication.javaClass.methods
Expand All @@ -79,6 +84,46 @@ private fun propagateMainClassToComposeApplication(
}
}

/**
* Fails with an actionable message when both `nucleus.application { }` and
* `compose.desktop.application { }` are configured in the same project. Nucleus ships its own
* (forked) Compose Desktop packaging and registers the same task names, so letting both DSLs
* drive packaging throws a cryptic `Cannot add task '…' as a task with that name already exists`.
* The Compose plugin itself can stay applied (for Hot Reload, IDE integration, `compose.desktop
* .currentOs`, …) — only its `application { }` packaging block must be removed.
*/
private fun checkNoComposeDesktopApplication(project: Project) {
val desktop = composeDesktopExtension(project) ?: return
if (!isComposeJvmApplicationInitialized(desktop)) return
error(
"Both `nucleus.application { }` and `compose.desktop.application { }` are configured in " +
"project '${project.path}'. Nucleus replaces Compose Desktop's packaging and registers " +
"the same Gradle tasks, so the two blocks conflict. Remove the " +
"`compose.desktop.application { }` block and configure packaging via " +
"`nucleus.application { }` instead — the Compose plugin can stay applied for Hot Reload " +
"and IDE integration, and `mainClass` is forwarded automatically.",
)
}

/** Returns the `compose.desktop` extension via reflection, or null when the Compose plugin is absent. */
private fun composeDesktopExtension(project: Project): ExtensionAware? {
val compose = project.extensions.findByName("compose") as? ExtensionAware ?: return null
return compose.extensions.findByName("desktop") as? ExtensionAware
}

/**
* Reads Compose's internal `_isJvmApplicationInitialized` flag without initializing the lazy
* `application` extension (unlike calling `getApplication`, which would trigger the Compose plugin
* to register its packaging tasks). Returns false if the flag cannot be read.
*/
private fun isComposeJvmApplicationInitialized(desktop: ExtensionAware): Boolean =
runCatching {
desktop.javaClass
.getMethod("get_isJvmApplicationInitialized\$compose")
.invoke(desktop) as? Boolean
?: false
}.getOrDefault(false)

/**
* Adds `-XstartOnFirstThread` to Compose Hot Reload's `JavaExec` tasks on macOS when the
* project depends on `decorated-window-tao`. The TAO backend drives a winit/Tao native
Expand Down
Loading