如何减小通过“jpackage”制作的可执行文件的大小

问题描述 投票:0回答:1

我有一个

jpackage
构建脚本,它采用我的一个项目(所有项目都具有完全相同的结构)并创建一个可执行文件。该脚本可以运行并生成一个安装程序,我可以运行它来创建我的可执行文件。

但是,我创建的可执行文件太大。对于一个简单的 Microsoft Paint 克隆,它大约有 70MB 大,所以这是 磁盘空间。我不一定有任何期望,但我需要这个数字下降。 这是该项目的

module-info.java

module PaintModule
{

   requires java.base;
   requires java.desktop;

}

这是构建脚本(的一个片段)。

   private void createExecutable() throws Exception
   {
   
      System.out.println("STARTING TO RUN JPACKAGE");
   
      final var jpackageCommand =
         java.util.spi.ToolProvider
            .findFirst("jpackage")
            .orElseThrow()
            ;
   
      final String executableName = prompt("Enter your executable name");
      final String description = prompt("Enter the description for your executable");
   
      jpackageCommand
         .run
         (
            System.out,
            System.err,
            "--verbose",
            "--type", "msi",
            "--name", STR."\{executableName}",
            "--install-dir", STR."davidalayachew_applications/\{executableName}",
            "--vendor", "David Alayachew",
            "--module-path", STR."\{this.projectDirectory.resolve(Path.of("run", "executable", "jar")).toString()}",
            "--module", STR."\{this.moduleName}/\{this.packageName}.Main",
            "--win-console",
            "--win-dir-chooser",
            "--win-shortcut",
            "--win-shortcut-prompt",
            "--java-options", "--enable-preview",
            "--dest", STR."\{this.projectDirectory.resolve(Path.of("run", "executable", "installer")).toString()}",
            "--description", STR."\{description}"
         )
         ;
   
   }

如您所见,我的项目是模块化的。我正在 Java 22 上运行。我想使用标准库附带的基本工具(

jpackage
jlink
等)使文件大小尽可能小。关于如何缩小这个尺寸有什么见解吗?

此外,这个项目没有任何资源或任何东西。这些都是基本的 Swing 代码。而且代码本身还不到100KB。我为 jar 创建命令打开了详细日志记录——除了我的

.class
文件和清单之外什么都没有被捆绑到 jar 中。需要明确的是,我将模块化 jar 位置作为我的模块路径。有一次,我认为问题出在我从 jar 构建安装程序这一事实上,但有问题的 jar 是 37KB。值得怀疑。

我可以做什么来降低这个数字?最好的情况是 <10MB, but I will take whatever drops that number down. I don't need an installer or any of that other stuff, I just want this application to run on a windows machine.

而且,我怀疑它有帮助,但这里是完整的构建脚本,以防您想自己运行它。


import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

public class CREATE_EXECUTABLE_FROM_PROJECT
{

   /** I am expecting there to be an environment variable on the machine for this key. If not, add it. */
   private static final int CURRENT_JAVA_VERSION = Integer.parseInt(System.getenv("CURRENT_JAVA_VERSION"));

   /** This is the folder that all of my projects are in. */
   private final Path workingDirectory = Path.of("C:", "Users", "david", "_WORKSPACE", "_PROGRAMMING", "_JAVA").toAbsolutePath();
   private final Path projectDirectory;
   private final Path sourceCodeDirectory;
   private final String projectName;
   private final String moduleName;
   private final String packageName;

   public CREATE_EXECUTABLE_FROM_PROJECT() throws Exception
   {
   
      final JFileChooser projectChooser = new JFileChooser(this.workingDirectory.toFile());
      projectChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
      projectChooser.setDialogTitle("Choose a project folder");
   
      var userResponse = projectChooser.showOpenDialog(null);
   
      if (JFileChooser.APPROVE_OPTION != userResponse)
      {
      
         throw new IllegalArgumentException("User did not press approve, so taking no action.");
         
      }
   
      this.projectDirectory = projectChooser.getSelectedFile().toPath().toAbsolutePath();
   
      this.sourceCodeDirectory =
         this
            .projectDirectory
            .resolve("src")
            .resolve("main")
            .resolve("java")
            .toAbsolutePath()
            ;
   
      this.projectName = this.projectDirectory.getFileName().toString();
      this.moduleName = this.projectName + "Module";
      this.packageName = this.projectName + "Package";
   
   }

