Copy-Task that renames the root folder

Hi, i have some hacks in my build.gradle, unfortunately there is no other way other than that. I create a distribution for linux and with that, i create the windows distribution and generate an .exe file.

To create that .exe file, i must use java -jar $myjar $config, then it creates the .exe and that .exe can then be packaged with the windows distribution (i dont have a windows github runner, so i must use it that way). all works like a charm but i have this problem with the copy task…

tasks.register("extractLinux32Distri", Copy) {
    dependsOn(tasks.bin_linux32DistZip)
    def zipFile = file(tasks.bin_linux32DistZip.outputs.files[0])
    def outputDir = file("${buildDir}/distributions/linux32distri/")

    from zipTree(zipFile).asFileTree
    into outputDir
}

tasks.register("createWindowsExe", Exec) {
    def version = ""
    doFirst {
        version = rootProject.version
    }
    dependsOn(tasks.extractLinux32Distri)
    workingDir("${buildDir}/distributions/linux32distri/launch4j-linux32-${version}");
    commandLine("java", '-jar', '-Djava.awt.headless=true', "./launch4j.jar", file("${rootDir}/launch4j_config.xml"))

}

First, i extract the linux distribution. That works like a charm but it is extracted to e.g. distributions/linux32distris/launch4j-linux32-5.20.1.0.0. This is a problem now because i dont know the version. I would love to extract the content of the distribution, without the root folder, into linux32distris, if possible.
Another way would be to somehow get the information and then use that in the exec for the workingDir, but when i use it like above, i get an error.
When i remove the doFirst and use rootProject.version instead of the version-variable, it says “unspecified”.

I am not that familiar with gradle, i guess the copy task has something like “extract without root folder” or that its a simple error i did with ${version}. But i am stuck here now for about 4h and its driving me crazy…

You have quiiiite some bad practices in that snippet that sooner or later will make problems and are not necessary.

But to answer your question, you can do eachFile { ... } within the Copy task configuration block and in there modify the relativePath to your liking.

thanks, but unfortunately i dont know how you mean that.
I tried

from zipTree(zipFile).asFileTree
    into outputDir
    eachFile {
        relativePath = relativePath.replaceFirst("launch4j-linux32-.*/")
        print("RELATIVEPATH:"  + relativePath)
    }

but that does not work, i get an error. Can you be more specific how to replace that?

relativePath is an instance of type org.gradle.api.file.RelativePath, you cannot assign a String to it.

I strongly suggest to change to Kotlin DSL.
By now it is the default DSL, you immediately get type-safe build scripts, actually helpful error messages if you mess up the syntax, and amazingly better IDE support if you use a good IDE like IntelliJ IDEA or Android Studio.

so i should switch from a language that i know a little (groovy) to a language i know nothing about (kotlin)?

i mean, i could check it out but… thought i’ll go with groovy. I will read about kotlin. I mean, this should be no rocket science shouldnt it? i only wanted to extract to a specific directory - now i need to switch to another language just for that? :smiley:
I use Intellij.

so i should switch from a language that i know a little (groovy) to a language i know nothing about (kotlin)?

That’s my recommendation, yes.
What you do with my recommendation is up to you. :slight_smile:

i only wanted to extract to a specific directory - now i need to switch to another language just for that?

No, you don’t need to.
As I said, it is just a recommendation that will you save much headache in the future and makes developing build scripts much easier.
You can of course stay with Groovy if you like.

IJ even warns you in this case:

But it also warns you about the right part which works fine, while in Kotlin DSL you just get a complaint about the left part, and there as hard compilation error right away:

i tried to migrate it and failed. can you please tell me how to replace the path in groovy. i guess its the exact same in groovy, maybe with a bit different syntax, but even in kotlin i would still have the same problem i guess. or does kotlin have some magic like “ignoreRootFolder=true”?

No, changing the language does not change the API, or at least not much. It just makes the IDE able to provide you way more substantial help.

