fvarrui / JavaPackager

:package: Gradle/Maven plugin to package Java applications as native Windows, MacOS, or Linux executables and create installers for them.
GNU General Public License v3.0
1.07k stars 133 forks source link

Mac Unable to open demo.app #311

Closed YaoOOoooolin closed 1 year ago

YaoOOoooolin commented 1 year ago

I'm submitting a…

Short description of the issue/suggestion: I have generated demo.app, but when I double-click to open it, it exits without displaying. When I open it using the open command on the terminal, the same result is obtained, and no error is reported.

Please tell us about your environment:

Other information (e.g. related issues, suggestions how to fix, links for us to have context)

fvarrui commented 1 year ago

Hi @YaoOOoooolin! Could you provide a link to your demo.app, please?

YaoOOoooolin commented 1 year ago

@fvarrui https://musetransfer.com/s/om92s8nfi This is my demo. app. You can check if it can be opened. Please let me know if it's not possible. thank you

YaoOOoooolin commented 1 year ago

Hi @YaoOOoooolin! Could you provide a link to your demo.app, please?


<plugin>
<groupId>io.github.fvarrui</groupId>
<artifactId>javapackager</artifactId>
<version>1.7.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>package</goal>
</goals>
<configuration>
<mainClass>com.front.CorporateValuationFrontEnd</mainClass>
<bundleJre>true</bundleJre>
<customizedJre>false</customizedJre>
<generateInstaller>true</generateInstaller>
<administratorRequired>false</administratorRequired>
<platform>mac</platform>
<macConfig>
<macStartup>UNIVERSAL</macStartup>
</macConfig>
</configuration>
</execution>
</executions>
</plugin>


            This is my code
fvarrui commented 1 year ago

Are you using JavaFX? Could you try to run your app from the command line? ./demo.app/Contents/MacOS/universalJavaApplicationStub

fvarrui commented 1 year ago

@fvarrui https://musetransfer.com/s/om92s8nfi This is my demo. app. You can check if it can be opened. Please let me know if it's not possible. thank you

I don't have a Mac right now, but I'll try it ASAP

YaoOOoooolin commented 1 year ago

Are you using JavaFX? Could you try to run your app from the command line? ./demo.app/Contents/MacOS/universalJavaApplicationStub

Yes, I use JavaFX as my GUI. I try to open this file: Error: The JavaFX runtime component is missing and needs to be used to run this application But in my mac,I have JavaFX component

fvarrui commented 1 year ago

Ok, try adding this to the plugin config:

<vmArgs>
    <vmArg>--module-path=libs</vmArg>
    <vmArg>--add-modules=javafx.fxml,javafx.controls,javafx.graphics</vmArg>
</vmArgs>

I'm not sure about the "libs" path ... fix it to meet your needs.

or try this workaround: https://github.com/fvarrui/JavaPackager/issues/20#issuecomment-599055530

YaoOOoooolin commented 1 year ago

Ok, try adding this to the plugin config:

<vmArgs>
    <vmArg>--module-path=libs</vmArg>
    <vmArg>--add-modules=javafx.fxml,javafx.controls,javafx.graphics</vmArg>
</vmArgs>

I'm not sure about the "libs" path ... fix it to meet your needs.

or try this workaround: #20 (comment)

Thank you for your new response. I am currently continuing my efforts with your help, but have encountered a new error and reported the following error: java.io.FileNotFoundException: back_ end/demo/src/main/resources/TestData/TestData.csv (No such file or directory) Because I used to read the csv file, but now demo.app cannot read the file.

fvarrui commented 1 year ago

Well, as a resource, TestData.csv is included in the JAR ... so you shouldn't think of it as a File. I mean, you can't use the File class to access a resource. Try getClass().getResource("/TestData/TestData.csv") or getClass().getResourceAsStream("/TestData/TestData.csv") ... which better meets your needs.

EasyG0ing1 commented 1 year ago

@fvarrui

I don't have a Mac right now

This is blasphemy! ☺

EasyG0ing1 commented 1 year ago

@YaoOOoooolin

I can tell by some of what you say, that there are some core things that I believe will benefit you moving forward in Java development. These tips were earned from countless hours of frustration over various aspects of Java when trying to write apps that would just work properly.

