Files
jpackage-maven-plugin/README.md
T
zkhandzkh 44f6205f89 feat: 初始化 jpackage-maven-plugin 通用原生打包插件
基于 OpenJDK 16+ 官方 jpackage 工具,将任意标准 Java 应用打包为内嵌最小运行时的
跨平台自包含应用镜像(app-image 绿色免安装版),不绑定任何应用框架。

核心能力:
- 零配置:自动从主 JAR 的 META-INF/MANIFEST.MF 探测 Main-Class
- 环境定位:优先 ${java.home}/bin/jpackage,兼容 JRE 布局、JAVA_HOME 与 PATH
- 输入目录隔离:使用独立的 target/jpackage-input,避免编译产物被误打包
- 版本清洗:1.0.0-SNAPSHOT 等 Maven 版本号自动转为 jpackage 合法版本
- 编码防御:默认注入全套 UTF-8 运行参数,规避跨平台中文乱码
- 平台自适应:--win-console 仅在 Windows 目标平台传递
- 启动脚本:生成 run.sh(755)与 CRLF 换行的 run.bat(含 chcp 65001)
- Wine 交叉打包:支持在 Linux 上借助 Wine 产出 Windows 应用镜像

交付内容:
- 插件工程与面向 Maven Central 的完整 pom 配置
- 核心 Mojo 与 10 个 internal 工具类
- 103 个单元测试用例
- examples/hello-cli 可运行示例与中文 README

Generated with AI

Co-Authored-By: zkh <zkh@mirenqinggege.com>
2026-10-14 14:46:44 +08:00

