JME3 Create an exe file - doesn't work

If anyone is interested, my solution was to create a exe that simply uses the c jni interface to spawn a jvm and run my application. This also allows for telling Windows that you want to use the high performance GPU when available (on systems such as laptops that have both an integrated and non-integrated).

// WindowsOutsideLauncher.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <windows.h>
#include <signal.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <jni.h>
#include <string>
#include <vector>
#include <algorithm>

#include "zip.h"
#include "zip.c"
#include "miniz.h"

//Define constants
//These will be passed in by the compiler
//Values will come from OutsideClient to ensure they are correct
#ifndef MAIN_JAR
#define MAIN_JAR "OutsideClient.jar"
#endif

#ifndef MAIN_CLASS
#define MAIN_CLASS "io/tlf/outside/client/Main"
#endif

using namespace std;


// high performance gpu for nvidia and amd
extern "C"
{
__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}

//Vars for parsing manifest
char *manifest;
int manifestSize;
std::map <std::string, std::string> manifestProperties;

//Manifest functions
void getProperties() {
    std::string manifestString = std::string(manifest, manifestSize);
    std::stringstream iss(manifestString);

    std::string key;
    std::string value;
    std::string line;
    bool first = true;

    while (std::getline(iss, line)) {
        if (!line.empty() && *line.rbegin() == '\r') {
            line.erase(line.length() - 1, 1);
        }
        if (line.find(' ') != 0) {
            if (line.find(':') > line.length() || line.find(':') <= 0) {
                break;
            }
            //std::cout << "Parsing line: " << line << std::endl;
            if (!first) {
                manifestProperties.insert(std::pair<std::string, std::string>(key, value));
            }
            value = line.substr(line.find(':') + 2, line.length());
            key = line.substr(0, line.find(':'));
            //std::cout << "Added: " << key << " : " << value << std::endl;
            first = false;
        } else {
            //std::cout << "Line continues: " << line << std::endl;
            //std::cout << "Value: " << value << std::endl;
            value.append(line);
        }
    }
    //std::cout << "Key: " << key << std::endl;
    //std::cout << "Value: " << value << std::endl;
    manifestProperties.insert(std::pair<std::string, std::string>(key, value));
}

void readManifest(std::string jarFile) {

    size_t bufsize;

    struct zip_t *zip = zip_open(jarFile.c_str(), 0, 'r');
    {
        std::cout << "File open" << std::endl;
        zip_entry_open(zip, "META-INF/MANIFEST.MF");
        {
            bufsize = zip_entry_size(zip);
            manifest = static_cast<char *>(calloc(sizeof(char), bufsize));

            zip_entry_noallocread(zip, (void *) manifest, bufsize);
            manifestSize = static_cast<int>(bufsize);
        }
        zip_entry_close(zip);
    }
    zip_close(zip);
    std::cout << "File close" << std::endl;
    getProperties();
    std::cout << "Manifest parsed!" << std::endl;
}

void reportJvmCrash() {
    ofstream myfile;
    myfile.open ("crash_test.txt");
    myfile << "The JVM Crashed!\n";
    myfile.close();
}

void SignalHandler(int signal)
{
    printf("Signal %d",signal);
    throw "JVM Crashed";
}

