Skip to content

Commit 222bc50

Browse files
authored
Merge pull request #13 from clash-space/agent/reduce-desktop-package-size
fix: reduce desktop installer size
2 parents 3eca5da + f92a431 commit 222bc50

10 files changed

Lines changed: 206 additions & 21 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,13 @@ jobs:
106106
matrix:
107107
include:
108108
- os: macos-latest
109-
platform: macOS
110-
script: pack:mac
111-
asset: apps/desktop/release/Clash-Desktop-macOS-universal.dmg
109+
platform: macOS-arm64
110+
script: pack:mac:arm64
111+
asset: apps/desktop/release/Clash-Desktop-macOS-arm64.dmg
112+
- os: macos-latest
113+
platform: macOS-x64
114+
script: pack:mac:x64
115+
asset: apps/desktop/release/Clash-Desktop-macOS-x64.dmg
112116
- os: windows-latest
113117
platform: Windows
114118
script: pack:win

‎.github/workflows/release.yml‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,13 @@ jobs:
9999
matrix:
100100
include:
101101
- os: macos-latest
102-
platform: macOS
103-
script: pack:mac
104-
asset: apps/desktop/release/Clash-Desktop-macOS-universal.dmg
102+
platform: macOS-arm64
103+
script: pack:mac:arm64
104+
asset: apps/desktop/release/Clash-Desktop-macOS-arm64.dmg
105+
- os: macos-latest
106+
platform: macOS-x64
107+
script: pack:mac:x64
108+
asset: apps/desktop/release/Clash-Desktop-macOS-x64.dmg
105109
- os: windows-latest
106110
platform: Windows
107111
script: pack:win

‎apps/desktop/electron-builder.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ productName: Clash
33
asar: true
44
npmRebuild: false
55
publish: null
6+
afterPack: scripts/prune-packaged-architectures.mjs
67

78
directories:
89
output: release