461 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# jpackage-maven-plugin
> 通用原生打包 Maven 插件:基于 OpenJDK 16+ 官方 `jpackage` 工具,把**任意标准 Java 应用**打包成
> 内嵌最小运行时的**跨平台自包含应用镜像(App Image / 绿色免安装版)**。
> 目标机器无需安装 Java、无需配置环境变量,解压即用。
- **不绑定任何框架**:不依赖 Spring Boot / JavaFX / Quarkus,只遵循纯 Java 与 Maven 标准;
- **智能开箱即用**:自动从主 JAR 的 `META-INF/MANIFEST.MF` 探测 `Main-Class`,绝大多数项目
只需引入插件即可打包;
- **跨平台自适应**:自动识别 Windows / Linux / macOS,按平台组装参数并生成对应的启动脚本。
---
## 目录
1. [环境要求](#环境要求)
2. [快速开始(零配置)](#快速开始零配置)
3. [输出结构](#输出结构)
4. [POM 配置示例](#pom-配置示例)
5. [完整参数说明](#完整参数说明)
6. [平台行为差异](#平台行为差异)
7. [在 Linux 上交叉打包 Windows 应用(Wine)](#在-linux-上交叉打包-windows-应用wine)
8. [常见问题](#常见问题)
9. [插件工程自身构建](#插件工程自身构建)
---
## 环境要求
| 项目 | 要求 |
| --- | --- |
| 运行 Maven 的 JDK | **JDK 16 及以上**(`jpackage` 自 JDK 14 引入,16 起稳定;建议 17/21 LTS) |
| Maven | 3.6.3 及以上 |
| 打包目标平台 | 必须与构建主机平台一致(除非使用 [Wine 交叉打包](#在-linux-上交叉打包-windows-应用wine)) |
`jpackage` 只能产出**当前所在平台**的可执行程序,这是 JDK 官方工具的限制:
- 在 Linux 上打包 → 得到 Linux 应用镜像;
- 在 Windows 上打包 → 得到 Windows 应用镜像;
- 在 macOS 上打包 → 得到 macOS 应用镜像。
需要多平台产物时,请在对应平台的 CI Runner 上分别构建。
## 快速开始(零配置)
只要项目能打出带 `Main-Class` 的 JAR,引入插件即可:
```xml
<build>
<plugins>
<plugin>
<groupId>com.mirenqinggege.maven</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>native-package</id>
<goals>
<goal>package-app</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
```
执行:
```bash
mvn clean package
```
插件会自动完成:定位 `jpackage` → 建立纯净输入目录 → 探测 `Main-Class` → 清洗版本号 →
组装命令行 → 调用 `jpackage` → 生成启动脚本 → 清理临时目录。
> 如果没有 `Main-Class` 且未配置 `<mainClass>`,插件会打印告警并交由 `jpackage`
> 自行从清单读取;此时若构建失败,请显式配置 `<mainClass>`。
## 输出结构
默认产物位于 `target/dist/<appName>/`:
```
target/dist/hello-cli/
├── bin/
│ └── hello-cli # jpackage 生成的平台可执行文件
├── lib/
│ ├── runtime/ # 内嵌的最小 JRE(jlink 裁剪)
│ ├── app/
│ │ ├── hello-cli.jar # 主 JAR 与全部运行时依赖
│ │ └── hello-cli.cfg # jpackage 启动配置
│ └── ...
├── run.sh # 插件生成的启动脚本(Linux / macOS,755 权限)
└── hello-cli.desktop # 插件生成的桌面入口(仅 Linux)
```
Windows 平台对应生成 `run.bat`。
整个 `<appName>` 目录即为绿色免安装版:压缩后拷贝到目标机器解压,直接执行 `run.sh`
(或 `run.bat`)即可运行。
## POM 配置示例
### 示例一:命令行工具(Linux 产物 + 自定义 JVM 参数)
```xml
<plugin>
<groupId>com.mirenqinggege.maven</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>native-package</id>
<goals>
<goal>package-app</goal>
</goals>
</execution>
</executions>
<configuration>
<appName>MyTool</appName>
<mainClass>com.example.MyTool</mainClass>
<vendor>Example Inc.</vendor>
<description>我的命令行工具</description>
<javaOptions>
<javaOption>-Xmx512m</javaOption>
<javaOption>-Duser.timezone=Asia/Shanghai</javaOption>
</javaOptions>
</configuration>
</plugin>
```
### 示例二:Windows 桌面应用(带图标、保留控制台)
```xml
<plugin>
<groupId>com.mirenqinggege.maven</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>native-package</id>
<goals>
<goal>package-app</goal>
</goals>
</execution>
</executions>
<configuration>
<icon>${project.basedir}/src/main/resources/app.ico</icon>
<winConsole>true</winConsole>
<!-- 不声明 mainClass,由插件从 MANIFEST.MF 自动探测 -->
</configuration>
</plugin>
```
### 示例三:已打成 fat jar 的项目
```xml
<configuration>
<!-- 依赖已在 fat jar 内,无需重复拷贝 -->
<includeDependencies>false</includeDependencies>
<!-- 自定义 finalName 时同步指定主 JAR 名 -->
<mainJar>${project.build.finalName}.jar</mainJar>
</configuration>
```
### 示例四:作为独立 goal 手动执行
不绑定生命周期,按需触发:
```bash
# 方式一:完整坐标(任何情况下都可用)
mvn com.mirenqinggege.maven:jpackage-maven-plugin:1.0.0:package-app
# 方式二:使用 goal 前缀(需先在 ~/.m2/settings.xml 中登记插件组)
mvn jpackage:package-app
```
启用前缀需在 `settings.xml` 中加入:
```xml
<pluginGroups>
<pluginGroup>com.mirenqinggege.maven</pluginGroup>
</pluginGroups>
```
### 示例五:CI 中按需跳过
```bash
mvn clean package -Djpackage.skip=true
```
完整可运行示例见 [`examples/hello-cli`](examples/hello-cli)。
## 完整参数说明
### 核心参数
| 参数 | 属性名 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `appName` | `jpackage.appName` | `${project.artifactId}` | 应用名称,决定目录名与可执行文件名 |
| `mainJar` | `jpackage.mainJar` | `${project.build.finalName}.jar` | 主 JAR 包文件名 |
| `mainClass` | `jpackage.mainClass` | 自动探测 | 主启动类;留空时从主 JAR 的 `MANIFEST.MF` 读取 `Main-Class` |
| `javaOptions` | — | 空 | `List<String>`,追加的自定义 JVM 参数 |
| `icon` | `jpackage.icon` | 空 | 图标文件:`.ico`(Windows)/ `.png`(Linux)/ `.icns`(macOS) |
| `type` | `jpackage.type` | `app-image` | jpackage 打包类型;`app-image` 即绿色免安装版 |
| `destinationDirectory` | `jpackage.dest` | `${project.build.directory}/dist` | 产物输出目录 |
| `skip` | `jpackage.skip` | `false` | 跳过原生打包 |
| `verbose` | `jpackage.verbose` | `false` | 输出 jpackage 完整日志 |
### 编码与平台参数
| 参数 | 属性名 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `injectUtf8Options` | `jpackage.injectUtf8Options` | `true` | 注入全套 UTF-8 运行参数(见下) |
| `winConsole` | `jpackage.winConsole` | `true` | **仅 Windows 目标平台**附加 `--win-console`;Linux/macOS 永不传递 |
默认注入的 UTF-8 参数:
```
-Dfile.encoding=UTF-8
-Dstdout.encoding=UTF-8
-Dstderr.encoding=UTF-8
-Dsun.stdout.encoding=UTF-8
-Dsun.stderr.encoding=UTF-8
```
用户通过 `javaOptions` 追加的同名参数排在默认值**之后**,因此可以覆盖默认值。
### 输入目录与依赖
| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `inputDirectory` | `${project.build.directory}/jpackage-input` | 纯净输入目录,插件会先清空再填充 |
| `includeDependencies` | `true` | 是否把 `compile`/`runtime`/`system` 作用域的 JAR 依赖一并拷贝 |
| `cleanInputDirectory` | `true` | 打包结束后删除临时输入目录 |
| `additionalArguments` | 空 | `List<String>`,原样追加到 jpackage 命令行的参数 |
> **输入目录隔离**:插件不会把 `target/` 根目录直接作为 `--input`,而是建立独立的
> `jpackage-input/` 目录,只放入主 JAR 与依赖,避免 `classes/`、`maven-status/`、
> 测试报告等编译产物被误打包进应用镜像。
### 启动脚本
| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `generateLauncherScript` | `true` | 生成 `run.sh` / `run.bat` |
| `generateDesktopEntry` | `true` | Linux 下额外生成 `<appName>.desktop` |
### jpackage 定位与交叉打包
| 参数 | 属性名 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `jpackageExecutable` | `jpackage.jpackageExecutable` | 自动探测 | 显式指定 jpackage 路径(可以是可执行文件、其所在目录或 JDK 根目录) |
| `useWine` | `jpackage.useWine` | `false` | 在 Linux 上借助 Wine 调用 Windows 版 jpackage |
| `windowsJdkHome` | `jpackage.windowsJdkHome` | 空 | `useWine=true` 时必填,Windows 版 JDK 解压目录 |
jpackage 的自动探测优先级:
1. 用户配置的 `jpackageExecutable`(配置错误时**直接报错**,不会静默回退);
2. `${java.home}/bin/jpackage`(当前运行 Maven 的 JDK,最可靠,兼容 `<jdk>/jre` 布局);
3. `$JAVA_HOME/bin/jpackage`;
4. 系统 `PATH` 上的 `jpackage`。
Windows 下自动查找 `jpackage.exe`。
## 平台行为差异
| 行为 | Windows | Linux | macOS |
| --- | --- | --- | --- |
| `jpackage` 文件名 | `jpackage.exe` | `jpackage` | `jpackage` |
| `--win-console` | 受 `winConsole` 控制 | **永不传递** | **永不传递** |
| 启动脚本 | `run.bat`(CRLF 换行) | `run.sh`(LF 换行) | `run.sh`(LF 换行) |
| 桌面入口 | — | `<appName>.desktop` | — |
| 可执行权限 | 不适用 | 自动设为 755 | 自动设为 755 |
生成的 `run.sh`:
```sh
#!/bin/sh
# 由 jpackage-maven-plugin 自动生成:切换到脚本所在目录后启动应用
set -e
APP_HOME=$(cd "$(dirname "$0")" && pwd)
exec "$APP_HOME/bin/<appName>" "$@"
```
生成的 `run.bat`:
```bat
@echo off
rem 由 jpackage-maven-plugin 自动生成:切换控制台代码页为 UTF-8 后启动应用
chcp 65001 > nul
setlocal
"%~dp0<appName>.exe" %*
endlocal
```
`chcp 65001` 把 Windows 控制台代码页切换为 UTF-8,配合注入的 `-Dfile.encoding=UTF-8` 等
参数,彻底解决中文日志乱码问题。
## 在 Linux 上交叉打包 Windows 应用(Wine)
`jpackage` 无法跨平台产出,但可以在 Linux 上通过 Wine 运行 **Windows 版 OpenJDK** 的
`jpackage.exe` 来生成 Windows 应用镜像。
前提:
1. Linux 构建机已安装 Wine(如 `apt install wine64`);
2. 已下载并解压 Windows 版 OpenJDK(JDK 而非 JRE,建议版本与目标一致)。
配置:
```xml
<configuration>
<useWine>true</useWine>
<windowsJdkHome>/opt/jdk-21.0.9-windows-x64</windowsJdkHome>
</configuration>
```
插件会:
1. 校验 `wine` 与 `<windowsJdkHome>/bin/jpackage.exe` 是否存在,缺失时快速失败并给出提示;
2. 通过 `winepath -w` 把宿主路径转换为 Windows 路径(失败时回退到 Wine 默认的 `Z:` 盘映射);
3. 以 `wine <jpackage.exe> ...` 形式执行,并设置 `WINEDEBUG=-all` 抑制 Wine 噪声;
4. 按 Windows 目标平台组装参数(附加 `--win-console`)并生成 `run.bat`。
> Wine 交叉打包属于尽力而为的方案,产物的可用性取决于 Wine 版本与 JDK 版本组合。
> 生产环境仍建议在真实 Windows Runner 上构建。
## 常见问题
**Q:报错 `Version [1.0.0-SNAPSHOT] is not valid`?**
A:插件已自动清洗版本号(`1.0.0-SNAPSHOT` → `1.0.0`)。若你通过 `additionalArguments`
手工传递了版本参数,请自行确保其为纯数字点分格式。
**Q:报错 `Invalid Option: [--win-console]`?**
A:`--win-console` 只能在 Windows 上使用。插件已在 Linux/macOS 目标平台自动屏蔽该参数;
如果你通过 `additionalArguments` 手工加入了它,请移除。
**Q:目标机器运行时报 `NoClassDefFoundError`?**
A:说明有依赖没被打进镜像。检查依赖作用域是否为 `provided` 或 `test`(这两者不会被拷贝),
或项目是否使用了 `shade`/`assembly` 打成 fat jar(此时应设置
`<includeDependencies>false</includeDependencies>`)。
**Q:想在 Windows 上打包但 `mvn` 运行的 JDK 不是 16+?**
A:插件的 `jpackageExecutable` 允许指向另一个 JDK:
`<jpackageExecutable>C:\jdk-21\bin\jpackage.exe</jpackageExecutable>`。
**Q:构建速度慢?**
A:`jpackage` 需要对内嵌运行时做 jlink 裁剪并拷贝数百个文件,属正常现象。日常开发可用
`-Djpackage.skip=true` 跳过。
**Q:中文输出乱码?**
A:确认不要通过 `javaOptions` 覆盖掉 `-Dfile.encoding=UTF-8`;Windows 上请使用插件生成的
`run.bat`(其中含 `chcp 65001`)而非直接双击 `bin\<appName>.exe`。
## 发布到 Maven 中央仓库
本项目已按 Maven Central 的要求配置完毕:`name` / `description` / `url` / `licenses` /
`developers` / `scm` 齐全,`sources` 与 `javadoc` 构件随包附加,GPG 签名在 `release`
profile 中启用。
### 1. 前置准备
1. **注册命名空间**:登录 <https://central.sonatype.com>,添加并验证命名空间
`com.mirenqinggege`(可通过 DNS TXT 记录或代码托管平台账号验证;本项目的 SCM 地址为
`git.mirenqinggege.com`,建议用域名方式验证)。
2. **生成 Portal Token**:在 Central Portal 的 *Account → Generate User Token* 中生成,
然后写入 `~/.m2/settings.xml`:
```xml
<servers>
<server>
<id>central</id>
<username>Portal Token 的用户名</username>
<password>Portal Token 的密码</password>
</server>
</servers>
```
3. **准备 GPG 密钥**:确认本地已有私钥,且公钥已发布到密钥服务器:
```bash
gpg --list-secret-keys --keyid-format=long
gpg --keyserver keyserver.ubuntu.com --send-keys <你的KEYID>
```
### 2. 发布
```bash
mvn clean deploy -P release
```
该命令会:编译 → 单元测试 → 生成 `plugin.xml` → 打包 `jar` / `sources` / `javadoc` →
GPG 签名(`release` profile)→ 上传到 Central Portal → 等待校验通过。
默认 `autoPublish=false`,构件校验通过后会停留在 Portal 的待发布状态,
需在 <https://central.sonatype.com/publishing> 手动点击 **Publish**;
若希望校验通过即自动发布,把 `pom.xml` 中的 `<autoPublish>` 改为 `true`。
### 3. 未使用 release profile 时的行为
```bash
mvn clean verify # 只构建与测试,不签名(默认 profile 会把 sign 执行置为 phase=none)
mvn clean install # 安装到本地仓库,同样不签名
```
### 4. 覆盖签名密钥
```bash
mvn clean deploy -P release -Dgpg.keyname=ABCDEF0123456789
```
### 5. 版本升级
发布新版本时需同步修改三处,避免 SCM 标签与实际发布不一致:
- `pom.xml` 的 `<version>`;
- `pom.xml` 的 `<scm><tag>`;
- `README.md` 中示例使用的 `jpackage.plugin.version`(`examples/hello-cli/pom.xml`)。
## 插件工程自身构建
```bash
mvn clean verify # 编译 + 单元测试 + 生成插件描述符
mvn install # 安装到本地仓库,供其他项目引用
mvn com.mirenqinggege.maven:jpackage-maven-plugin:1.0.0:help # 查看 goal 帮助
```
### 源码结构
```
src/main/java/com/mirenqinggege/maven/jpackage/
├── PackageAppMojo.java # 核心 Mojo:流程编排与错误处理
└── internal/
├── Platform.java # 平台探测与平台差异
├── JdkToolLocator.java # jpackage 可执行文件定位
├── VersionSanitizer.java # Maven 版本号 → jpackage 版本号
├── JarManifestInspector.java # MANIFEST.MF 解析(Main-Class / Class-Path)
├── JpackageOptions.java # 命令行组装(平台自适应、UTF-8 注入)
├── LauncherScriptGenerator.java # run.sh / run.bat / .desktop 生成
├── FileTreeUtils.java # 目录清理与拷贝
├── FilePermissions.java # 跨平台 POSIX 权限设置
├── JpackageExecutor.java # 进程调用抽象(可注入,便于测试)
└── WineSupport.java # Wine 交叉打包支持
```
设计上把「可测试的纯逻辑」全部下沉到 `internal` 包,Mojo 只负责编排:这使得命令行组装、
清单解析、脚本生成、版本清洗等核心行为都能在无 Maven 容器、无真实 jpackage 的条件下被
单元测试覆盖(共 103 个测试用例)。
### 编写测试的关键注入点
`PackageAppMojo` 的配置字段为包级可见,测试可直接赋值;另外提供三个注入点:
- `executor`:替换进程执行器,避免真的启动 `jpackage`;
- `environment`:替换 `java.home` / `JAVA_HOME`;
- `platformOverride`:覆盖平台探测结果,用于在任意机器上验证 Windows 分支逻辑。
示例见 [`src/test/java/com/mirenqinggege/maven/jpackage/PackageAppMojoTest.java`](src/test/java/com/mirenqinggege/maven/jpackage/PackageAppMojoTest.java)。