//Main function
int main(int argc, char **argv) {
    typedef void (*SignalHandlerPointer)(int);
    SignalHandlerPointer previousHandler;
    previousHandler = signal(SIGSEGV , SignalHandler);

    SetDllDirectory("./jvm/bin/server");
    SetEnvironmentVariable("JAVA_HOME", "./jvm");
    SetEnvironmentVariable("OUTSIDE_EXE", argv[0]);
    cout << "Launched from exe: " << argv[0] << endl;
    cout << "Preparing to launch Outside Client" << endl;
    JavaVM *jvm; // Pointer to the JVM (Java Virtual Machine)
    JNIEnv *env; // Pointer to native interface
    vector<char *> jvmArgs;
    bool classpathSet = false;
    char *mainClass = (char *) MAIN_CLASS;
    char *jarFile;

    JavaVMInitArgs vm_args; // Initialization arguments

    for (int i = 1; i < argc; i++) {
        cout << "JVM Option " << i << ": " << argv[i] << endl;
        char *opt;
        //Check if this is the -jar argument, if so we convert to a classpath argument
        if (strcmp(argv[i], "-jar") == 0) {
            opt = (char *) "-Djava.class.path=";
            jarFile = argv[i + 1];
            char *full_text;
            full_text = (char *) malloc(strlen(opt) + strlen(argv[i + 1]) + 1);
            strcpy(full_text, opt);
            strcat(full_text, jarFile);
            opt = full_text;
            jvmArgs.push_back(opt);
            classpathSet = true;
            cout << "Jar override to classpath: " << opt << endl;
            i++;
        } else {
            //Check if this is a -Djava.class.path override
            char *search = (char *) "-Djava.class.path=";
            if (strncmp(argv[i], search, strlen(search)) == 0) {
                classpathSet = true;
                //Get name of jar file
                jarFile = (char *) malloc(strlen(argv[i]) - strlen(search) + 1);
                strncpy(argv[i] + strlen(search), jarFile, strlen(argv[i]) - strlen(search));
            }
            //For all arguments, we will store them
            opt = argv[i];
            jvmArgs.push_back(opt);
        }
    }

    //Set -XX:+UseShenandoahGC
    jvmArgs.push_back("-XX:+UseShenandoahGC");

    if (!classpathSet) {
        //Build string for default branded jar
        char *optSub = (char *) "-Djava.class.path=";
        char *opt = (char *) malloc(strlen(optSub) + strlen(MAIN_JAR));
        jarFile = (char *) MAIN_JAR;
        strcpy(opt, optSub);
        strcat(opt, MAIN_JAR);
        jvmArgs.push_back(opt);
    }

    //Store jvm optopns
    JavaVMOption *options = new JavaVMOption[jvmArgs.size()]; // JVM invocation options
    for (unsigned int i = 0; i < jvmArgs.size(); i++) {
        options[i].optionString = jvmArgs[i];
    }

    vm_args.version = JNI_VERSION_10;             // minimum Java version
    vm_args.nOptions = jvmArgs.size();                          // number of options
    vm_args.options = options;
    vm_args.ignoreUnrecognized = false;     // invalid options make the JVM init fail

    cout << "Creating jvm" << endl;
    jint rc = JNI_CreateJavaVM(&jvm, (void **) &env, &vm_args);  // Build JVM
    delete options;  // we then no longer need the initialisation options.

    if (rc != JNI_OK) {
        cerr << "Failed to create jvm" << endl;
        cin.get();
        exit(EXIT_FAILURE);
    }

    cout << "JVM load succeeded";

    //Read jar
    readManifest(jarFile);
    //Swap out the '.' for '/'
    string mainClassStr = manifestProperties["Main-Class"];
    std::replace(mainClassStr.begin(), mainClassStr.end(), '.', '/');
    mainClass = (char *) mainClassStr.c_str();
    std::cout << "Main Class: " << manifestProperties["Main-Class"] << std::endl;

    jclass cls2 = env->FindClass(mainClass); // try to find the class
    if (cls2 == nullptr) {
        cerr << "ERROR: class not found: " << mainClass << endl;
    } else { // if class found, continue
        cout << "Class Main found" << endl;
        jmethodID mid = env->GetStaticMethodID(cls2, "main", "([Ljava/lang/String;)V");
        if (mid == nullptr)
            cerr << "ERROR: Main method not found" << endl;
        else {
            cout << "Main found, loading outside" << endl;
            jobjectArray arr = env->NewObjectArray(
                    0, // constructs java array of 0
                    env->FindClass("java/lang/String"), // Strings
                    env->NewStringUTF("")
            ); // each initialized with value "str"

            try {
                env->CallStaticVoidMethod(cls2, mid, arr); // call the method with the arr as argument.
                env->DeleteLocalRef(arr); // release the object
            } catch (char *e) {
                cerr << "ERROR: JVM has crashed!" << endl;
                reportJvmCrash();
            }
        }
    }

    jvm->DestroyJavaVM();
    return 0;
}

Resource file: WindowsOutsideLauncher.rc
Note: You will need to modify the path to the icon relative to the resource file.

// Icons
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
128           ICON                    "..\\dist\\outside.ico"