Starting with actually starting JavaFX apps outside of the IDE (from compiled code), when modularity was introduced in Java 9, and subsequently they removed the bundled JavaFX from Oracles Java, you can only natively start JavaFX when you have a proper module-info.java file in root of your source code. The problem with that, is that JavaPackager doesn't support modules yet, and I never looked into trying to figure out how to make it work, but the short answer is that the module file isn't needed at all for apps that are not large and being developed by teams for corporate deployment etc. Us hobby devs tend to write fairly simple apps that don't need to be compiled into Fort Knox and resource optimization is of little concern when 4TB hard drives are available for less than $100 and SSDs for a little over $200 ... whats a measly 100 megabytes in contrast?

So then you have to have the program launched from a class that DOES NOT extend Application, then issue a little command, then call your Main class and go from there. The downside is that you'll get a warning message when you start the app saying that the configuration isn't supported and I have never looked into whether or not that message can be suppressed, but it's really no big deal once you get used to it.

So then here is my typical Launcher class:

import java.awt.*;

public class Launcher {
    public static void main(String[] args) {
        System.setProperty("apple.awt.UIElement", "false");
        Toolkit.getDefaultToolkit();
        Main.main(args);
    }
}

Once it calls Main, everything just keeps flowing normally and JavaFX will run from a packaged application.

It should be noted that the "false" setting keeps the Dock icon with a Mac. and changing it to "true" removes the dock icon.

Next, when you have a Resources folder, gaining access to those files when running the app from the IDE is no big deal, but when you package the app into a self contained runnable program, those same calls to those files won't always work because they have to be extracted from the .jar files first and what I noticed is that when you're running from a compiled .jar file, that process can struggle or just not work at all and then it will throw errors during run time.

So what I do instead of using class.getResource() is I use a library that will give you access to every single file in the package during runtime. I use it to first copy the files I need for the app out to a folder. If its running on mac, the folder is a hidden folder from the Users profile folder. And if Windows, then a folder that is created from the AppData environment variable.

So lets say that my app is called "GreatApp". In the MacOS environment, there would be a folder created at /Users/username/.GreatApp

And in Windows: %APPDATA%\GreatApp

From there, I merely model the folder structure from the packaged Resources folder that I created in the IDE and copy out what I need then I set universally accessible methods to required files using static context in a java class so I can get to that stuff from anywhere in the program.

This is a typical class structure that I use to accomplish this:

First, the dependencies in the pom file are these

<dependency>
    <groupId>io.github.classgraph</groupId>
    <artifactId>classgraph</artifactId>
    <version>4.8.154</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

And here is a typical class that I use:

