Marcel Bro
Marcel Bro

Reputation: 5025

Add custom Gradle task after packaging of Compose Desktop app

I'm building a desktop application with Jetpack Compose and found that when I package the app for macOS, it requires a manual step to set executable permission on the app binary. To remove this manual step, I'd like to add a Gradle task which does that automatically once the application is packaged.

tasks.register<Copy>("setExecutablePermission") {
    description = "Sets executable permission for the generated .app bundle on macOS"
    doLast {
        val osName = System.getProperty("os.name")
        if (osName.startsWith("Mac OS")) {
            val appBundle = file("${layout.buildDirectory}/compose/binaries/main/app/simple-card-game.app")
            if (appBundle.exists()) {
                val executableFile = file("${appBundle}/Contents/MacOS/simple-card-game")
                executableFile.setExecutable(true, false)
                println("Executable permission set for: $executableFile")
            } else {
                println("Warning: .app bundle not found. Skipping setExecutablePermission task.")
            }
        } else {
            println("Not running on macOS. Skipping setExecutablePermission task.")
        }
    }

    project.afterEvaluate {
        val packageTask = tasks.findByName("createDistributable")
        if (packageTask != null) packageTask.finalizedBy(this) else println("Warning: 'createDistributable' task not found.")
    }
}

However, I'm not able to add this custom task to the execution graph.

The following block doesn't have any effect. I have tried finalizedBy and mustRunAfter to chain it with createDistributable, packageDistributionForCurrentOS or package or even jar but my task still doesn't seem to get added.

project.afterEvaluate {
    val packageTask = tasks.findByName("createDistributable")
    if (packageTask != null) packageTask.finalizedBy(this) else println("Warning: 'createDistributable' task not found.")
}

Any tips on how to add a custom task to Compose Desktop app packaging process or at least how to debug this, please?

Upvotes: 1

Views: 399

Answers (3)

Mahozad
Mahozad

Reputation: 24612

Here is another way I used in this plugin for embedding manifest in app exe:

project
    .tasks
    .withType(AbstractJPackageTask::class.java)
    // Filters out packageExe etc. taskspackaged in the installer
    .matching { "package" !in it.name }
    .all { composePackagingTask ->
        val embedTask = project.tasks.register(
            "embedManifestInExeFor${composePackagingTask.name.capitalized()}",
            EmbedTask::class.java // Or your task class
        ) {
            it.exeDirectory = composePackagingTask.destinationDir
        }
        composePackagingTask.finalizedBy(embedTask)
    }
abstract class EmbedTask : DefaultTask() {

    init {
        group = "My tasks"
        description = "Embeds a manifest"
    }

    @get:InputDirectory
    lateinit var exeDirectory: Provider<Directory>

   // ...
}

Upvotes: 0

mipa
mipa

Reputation: 10640

Even though this may work now, I am wondering why you have do this at all. I never had to do this with my own Compose multi-platform desktop apps on Mac and I think a lot of people would have already complained if this were really necessary.

Upvotes: 0

Simon Jacobs
Simon Jacobs

Reputation: 6588

Your task dependency is added inside another task's configuration block

The reason your block doesn't have any effect is that it is inside of the register block for setExecutablePermission. register does lazy configuration, so such a block only gets called if setExecutablePermission is in the task execution graph Gradle computes for a given invocation of the build.

That task is not selected, so none of your code is executed (and even if it was, it would be too late in the build lifecycle to add any task dependency anyhow).

Adding the dependency outside of another block

Instead, what you need to do is configure createDistributable outside of such a block. To do this, first grab a reference to your new task1:

val setExecutablePermissionTask = tasks.register<Copy>("setExecutablePermission") {
    // ...
}

Then you can do:

tasks.named("createDistributable") {
    finalizedBy(setExecutablePermissionTask)
}

named is used as it is lazy and so prevents unnecessary task configuration. So it's the same as register but is for use with an existing task2.

Also, afterEvaluate...

Additionally, I'm not sure why you wanted to used afterEvaluate. (Perhaps in the hopes it would magically make it work; I don't blame you for that. I've been there.)

However, not only is the use of afterEvaluate rarely necessary, it is evil and should only be used as a last resort. It is only necessary in exceptional cases, or when you have to work around suboptimal code in another plugin. Gradle has a whole lazy configuration API to facilitate configuring the same objects at different times and in different orders with the same result, and which is in large part about rendering unnecessary the use of sticking plasters like afterEvaluate.

The reason it is evil because it forces you to consider what order such hooks are running in, and changing the order, or introducing new hooks can be difficult to reason about and introduce unexpected behaviour.


1You can just use the name but I hope you agree this is more elegant and DRY.

2I don't see any need to check if the task is present, as your code was doing. That isn't going to vary from build to build, eg based on user configuration. Instead such a task is either added by the plugin, so that the call works, or it was not, and the build fails and will tell you the task was absent, allowing you to easily fix the issue.

Upvotes: 2

Related Questions