Reputation: 5025
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
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
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
Reputation: 6588
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).
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.
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