|
@@ -29,8 +29,10 @@ public class Launch4jHelper {
|
|
|
"https://downloads.sourceforge.net/project/launch4j/launch4j-3/3.50/launch4j-3.50-win32.zip";
|
|
"https://downloads.sourceforge.net/project/launch4j/launch4j-3/3.50/launch4j-3.50-win32.zip";
|
|
|
|
|
|
|
|
private final File launch4jDir;
|
|
private final File launch4jDir;
|
|
|
|
|
+ private final File toolRoot;
|
|
|
|
|
|
|
|
public Launch4jHelper(File toolRootDir) {
|
|
public Launch4jHelper(File toolRootDir) {
|
|
|
|
|
+ this.toolRoot = toolRootDir;
|
|
|
this.launch4jDir = new File(toolRootDir, "launch4j");
|
|
this.launch4jDir = new File(toolRootDir, "launch4j");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -85,21 +87,50 @@ public class Launch4jHelper {
|
|
|
*/
|
|
*/
|
|
|
public File convert(ConvertOptions options, LogCallback callback) throws IOException, InterruptedException {
|
|
public File convert(ConvertOptions options, LogCallback callback) throws IOException, InterruptedException {
|
|
|
File workspace = null;
|
|
File workspace = null;
|
|
|
|
|
+ File patchDir = null;
|
|
|
ConvertOptions workOptions = options;
|
|
ConvertOptions workOptions = options;
|
|
|
- boolean useWorkspace = needsAsciiWorkspace(options);
|
|
|
|
|
-
|
|
|
|
|
- if (useWorkspace) {
|
|
|
|
|
- callback.log("检测到路径包含中文或非 ASCII 字符,已切换到临时目录打包...");
|
|
|
|
|
- workspace = createAsciiWorkspace(options, callback);
|
|
|
|
|
- workOptions = workspaceOptions(workspace, options);
|
|
|
|
|
- } else {
|
|
|
|
|
- File outputParent = options.outputExe.getParentFile();
|
|
|
|
|
- if (outputParent != null && !outputParent.exists() && !outputParent.mkdirs()) {
|
|
|
|
|
- throw new IOException("无法创建输出目录: " + outputParent.getAbsolutePath());
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 可选:打包时注入中文登录补丁(不改业务工程源码)
|
|
|
|
|
+ if (options.patchChineseLogin) {
|
|
|
|
|
+ patchDir = new File(System.getProperty("java.io.tmpdir"), "jar2exe-patch-" + System.nanoTime());
|
|
|
|
|
+ File patchedJar = ChineseLoginPatcher.apply(options.jarFile, toolRoot, patchDir, callback);
|
|
|
|
|
+ workOptions = copyOptions(options);
|
|
|
|
|
+ workOptions.jarFile = patchedJar;
|
|
|
|
|
+
|
|
|
|
|
+ // 分离模式:把补丁 JAR 落到输出目录,避免临时文件删除后 EXE 找不到 JAR
|
|
|
|
|
+ if (options.dontWrapJar) {
|
|
|
|
|
+ File outputParent = options.outputExe.getParentFile();
|
|
|
|
|
+ if (outputParent != null && !outputParent.exists() && !outputParent.mkdirs()) {
|
|
|
|
|
+ throw new IOException("无法创建输出目录: " + outputParent.getAbsolutePath());
|
|
|
|
|
+ }
|
|
|
|
|
+ String baseName = options.jarFile.getName();
|
|
|
|
|
+ if (baseName.toLowerCase().endsWith(".jar")) {
|
|
|
|
|
+ baseName = baseName.substring(0, baseName.length() - 4) + "-patched.jar";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ baseName = baseName + "-patched.jar";
|
|
|
|
|
+ }
|
|
|
|
|
+ File retainedJar = new File(outputParent, baseName);
|
|
|
|
|
+ copyFile(patchedJar, retainedJar);
|
|
|
|
|
+ workOptions.jarFile = retainedJar;
|
|
|
|
|
+ callback.log("分离模式补丁 JAR: " + retainedJar.getAbsolutePath());
|
|
|
|
|
+ }
|
|
|
|
|
+ callback.log("已启用中文登录兼容补丁(Base64 固定 UTF-8,对齐 Jeesite 后端)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ boolean useWorkspace = needsAsciiWorkspace(workOptions);
|
|
|
|
|
+ if (useWorkspace) {
|
|
|
|
|
+ callback.log("检测到路径包含中文或非 ASCII 字符,已切换到临时目录打包...");
|
|
|
|
|
+ workspace = createAsciiWorkspace(workOptions, callback);
|
|
|
|
|
+ ConvertOptions beforeWorkspace = workOptions;
|
|
|
|
|
+ workOptions = workspaceOptions(workspace, beforeWorkspace);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ File outputParent = workOptions.outputExe.getParentFile();
|
|
|
|
|
+ if (outputParent != null && !outputParent.exists() && !outputParent.mkdirs()) {
|
|
|
|
|
+ throw new IOException("无法创建输出目录: " + outputParent.getAbsolutePath());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
File launch4jExe = ensureLaunch4j(callback);
|
|
File launch4jExe = ensureLaunch4j(callback);
|
|
|
File configFile = createConfig(workOptions, workspace != null ? workspace : workOptions.outputExe.getParentFile());
|
|
File configFile = createConfig(workOptions, workspace != null ? workspace : workOptions.outputExe.getParentFile());
|
|
|
|
|
|
|
@@ -115,7 +146,7 @@ public class Launch4jHelper {
|
|
|
throw new IOException("EXE 未生成,请查看上方日志");
|
|
throw new IOException("EXE 未生成,请查看上方日志");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (useWorkspace) {
|
|
|
|
|
|
|
+ if (workspace != null) {
|
|
|
File outputParent = options.outputExe.getParentFile();
|
|
File outputParent = options.outputExe.getParentFile();
|
|
|
if (outputParent != null && !outputParent.exists() && !outputParent.mkdirs()) {
|
|
if (outputParent != null && !outputParent.exists() && !outputParent.mkdirs()) {
|
|
|
throw new IOException("无法创建输出目录: " + outputParent.getAbsolutePath());
|
|
throw new IOException("无法创建输出目录: " + outputParent.getAbsolutePath());
|
|
@@ -124,21 +155,52 @@ public class Launch4jHelper {
|
|
|
callback.log("已复制 EXE 到: " + options.outputExe.getAbsolutePath());
|
|
callback.log("已复制 EXE 到: " + options.outputExe.getAbsolutePath());
|
|
|
|
|
|
|
|
if (options.dontWrapJar) {
|
|
if (options.dontWrapJar) {
|
|
|
- File targetJar = new File(outputParent, options.jarFile.getName());
|
|
|
|
|
- copyFile(options.jarFile, targetJar);
|
|
|
|
|
|
|
+ // 工作区里的 jar 可能是 input.jar,需复制为最终补丁名或原名
|
|
|
|
|
+ File targetJar;
|
|
|
|
|
+ if (options.patchChineseLogin) {
|
|
|
|
|
+ String baseName = options.jarFile.getName();
|
|
|
|
|
+ if (baseName.toLowerCase().endsWith(".jar")) {
|
|
|
|
|
+ baseName = baseName.substring(0, baseName.length() - 4) + "-patched.jar";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ baseName = baseName + "-patched.jar";
|
|
|
|
|
+ }
|
|
|
|
|
+ targetJar = new File(outputParent, baseName);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ targetJar = new File(outputParent, options.jarFile.getName());
|
|
|
|
|
+ }
|
|
|
|
|
+ copyFile(workOptions.jarFile, targetJar);
|
|
|
callback.log("已复制 JAR 到: " + targetJar.getAbsolutePath());
|
|
callback.log("已复制 JAR 到: " + targetJar.getAbsolutePath());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ writeEncodingIni(options.outputExe, options.fileEncoding, callback);
|
|
|
return options.outputExe;
|
|
return options.outputExe;
|
|
|
} finally {
|
|
} finally {
|
|
|
if (workspace != null) {
|
|
if (workspace != null) {
|
|
|
deleteDirectory(workspace);
|
|
deleteDirectory(workspace);
|
|
|
callback.log("已清理临时目录");
|
|
callback.log("已清理临时目录");
|
|
|
}
|
|
}
|
|
|
|
|
+ if (patchDir != null) {
|
|
|
|
|
+ deleteDirectory(patchDir);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static ConvertOptions copyOptions(ConvertOptions source) {
|
|
|
|
|
+ ConvertOptions copy = new ConvertOptions();
|
|
|
|
|
+ copy.jarFile = source.jarFile;
|
|
|
|
|
+ copy.outputExe = source.outputExe;
|
|
|
|
|
+ copy.mainClass = source.mainClass;
|
|
|
|
|
+ copy.appTitle = source.appTitle;
|
|
|
|
|
+ copy.minJreVersion = source.minJreVersion;
|
|
|
|
|
+ copy.iconFile = source.iconFile;
|
|
|
|
|
+ copy.headerType = source.headerType;
|
|
|
|
|
+ copy.dontWrapJar = source.dontWrapJar;
|
|
|
|
|
+ copy.fileEncoding = source.fileEncoding;
|
|
|
|
|
+ copy.patchChineseLogin = source.patchChineseLogin;
|
|
|
|
|
+ return copy;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 生成 Launch4j 配置文件。
|
|
* 生成 Launch4j 配置文件。
|
|
|
*/
|
|
*/
|
|
@@ -186,6 +248,18 @@ public class Launch4jHelper {
|
|
|
xml.append(" <maxVersion></maxVersion>\n");
|
|
xml.append(" <maxVersion></maxVersion>\n");
|
|
|
xml.append(" <jdkPreference>preferJre</jdkPreference>\n");
|
|
xml.append(" <jdkPreference>preferJre</jdkPreference>\n");
|
|
|
xml.append(" <runtimeBits>64/32</runtimeBits>\n");
|
|
xml.append(" <runtimeBits>64/32</runtimeBits>\n");
|
|
|
|
|
+ // 显式指定 JVM 编码,避免 EXE 启动后接口中文乱码(业务 JAR 无需改代码)
|
|
|
|
|
+ String encoding = normalizeEncoding(options.fileEncoding);
|
|
|
|
|
+ if (encoding != null) {
|
|
|
|
|
+ xml.append(" <opt>-Dfile.encoding=").append(escapeXml(encoding)).append("</opt>\n");
|
|
|
|
|
+ xml.append(" <opt>-Dsun.jnu.encoding=").append(escapeXml(encoding)).append("</opt>\n");
|
|
|
|
|
+ xml.append(" <opt>-Dclient.encoding.override=").append(escapeXml(encoding)).append("</opt>\n");
|
|
|
|
|
+ if ("GBK".equalsIgnoreCase(encoding) || "GB2312".equalsIgnoreCase(encoding)
|
|
|
|
|
+ || "GB18030".equalsIgnoreCase(encoding)) {
|
|
|
|
|
+ xml.append(" <opt>-Duser.language=zh</opt>\n");
|
|
|
|
|
+ xml.append(" <opt>-Duser.country=CN</opt>\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
xml.append(" </jre>\n");
|
|
xml.append(" </jre>\n");
|
|
|
xml.append("</launch4jConfig>\n");
|
|
xml.append("</launch4jConfig>\n");
|
|
|
|
|
|
|
@@ -227,6 +301,38 @@ public class Launch4jHelper {
|
|
|
return process.waitFor();
|
|
return process.waitFor();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 在 EXE 同目录生成 Launch4j 运行时 ini,双重保障编码参数生效。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void writeEncodingIni(File outputExe, String fileEncoding, LogCallback callback) throws IOException {
|
|
|
|
|
+ String encoding = normalizeEncoding(fileEncoding);
|
|
|
|
|
+ if (encoding == null || outputExe == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ String exeName = outputExe.getName();
|
|
|
|
|
+ int dot = exeName.lastIndexOf('.');
|
|
|
|
|
+ String baseName = dot > 0 ? exeName.substring(0, dot) : exeName;
|
|
|
|
|
+ File iniFile = new File(outputExe.getParentFile(), baseName + ".l4j.ini");
|
|
|
|
|
+
|
|
|
|
|
+ StringBuilder content = new StringBuilder();
|
|
|
|
|
+ content.append("# Auto generated by jar2exe - do not delete\r\n");
|
|
|
|
|
+ content.append("# Force JVM encoding so Chinese APIs work without changing business code\r\n");
|
|
|
|
|
+ content.append("-Dfile.encoding=").append(encoding).append("\r\n");
|
|
|
|
|
+ content.append("-Dsun.jnu.encoding=").append(encoding).append("\r\n");
|
|
|
|
|
+ content.append("-Dclient.encoding.override=").append(encoding).append("\r\n");
|
|
|
|
|
+ if ("GBK".equalsIgnoreCase(encoding) || "GB2312".equalsIgnoreCase(encoding)
|
|
|
|
|
+ || "GB18030".equalsIgnoreCase(encoding)) {
|
|
|
|
|
+ content.append("-Duser.language=zh\r\n");
|
|
|
|
|
+ content.append("-Duser.country=CN\r\n");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try (FileOutputStream fos = new FileOutputStream(iniFile)) {
|
|
|
|
|
+ fos.write(content.toString().getBytes(StandardCharsets.US_ASCII));
|
|
|
|
|
+ }
|
|
|
|
|
+ callback.log("已生成编码配置: " + iniFile.getAbsolutePath());
|
|
|
|
|
+ callback.log("说明: 请将 " + iniFile.getName() + " 与 EXE 放在同一目录一起分发");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static boolean needsAsciiWorkspace(ConvertOptions options) {
|
|
private static boolean needsAsciiWorkspace(ConvertOptions options) {
|
|
|
if (containsNonAscii(options.jarFile.getAbsolutePath())) {
|
|
if (containsNonAscii(options.jarFile.getAbsolutePath())) {
|
|
|
return true;
|
|
return true;
|
|
@@ -285,9 +391,33 @@ public class Launch4jHelper {
|
|
|
work.iconFile = iconCopy;
|
|
work.iconFile = iconCopy;
|
|
|
work.headerType = options.headerType;
|
|
work.headerType = options.headerType;
|
|
|
work.dontWrapJar = options.dontWrapJar;
|
|
work.dontWrapJar = options.dontWrapJar;
|
|
|
|
|
+ work.fileEncoding = options.fileEncoding;
|
|
|
|
|
+ work.patchChineseLogin = options.patchChineseLogin;
|
|
|
return work;
|
|
return work;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 规范化编码名称;空或“系统默认”时返回 null(不写入 JVM 参数)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static String normalizeEncoding(String encoding) {
|
|
|
|
|
+ if (encoding == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String value = encoding.trim();
|
|
|
|
|
+ if (value.isEmpty() || "system".equalsIgnoreCase(value) || "default".equalsIgnoreCase(value)
|
|
|
|
|
+ || "系统默认".equals(value)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("UTF8".equalsIgnoreCase(value) || "utf-8".equalsIgnoreCase(value)) {
|
|
|
|
|
+ return "UTF-8";
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("GB2312".equalsIgnoreCase(value) || "GB18030".equalsIgnoreCase(value)
|
|
|
|
|
+ || "gbk".equalsIgnoreCase(value)) {
|
|
|
|
|
+ return value.toUpperCase().startsWith("GB") ? value.toUpperCase() : "GBK";
|
|
|
|
|
+ }
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static void copyFile(File source, File target) throws IOException {
|
|
private static void copyFile(File source, File target) throws IOException {
|
|
|
File parent = target.getParentFile();
|
|
File parent = target.getParentFile();
|
|
|
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
|
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
|
@@ -415,6 +545,10 @@ public class Launch4jHelper {
|
|
|
public File iconFile;
|
|
public File iconFile;
|
|
|
public String headerType = "gui";
|
|
public String headerType = "gui";
|
|
|
public boolean dontWrapJar = false;
|
|
public boolean dontWrapJar = false;
|
|
|
|
|
+ /** JVM 文件编码。Jeesite 后端请用 UTF-8 */
|
|
|
|
|
+ public String fileEncoding = "UTF-8";
|
|
|
|
|
+ /** 打包时注入中文登录补丁(Base64 固定 UTF-8),不改业务工程源码 */
|
|
|
|
|
+ public boolean patchChineseLogin = true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|