   public static void main(final String[] args) throws Exception
   {
   
      enum ExecutionOption
      {
      
         COMPILE_CODE,
         RUN_CODE,
         CREATE_JAR,
         RUN_JAR,
         CREATE_EXECUTABLE,
         ;
      
      }
   
      final CREATE_EXECUTABLE_FROM_PROJECT exeCreator;
   
      CHOOSE_PROJECT:
      {
      
         try
         {
         
            exeCreator = new CREATE_EXECUTABLE_FROM_PROJECT();
         
         }
         
         catch (final Exception exception)
         {
         
            System.out.println(exception.getMessage());
            return;
         
         }
      
      }
   
      final java.util.EnumMap<ExecutionOption, Boolean> map = new java.util.EnumMap<>(ExecutionOption.class);
   
      CHOOSE_EXECUTION_OPTIONS:
      {
      
         final var list = new javax.swing.JPanel(new java.awt.GridLayout(0, 1));
         
         final var instructions =
            new javax.swing.JLabel("<html>Choose your execution options.<br>They will occur in numerical order.</html>");
         
         list.add(instructions);
      
         for (var option : ExecutionOption.values())
         {
         
            final String label = "<html>" + (option.ordinal() + 1) + "\t" + option.name().replaceAll("_", " ") + "</html>";
         
            final var checkBox = new javax.swing.JCheckBox(label,  true);
         
            map.put(option, true);
         
            checkBox.addActionListener(_ -> map.put(option, checkBox.isSelected()));
         
            list.add(checkBox);
         
         }
      
         final var userResponse =
            JOptionPane 
            .showConfirmDialog
            (
               null, 
               list, 
               "Choose your execution options.",
               JOptionPane.OK_CANCEL_OPTION
            )
            ;
      
         if (JOptionPane.OK_OPTION != userResponse || map.entrySet().stream().noneMatch(entry -> entry.getValue()))
         {
         
            System.out.println("User did not select an execution option.");
         
            return;
         
         }
      
      }
   
      CONFIRM_BEFORE_RUNNING:
      {
      
         final int userResponse =
            JOptionPane
            .showConfirmDialog
            (
               null,
               STR.
                  """
                  <html>
                     Are you sure you want to create an executable for \{exeCreator.projectName}?<br>
                     projectDirectory = \{exeCreator.projectDirectory}<br>
                     projectName = \{exeCreator.projectName}<br>
                     moduleName = \{exeCreator.moduleName}<br>
                     packageName = \{exeCreator.packageName}
                  </html>
                  """
            )
            ;
      
         if (userResponse != JOptionPane.YES_OPTION)
         {
         
            return;
         
         }
      
      }
      
      EXECUTE_OPTIONS:
      for (var entry : map.entrySet())
      {
      
         if (false == entry.getValue())
         {
         
            continue EXECUTE_OPTIONS;
         
         }
      
         var option = entry.getKey();
         
         switch (option)
         {
         
            case COMPILE_CODE       -> exeCreator.compileCode();
            case RUN_CODE           -> exeCreator.runCode();
            case CREATE_JAR         -> exeCreator.createJar();
            case RUN_JAR            -> exeCreator.runJar();
            case CREATE_EXECUTABLE  -> exeCreator.createExecutable();
         
         }
      
      }
   
   }

   private static String prompt(final String question)
   {
   
      String response = "";
   
      do
      {
      
         response = JOptionPane.showInputDialog(null, question);
      
      }
      
      while (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(null, STR."Please confirm that this is correct -- {\{response}}"));
   
      return response;
   
   }

   private void compileCode() throws Exception
   {
   
      System.out.println("STARTING TO COMPILE MODULAR SOURCE CODE");
   
      final JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
   
      Objects.requireNonNull(javac);
   
      final StandardJavaFileManager standardJavaFileManager = javac.getStandardFileManager(null, null, null);
   
      final var compilerOptions =
         List
            .of
            (
               // STR."--module-source-path=\{mainModulePath}",
               STR."--module-source-path=\{this.sourceCodeDirectory.toString()}",
               STR."--module-path=\{this.workingDirectory.toString()}",
               STR."--module=\{this.moduleName}",
               "--add-modules=com.formdev.flatlaf",
               "--enable-preview",
               "--source=" + CURRENT_JAVA_VERSION,
               "-d", STR."\{this.projectDirectory.resolve("classes").toString()}"
            )
            ;
   
      final var compilationTask = javac.getTask(null, null, null, compilerOptions, null, null);
   
      compilationTask.call();
   
   }