Gradle file (Note: this is setup for my project, it will take some tweaks for whoever uses it)

plugins {
    id 'cpp-application'
    //
    id "de.undercouch.download" version "$gradleDownloadPluginVersion"
}

apply from: "$rootDir/common.gradle"

ext {
    exeName = "OutsideClient.exe"
    mainJarMacro = rootProject.project(":modules:io.tlf.outside.client").clientJarName
    mainClassMacro = rootProject.project(":modules:io.tlf.outside.client").clientMainClass
    libDir = new File(projectDir, "lib")
}

task makeLibDir {
    doLast {
        mkdir libDir
    }
}

task cleanLibDir {
    doLast {
        delete files(libDir)
    }
}

task cloneZip(dependsOn: makeLibDir) {
    doLast {
        if (!file("$libDir/zip").exists()) {
            exec {
                workingDir "${libDir}"
                commandLine 'git', 'clone', '-b', 'v0.2.0', 'https://github.com/kuba--/zip'
            }
        }
    }
}

application {
    File jdk32 = new File(libDir, 'jdk32')
    //println new File(jdk32, "include").getPath()
    File jdk64 = new File(libDir, 'jdk64')
    //println new File(jdk64, "include").getPath()
    File zip = new File(libDir, 'zip')

    //cpp
    baseName = exeName.replaceAll('.exe$', '') //Remove the exe from the end as the compiler adds it
    targetMachines = [machines.windows.x86, machines.windows.x86_64]

    binaries.configureEach(CppExecutable) { binary ->
        // Define a preprocessor macro for every binary
        compileTask.get().macros.put("NDEBUG", null)

        // Define a compiler options
        compileTask.get().compilerArgs.add '-W3'

        //Set correct jdk based on build arch
        def jdk
        switch (compileTask.get().getTargetPlatform().get().architecture.name) {
            case "x86-64":
                jdk = jdk64
                break
            case "x86":
            default:
                jdk = jdk32
        }

        // Define toolchain-specific compiler options
        if (toolChain in VisualCpp) {
            //==== Compiler
            compileTask.get().compilerArgs.add('/v')
            compileTask.get().compilerArgs.add('/Zi')
            compileTask.get().compilerArgs.add('/D _CRT_SECURE_NO_WARNINGS')
            compileTask.get().compilerArgs.add("/D MAIN_JAR=$mainJarMacro")
            compileTask.get().compilerArgs.add("/D MAIN_CLASS=$mainClassMacro")
            compileTask.get().compilerArgs.add('-I' + new File(zip, "src").getPath())
            compileTask.get().compilerArgs.add('-I' + new File(jdk, "include").getPath())
            compileTask.get().compilerArgs.add('-I' + new File(jdk, "include/win32").getPath())

            //==== Linker
            linkTask.get().linkerArgs.add(new File(jdk, "lib/jvm.lib").getPath())

            //Link DLL on demand in application. Allows launcher to select where the jre is at run time
            linkTask.get().linkerArgs.add('/DELAYLOAD:jvm.dll')
            linkTask.get().linkerArgs.add('Delayimp.lib')

            //Hide console window
            if (releaseType == 'release') {
                linkTask.get().linkerArgs.add('/SUBSYSTEM:windows')
                linkTask.get().linkerArgs.add('/ENTRY:mainCRTStartup')
            }

            def compileResources = tasks.register("compileResources${binary.name.capitalize()}", WindowsResourceCompile) { wrc ->
                wrc.targetPlatform = binary.compileTask.get().targetPlatform
                wrc.toolChain = binary.toolChain
                wrc.includes.from file("src/main/headers")
                wrc.compilerArgs.add "/v"

                wrc.macros.put("OUTSIDE_VERSION_STRING", versionString)
                wrc.macros.put("OUTSIDE_VERSION_ID_STRING", versionId.toString())
                wrc.macros.put("OUTSIDE_EXE_NAME", binary.name)
                wrc.macros.put("OUTSIDE_PRODUCT_NAME", "The Outside Engine")
                wrc.macros.put("OUTSIDE_COMPANY_NAME", "Liquid Crystal Studios")

                wrc.source.from fileTree(dir: "src/main/rc", includes: ["**/*.rc"])
                wrc.outputDir = layout.buildDirectory.dir("${binary.name}").get().asFile
            }

            linkTask.get().configure {
                dependsOn compileResources
                source.from compileResources.map({ return fileTree(dir: it.outputDir, includes: ["**/*.res", "**/*.obj"]) })
                linkerArgs.add "user32.lib"
            }
        }
    }
}