‎apps/desktop/package.json‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
"build": "tsup",
1111
"dev": "tsx src/dev.ts",
1212
"pack:dir": "pnpm prepare:pack && electron-builder --dir --config electron-builder.yml",
13-
"pack:dmg": "pnpm prepare:pack && electron-builder --mac dmg --universal --config electron-builder.yml --publish never",
14-
"pack:mac": "pnpm prepare:pack && electron-builder --mac dmg --universal --config electron-builder.yml --publish never",
13+
"pack:dmg": "pnpm prepare:pack && electron-builder --mac dmg --arm64 --config electron-builder.yml --publish never",
14+
"pack:mac": "pnpm pack:mac:arm64",
15+
"pack:mac:arm64": "pnpm prepare:pack && electron-builder --mac dmg --arm64 --config electron-builder.yml --publish never",
16+
"pack:mac:x64": "pnpm prepare:pack && electron-builder --mac dmg --x64 --config electron-builder.yml --publish never",
17+
"pack:mac:universal": "pnpm prepare:pack && electron-builder --mac dmg --universal --config electron-builder.yml --publish never",
1518
"pack:win": "pnpm prepare:pack && electron-builder --win nsis --x64 --config electron-builder.yml --publish never",
1619
"pack:linux": "pnpm prepare:pack && electron-builder --linux AppImage --x64 --config electron-builder.yml --publish never",
1720
"prepare:harnesses": "node scripts/prepare-acp-harnesses.mjs",
@@ -44,7 +47,6 @@
4447
"test:watch": "vitest"
4548
},
4649
"dependencies": {
47-
"@remotion/bundler": "4.0.370",
4850
"@remotion/renderer": "4.0.370",
4951
"@clash-space/cli": "workspace:*",
5052
"@clash/shared-runtime": "workspace:*",
@@ -53,6 +55,7 @@
5355
"devDependencies": {
5456
"@agentclientprotocol/claude-agent-acp": "^0.47.0",
5557
"@agentclientprotocol/codex-acp": "^1.1.2",
58+
"@remotion/bundler": "4.0.370",
5659
"@types/node": "^22.13.0",
5760
"electron": "^42.3.2",
5861
"electron-builder": "26.15.3",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { rm } from "node:fs/promises";
2+
import { join } from "node:path";
3+
4+
const X64 = 1;
5+
const ARM64 = 3;
6+
7+
export function packageDirectoriesToPrune(arch) {
8+
if (arch === ARM64) {
9+
return [
10+
"@anthropic-ai/claude-agent-sdk-darwin-x64",
11+
"@esbuild/darwin-x64",
12+
"@remotion/compositor-darwin-x64",
13+
];
14+
}
15+
if (arch === X64) {
16+
return [
17+
"@anthropic-ai/claude-agent-sdk-darwin-arm64",
18+
"@esbuild/darwin-arm64",
19+
"@remotion/compositor-darwin-arm64",
20+
];
21+
}
22+
return [];
23+
}
24+
25+
export default async function prunePackagedArchitectures(context) {
26+
if (context.electronPlatformName !== "darwin") return;
27+
28+
const packageDirectories = packageDirectoriesToPrune(context.arch);
29+
if (packageDirectories.length === 0) return;
30+
31+
const nodeModules = join(
32+
context.appOutDir,
33+
`${context.packager.appInfo.productFilename}.app`,
34+
"Contents",
35+
"Resources",
36+
"app.asar.unpacked",
37+
"node_modules",
38+
);
39+
40+
await Promise.all(
41+
packageDirectories.map((packageName) =>
42+
rm(join(nodeModules, ...packageName.split("/")), {
43+
recursive: true,
44+
force: true,
45+
}),
46+
),
47+
);
48+
}

‎apps/desktop/src/electron-version.test.ts‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
33

44
interface DesktopPackage {
55
scripts?: Record<string, string>;
6+
dependencies?: Record<string, string>;
67
devDependencies?: Record<string, string>;
78
}
89

@@ -60,15 +61,23 @@ describe("desktop Electron runtime", () => {
6061
);
6162

6263
expect(manifest.scripts ?? {}).toHaveProperty("pack:mac");
64+
expect(manifest.scripts ?? {}).toHaveProperty("pack:mac:arm64");
65+
expect(manifest.scripts ?? {}).toHaveProperty("pack:mac:x64");
6366
expect(manifest.scripts ?? {}).toHaveProperty("pack:win");
6467
expect(manifest.scripts ?? {}).toHaveProperty("pack:linux");
65-
expect(manifest.scripts?.["pack:mac"] ?? "").toContain(
66-
"--mac dmg --universal",
68+
expect(manifest.scripts?.["pack:mac:arm64"] ?? "").toContain(
69+
"--mac dmg --arm64",
6770
);
71+
expect(manifest.scripts?.["pack:mac:x64"] ?? "").toContain(
72+
"--mac dmg --x64",
73+
);
74+
expect(manifest.scripts?.["pack:mac"] ?? "").toContain("pack:mac:arm64");
6875
expect(manifest.scripts?.["pack:win"] ?? "").toContain("--win nsis --x64");
6976
expect(manifest.scripts?.["pack:linux"] ?? "").toContain(
7077
"--linux AppImage --x64",
7178
);
79+
expect(manifest.dependencies ?? {}).not.toHaveProperty("@remotion/bundler");
80+
expect(manifest.devDependencies?.["@remotion/bundler"]).toBe("4.0.370");
7281
expect(manifest.scripts?.["prepare:pack"] ?? "").toContain(
7382
"pnpm --filter @master-clash/web... build",
7483
);
@@ -95,6 +104,9 @@ describe("desktop Electron runtime", () => {
95104
expect(builderConfig).toContain(
96105
'x64ArchFiles: "**/node_modules/{@anthropic-ai/claude-agent-sdk-*,@esbuild/*,@remotion/compositor-*}/**"',
97106
);
107+
expect(builderConfig).toContain(
108+
"afterPack: scripts/prune-packaged-architectures.mjs",
109+
);
98110
expect(workspaceConfig).toMatch(
99111
/supportedArchitectures:\n\s+cpu:\s+\[arm64, x64\]/,
100112
);
@@ -131,6 +143,14 @@ describe("desktop Electron runtime", () => {
131143
for (const workflow of [ci, release]) {
132144
expect(workflow).toContain("package-desktop:");
133145
expect(workflow).toContain("macos-latest");
146+
expect(workflow).toContain("platform: macOS-arm64");
147+
expect(workflow).toContain("platform: macOS-x64");
148+
expect(workflow).toContain(
149+
"apps/desktop/release/Clash-Desktop-macOS-arm64.dmg",
150+
);
151+
expect(workflow).toContain(
152+
"apps/desktop/release/Clash-Desktop-macOS-x64.dmg",
153+
);
134154
expect(workflow).toContain("windows-latest");
135155
expect(workflow).toContain("ubuntu-latest");
136156
expect(workflow).toContain("actions/upload-artifact@v4");
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { existsSync } from "node:fs";
2+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { afterEach, describe, expect, it } from "vitest";
6+
7+
// @ts-expect-error The electron-builder hook is an ESM script without declarations.
8+
import * as pruningHook from "../scripts/prune-packaged-architectures.mjs";
9+
10+
const {
11+
default: prunePackagedArchitectures,
12+
packageDirectoriesToPrune,
13+
} = pruningHook as unknown as {
14+
default: (context: {
15+
appOutDir: string;
16+
arch: number;
17+
electronPlatformName: string;
18+
packager: { appInfo: { productFilename: string } };
19+
}) => Promise<void>;
20+
packageDirectoriesToPrune: (arch: number) => string[];
21+
};
22+
23+
const temporaryDirectories: string[] = [];
24+
25+
afterEach(async () => {
26+
await Promise.all(
27+
temporaryDirectories.splice(0).map((directory) =>
28+
rm(directory, { recursive: true, force: true }),
29+
),
30+
);
31+
});
32+
33+
describe("desktop package architecture pruning", () => {
34+
it("maps each single-architecture macOS build to the opposite native packages", () => {
35+
expect(packageDirectoriesToPrune(3)).toEqual([
36+
"@anthropic-ai/claude-agent-sdk-darwin-x64",
37+
"@esbuild/darwin-x64",
38+
"@remotion/compositor-darwin-x64",
39+
]);
40+
expect(packageDirectoriesToPrune(1)).toEqual([
41+
"@anthropic-ai/claude-agent-sdk-darwin-arm64",
42+
"@esbuild/darwin-arm64",
43+
"@remotion/compositor-darwin-arm64",
44+
]);
45+
expect(packageDirectoriesToPrune(4)).toEqual([]);
46+
});
47+
48+
it("removes only the opposite-architecture packages from the staged macOS app", async () => {
49+
const appOutDir = await mkdtemp(
50+
join(tmpdir(), "clash-package-pruning-"),
51+
);
52+
temporaryDirectories.push(appOutDir);
53+
const nodeModules = join(
54+
appOutDir,
55+
"Clash.app",
56+
"Contents",
57+
"Resources",
58+
"app.asar.unpacked",
59+
"node_modules",
60+
);
61+
const x64Directories = packageDirectoriesToPrune(3);
62+
const preservedArm64 = join(
63+
nodeModules,
64+
"@anthropic-ai",
65+
"claude-agent-sdk-darwin-arm64",
66+
);
67+
68+
for (const packageName of [...x64Directories, "@anthropic-ai/claude-agent-sdk-darwin-arm64"]) {
69+
const packageDirectory = join(nodeModules, ...packageName.split("/"));
70+
await mkdir(packageDirectory, { recursive: true });
71+
await writeFile(join(packageDirectory, "native-binary"), packageName);
72+
}
73+
74+
await prunePackagedArchitectures({
75+
appOutDir,
76+
arch: 3,
77+
electronPlatformName: "darwin",
78+
packager: { appInfo: { productFilename: "Clash" } },
79+
});
80+
81+
for (const packageName of x64Directories) {
82+
expect(
83+
existsSync(join(nodeModules, ...packageName.split("/"))),
84+
).toBe(false);
85+
}
86+
expect(existsSync(preservedArm64)).toBe(true);
87+
});
88+
});

‎apps/web/app/routes/desktop-first-landing.test.tsx‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,25 @@ describe("desktop-first public landing", () => {
105105
).toBe(false);
106106
});
107107

108-
it("provides direct installer assets for macOS, Windows, and Linux", () => {
108+
it("provides architecture-specific macOS installers plus Windows and Linux", () => {
109109
render(<DownloadRoute />);
110110

111111
expect(
112112
screen.getByRole("heading", { name: /download clash desktop/i }),
113113
).toBeTruthy();
114114
expect(
115115
screen
116-
.getByRole("link", { name: /download for macos/i })
116+
.getByRole("link", { name: /download for macos apple silicon/i })
117117
.getAttribute("href"),
118118
).toBe(
119-
"https://github.com/clash-space/clash/releases/download/desktop-preview/Clash-Desktop-macOS-universal.dmg",
119+
"https://github.com/clash-space/clash/releases/download/desktop-preview/Clash-Desktop-macOS-arm64.dmg",
120+
);
121+
expect(
122+
screen
123+
.getByRole("link", { name: /download for macos intel/i })
124+
.getAttribute("href"),
125+
).toBe(
126+
"https://github.com/clash-space/clash/releases/download/desktop-preview/Clash-Desktop-macOS-x64.dmg",
120127
);
121128
expect(
122129
screen

‎apps/web/app/routes/download.tsx‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,29 @@ import {
1111
const downloads = [
1212
{
1313
platform: "macOS",
14-
detail: "Universal · Apple silicon and Intel",
15-
href: "https://github.com/clash-space/clash/releases/download/desktop-preview/Clash-Desktop-macOS-universal.dmg",
14+
detail: "Apple silicon · M1 and newer",
15+
ariaLabel: "Download for macOS Apple silicon",
16+
href: "https://github.com/clash-space/clash/releases/download/desktop-preview/Clash-Desktop-macOS-arm64.dmg",
17+
icon: AppleLogo,
18+
},
19+
{
20+
platform: "macOS",
21+
detail: "Intel · x64",
22+
ariaLabel: "Download for macOS Intel",
23+
href: "https://github.com/clash-space/clash/releases/download/desktop-preview/Clash-Desktop-macOS-x64.dmg",
1624
icon: AppleLogo,
1725
},
1826
{
1927
platform: "Windows",
2028
detail: "64-bit · NSIS installer",
29+
ariaLabel: "Download for Windows",
2130
href: "https://github.com/clash-space/clash/releases/download/desktop-preview/Clash-Desktop-Windows-x64.exe",
2231
icon: WindowsLogo,
2332
},
2433
{
2534
platform: "Linux",
2635
detail: "64-bit · AppImage",
36+
ariaLabel: "Download for Linux",
2737
href: "https://github.com/clash-space/clash/releases/download/desktop-preview/Clash-Desktop-Linux-x64.AppImage",
2838
icon: LinuxLogo,
2939
},
@@ -51,11 +61,11 @@ export default function DownloadRoute() {
5161
aria-label="Desktop installers"
5262
>
5363
{downloads.map((download) => (
54-
<li key={download.platform}>
64+
<li key={download.ariaLabel}>
5565
<a
5666
href={download.href}
5767
className="clash-download-card"
58-
aria-label={`Download for ${download.platform}`}
68+
aria-label={download.ariaLabel}
5969
>
6070
<download.icon
6171
className="h-8 w-8"

‎pnpm-lock.yaml‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)