I’m sorry, but I’m not going to write your code for you, especially not on mobile. But I already told you the problem in your code, so what is the problem?

I never asked you to write the code. But some hint like “replace the existing relativePath with a new generated one, just replace the first segment” would have helped a lot. Anyway, thanks for pointing to relativePath.

for anyone else looking for a solution, i fixed it that way, i guess its not perfect but at least it works:


    dependsOn(tasks.bin_linux32DistZip)
    def zipFile = file(tasks.bin_linux32DistZip.outputs.files[0])
    def outputDir = file("${buildDir}/distributions/linux32distri/")

    from zipTree(zipFile)
    into outputDir
    eachFile {
        ArrayList<String> segments = relativePath.segments
        if(segments.get(0).startsWith("launch4j-linux"))
            segments.remove(0);
        String[] stockArr = new String[segments.size()];
        stockArr = segments.toArray(stockArr);
        relativePath = new RelativePath(true, stockArr)
    }

Just wierd that this is not implemented in the DSL. So much boilerplate code for a … guess not so uncommon use-case.

I have now 3 weeks vacation and will see if i can migrate to kotlin. I failed on things like

jar {
    manifest {
        attributes 'Main-Class': 'net.sf.launch4j.Main'
    }
}

and all the distributions. Never worked with kotlin and need to first learn the basics.

I never asked you to write the code.

Oh, I’m sorry, then I must have misunderstood “can you please tell me how to replace the path in groovy.”.

But some hint like “replace the existing relativePath with a new generated one, just replace the first segment” would have helped a lot. Anyway, thanks for pointing to relativePath.

To be honest, I was on mobile and don’t have all of Gradle’s API in my head, I’m just a user like you and giving hints and help where I can, but using your IDE and the docs is still necessary. :slight_smile:

So much boilerplate code

Well, that might more be a problem of your code though. :smiley:
I guess this should also be fine:

eachFile {
   relativePath = new RelativePath(relativePath.isFile(), relativePath.segments.tail())
}

Just wierd that this is not implemented in the DSL. […] for a … guess not so uncommon use-case.

I actually don’t agree, I think the use-case is pretty rare.
But YMMV, feel free to open a feature request and find out what the Gradle guys think about it. :slight_smile:

I failed on things like

tasks.jar {
    manifest {
        attributes("Main-Class" to "net.sf.launch4j.Main")
    }
}

But I really wonder why you build a jar that launches launch4j.
That is another thing that sounds pretty wrong to do. :smiley: :man_shrugging:

Thanks, i really appreciate that! But you are not like me, because you know groovy :wink:

See, just what i mentioned :smiley:
Awesome, i will test that later that day!

All command line tools i know can output to another directory, like tar, zip, wget/curl, … maybe this is different in the application != library world, dont know…

Because launch4j (my version, updated all deps etc.) is not in any repository. I first build the shadowJar, i mean this is required at least to produce any release artefact. Then i use this to produce the .exe for the windows distribution.
If you have any idea how to get this better, please be so kind and tell me, because i have no clue how this should work otherwise. Is there any groovy/gradle-task like “produceExe”?

All command line tools i know can output to another directory, like tar, zip, wget/curl

The Copy and Sync tasks can also do with into or destinationDir.
But you are talking about modifying the extracted paths.
Most commandline tools cannot do that, but with the eachFile { ... } Gradle can easily. :slight_smile:

If you have any idea how to get this better, please be so kind and tell me, because i have no clue how this should work otherwise. Is there any groovy/gradle-task like “produceExe”?

Using launch4j to build an EXE launcher, is fine, I also do that.
But building a bad-practice fat jar just to execute some code is, what is strange.
You can just use a JavaExec task or javaExec { ... } call to execute some Java program.
Building a special jar first is just a waste of time.