model {
    toolChains {
        vc(VisualCpp) {
            if (new File("C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools").exists()) {
                installDir "C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools"
            }
        }
    }
}

task setupJdk64() {
    doLast {
        File jvmZip = new File(libDir, 'jdk64.zip')
        if (!jvmZip.exists()) {
            download {
                src 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x64_windows_hotspot_17_35.zip'
                dest jvmZip
                overwrite false
            }
        }
        File jdkDir = new File(libDir, 'jdk64')
        if (!jdkDir.exists()) {
            copy {
                from zipTree(jvmZip)
                into jdkDir
            }
            File extracted = jdkDir.listFiles()[0] //Get the contents of the jvm folder that was created.
            copy {
                from extracted
                into jdkDir
            }
            extracted.deleteDir()
        }
    }
}

task setupJdk32() {
    doLast {
        File jvmZip = new File(libDir, 'jdk32.zip')
        if (!jvmZip.exists()) {
            download {
                src 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x86-32_windows_hotspot_17_35.zip'
                dest jvmZip
                overwrite false
            }
        }
        File jdkDir = new File(libDir, 'jdk32')
        if (!jdkDir.exists()) {
            copy {
                from zipTree(jvmZip)
                into jdkDir
            }
            File extracted = jdkDir.listFiles()[0] //Get the contents of the jvm folder that was created.
            copy {
                from extracted
                into jdkDir
            }
            extracted.deleteDir()
        }
    }
}

task subDist64(dependsOn: ['setupJdk64', 'assembleReleaseX86-64', ':copyDist']) {
    doLast {
        File jvmZip = new File(libDir, 'jdk64.zip') //TODO: Make this jre64.zip
        if (!jvmZip.exists()) {
            download {
                src 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x64_windows_hotspot_17_35.zip'
                dest jvmZip
                overwrite false
            }
        }
        File jreDir = new File(libDir, 'jre64')
        if (!jreDir.exists()) {
            copy {
                from zipTree(jvmZip)
                into jreDir
            }
            File extracted = jreDir.listFiles()[0] //Get the contents of the jvm folder that was created.
            copy {
                from extracted
                into jreDir
            }
            extracted.deleteDir()
        }

        //Clean
        File clientWin = new File("$buildDir\\client_win64")

        //Copy client
        copy {
            from new File("${rootProject.buildDir}\\client_dist")
            into clientWin
        }

        //Copy jvm
        copy {
            from new File(libDir, 'jre64')
            into new File(clientWin, 'jvm')
        }

        copy {
            from new File(buildDir, "exe\\main\\release\\x86-64\\$exeName")
            into clientWin
        }

        copy {
            from('src/main/dist')
            into clientWin
        }
    }
}

task subDist32(dependsOn: ['setupJdk32', 'assembleReleaseX86', ':copyDist']) {
    doLast {
        File jvmZip = new File(libDir, 'jdk32.zip') //TODO: Make this jre32.zip
        if (!jvmZip.exists()) {
            download {
                src 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x86-32_windows_hotspot_17_35.zip'
                dest jvmZip
                overwrite false
            }
        }
        File jreDir = new File(libDir, 'jre32')
        if (!jreDir.exists()) {
            copy {
                from zipTree(jvmZip)
                into jreDir
            }
            File extracted = jreDir.listFiles()[0] //Get the contents of the jvm folder that was created.
            copy {
                from extracted
                into jreDir
            }
            extracted.deleteDir()
        }

        //Clean
        File clientWin = new File("$buildDir\\client_win32")

        //Copy client
        copy {
            from new File("${rootProject.buildDir}\\client_dist")
            into clientWin
        }

        //Copy jvm
        copy {
            from new File(libDir, 'jre32')
            into new File(clientWin, 'jvm')
        }

        copy {
            from new File(buildDir, "exe\\main\\release\\x86\\$exeName")
            into clientWin
        }

        copy {
            from('src/main/dist')
            into clientWin
        }
    }
}