import io.github.classgraph.ClassGraph;
import io.github.classgraph.ResourceList;
import io.github.classgraph.ScanResult;
import javafx.scene.text.Font;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Resources {

    private enum OS {
        MACOS,
        WINDOWS,
        OTHER
    }

    private static final Resources  INSTANCE   = new Resources();
    private final        ScanResult scanResult = new ClassGraph().enableAllInfo().scan();
    private final OS os;

    private Resources() {
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("mac")) {
            this.os = OS.MACOS;
        }
        else if (os.contains("win")) {
            this.os = OS.WINDOWS;
        }
        else {
            this.os = OS.OTHER;
        }
        setPaths();
    }

    private static final String version = "1.0";
    private static final String app     = "Utilities2003";
    private              Path   appFilesPath;
    private              Path   fontPath;
    private              Path stylePath;
    private              Path imgPath;

    private File buttonStyleSheet;
    private File starIconFile;
    private File monacoTTF;

    public static void manageResources(boolean devMode) {
        boolean replace = devMode;
        INSTANCE.copyResources(replace);
    }

    private void copyResources(boolean replace) {
        try {
            ResourceList resources = scanResult.getAllResources();
            for (URL url : resources.getURLs()) {
                if (url.toString().contains("ttf")) {
                    String filename    = FilenameUtils.getName(url.toString());
                    File   destination = new File(fontPath.toString(), filename);
                    if (!destination.exists() || replace) {
                        FileUtils.copyURLToFile(url, destination);
                    }
                }
                if (url.toString().contains("css")) {
                    String filename    = FilenameUtils.getName(url.toString());
                    File   destination = new File(stylePath.toString(), filename);
                    if (!destination.exists() || replace) {
                        FileUtils.copyURLToFile(url, destination);
                    }
                }
                if (url.toString().contains("png")) {
                    String filename    = FilenameUtils.getName(url.toString());
                    File   destination = new File(imgPath.toString(), filename);
                    if (!destination.exists() || replace) {
                        FileUtils.copyURLToFile(url, destination);
                    }
                }
            }
            setFilePaths();
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private void setFilePaths(){
        starIconFile = Paths.get(getIMGPath().toString(), "StarIcon.png").toFile();
        buttonStyleSheet = Paths.get(getStylePath().toString(), "buttons.css").toFile();
        monacoTTF = Paths.get(getFontPath().toString(),"Monaco.ttf").toFile();
    }

    public static File getButtonStyleSheet() {
        return INSTANCE.buttonStyleSheet;
    }

    public static File getStarIcon() {
        return INSTANCE.starIconFile;
    }

    public static Font getMonacoFont(double size) {
        return Font.loadFont("file:" + INSTANCE.monacoTTF.getAbsolutePath(), size);
    }

    public static Path getFontPath() {
        return INSTANCE.fontPath;
    }

    public static Path getStylePath() {
        return INSTANCE.stylePath;
    }

    public static Path getIMGPath() {
        return INSTANCE.imgPath;
    }

    private void setPaths() {
        String os = System.getProperty("os.name").toLowerCase();
        switch(os) {
            case MACOS -> {
                String path = System.getProperty("user.home");
                appFilesPath = Paths.get(path, ".", app);
            }
            case WINDOWS -> {
                String path = System.getenv("APPDATA");
                appFilesPath = Paths.get(path, app);
            }
        }
        fontPath  = Paths.get(appFilesPath.toString(), "Fonts");
        stylePath = Paths.get(appFilesPath.toString(), "StyleSheets");
        imgPath   = Paths.get(appFilesPath.toString(), "Images");
    }
}

This class uses the single instance model. The constructor is private, so it cannot be instantiated anywhere else in the program. But the object INSTANCE is the only object that has the only instance of the class that is ever created and since its final, it cannot be changed. And it's static so it can be referenced from a static context. The methods that provide access to the classes resources are declared as static of course, then they can reference INSTANCE and get to anything that exists within the only instance of the class.

When you call any of the static methods for the first time, INSTANCE instantiates the class which calls the class constructor. The constructor sets the final object os (of type enum OS) then it calls setPaths.

setPaths first determines what the path is going to be for the storage of the apps files based on which OS is running, then it sets the specific paths for the specific kinds of files.

Notice that setPaths loops through the list that is generated by scanResults through the resources variable. That variable literally holds the paths of every single resoource in the entire project including all dependency libraries etc. It's an exhaustive list and can contain thousands of file references. So I specifically loop through that and look for the file extensions that are in my Resources folder that I need for the app. You could also have it match the name of a subfolder off the root of the Resources folder. The point is that whatever you have it look for must be unique among all the other thousands of files it returns, but file extensions like css, png, jpeg etc. are not files that are part of a packaged Java program so they are safe to use as search terms.

Once it hits a file that I am searching for, Apache IO copies the file out to the designated path that was defined at instantiation.

So what I do is from my Main class, is I call the manageResources(boolean devMode) method and devMode is set through a command line argument. Because I'm always making changes to the files in the Resources folder, if Im still developing the app, I set that command line argument in the IDE and then when the copyResources() method is called, devMode gets passed in and if its true then it overwrites the files in the destination. Otherwise if the file already exists in the destination, it doesn't re-copy it every time the app is launched.

Then for the rest of the program, I have static methods like getMonacoFont(double size) which lets me easily get that font and size it for a JavaFX control, like a Text object or a Label, and I would use it like this:

myLabel.setFont(Resources.getMonacoFont(13));
myLabel.setStyle('fx-text-fill: rgb(255,0,0);') //Sets font to Red

Makes things really easy when you have a class like this all setup.

Prep work is your best friend.

:-)

Mike Sims

fvarrui commented 1 year ago

Hi @YaoOOoooolin! Can we mark this issue as fixed?

YaoOOoooolin commented 1 year ago

Hi @YaoOOoooolin! Can we mark this issue as fixed?

Yes Thank you very much for your two predecessors' help. My code is very chaotic and ultimately failed, but I will continue to work hard.

EasyG0ing1 commented 1 year ago

@YaoOOoooolin Patience is a virtue in Java coding and when you take things slow initially, certain things that you do end up becoming more like "muscle memory" so that over time, you get faster and faster while producing the high-quality code that use to take you a long time to generate.

Writing clear and simple-looking code is also paramount because when you go back and look at it at a later date, you will find yourself frustrated at not being able to immediately understand it when it's not organized, simplified, and using words that are relevant to the context of what you're actually doing in the code.

If you want to talk about any of your code or whatever, you can email me at: sims.mike@gmail.com

fvarrui commented 1 year ago

Thanks @EasyG0ing1 for your support on JP!!