beryx / badass-jlink-plugin

Create a custom runtime image of your modular application
https://badass-jlink-plugin.beryx.org
Apache License 2.0
379 stars 25 forks source link

DLLs in root folder #130

Closed daniel-jirca closed 1 year ago

daniel-jirca commented 4 years ago

Hello, When building a project with jpackage on windows, the main directory where the executable is created also contains a long list of libraries like api-ms-win*.dll, applauncher.dll and other.

Is it possible to put all these libraries in a separate subfolder named lib or something like that and keep the root folder clean?

siordache commented 4 years ago

There is no jpackage option for doing this, so you need to configure Gradle to take care of moving the libraries:

tasks.jpackageImage.doLast {
    def appName = jpackageData.imageName
    def dir = "$jpackageData.imageOutputDir/$appName"
    file("$dir/lib").mkdirs()
    ant.move(todir: "$dir/lib") {
        fileset(dir: dir) {
            include name: "*.dll"
        }
    }
}

However, this is problematic. Your application will no longer start, because the required libraries are not found in the directories specified by the PATH environment variable. My proposed workaround is to write a custom launcher that adds the lib directory to the PATH and then starts the original launcher.

In your Gradle script, you need to rename the original launcher, copy the custom launcher in its place, and also duplicate the .ico and .cfg files.

You can see a complete example here, along with the resulting MSI installer.

daniel-jirca commented 4 years ago

I have integrated the new gradle task into my build.gradle file and now it looks like this:

plugins {
    id 'org.springframework.boot' version '2.2.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
//    id "org.javamodularity.moduleplugin" version "1.6.0"
    id 'org.openjfx.javafxplugin' version '0.0.8'
    id "org.beryx.jlink" version "2.17.7"
    id "com.jfrog.bintray" version "1.7"
}

group 'design.alecsa'
version '1.1.0'

sourceCompatibility = '14'
targetCompatibility = '14'
tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
    maven {
        url  "https://dl.bintray.com/jerady/maven"
    }
}

defaultTasks 'clean', 'jlinkZip'

configurations {
   springFactoriesHolder { transitive = false }
}

javafx {
    version = "14"
    modules = ['javafx.controls', 'javafx.fxml', 'javafx.swing']
}

dependencies {
    springFactoriesHolder 'org.springframework.boot:spring-boot-actuator-autoconfigure'
    springFactoriesHolder 'org.springframework.boot:spring-boot-autoconfigure'
    springFactoriesHolder 'org.springframework.boot:spring-boot'
    implementation 'org.springframework.boot:spring-boot-starter'

    implementation 'de.jensd:fontawesomefx-commons:11.0', {
        exclude group: 'org.openjfx'
    }
    implementation 'de.jensd:fontawesomefx-controls:11.0', {
        exclude group: 'org.openjfx'
    }
    implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-11', {
        exclude group: 'org.openjfx'
    }
//    implementation 'de.jensd:fontawesomefx-emojione:2.2.7-11', {
//        exclude group: 'org.openjfx'
//    }
//    implementation 'de.jensd:fontawesomefx-icons525:3.0.0-11', {
//        exclude group: 'org.openjfx'
//    }
//    implementation 'de.jensd:fontawesomefx-materialdesignfont:1.7.22-11', {
//        exclude group: 'org.openjfx'
//    }
//    implementation 'de.jensd:fontawesomefx-materialicons:2.2.0-11', {
//        exclude group: 'org.openjfx'
//    }
//    implementation 'de.jensd:fontawesomefx-materialstackicons:2.1-11', {
//        exclude group: 'org.openjfx'
//    }
//    implementation 'de.jensd:fontawesomefx-octicons:4.3.0-11', {
//        exclude group: 'org.openjfx'
//    }
//    implementation 'de.jensd:fontawesomefx-weathericons:2.0.10-11', {
//        exclude group: 'org.openjfx'
//    }

//    implementation 'org.springframework.boot:spring-boot-starter-actuator'
//    implementation 'org.springframework.boot:spring-boot-starter-cache'
//    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
//    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'javax.cache:cache-api'
    implementation 'org.ehcache:ehcache'

    implementation 'javax.servlet:javax.servlet-api:4.0.1'

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    compile group: 'org.yaml', name: 'snakeyaml', version: '1.26'
    // https://mvnrepository.com/artifact/commons-io/commons-io
    compile group: 'commons-io', name: 'commons-io', version: '2.6'
    // https://mvnrepository.com/artifact/org.ehcache/ehcache
    compile group: 'org.ehcache', name: 'ehcache', version: '3.8.1'
    // https://mvnrepository.com/artifact/javax.cache/cache-api
    compile group: 'javax.cache', name: 'cache-api', version: '1.1.1'

}