tasks.whenTaskAdded { task ->
    if (task.name == 'compileReleaseX86Cpp') {
        task.dependsOn setupJdk32
        task.dependsOn cloneZip
    }
    if (task.name == 'compileDebugX86Cpp') {
        task.dependsOn setupJdk32
        task.dependsOn cloneZip
    }
    if (task.name == 'compileReleaseX86-64Cpp') {
        task.dependsOn setupJdk64
        task.dependsOn cloneZip
    }
    if (task.name == 'compileDebugX86-64Cpp') {
        task.dependsOn setupJdk64
        task.dependsOn cloneZip
    }
}

This does not create an installer. You install directory will need to contain the following files:

  1. You jar file and all associated files for your application
  2. The exe generated to launch the jar
  3. The jvm folder containing the JVM.

With this, you develop your application like you normally would, then just use the exe to launch your application.

Example:

I hope this helps, if nothing else you might get an idea on building your own wrapper utility.
~Trevor

3 Likes

If you do not want an installer you can disable it with generateInstaller = false.

Just in case, this is my java packager config that I use to package a test app for all platforms.

javapackager {
    // common configuration
    // mandatory
    mainClass = mainClassName
    // optional
    bundleJre = true
    customizedJre = true
    generateInstaller = false
    administratorRequired = false
    additionalResources = files('blue-cube.l4j.ini', 'src/dist/config', 'src/dist/assets', 'src/dist/updates').asList()
    version = ""
    name = "blue-cube"
    displayName = "blue-cube"
    //vmArgs = applicationDefaultJvmArgs //loaded via l4j.ini file

    classpath = "libs/*;assets;config"
    manifest {
        additionalEntries = [
                'Class-Path': '' // clear manifest classpath as I am going to pass it via -cp command from start script.
        ]
    }

    linuxConfig {
        pngFile = file('icon.png')
        // Wraps JAR file inside the executable if true
        wrapJar = true
    }

    macConfig {
        icnsFile = file('icon.icns')
        relocateJar = false
    }

    winConfig {
        icoFile = file('icon.ico')
        // Wrap JAR file in native EXE
        wrapJar = true
    }
}

task packageForLinux(type: io.github.fvarrui.javapackager.gradle.PackageTask, dependsOn: build) {
    platform = io.github.fvarrui.javapackager.model.Platform.linux
    createTarball = true
    //jdkPath = file('X:\\path\to\linux\jdk')
    outputDirectory = file("$buildDir/bundles/linux-x64")
    additionalResources = javapackager.additionalResources + [file('blue-cube.desktop')]
}

task packageForLinuxArm(type: io.github.fvarrui.javapackager.gradle.PackageTask, dependsOn: build) {
    platform = io.github.fvarrui.javapackager.model.Platform.linux
    createTarball = true
    jdkPath = file('/home/ali/opt/jvm/jdk-14.0.2+12-linux-aarch64')
    outputDirectory = file("$buildDir/bundles/linux-aarch64")
}

task packageForMac(type: io.github.fvarrui.javapackager.gradle.PackageTask, dependsOn: build) {
    platform = io.github.fvarrui.javapackager.model.Platform.mac
    createTarball = true
    vmArgs = ['-XstartOnFirstThread']
    jdkPath = file('/home/ali/opt/jvm/jdk-14.0.2+12-mac-x64/jdk-14.0.2+12/Contents/Home/')
    outputDirectory = file("$buildDir/bundles/mac-x64")
}

task packageForWindows(type: io.github.fvarrui.javapackager.gradle.PackageTask, dependsOn: build) {
    platform = io.github.fvarrui.javapackager.model.Platform.windows
    createZipball = true
    jdkPath = file('/home/ali/opt/jvm/jdk-17.0.2+8-win-x64')
    outputDirectory = file("$buildDir/bundles/win-x64")
}

task packageMyApp(dependsOn: [ 'packageForLinux', 'packageForLinuxArm', 'packageForMac', 'packageForWindows'])

3 Likes

Thanks a lot!

1 Like

You’re welcome!

Provided updated steps to bundle JRE with JPackage using Java 14+

Package Runtime with Jar File for Distribution in Steam

1 Like