mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-21 16:52:40 +00:00
* fix(desktop): preserve Linux sandboxing wherever the host allows it Chromium checks its Linux sandbox before main.js, while package installation probes run as root and miss AppArmor restrictions on desktop users. Choose the launch flags before exec and always install the SUID alternative for native packages. Test published artifacts as an ordinary user with namespaces both denied and allowed; never repair permissions in the smoke harness. * fix(desktop): retain the Linux release build baseline * fix(ci): allow canceled desktop smoke uploads to stop * test(desktop): verify the installed RPM sandbox under AppArmor * fix(ci): resolve packaged desktop dependencies before installation * chore(desktop): keep QA evidence out of the repository
144 lines
4.3 KiB
JavaScript
144 lines
4.3 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const { smokePackagedDesktopApp } = require("../e2e/packaged-app-smoke.js");
|
|
|
|
const { installLinuxLauncher } = require("./linux-sandbox");
|
|
|
|
const EXECUTABLE_NAME = "Paseo";
|
|
|
|
// electron-builder arch enum → Node.js arch string
|
|
const ARCH_MAP = { 0: "ia32", 1: "x64", 2: "armv7l", 3: "arm64", 4: "universal" };
|
|
|
|
const RIPGREP_PLATFORM_DIR = {
|
|
darwin: { arm64: "arm64-darwin", x64: "x64-darwin" },
|
|
linux: { arm64: "arm64-linux", x64: "x64-linux" },
|
|
win32: { arm64: "arm64-win32", x64: "x64-win32" },
|
|
};
|
|
|
|
function rmSafe(target) {
|
|
fs.rmSync(target, { recursive: true, force: true });
|
|
}
|
|
|
|
function pruneChildrenExcept(parent, keep) {
|
|
if (!fs.existsSync(parent)) return;
|
|
for (const entry of fs.readdirSync(parent)) {
|
|
if (!keep.has(entry)) {
|
|
rmSafe(path.join(parent, entry));
|
|
}
|
|
}
|
|
}
|
|
|
|
function pruneClaudeAgentSdk(nodeModules, platform, arch) {
|
|
const vendorRoot = path.join(nodeModules, "@anthropic-ai", "claude-agent-sdk", "vendor");
|
|
const keepName = RIPGREP_PLATFORM_DIR[platform]?.[arch];
|
|
if (keepName) {
|
|
pruneChildrenExcept(path.join(vendorRoot, "ripgrep"), new Set(["COPYING", keepName]));
|
|
pruneChildrenExcept(path.join(vendorRoot, "tree-sitter-bash"), new Set([keepName]));
|
|
}
|
|
|
|
// SDK ≥0.2.113 ships per-platform Claude Code binaries via optionalDependencies
|
|
// (~210 MB each). Paseo requires user-installed `claude` on PATH, matching how
|
|
// Codex/OpenCode are integrated, so drop every bundled copy.
|
|
const anthropicDir = path.join(nodeModules, "@anthropic-ai");
|
|
if (fs.existsSync(anthropicDir)) {
|
|
for (const entry of fs.readdirSync(anthropicDir)) {
|
|
if (entry.startsWith("claude-agent-sdk-")) {
|
|
rmSafe(path.join(anthropicDir, entry));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function pruneNodePty(nodeModules, platform, arch) {
|
|
const prebuilds = path.join(nodeModules, "node-pty", "prebuilds");
|
|
pruneChildrenExcept(prebuilds, new Set([`${platform}-${arch}`]));
|
|
|
|
if (platform !== "win32") {
|
|
rmSafe(path.join(nodeModules, "node-pty", "third_party"));
|
|
}
|
|
}
|
|
|
|
function pruneSharpLibvips(nodeModules, platform, arch) {
|
|
const prefix = `sharp-libvips-${platform}-${arch}`;
|
|
const imgDir = path.join(nodeModules, "@img");
|
|
if (!fs.existsSync(imgDir)) return;
|
|
|
|
for (const entry of fs.readdirSync(imgDir)) {
|
|
if (
|
|
entry.startsWith("sharp-") &&
|
|
entry !== prefix &&
|
|
!entry.startsWith(`sharp-${platform}-${arch}`)
|
|
) {
|
|
rmSafe(path.join(imgDir, entry));
|
|
}
|
|
}
|
|
}
|
|
|
|
function pruneNativeModules(appOutDir, platform, arch) {
|
|
const resourcesDir =
|
|
platform === "darwin"
|
|
? path.join(appOutDir, `${EXECUTABLE_NAME}.app`, "Contents", "Resources")
|
|
: path.join(appOutDir, "resources");
|
|
|
|
const nodeModules = path.join(resourcesDir, "app.asar.unpacked", "node_modules");
|
|
if (!fs.existsSync(nodeModules)) return;
|
|
|
|
const before = dirSizeSync(nodeModules);
|
|
|
|
pruneClaudeAgentSdk(nodeModules, platform, arch);
|
|
pruneNodePty(nodeModules, platform, arch);
|
|
pruneSharpLibvips(nodeModules, platform, arch);
|
|
|
|
const after = dirSizeSync(nodeModules);
|
|
const savedMB = ((before - after) / 1024 / 1024).toFixed(1);
|
|
console.log(`Pruned native modules: ${savedMB} MB removed (${fmtMB(before)} → ${fmtMB(after)})`);
|
|
}
|
|
|
|
function dirSizeSync(dir) {
|
|
let total = 0;
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true, recursive: true })) {
|
|
if (entry.isFile()) {
|
|
try {
|
|
total += fs.statSync(path.join(entry.parentPath || entry.path, entry.name)).size;
|
|
} catch {}
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
function fmtMB(bytes) {
|
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
}
|
|
|
|
exports.default = async function afterPack(context) {
|
|
const platform = context.electronPlatformName;
|
|
const arch = ARCH_MAP[context.arch] || process.arch;
|
|
|
|
pruneNativeModules(context.appOutDir, platform, arch);
|
|
|
|
if (platform === "linux") {
|
|
installLinuxLauncher(context.appOutDir);
|
|
}
|
|
|
|
if (platform === "linux" || platform === "win32") {
|
|
if (arch !== process.arch) {
|
|
console.log(
|
|
`Skipping packaged-app smoke: build arch ${arch} differs from host ${process.arch}.`,
|
|
);
|
|
} else {
|
|
await smokeUnpackedAppIfRequested(context.appOutDir);
|
|
}
|
|
}
|
|
};
|
|
|
|
async function smokeUnpackedAppIfRequested(appOutDir) {
|
|
if (process.env.PASEO_DESKTOP_SMOKE !== "1") {
|
|
return;
|
|
}
|
|
|
|
await smokePackagedDesktopApp({
|
|
appPath: appOutDir,
|
|
});
|
|
}
|