jar {
    enabled = true
}

prepareMergedJarsDir.doLast {
    // extract and merge META-INF/spring.factories from springFactoriesHolder
    def factories = configurations.springFactoriesHolder.files.collect {
        def props = new Properties()
        props.load(zipTree(it).matching { include 'META-INF/spring.factories' }.singleFile.newInputStream())
        props
    }
    def mergedProps = new Properties()
    factories.each { props ->
        props.each { key, value ->
            def oldVal = mergedProps[key]
            mergedProps[key] = oldVal ? "$oldVal,$value" : value
        }
    }
    def content = mergedProps.collect { key, value ->
        def v = (value as String).replace(',', ',\\\n')
        "$key=$v"
    }.join('\n\n')
    mkdir("$jlinkBasePath/META-INF")
    new File("$jlinkBasePath/META-INF/spring.factories").text = content

    // insert META-INF/spring.factories into the main jar
    ant.zip(update: "true", destfile: jar.archivePath, keepcompression: true) {
        fileset(dir: "$jlinkBasePath", includes: 'META-INF/**')
    }
}

jlink {
    imageZip = file("$buildDir/image-zip/krop-image.zip")
    forceMerge 'log4j-api', 'javafx.base'

    mergedModule {
        additive = true
        uses 'ch.qos.logback.classic.spi.Configurator'
        uses 'org.ehcache.core.spi.service.ServiceFactory'
        uses 'org.ehcache.xml.CacheManagerServiceConfigurationParser'
        uses 'org.ehcache.xml.CacheServiceConfigurationParser'
        excludeProvides implementation: 'com.sun.xml.bind.v2.ContextFactory'
        excludeRequires 'com.fasterxml.jackson.module.paramnames'
        excludeProvides implementation: 'com.sun.xml.bind.v2.ContextFactory'
        excludeProvides servicePattern: 'javax.enterprise.inject.*'
        excludeProvides service: 'org.apache.logging.log4j.spi.Provider'
    }

    options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
    launcher {
        name = 'krop'
        customImage {
            appModules = ['design.alecsa.merged.module']
        }
        jvmArgs = [
                '--add-reads', 'design.alecsa.merged.module=design.alecsa.krop',
                "--add-opens", "javafx.graphics/javafx.css=de.jensd.fx.fontawesomefx.commons",
                '-cp', 'config/'
        ]
    }

    jpackage {
        imageName = 'Krop'
        skipInstaller = true
        installerName = 'Krop'
        installerType = 'exe'
        if (org.gradle.internal.os.OperatingSystem.current().windows) {
            installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu']
        }
    }
    forceMerge('log4j-api')
}

tasks.jlink.doLast {
    // Spring performs its magic by scanning the classpath, but in a modular application the classpath is replaced by the module-path.
    // To circumvent this problem, we copy all resources into the 'config' directory and set this directory as classpath.
    copy {
        from "src/main/resources"
        into "$imageDir.asFile/bin/config"
    }
    copy {
        from 'resources/css'
        into "$imageDir.asFile/bin/config/static/resources/css"
    }
}

tasks.jpackageImage.doLast {
    if (org.gradle.internal.os.OperatingSystem.current().windows) {
        def appName = jpackageData.imageName
        def dir = "$jpackageData.imageOutputDir/$appName"
        file("$dir/lib").mkdirs()
        ant.move(todir: "$dir/lib") {
            fileset(dir: dir) {
                include name: "*.dll"
            }
        }

        file("$dir/${appName}.exe").renameTo "$dir/__app.exe"

        copy {
            from dir
            into dir
            include "${appName}.ico"
            rename("${appName}.ico", "__app.ico")
        }
        copy {
            from "$dir/app"
            into "$dir/app"
            include "${appName}.cfg"
            rename("${appName}.cfg", "__app.cfg")
        }
        copy {
            from "src/main/resources"
            into dir
            include "launcher.exe"
            rename("launcher.exe", "${appName}.exe")
        }
    }
}

    mainClassName = 'design.alecsa.krop.JavaFxSpringApp'

Unfortunately when I run the launcher executable, I get an error window with the title __app.exe that says: failed to launch JVM. If I manually add the lib folder to the system path setx path "%path%;lib" I can start the application with the __app.exe executable in the root folder. The launcher doesn't seem to work though. I even tried rebuilding the launcher.exe file from the launcher.go. Is there anything specific to my gradle file that might be causing the launcher to error out? I'm thinking that maybe the jvmArgs could interfere, but since __app.exe works if it has the correct path, then launcher.exe should also work..

siordache commented 4 years ago

I don't see anything wrong in your build.gradle. If you can upload to GitHub an example project that allows reproducing this issue, I will investigate further.