   private void runCode() throws Exception
   {
   
      System.out.println("STARTING TO RUN MODULAR SOURCE CODE");
   
      final ProcessBuilder processBuilder =
         new
            ProcessBuilder
            (
               "java",
               "--enable-preview",
               STR."--patch-module=\{this.moduleName}=src/main/resources",
               "--module-path=classes;..",
               STR."--module=\{this.moduleName}/\{this.packageName}.Main"
            )
            .inheritIO()
            .directory(this.projectDirectory.toFile())
            ;
   
      final Process process = processBuilder.start();
   
      final int exitCode = process.waitFor();
   
      System.out.println("exitCode = " + exitCode);
   
      if (exitCode != 0)
      {
      
         throw new IllegalArgumentException("Failure");
      
      }
   
   }

   private void createJar() throws Exception
   {
   
      System.out.println("STARTING TO BUILD A MODULAR JAR");
   
      final var jarCommand =
         java.util.spi.ToolProvider
            .findFirst("jar")
            .orElseThrow()
            ;
   
      final Path pathToJar = this.projectDirectory.resolve(Path.of("run", "executable", "jar", this.projectName));
   
      final Path pathToResources = this.projectDirectory.resolve(Path.of("src", "main", "resources"));
      
      final Path pathToModuleClassFiles = this.projectDirectory.resolve("classes").resolve(this.moduleName);
   
      jarCommand
         .run
         (
            System.out,
            System.err,
            "--verbose",
            "--create",
            STR."--file=\{pathToJar.toString()}.jar",
            STR."--main-class=\{this.packageName}.Main",
            "-C", STR."\{pathToModuleClassFiles.toString()}", ".",
            "-C", STR."\{pathToResources.toString()}", "."
         )
         ;
   
   }

   private void runJar() throws Exception
   {
   
      System.out.println("STARTING TO RUN A MODULAR JAR");
   
      final ProcessBuilder processBuilder =
         new
            ProcessBuilder
            (
               "java",
               "--enable-preview",
               "--module-path=run/executable/jar",
               STR."--module=\{this.moduleName}/\{this.packageName}.Main"
            )
            .inheritIO()
            .directory(this.projectDirectory.toFile())
            ;
   
      final Process process = processBuilder.start();
   
      final int exitCode = process.waitFor();
   
      System.out.println("exitCode = " + exitCode);
   
   }

   private void createExecutable() throws Exception
   {
   
      System.out.println("STARTING TO RUN JPACKAGE");
   
      final var jpackageCommand =
         java.util.spi.ToolProvider
            .findFirst("jpackage")
            .orElseThrow()
            ;
   
      final String executableName = prompt("Enter your executable name");
      final String description = prompt("Enter the description for your executable");
   
      jpackageCommand
         .run
         (
            System.out,
            System.err,
            "--verbose",
            "--type", "msi",
            "--name", STR."\{executableName}",
            "--install-dir", STR."davidalayachew_applications/\{executableName}",
            "--vendor", "David Alayachew",
            "--module-path", STR."\{this.projectDirectory.resolve(Path.of("run", "executable", "jar")).toString()}",
            "--module", STR."\{this.moduleName}/\{this.packageName}.Main",
            "--win-console",
            "--win-dir-chooser",
            "--win-shortcut",
            "--win-shortcut-prompt",
            "--java-options", "--enable-preview",
            "--dest", STR."\{this.projectDirectory.resolve(Path.of("run", "executable", "installer")).toString()}",
            "--description", STR."\{description}"
         )
         ;
   
   }

}

java executable filesize jlink jpackage
1个回答
0
投票

java.base
java.desktop
是大模块。
java.base
在 Windows 中约为 21 MB,在 Linux 中约为 24 MB。
java.desktop
在 Windows 中约为 12 MB,在 Linux 中约为 14 MB。因此,仅模块就构成了 33–38 MB。

除此之外,还需要添加本机库,总共大约 20-30 MB。其中大部分是 jvm.so 或 jvm.dll。

因此,使用

java.desktop
的应用程序映像已经至少 50 MB,不包括应用程序类和资源。 (这些数字会因不同的 Java 版本和平台而异。)

通过将

--jlink-options --compress=2
添加到
jpackage
命令中,您可能会获得稍微更好的结果,但在实践中,我发现这并没有太大区别。 (我依稀记得读过 jmod 和图像模块 blob 已经包含嵌入式 zip 格式的数据。)

这可能看起来不守规矩,但大多数大型非 Java 应用程序都是同样的方式。有时他们使用系统上已安装的库,但通常不这样做,在这种情况下,它们的大小将与 Java 应用程序映像相当。

尽管我很喜欢轻量级可执行文件的想法,但对于现代应用程序来说 70 MB 可能是正常的。

© www.soinside.com 2019 - 2024. All rights reserved.