# 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
com.mirenqinggege.maven
jpackage-maven-plugin
1.0.0
native-package
package-app
```
执行:
```bash
mvn clean package
```
插件会自动完成:定位 `jpackage` → 建立纯净输入目录 → 探测 `Main-Class` → 清洗版本号 →
组装命令行 → 调用 `jpackage` → 生成启动脚本 → 清理临时目录。
> 如果没有 `Main-Class` 且未配置 ``,插件会打印告警并交由 `jpackage`
> 自行从清单读取;此时若构建失败,请显式配置 ``。
## 输出结构
默认产物位于 `target/dist//`:
```
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`。
整个 `` 目录即为绿色免安装版:压缩后拷贝到目标机器解压,直接执行 `run.sh`
(或 `run.bat`)即可运行。
## POM 配置示例
### 示例一:命令行工具(Linux 产物 + 自定义 JVM 参数)
```xml
com.mirenqinggege.maven
jpackage-maven-plugin
1.0.0
native-package
package-app
MyTool
com.example.MyTool
Example Inc.
我的命令行工具
-Xmx512m
-Duser.timezone=Asia/Shanghai
```
### 示例二:Windows 桌面应用(带图标、保留控制台)
```xml
com.mirenqinggege.maven
jpackage-maven-plugin
1.0.0
native-package
package-app
${project.basedir}/src/main/resources/app.ico
true
```
### 示例三:已打成 fat jar 的项目
```xml
false
${project.build.finalName}.jar
```
### 示例四:作为独立 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
com.mirenqinggege.maven
```
### 示例五: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`,追加的自定义 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`,原样追加到 jpackage 命令行的参数 |
> **输入目录隔离**:插件不会把 `target/` 根目录直接作为 `--input`,而是建立独立的
> `jpackage-input/` 目录,只放入主 JAR 与依赖,避免 `classes/`、`maven-status/`、
> 测试报告等编译产物被误打包进应用镜像。
### 启动脚本
| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `generateLauncherScript` | `true` | 生成 `run.sh` / `run.bat` |
| `generateDesktopEntry` | `true` | Linux 下额外生成 `.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,最可靠,兼容 `/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 换行) |
| 桌面入口 | — | `.desktop` | — |
| 可执行权限 | 不适用 | 自动设为 755 | 自动设为 755 |
生成的 `run.sh`:
```sh
#!/bin/sh
# 由 jpackage-maven-plugin 自动生成:切换到脚本所在目录后启动应用
set -e
APP_HOME=$(cd "$(dirname "$0")" && pwd)
exec "$APP_HOME/bin/" "$@"
```
生成的 `run.bat`:
```bat
@echo off
rem 由 jpackage-maven-plugin 自动生成:切换控制台代码页为 UTF-8 后启动应用
chcp 65001 > nul
setlocal
"%~dp0.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
true
/opt/jdk-21.0.9-windows-x64
```
插件会:
1. 校验 `wine` 与 `/bin/jpackage.exe` 是否存在,缺失时快速失败并给出提示;
2. 通过 `winepath -w` 把宿主路径转换为 Windows 路径(失败时回退到 Wine 默认的 `Z:` 盘映射);
3. 以 `wine ...` 形式执行,并设置 `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(此时应设置
`false`)。
**Q:想在 Windows 上打包但 `mvn` 运行的 JDK 不是 16+?**
A:插件的 `jpackageExecutable` 允许指向另一个 JDK:
`C:\jdk-21\bin\jpackage.exe`。
**Q:构建速度慢?**
A:`jpackage` 需要对内嵌运行时做 jlink 裁剪并拷贝数百个文件,属正常现象。日常开发可用
`-Djpackage.skip=true` 跳过。
**Q:中文输出乱码?**
A:确认不要通过 `javaOptions` 覆盖掉 `-Dfile.encoding=UTF-8`;Windows 上请使用插件生成的
`run.bat`(其中含 `chcp 65001`)而非直接双击 `bin\.exe`。
## 发布到 Maven 中央仓库
本项目已按 Maven Central 的要求配置完毕:`name` / `description` / `url` / `licenses` /
`developers` / `scm` 齐全,`sources` 与 `javadoc` 构件随包附加,GPG 签名在 `release`
profile 中启用。
### 1. 前置准备
1. **注册命名空间**:登录 ,添加并验证命名空间
`com.mirenqinggege`(可通过 DNS TXT 记录或代码托管平台账号验证;本项目的 SCM 地址为
`git.mirenqinggege.com`,建议用域名方式验证)。
2. **生成 Portal Token**:在 Central Portal 的 *Account → Generate User Token* 中生成,
然后写入 `~/.m2/settings.xml`:
```xml
central
Portal Token 的用户名
Portal Token 的密码
```
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 的待发布状态,
需在 手动点击 **Publish**;
若希望校验通过即自动发布,把 `pom.xml` 中的 `` 改为 `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` 的 ``;
- `pom.xml` 的 ``;
- `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)。