Unverified 提交 e0dcedc1 authored 作者: Ryan Groch's avatar Ryan Groch 提交者: GitHub

Add AppImage maker (#2068)

Closes #817. A while back, I had opened #960 for the same issue, and I thought I would give it a second try. Unlike my previous PR, this time I implemented the AppImage maker myself. Given that security concerns came up last time, I feel obligated to mention the following: 1. This code does run `mksquashfs` (technically a third-party executable) in the same environment as the signing keys. However, it's widely used and is present on the GitHub runner by default. 2. My code also fetches the AppImage runtime during build, but this gets embedded in the AppImage and never runs on the server. As such, it would not be able to access the signing keys even if it theoretically had a vulnerability or was infected. If we're including an AppImage in the releases going forward, I don't think that either of the above two points are easily avoidable. We _could_ handle the second point by storing our own static version of the AppImage runtime, but I don't think that's particularly elegant or desirable. Of course, please feel free to let me know if you'd like any changes. Thanks! <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds an AppImage maker to Electron Forge and includes a Linux x86_64 AppImage in release assets. - **New Features** - Implements MakerAppImage that builds the AppDir, generates the .desktop file, embeds the app icon, sets AppRun, and packs with mksquashfs. - Fetches the AppImage runtime at build time, verifies its SHA-256, and embeds it; it never executes on CI. - Integrates into forge.config and release asset checks. Output: dyad_{version}_x86_64.AppImage. - **Dependencies** - Requires mksquashfs on the build host (available on GitHub runners). <sup>Written for commit 8bdb3840b906cc74a852704be50acb1b4d08fc97. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces Linux AppImage packaging and aligns release checks. > > - Adds `MakerAppImage` that assembles `AppDir`, generates `.desktop`, embeds icon, creates `AppRun` symlink, packs with `mksquashfs`, and prepends the fetched AppImage runtime after SHA-256 verification > - Integrates maker in `forge.config.ts` (outputs `dyad_{version}_x86_64.AppImage`; linux x64; requires `mksquashfs`) > - Updates `scripts/verify-release-assets.js` to include the AppImage in expected assets > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8bdb3840b906cc74a852704be50acb1b4d08fc97. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
上级 afa184b4
...@@ -3,6 +3,7 @@ import { MakerSquirrel } from "@electron-forge/maker-squirrel"; ...@@ -3,6 +3,7 @@ import { MakerSquirrel } from "@electron-forge/maker-squirrel";
import { MakerZIP } from "@electron-forge/maker-zip"; import { MakerZIP } from "@electron-forge/maker-zip";
import { MakerDeb } from "@electron-forge/maker-deb"; import { MakerDeb } from "@electron-forge/maker-deb";
import { MakerRpm } from "@electron-forge/maker-rpm"; import { MakerRpm } from "@electron-forge/maker-rpm";
import { MakerAppImage } from "./makers/MakerAppImage";
import { VitePlugin } from "@electron-forge/plugin-vite"; import { VitePlugin } from "@electron-forge/plugin-vite";
import { FusesPlugin } from "@electron-forge/plugin-fuses"; import { FusesPlugin } from "@electron-forge/plugin-fuses";
import { FuseV1Options, FuseVersion } from "@electron/fuses"; import { FuseV1Options, FuseVersion } from "@electron/fuses";
...@@ -153,6 +154,9 @@ const config: ForgeConfig = { ...@@ -153,6 +154,9 @@ const config: ForgeConfig = {
mimeType: ["x-scheme-handler/dyad"], mimeType: ["x-scheme-handler/dyad"],
}, },
}), }),
new MakerAppImage({
icon: "./assets/icon/logo.png",
}),
], ],
publishers: [ publishers: [
{ {
......
import { MakerBase, MakerOptions } from "@electron-forge/maker-base";
import { execFile } from "child_process";
import {
writeFile,
appendFile,
mkdtemp,
mkdir,
cp,
symlink,
chmod,
readFile,
copyFile,
rm,
} from "fs/promises";
import { tmpdir } from "os";
import { promisify } from "util";
import { resolve, relative, extname } from "path";
import { createHash } from "crypto";
// AppImage runtime version and location
const RUNTIME_VERSION = "20251108";
const RUNTIME_URL = `https://github.com/AppImage/type2-runtime/releases/download/${RUNTIME_VERSION}/runtime-x86_64`;
// SHA256 hash of the expected runtime binary
// Can be generated with: curl -sL <URL> | sha256sum
// Also visible directly on the GitHub releases page; see 'runtime-x86_64' on:
// https://github.com/AppImage/type2-runtime/releases/tag/20251108
const RUNTIME_SHA256 =
"2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d";
// For creating temporary work directories; largely arbitrary
const APPDIR_PREFIX = "AppDir";
const WORKDIR_PREFIX = "AppImageWorkDir";
/**
* Minimalist Forge maker for AppImages
*/
export class MakerAppImage extends MakerBase<{ icon?: string }> {
override defaultPlatforms = ["linux"];
override name = "AppImage";
override requiredExternalBinaries = ["mksquashfs"];
override isSupportedOnCurrentPlatform(): boolean {
return process.platform === "linux" && process.arch === "x64";
}
override async make({
appName,
dir,
makeDir,
packageJSON,
}: MakerOptions): Promise<string[]> {
const version = packageJSON["version"];
if (!version || typeof version !== "string")
throw new Error("Could not access version information");
const { icon } = this.config;
const exeName = `${appName}_${version}_x86_64.AppImage`;
const outputDir = resolve(makeDir, "AppImage");
const outputFilePath = resolve(outputDir, exeName);
// Fetch AppImage runtime
const res = await fetch(RUNTIME_URL, {
signal: AbortSignal.timeout(10000), // 10 second timeout
});
if (!res.ok)
throw new Error(
`Could not fetch AppImage runtime: ${res.status} ${res.statusText}`,
);
const runtime = Buffer.from(await res.arrayBuffer());
// Verify SHA256 hash
const hash = createHash("sha256").update(runtime).digest("hex");
if (hash !== RUNTIME_SHA256)
throw new Error(
[
"AppImage runtime integrity check failed.",
`Expected: ${RUNTIME_SHA256}`,
`Got: ${hash}`,
"The runtime binary may have been tampered with or updated.",
"If this was intentional, please update RUNTIME_SHA256 in makers/MakerAppImage.ts.",
].join("\n"),
);
// Names of temporary directories to clean up later
let appDir: string | undefined;
let workDir: string | undefined;
try {
// Create directory structure of AppDir.
// For conventions, see: https://docs.appimage.org/reference/appdir.html#conventions
appDir = await mkdtemp(
resolve(tmpdir(), `${APPDIR_PREFIX}_${appName}_${version}_`),
);
const binDir = resolve(appDir, "usr/bin");
const libDir = resolve(appDir, `usr/lib/${appName}`);
await mkdir(binDir, { recursive: true, mode: 0o755 });
await mkdir(libDir, { recursive: true, mode: 0o755 });
// Add the actual application code to the AppDir
await cp(dir, libDir, { recursive: true });
// Generate .desktop file
// See: https://docs.appimage.org/reference/desktop-integration.html#desktop-files
// Also: https://specifications.freedesktop.org/desktop-entry/latest/recognized-keys.html
const desktopFile = [
"[Desktop Entry]",
"Type=Application",
"Version=1.5",
`Name=${appName}`,
...(icon ? [`Icon=${appName}`] : []),
"Exec=AppRun %U",
`X-AppImage-Name=${appName}`,
`X-AppImage-Version=${version}`,
"X-AppImage-Arch=x86_64",
].join("\n");
await writeFile(resolve(appDir, `${appName}.desktop`), desktopFile);
// Add the icon
if (icon) {
const ext = extname(icon);
if (!ext || ext !== ".png")
throw new Error(`Invalid icon extension: ${ext || "[None]"}`);
const finalIconName = `${appName}${ext}`;
const finalIconPath = resolve(appDir, finalIconName);
await copyFile(icon, finalIconPath);
await symlink(finalIconName, resolve(appDir, ".DirIcon"), "file");
}
// By convention, executables should be in /bin
await symlink(
relative(binDir, resolve(libDir, appName)),
resolve(binDir, appName),
"file",
);
// The entry point of an AppImage should be the AppRun file.
// See: https://docs.appimage.org/reference/appdir.html#general-description
await symlink(
relative(appDir, resolve(binDir, appName)),
resolve(appDir, "AppRun"),
"file",
);
// mksquashfs emits a file, so we create a temporary file
// inside a temporary directory to hold the output
workDir = await mkdtemp(
resolve(tmpdir(), `${WORKDIR_PREFIX}_${appName}_${version}_`),
);
const tempSquashedFsPath = resolve(workDir, "temp");
const execFileAsync = promisify(execFile);
try {
await execFileAsync("mksquashfs", [appDir, tempSquashedFsPath]);
} catch (err: any) {
const stderr = err?.stderr?.toString?.() ?? "";
const stdout = err?.stdout?.toString?.() ?? "";
throw new Error(
[
"mksquashfs failed",
`exit code: ${err?.code ?? "unknown"}`,
stderr && `stderr:\n${stderr}`,
stdout && `stdout:\n${stdout}`,
]
.filter(Boolean)
.join("\n"),
);
}
// Directory to hold final executable
await mkdir(outputDir, { recursive: true, mode: 0o755 });
// Per the documentation, AppImages should consist
// of the runtime prepended to the squashed fs.
// See: https://docs.appimage.org/reference/architecture.html
await writeFile(outputFilePath, runtime);
await appendFile(outputFilePath, await readFile(tempSquashedFsPath));
await chmod(outputFilePath, 0o755);
return [outputFilePath];
} finally {
// Clean up temporary directories
if (appDir)
await rm(appDir, {
recursive: true,
force: true,
});
if (workDir)
await rm(workDir, {
recursive: true,
force: true,
});
}
}
}
...@@ -87,6 +87,7 @@ async function verifyReleaseAssets() { ...@@ -87,6 +87,7 @@ async function verifyReleaseAssets() {
`dyad-darwin-arm64-${version}.zip`, `dyad-darwin-arm64-${version}.zip`,
`dyad-darwin-x64-${version}.zip`, `dyad-darwin-x64-${version}.zip`,
`dyad_${normalizeVersionForPlatform(version, "deb")}_amd64.deb`, `dyad_${normalizeVersionForPlatform(version, "deb")}_amd64.deb`,
`dyad_${version}_x86_64.AppImage`,
"RELEASES", "RELEASES",
]; ];
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论