I was thinking about this a couple of hours because you are absolutely right. It feels so wrong to create the .exe from the .jar because the code is already there. The problem i had was, when i run the code in IJ, it did not work because there is a folder like: bin/win32 and a folder like bin/linux. In the distribution, those are moved to $ROOT/bin/ and then used. So i was not able to simply run the java code to produce the exe. So i thought “easy, i just use the distribution of the linux package and create the windows exe out of it and then create with that the windows distribution”. Now i checked the code again, its Launch4j Executable Wrapper / Git / [c48081] /src/net/sf/launch4j/Builder.java - i noticed now “launch4j.bindir”. I have now vacation and will dig a bit deeper into that, and maybe i get it working without all those nasty hacks. I just dont want to modify the code too much. The author of the project did not change it for quite a while, no dependency updates, some serious CVEs went into it, so i thought i’ll create the exact same distributions like he did but instead with ant i use gradle and update all deps. And when i dont touch the code too much, i can cherry-pick if he really does an update to anything.

1 Like

I was able to run a simple JavaExec task with -Dlaunch4j.bindir=./bin/bin-linux, now it works like a charm! Thanks! Dont need to hack around with any distribution extraction etc. at all.

I will see if i get it running with Kotlin. After that, if you have some time and might have a look at the .kts again if you see something very nasty that should be optimized. I will write here again when i migrated to Kotlin… unfortunately, i somehow cant edit my last response.
I am not that familiar with groovy and i have 0 knowlege of Kotlin, so please show some mercy :slight_smile:

here we go:

Sure.
You asked for it, so don’t complain. :stuck_out_tongue:
Most points are not related to Kotlin DSL but would be just the same for Groovy DSL.

  • I would use the accessors for the plugins where available (application instead of id("application"), …).
  • Never use mavenLocal() anywhere if you can in any way prevent it, if really needing to use it, always have it last in the list and optimally always with a repository content filter. mavenLocal(() is broken by design in Maven already, it makes your builds slow and unreliable at best, silently working wrongly at worst.
  • You can use maven("...") for the repositories if you like it a bit more succinct.
  • You can use mavenCentral() for Maven Central instead of the explicit.
  • I would use JVM Toolchains feature instead of setting *Compatibility.
  • By using jar.get() you break task-configuration avoidance for the jar task, it is probably better to use val foo = java.manifest { ... } to create a Manifest instance that you then can use further on.
  • Practically any explicit dependsOn that does not have a lifecycle task on the left-hand side is bad practice and a sign that you did not properly wire task outputs to task inputs.
  • Regarding the previous it is probably superfluous anyway as you do not run the shadow jar but use the main source sets runtime classpath, and for the ...DistZip you configured the task as from so the dependency should already be there implicitly as is idomatic.
  • There is no need to define the main class multiple times, wire the application.mainClass property to your task’s mainClass property instead.
  • Don’t use tasks.withType<...> { ... }, that breaks task-configuration avoidance for all tasks of that type, instead use tasks.withType<...>().configureEach { ... }.
  • Don’t use jvmArgs to set system properties, but systemProperty.
  • Generally don’t use ...["..."] on domain collections, in case they are or will be handled lazily, you break that laziness. So for example instead of classpath = sourceSets["main"].runtimeClasspath use classpath(sourceSets.main.map { it.runtimeClasspath }), that also makes it more type-safe and less string-y.
  • Similarly you could do things like val foo by tasks.registering(JavaExec::class) { ... } to define the name from the variable name, or val bar by tasks.existing { ... } to get an existing element, especially if you need to use the task further on later, but that is more a matter of preference.
  • “//will make run & runShadow tasks work. Have no idea why it can’t take main class from jar/shadowJar manifests” => building a jar just to run Java code is wasted time usually (as you also just found out) so the run tasks does not do that, but uses the classes from the compiler output directly, thus there is no manifest to take the main class from. If a JavaExec task would have only one jar in its classpath and no main class set, then it would indeed take it from the manifest. The runShadow task on the other hand uses the shadow jar as classpath, so I think there it should indeed work without the application extension configuration.
1 Like