From 01805cb1ed2eabf4ae09d1bb2191f8db9b52368b Mon Sep 17 00:00:00 2001 From: czyt Date: Fri, 16 Oct 2026 18:37:46 +0800 Subject: [PATCH 01/11] Verify mounted macOS DMG resources and extract Linux AppImage tools --- .github/workflows/build.yml | 1 + scripts/check-bundled-java.py | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c05501aa..2342353c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,6 +165,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf libssl-dev rpm + echo 'APPIMAGE_EXTRACT_AND_RUN=1' >> "$GITHUB_ENV" - run: npm --prefix web ci - name: Generate platform icons from provided transparent logo working-directory: desktop diff --git a/scripts/check-bundled-java.py b/scripts/check-bundled-java.py index 52354f56..54ae0d32 100644 --- a/scripts/check-bundled-java.py +++ b/scripts/check-bundled-java.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Run a packaged JVM and real project API with external Java configuration cleared.""" import argparse +from contextlib import ExitStack, contextmanager import json import os from pathlib import Path @@ -49,9 +50,27 @@ def generate(base, setup): return {'engine': artifact['engine'], 'files': len(artifact['files']), 'source_sha256': artifact['source_sha256']} +@contextmanager +def mounted_macos_resources(binary, temporary): + images = list(binary.parent.glob('bundle/dmg/*.dmg')) + if len(images) != 1: + raise ValueError('Expected exactly one packaged macOS DMG') + mount = Path(temporary) / 'mounted-dmg' + mount.mkdir() + subprocess.run(['hdiutil', 'attach', '-readonly', '-nobrowse', '-mountpoint', str(mount), str(images[0])], + timeout=90, check=True) + try: + applications = list(mount.glob('*.app/Contents/Resources')) + if len(applications) != 1: + raise ValueError('Expected exactly one application resource directory in the packaged DMG') + yield applications[0] + finally: + subprocess.run(['hdiutil', 'detach', str(mount), '-force'], timeout=90, check=True) + + def main(args): process = None - with tempfile.TemporaryDirectory(prefix='moleapi-bundled-java-') as temporary: + with tempfile.TemporaryDirectory(prefix='moleapi-bundled-java-') as temporary, ExitStack() as stack: if args.container: assert os.environ.get('GITHUB_ACTIONS') == 'true', 'Container verification requires the explicit CI fixture' version = subprocess.run(['docker', 'exec', args.container, 'env', '-i', @@ -69,10 +88,7 @@ def main(args): target = host_target() resource_root = args.resources.resolve() if args.resources else binary.parent if args.runtime_only and sys.platform == 'darwin' and not args.resources: - applications = list(binary.parent.glob('bundle/macos/*.app/Contents/Resources')) - if len(applications) != 1: - raise ValueError('Expected exactly one packaged macOS application resource directory') - resource_root = applications[0] + resource_root = stack.enter_context(mounted_macos_resources(binary, temporary)) resources = resource_root / 'runtime/java' verify_payload(resources, manifest, target) jvm = resources / manifest['assets'][target]['java'] From df12bf4a76871180724a54033a7cac13efb91178 Mon Sep 17 00:00:00 2001 From: czyt Date: Fri, 16 Oct 2026 23:30:31 +0800 Subject: [PATCH 02/11] Expose Linux AppImage packaging diagnostics --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2342353c..d2b4c219 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -177,12 +177,13 @@ jobs: with: projectPath: desktop tauriScript: ../web/node_modules/.bin/tauri - args: --config tauri.java.json --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} + args: --verbose --config tauri.java.json --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} - name: Verify desktop worker without opening a GUI run: python scripts/check-script-worker.py target/${{ matrix.target }}/release/moleapi - name: Verify actual bundled desktop JVM and original runtime files run: python scripts/check-bundled-java.py target/${{ matrix.target }}/release/moleapi --runtime-only - uses: actions/upload-artifact@v4 + if: ${{ success() || (failure() && runner.os == 'Linux') }} with: name: desktop-${{ matrix.target }} path: | @@ -192,7 +193,7 @@ jobs: target/${{ matrix.target }}/release/bundle/**/*.dmg target/${{ matrix.target }}/release/bundle/**/*.exe target/${{ matrix.target }}/release/bundle/**/*.msi - if-no-files-found: error + if-no-files-found: warn objc-qa: if: github.event_name == 'workflow_dispatch' && inputs.objc_qa == true timeout-minutes: 180 From 8881438526b4b634699a15210944b343e9a14880 Mon Sep 17 00:00:00 2001 From: czyt Date: Sat, 17 Oct 2026 01:01:29 +0800 Subject: [PATCH 03/11] Resolve bundled JVM libraries during Linux AppImage packaging --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2b4c219..0c13850c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -173,6 +173,12 @@ jobs: - run: python scripts/prepare_openapi_generator.py - run: python scripts/prepare_dubbo_worker.py - run: python scripts/prepare_java_runtime.py --target ${{ matrix.target }} + - name: Expose bundled JVM libraries to Linux AppImage dependency scan + if: runner.os == 'Linux' + run: | + java_lib="$GITHUB_WORKSPACE/vendor/java-runtime/current/payload/lib" + test -f "$java_lib/server/libjvm.so" + echo "LD_LIBRARY_PATH=$java_lib/server:$java_lib" >> "$GITHUB_ENV" - uses: tauri-apps/tauri-action@v0 with: projectPath: desktop From 4b103ce30d2d338b46c31bb0d1a3e4ecc76a8107 Mon Sep 17 00:00:00 2001 From: czyt Date: Sat, 17 Oct 2026 02:21:51 +0800 Subject: [PATCH 04/11] Smoke extracted Linux desktop packages in CI --- .github/workflows/build.yml | 5 +- scripts/check-linux-desktop-packages.py | 80 +++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 scripts/check-linux-desktop-packages.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c13850c..f0797f95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -164,7 +164,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf libssl-dev rpm + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf libssl-dev rpm rpm2cpio cpio echo 'APPIMAGE_EXTRACT_AND_RUN=1' >> "$GITHUB_ENV" - run: npm --prefix web ci - name: Generate platform icons from provided transparent logo @@ -188,6 +188,9 @@ jobs: run: python scripts/check-script-worker.py target/${{ matrix.target }}/release/moleapi - name: Verify actual bundled desktop JVM and original runtime files run: python scripts/check-bundled-java.py target/${{ matrix.target }}/release/moleapi --runtime-only + - name: Smoke extracted Linux deb, rpm and AppImage packages + if: runner.os == 'Linux' + run: python scripts/check-linux-desktop-packages.py target/${{ matrix.target }}/release/bundle - uses: actions/upload-artifact@v4 if: ${{ success() || (failure() && runner.os == 'Linux') }} with: diff --git a/scripts/check-linux-desktop-packages.py b/scripts/check-linux-desktop-packages.py new file mode 100644 index 00000000..67c6167c --- /dev/null +++ b/scripts/check-linux-desktop-packages.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Extract each Linux desktop package and smoke its actual bundled payload.""" +import argparse +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def one_package(directory: Path, suffix: str) -> Path: + matches = list(directory.rglob(f"*{suffix}")) + if len(matches) != 1: + raise ValueError(f"Expected one {suffix} package, found {len(matches)}") + return matches[0].resolve(strict=True) + + +def extract_rpm(package: Path, destination: Path) -> None: + producer = subprocess.Popen(["rpm2cpio", str(package)], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + try: + subprocess.run(["cpio", "-idmu", "--quiet"], stdin=producer.stdout, + cwd=destination, check=True, timeout=180) + if producer.stdout: + producer.stdout.close() + if producer.wait(timeout=20): + raise RuntimeError("rpm2cpio failed") + finally: + if producer.poll() is None: + producer.kill() + producer.wait(timeout=5) + + +def verify_extracted(name: str, root: Path, executable: Path) -> None: + resources = root / "usr/lib/MoleAPI" + if not executable.is_file() or not resources.is_dir(): + raise ValueError(f"{name}: installed executable or resources missing") + subprocess.run([sys.executable, str(ROOT / "scripts/check-script-worker.py"), str(executable)], + check=True, timeout=45) + subprocess.run([sys.executable, str(ROOT / "scripts/check-bundled-java.py"), str(executable), + "--resources", str(resources), "--runtime-only"], check=True, timeout=45) + subprocess.run([sys.executable, str(ROOT / "scripts/check-bundled-dubbo.py"), str(executable), + "--resource-root", str(resources)], check=True, timeout=45) + print(f"PASS extracted {name}: worker, exact JRE inventory and offline Dubbo worker", flush=True) + + +def main(directory: Path) -> None: + directory = directory.resolve(strict=True) + deb = one_package(directory, ".deb") + rpm = one_package(directory, ".rpm") + appimage = one_package(directory, ".AppImage") + with tempfile.TemporaryDirectory(prefix="moleapi-linux-packages-") as temporary: + base = Path(temporary) + deb_root = base / "deb" + deb_root.mkdir() + subprocess.run(["dpkg-deb", "-x", str(deb), str(deb_root)], check=True, timeout=180) + verify_extracted("deb", deb_root, deb_root / "usr/bin/moleapi") + + rpm_root = base / "rpm" + rpm_root.mkdir() + extract_rpm(rpm, rpm_root) + verify_extracted("rpm", rpm_root, rpm_root / "usr/bin/moleapi") + + app_root = base / "appimage" + app_root.mkdir() + environment = {**os.environ, "APPIMAGE_EXTRACT_AND_RUN": "1"} + subprocess.run([str(appimage), "--appimage-extract"], cwd=app_root, + env=environment, check=True, timeout=180, + stdout=subprocess.DEVNULL) + extracted = app_root / "squashfs-root" + verify_extracted("AppImage", extracted, extracted / "AppRun") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("bundle_directory", type=Path) + args = parser.parse_args() + main(args.bundle_directory) From 334f71d71438f7e31370410d9f8ec07869e325b2 Mon Sep 17 00:00:00 2001 From: czyt Date: Sat, 17 Oct 2026 04:06:07 +0800 Subject: [PATCH 05/11] Spool RPM payload before extracting package smoke --- scripts/check-linux-desktop-packages.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/scripts/check-linux-desktop-packages.py b/scripts/check-linux-desktop-packages.py index 67c6167c..3bb7c943 100644 --- a/scripts/check-linux-desktop-packages.py +++ b/scripts/check-linux-desktop-packages.py @@ -18,19 +18,18 @@ def one_package(directory: Path, suffix: str) -> Path: def extract_rpm(package: Path, destination: Path) -> None: - producer = subprocess.Popen(["rpm2cpio", str(package)], stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL) - try: - subprocess.run(["cpio", "-idmu", "--quiet"], stdin=producer.stdout, + # cpio stops at TRAILER!!! and may close a pipeline while rpm2cpio is still + # writing padding. Keep the finite archive in a temporary file so both exit + # codes describe real decode failures, and retain the producer's error text. + with tempfile.TemporaryFile() as archive: + producer = subprocess.run(["rpm2cpio", str(package)], stdout=archive, + stderr=subprocess.PIPE, timeout=180, check=False) + if producer.returncode: + detail = producer.stderr.decode("utf-8", errors="replace")[-1024:].strip() + raise RuntimeError(f"rpm2cpio failed ({producer.returncode}): {detail}") + archive.seek(0) + subprocess.run(["cpio", "-idmu", "--quiet"], stdin=archive, cwd=destination, check=True, timeout=180) - if producer.stdout: - producer.stdout.close() - if producer.wait(timeout=20): - raise RuntimeError("rpm2cpio failed") - finally: - if producer.poll() is None: - producer.kill() - producer.wait(timeout=5) def verify_extracted(name: str, root: Path, executable: Path) -> None: From 7de1ee9644dbd70b1d32cea61eed62296abb29ac Mon Sep 17 00:00:00 2001 From: czyt Date: Sat, 17 Oct 2026 04:34:07 +0800 Subject: [PATCH 06/11] Restore pinned Java bytes in Linux AppImage --- .github/workflows/build.yml | 3 + docs/BUILD-AND-RELEASE.md | 2 + scripts/repair-linux-appimage-java.py | 111 ++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 scripts/repair-linux-appimage-java.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0797f95..66003bfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -184,6 +184,9 @@ jobs: projectPath: desktop tauriScript: ../web/node_modules/.bin/tauri args: --verbose --config tauri.java.json --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} + - name: Restore original bundled Java in Linux AppImage + if: runner.os == 'Linux' + run: python scripts/repair-linux-appimage-java.py target/${{ matrix.target }}/release/bundle - name: Verify desktop worker without opening a GUI run: python scripts/check-script-worker.py target/${{ matrix.target }}/release/moleapi - name: Verify actual bundled desktop JVM and original runtime files diff --git a/docs/BUILD-AND-RELEASE.md b/docs/BUILD-AND-RELEASE.md index ca0223bb..1c8d23ae 100644 --- a/docs/BUILD-AND-RELEASE.md +++ b/docs/BUILD-AND-RELEASE.md @@ -47,6 +47,8 @@ npm --prefix web run desktop:build 读取对应资源,Docker 使用固定 /opt/moleapi/runtime/java/payload/bin/java。 MOLEAPI_CODEGEN_JAVA 与已有桌面用户选择可覆盖默认自带 JRE。 +Linux AppImage 的 `linuxdeploy` 会改写 JRE 内 ELF 文件,导致固定原始清单哈希失效。桌面构建在 Tauri 打包后调用 `scripts/repair-linux-appimage-java.py`:从固定源恢复完整 JRE,使用 SHA256 固定的 `linuxdeploy-plugin-appimage` 内 `appimagetool` 和原 AppImage runtime 重新封装,解包复核 JRE、worker 与 Dubbo。脚本拒绝修改已签名的 AppImage;启用签名时必须在重新封装后签名。旧构建 `38946817315` 的真实包已在本机按同一流程重新封装并通过这三项检查,当前分支的原生 CI 仍待结果。 + 经典 Dubbo 使用同一份固定 Temurin JRE 和独立固定哈希的 Apache Dubbo worker JAR。构建前运行 `python scripts/prepare_dubbo_worker.py`(首次需要显式 Maven/JDK 或提供已构建且哈希匹配的 JAR);独立服务端压缩包和桌面资源均放在 `runtime/dubbo/engine.jar`,Docker 固定路径为 `/opt/moleapi/runtime/dubbo/engine.jar`。`scripts/check-bundled-dubbo.py` 检查 JAR/JRE 原始字节并实际启动 worker;真实业务调用、原生安装与容器 smoke 仍以对应平台流水线为准。开发者可显式设置绝对路径 `MOLEAPI_DUBBO_WORKER_JAR`,运行时不会下载或接受工作区提供的可执行文件路径。 发行验证用 scripts/check-bundled-java.py:在清空外部 Java 配置、使 PATH 不可用后, diff --git a/scripts/repair-linux-appimage-java.py b/scripts/repair-linux-appimage-java.py new file mode 100644 index 00000000..4e7761f6 --- /dev/null +++ b/scripts/repair-linux-appimage-java.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Restore the exact pinned JRE after linuxdeploy mutates AppImage ELF resources.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +from urllib.request import Request, urlopen + +from prepare_java_runtime import verify_payload + +ROOT = Path(__file__).resolve().parents[1] +TARGET = "x86_64-unknown-linux-gnu" +PLUGIN_URL = ("https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/" + "download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage") +PLUGIN_SHA256 = "49d6a17160675a6bd1781699aae6bdf7692d98552e02a3671d2183d10547842e" +MAX_PLUGIN = 32 * 1024 * 1024 +MAX_RUNTIME = 2 * 1024 * 1024 + + +def checked_plugin(path: Path | None, temporary: Path) -> Path: + if path is None: + path = temporary / "linuxdeploy-plugin-appimage-x86_64.AppImage" + with urlopen(Request(PLUGIN_URL, headers={"User-Agent": "moleapi-build"}), timeout=60) as response: + total = 0 + with path.open("wb") as output: + while chunk := response.read(1024 * 1024): + total += len(chunk) + if total > MAX_PLUGIN: + raise ValueError("AppImage packaging plugin exceeds size limit") + output.write(chunk) + if hashlib.sha256(path.read_bytes()).hexdigest() != PLUGIN_SHA256: + raise ValueError("AppImage packaging plugin hash mismatch") + path.chmod(path.stat().st_mode | 0o100) + return path + + +def extract(image: Path, destination: Path) -> Path: + destination.mkdir() + subprocess.run([str(image), "--appimage-extract"], cwd=destination, + env={**os.environ, "APPIMAGE_EXTRACT_AND_RUN": "1"}, + stdout=subprocess.DEVNULL, check=True, timeout=180) + appdir = destination / "squashfs-root" + if not appdir.is_dir(): + raise ValueError("AppImage extraction did not create an AppDir") + return appdir + + +def verify_java(resources: Path) -> None: + manifest = json.loads((ROOT / "vendor/java-runtime/manifest.json").read_text()) + verify_payload(resources / "runtime/java", manifest, TARGET) + + +def repair(bundle_directory: Path, source_java: Path, plugin: Path | None) -> None: + images = list(bundle_directory.resolve(strict=True).rglob("*.AppImage")) + if len(images) != 1: + raise ValueError(f"Expected one Linux AppImage, found {len(images)}") + image = images[0] + if image.with_suffix(image.suffix + ".sig").exists(): + raise ValueError("Refusing to alter an already signed AppImage") + source_java = source_java.resolve(strict=True) + manifest = json.loads((ROOT / "vendor/java-runtime/manifest.json").read_text()) + verify_payload(source_java, manifest, TARGET) + with tempfile.TemporaryDirectory(prefix="moleapi-appimage-repair-", dir=image.parent) as temporary: + base = Path(temporary) + appdir = extract(image, base / "original") + resources = appdir / "usr/lib/MoleAPI" + installed_java = resources / "runtime/java" + if not resources.is_dir() or not installed_java.is_dir(): + raise ValueError("AppImage bundled Java directory missing") + if not installed_java.samefile(source_java): + shutil.rmtree(installed_java) + shutil.copytree(source_java, installed_java, symlinks=True) + verify_java(resources) + + offset = int(subprocess.check_output([str(image), "--appimage-offset"], text=True, timeout=15).strip()) + if not 0 < offset <= MAX_RUNTIME: + raise ValueError("AppImage runtime offset exceeds limit") + runtime = base / "runtime-x86_64" + with image.open("rb") as original, runtime.open("wb") as output: + output.write(original.read(offset)) + tool_image = checked_plugin(plugin, base) + tool = extract(tool_image, base / "tool") / "usr/bin/appimagetool" + if not tool.is_file(): + raise ValueError("Pinned AppImage plugin has no appimagetool") + repacked = base / image.name + subprocess.run([str(tool), "--no-appstream", "--runtime-file", str(runtime), + str(appdir), str(repacked)], env={**os.environ, "ARCH": "x86_64"}, + stdout=subprocess.DEVNULL, check=True, timeout=180) + verified = extract(repacked, base / "verified") + verify_java(verified / "usr/lib/MoleAPI") + subprocess.run([sys.executable, str(ROOT / "scripts/check-script-worker.py"), + str(verified / "AppRun")], check=True, timeout=45) + subprocess.run([sys.executable, str(ROOT / "scripts/check-bundled-dubbo.py"), + str(verified / "AppRun"), "--resource-root", + str(verified / "usr/lib/MoleAPI")], check=True, timeout=45) + os.replace(repacked, image) + print(f"PASS restored original JRE in {image.name}", flush=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("bundle_directory", type=Path) + parser.add_argument("--java-source", type=Path, default=ROOT / "vendor/java-runtime/current") + parser.add_argument("--plugin", type=Path, help="Pinned plugin file already downloaded") + args = parser.parse_args() + repair(args.bundle_directory, args.java_source, args.plugin) From 6de169cca02efc3e4c296d4c68513b112eeeb0bc Mon Sep 17 00:00:00 2001 From: czyt Date: Sat, 17 Oct 2026 04:35:59 +0800 Subject: [PATCH 07/11] Reject embedded AppImage signatures before repacking --- scripts/repair-linux-appimage-java.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/repair-linux-appimage-java.py b/scripts/repair-linux-appimage-java.py index 4e7761f6..3747b828 100644 --- a/scripts/repair-linux-appimage-java.py +++ b/scripts/repair-linux-appimage-java.py @@ -62,6 +62,10 @@ def repair(bundle_directory: Path, source_java: Path, plugin: Path | None) -> No image = images[0] if image.with_suffix(image.suffix + ".sig").exists(): raise ValueError("Refusing to alter an already signed AppImage") + signature = subprocess.run([str(image), "--appimage-signature"], + capture_output=True, check=True, timeout=15) + if signature.stdout.strip(): + raise ValueError("Refusing to alter an AppImage with an embedded signature") source_java = source_java.resolve(strict=True) manifest = json.loads((ROOT / "vendor/java-runtime/manifest.json").read_text()) verify_payload(source_java, manifest, TARGET) From b575231507156dc900e96ac3ee569052818a5763 Mon Sep 17 00:00:00 2001 From: czyt Date: Sat, 17 Oct 2026 05:51:55 +0800 Subject: [PATCH 08/11] Query AppImage runtime without extract-and-run mode --- docs/BUILD-AND-RELEASE.md | 2 ++ scripts/repair-linux-appimage-java.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/BUILD-AND-RELEASE.md b/docs/BUILD-AND-RELEASE.md index 1c8d23ae..65ff8106 100644 --- a/docs/BUILD-AND-RELEASE.md +++ b/docs/BUILD-AND-RELEASE.md @@ -49,6 +49,8 @@ MOLEAPI_CODEGEN_JAVA 与已有桌面用户选择可覆盖默认自带 JRE。 Linux AppImage 的 `linuxdeploy` 会改写 JRE 内 ELF 文件,导致固定原始清单哈希失效。桌面构建在 Tauri 打包后调用 `scripts/repair-linux-appimage-java.py`:从固定源恢复完整 JRE,使用 SHA256 固定的 `linuxdeploy-plugin-appimage` 内 `appimagetool` 和原 AppImage runtime 重新封装,解包复核 JRE、worker 与 Dubbo。脚本拒绝修改已签名的 AppImage;启用签名时必须在重新封装后签名。旧构建 `38946817315` 的真实包已在本机按同一流程重新封装并通过这三项检查,当前分支的原生 CI 仍待结果。 +CI 为无 FUSE 环境全局设置 `APPIMAGE_EXTRACT_AND_RUN=1`。修复脚本读取 AppImage runtime 的签名和偏移时临时移除该变量,避免把 runtime 查询误当成应用启动;实际解包步骤仍显式启用它。run `38963507351` 首次执行修复脚本时在签名查询处返回101,本地用相同环境复现并验证修复;该次打包结果不算通过。 + 经典 Dubbo 使用同一份固定 Temurin JRE 和独立固定哈希的 Apache Dubbo worker JAR。构建前运行 `python scripts/prepare_dubbo_worker.py`(首次需要显式 Maven/JDK 或提供已构建且哈希匹配的 JAR);独立服务端压缩包和桌面资源均放在 `runtime/dubbo/engine.jar`,Docker 固定路径为 `/opt/moleapi/runtime/dubbo/engine.jar`。`scripts/check-bundled-dubbo.py` 检查 JAR/JRE 原始字节并实际启动 worker;真实业务调用、原生安装与容器 smoke 仍以对应平台流水线为准。开发者可显式设置绝对路径 `MOLEAPI_DUBBO_WORKER_JAR`,运行时不会下载或接受工作区提供的可执行文件路径。 发行验证用 scripts/check-bundled-java.py:在清空外部 Java 配置、使 PATH 不可用后, diff --git a/scripts/repair-linux-appimage-java.py b/scripts/repair-linux-appimage-java.py index 3747b828..f1b577e2 100644 --- a/scripts/repair-linux-appimage-java.py +++ b/scripts/repair-linux-appimage-java.py @@ -62,8 +62,10 @@ def repair(bundle_directory: Path, source_java: Path, plugin: Path | None) -> No image = images[0] if image.with_suffix(image.suffix + ".sig").exists(): raise ValueError("Refusing to alter an already signed AppImage") + runtime_env = {key: value for key, value in os.environ.items() + if key != "APPIMAGE_EXTRACT_AND_RUN"} signature = subprocess.run([str(image), "--appimage-signature"], - capture_output=True, check=True, timeout=15) + capture_output=True, check=True, timeout=15, env=runtime_env) if signature.stdout.strip(): raise ValueError("Refusing to alter an AppImage with an embedded signature") source_java = source_java.resolve(strict=True) @@ -81,7 +83,8 @@ def repair(bundle_directory: Path, source_java: Path, plugin: Path | None) -> No shutil.copytree(source_java, installed_java, symlinks=True) verify_java(resources) - offset = int(subprocess.check_output([str(image), "--appimage-offset"], text=True, timeout=15).strip()) + offset = int(subprocess.check_output([str(image), "--appimage-offset"], + text=True, timeout=15, env=runtime_env).strip()) if not 0 < offset <= MAX_RUNTIME: raise ValueError("AppImage runtime offset exceeds limit") runtime = base / "runtime-x86_64" From 1eaf96cdb7a2e68d332af4780345de49227afd79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=AB=E5=AD=90=E6=A8=B1=E6=A1=83?= Date: Sat, 17 Oct 2026 07:28:30 +0800 Subject: [PATCH 09/11] ci: use libarchive to extract desktop RPM --- scripts/check-linux-desktop-packages.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/scripts/check-linux-desktop-packages.py b/scripts/check-linux-desktop-packages.py index 3bb7c943..4a06f693 100644 --- a/scripts/check-linux-desktop-packages.py +++ b/scripts/check-linux-desktop-packages.py @@ -18,18 +18,8 @@ def one_package(directory: Path, suffix: str) -> Path: def extract_rpm(package: Path, destination: Path) -> None: - # cpio stops at TRAILER!!! and may close a pipeline while rpm2cpio is still - # writing padding. Keep the finite archive in a temporary file so both exit - # codes describe real decode failures, and retain the producer's error text. - with tempfile.TemporaryFile() as archive: - producer = subprocess.run(["rpm2cpio", str(package)], stdout=archive, - stderr=subprocess.PIPE, timeout=180, check=False) - if producer.returncode: - detail = producer.stderr.decode("utf-8", errors="replace")[-1024:].strip() - raise RuntimeError(f"rpm2cpio failed ({producer.returncode}): {detail}") - archive.seek(0) - subprocess.run(["cpio", "-idmu", "--quiet"], stdin=archive, - cwd=destination, check=True, timeout=180) + subprocess.run(["bsdtar", "-xf", str(package)], cwd=destination, + check=True, timeout=180) def verify_extracted(name: str, root: Path, executable: Path) -> None: From 3a8b04ae3056e89ffecab15811a82eb85ef0aab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=AB=E5=AD=90=E6=A8=B1=E6=A1=83?= Date: Sat, 17 Oct 2026 07:30:26 +0800 Subject: [PATCH 10/11] ci: bootstrap libarchive for hosted RPM check --- scripts/check-linux-desktop-packages.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/check-linux-desktop-packages.py b/scripts/check-linux-desktop-packages.py index 4a06f693..ce03156d 100644 --- a/scripts/check-linux-desktop-packages.py +++ b/scripts/check-linux-desktop-packages.py @@ -3,6 +3,7 @@ import argparse import os from pathlib import Path +import shutil import subprocess import sys import tempfile @@ -18,7 +19,15 @@ def one_package(directory: Path, suffix: str) -> Path: def extract_rpm(package: Path, destination: Path) -> None: - subprocess.run(["bsdtar", "-xf", str(package)], cwd=destination, + bsdtar = shutil.which("bsdtar") + if bsdtar is None and os.environ.get("GITHUB_ACTIONS") == "true" and os.environ.get("RUNNER_OS") == "Linux": + # The workflow already updated apt metadata before building this package. + subprocess.run(["sudo", "apt-get", "install", "-y", "libarchive-tools"], + check=True, timeout=180) + bsdtar = shutil.which("bsdtar") + if bsdtar is None: + raise RuntimeError("bsdtar (libarchive-tools) is required to inspect RPM payloads") + subprocess.run([bsdtar, "-xf", str(package)], cwd=destination, check=True, timeout=180) From 4d1a8ad612d96a5099d61ea7dc2e7643b4a5780e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=AB=E5=AD=90=E6=A8=B1=E6=A1=83?= Date: Sat, 17 Oct 2026 07:30:28 +0800 Subject: [PATCH 11/11] docs: record RPM extraction evidence --- docs/BUILD-AND-RELEASE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/BUILD-AND-RELEASE.md b/docs/BUILD-AND-RELEASE.md index 65ff8106..9ac5b369 100644 --- a/docs/BUILD-AND-RELEASE.md +++ b/docs/BUILD-AND-RELEASE.md @@ -51,6 +51,8 @@ Linux AppImage 的 `linuxdeploy` 会改写 JRE 内 ELF 文件,导致固定原 CI 为无 FUSE 环境全局设置 `APPIMAGE_EXTRACT_AND_RUN=1`。修复脚本读取 AppImage runtime 的签名和偏移时临时移除该变量,避免把 runtime 查询误当成应用启动;实际解包步骤仍显式启用它。run `38963507351` 首次执行修复脚本时在签名查询处返回101,本地用相同环境复现并验证修复;该次打包结果不算通过。 +分支构建 `38972262886` 已产出真实 Linux deb/rpm/AppImage,独立 deb 解包、JRE 原始清单与 Dubbo worker 自检通过,但 Ubuntu 22.04 上的 `rpm2cpio` 对该 RPM 返回1且没有错误正文,导致 RPM 与 AppImage 后续自检未执行。下载该次构建的原始 RPM 后,本地 `bsdtar` 可完整列出并解包;解包的 JRE 清单与 Dubbo worker 校验通过。本地 Arch 缺少 Ubuntu 打包程序所需的 WebKitGTK4.1 动态库,不能替代原生桌面 worker 检查。自检脚本已改为使用 `libarchive-tools` 的 `bsdtar` 提取真实 RPM;仅在 GitHub Linux runner 缺少该命令时,利用工作流先前更新的 apt 索引安装此工具。本地缺少工具会明确报错。新的原生 CI 结果仍待验证。 + 经典 Dubbo 使用同一份固定 Temurin JRE 和独立固定哈希的 Apache Dubbo worker JAR。构建前运行 `python scripts/prepare_dubbo_worker.py`(首次需要显式 Maven/JDK 或提供已构建且哈希匹配的 JAR);独立服务端压缩包和桌面资源均放在 `runtime/dubbo/engine.jar`,Docker 固定路径为 `/opt/moleapi/runtime/dubbo/engine.jar`。`scripts/check-bundled-dubbo.py` 检查 JAR/JRE 原始字节并实际启动 worker;真实业务调用、原生安装与容器 smoke 仍以对应平台流水线为准。开发者可显式设置绝对路径 `MOLEAPI_DUBBO_WORKER_JAR`,运行时不会下载或接受工作区提供的可执行文件路径。 发行验证用 scripts/check-bundled-java.py:在清空外部 Java 配置、使 PATH 不可用后,