Skip to content

Commit d2f019a

Browse files
committed
feat: add native arch package builds
1 parent 63cc25d commit d2f019a

5 files changed

Lines changed: 244 additions & 7 deletions

File tree

CODEBASE_DOCUMENTATION.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ PACKAGING: scripts/tauri/prepare-backend-resources.js - Bundles backend resourc
2222
scripts/tauri/run-tauri-build.js - Centralized Tauri build entrypoint (local Windows fast-cache pinning + profile dispatch)
2323
scripts/tauri/get-release-version.js - Extracts the release version (tag or package.json) and exports it for CI jobs.
2424
scripts/tauri/sync-tauri-version.js - Mirrors the release version into `src-tauri/tauri.conf.json` and `src-tauri/Cargo.toml` before packaging.
25+
scripts/release/build-arch-package.js - Generates a pacman-installable `pkg.tar.zst` from the Tauri Debian staging tree for native Arch Linux installs.
2526
scripts/release/check-version-consistency.js - Fails builds/tags when package/Tauri/Cargo/tag versions drift.
2627
scripts/release/verify-bundle-version.js - Rejects stale bundle filenames before GitHub release upload.
2728
DIFF: diff-viewer/ - Advanced diff viewer component
@@ -124,15 +125,21 @@ scripts/tauri/run-tauri-build.js - Shared local/CI Tauri build launcher
124125
├─ Profiles: dispatches `release` vs `fast` builds from one script instead of duplicating shell commands
125126
├─ Windows fast-cache pinning: local non-CI `fast` builds use a stable `%LOCALAPPDATA%\\AgentWorkspaceBuildCache\\tauri-target` root so repo renames/worktree moves do not discard Cargo incremental state
126127
├─ Local installer trim: local non-CI Windows `fast` builds default to `nsis` instead of building both Windows installer formats
127-
├─ Arch Linux fast-build guard: local non-CI Linux `fast` builds on Arch-family distros default to `deb`, avoiding the known linuxdeploy AppImage strip failure on `.relr.dyn` system libraries and keeping local desktop smoke-test builds reliable
128+
├─ Arch Linux fast-build guard: local non-CI Linux `fast` builds on Arch-family distros default to a native `pacman` package target instead of AppImage
129+
├─ Post-process bundles: pseudo-targets like `pacman` are generated after Tauri finishes its native bundles, reusing the staged `/usr` install tree from the Debian bundle
128130
├─ Version guardrails: syncs Tauri/Cargo metadata from `package.json`, runs release consistency checks, and clears stale bundle output before each build
129131
├─ Artifact verification: blocks CI/local release builds if installer filenames in `bundle/` do not include the expected version
130132
└─ Overrides: respects explicit `CARGO_TARGET_DIR` / `ORCHESTRATOR_TAURI_TARGET_DIR` when callers want a custom target root
133+
scripts/release/build-arch-package.js - Native Arch package generator
134+
├─ Input: reuses the extracted Tauri `deb` bundle payload under `src-tauri/target/<profile>/bundle/deb/*/data`
135+
├─ Output: writes `bundle/pacman/agent-workspace-<version>-1-x86_64.pkg.tar.zst`
136+
├─ Metadata: emits pacman `.PKGINFO` with runtime deps (`gtk3`, `webkit2gtk-4.1`, `libayatana-appindicator`, `hicolor-icon-theme`)
137+
└─ Install flow: resulting artifact is suitable for `sudo pacman -U ...pkg.tar.zst`
131138
scripts/release/check-version-consistency.js - Release metadata guardrail
132139
├─ Validates: `package.json`, `src-tauri/tauri.conf.json`, `src-tauri/Cargo.toml`, and the active Git tag (when present)
133140
└─ CI usage: runs in PR/main workflows so version drift cannot merge silently
134141
scripts/release/verify-bundle-version.js - Bundle filename verifier
135-
├─ Validates: Windows `.exe`/`.msi` and macOS `.dmg` filenames include the expected release version
142+
├─ Validates: Windows `.exe`/`.msi`, macOS `.dmg`, Linux `.deb`/`.rpm`/`.AppImage`, and Arch `.pkg.tar.zst` filenames include the expected release version
136143
└─ Failure mode: catches stale cached artifacts that wildcard GitHub release uploads would otherwise attach
137144
scripts/debug/ - Manual debug helpers kept out of the repo root
138145
├─ `test-button-merge.js` verifies config merge behavior against `WorkspaceManager`
@@ -538,6 +545,7 @@ npm run dev:client - Start client dev server
538545
npm run tauri:dev - Start native app development
539546
npm run tauri:build - Release build (slow, optimized — for distribution)
540547
npm run tauri:build:fast - Fast build (~3-5x faster — for local testing)
548+
npm run tauri:build:arch - Fast native Arch package build (`pkg.tar.zst`) for install testing via `pacman -U`
541549
npm run dev:all - Start all services concurrently
542550
543551
# Diff viewer specific

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"tauri:dev": "tauri dev",
1313
"tauri:build": "node scripts/tauri/run-tauri-build.js --profile release",
1414
"tauri:build:fast": "node scripts/tauri/run-tauri-build.js --profile fast",
15+
"tauri:build:arch": "node scripts/tauri/run-tauri-build.js --profile fast --bundles pacman",
1516
"release:sync-version": "node scripts/tauri/sync-tauri-version.js",
1617
"release:check-version": "node scripts/release/check-version-consistency.js",
1718
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { spawnSync } = require('child_process');
6+
const { getProjectRoot, readPackageVersion } = require('./version-utils');
7+
8+
const PACKAGE_NAME = 'agent-workspace';
9+
const PACKAGE_ARCH = 'x86_64';
10+
const PACKAGE_RELEASE = '1';
11+
const PACKAGE_URL = 'https://github.com/web3dev1337/agent-workspace';
12+
const PACKAGE_DESCRIPTION = 'Multi-terminal orchestrator for Claude Code sessions';
13+
const PACKAGE_LICENSE = 'MIT';
14+
const PACKAGE_DEPENDS = [
15+
'gtk3',
16+
'webkit2gtk-4.1',
17+
'libayatana-appindicator',
18+
'hicolor-icon-theme'
19+
];
20+
21+
function parseArgs(argv) {
22+
const args = argv.slice(2);
23+
let profile = 'release';
24+
let targetDir = null;
25+
let version = null;
26+
27+
for (let i = 0; i < args.length; i += 1) {
28+
const arg = args[i];
29+
if ((arg === '--profile' || arg === '-p') && args[i + 1]) {
30+
profile = args[i + 1];
31+
i += 1;
32+
continue;
33+
}
34+
if ((arg === '--target-dir' || arg === '-t') && args[i + 1]) {
35+
targetDir = args[i + 1];
36+
i += 1;
37+
continue;
38+
}
39+
if ((arg === '--version' || arg === '-v') && args[i + 1]) {
40+
version = args[i + 1];
41+
i += 1;
42+
}
43+
}
44+
45+
return { profile, targetDir, version };
46+
}
47+
48+
function ensureDir(dirPath) {
49+
fs.mkdirSync(dirPath, { recursive: true });
50+
}
51+
52+
function removeIfExists(targetPath) {
53+
if (fs.existsSync(targetPath)) {
54+
fs.rmSync(targetPath, { recursive: true, force: true });
55+
}
56+
}
57+
58+
function run(cmd, args, opts = {}) {
59+
const result = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
60+
if (result.error) {
61+
throw new Error(`${cmd} ${args.join(' ')} failed: ${result.error.message}`);
62+
}
63+
if (result.status !== 0) {
64+
throw new Error(`${cmd} ${args.join(' ')} failed (exit ${result.status})`);
65+
}
66+
}
67+
68+
function resolveTargetDir(repoRoot, targetDir) {
69+
return path.resolve(targetDir || path.join(repoRoot, 'src-tauri', 'target'));
70+
}
71+
72+
function resolveBundleRoot(targetDir, profile) {
73+
return path.join(targetDir, profile, 'bundle');
74+
}
75+
76+
function findDebDataDir(bundleRoot, expectedVersion) {
77+
const debRoot = path.join(bundleRoot, 'deb');
78+
if (!fs.existsSync(debRoot)) {
79+
return null;
80+
}
81+
82+
for (const entry of fs.readdirSync(debRoot, { withFileTypes: true })) {
83+
if (!entry.isDirectory()) continue;
84+
if (!entry.name.includes(expectedVersion) || !entry.name.endsWith('_amd64')) continue;
85+
const dataDir = path.join(debRoot, entry.name, 'data');
86+
const launcherPath = path.join(dataDir, 'usr', 'bin', 'agent-workspace');
87+
if (fs.existsSync(launcherPath)) {
88+
return dataDir;
89+
}
90+
}
91+
92+
return null;
93+
}
94+
95+
function sumFileSizes(rootDir) {
96+
let total = 0;
97+
const stack = [rootDir];
98+
while (stack.length > 0) {
99+
const current = stack.pop();
100+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
101+
const fullPath = path.join(current, entry.name);
102+
if (entry.isDirectory()) {
103+
stack.push(fullPath);
104+
continue;
105+
}
106+
if (entry.isFile()) {
107+
total += fs.statSync(fullPath).size;
108+
}
109+
}
110+
}
111+
return total;
112+
}
113+
114+
function writePkgInfo(pkgRoot, version) {
115+
const installedSize = sumFileSizes(path.join(pkgRoot, 'usr'));
116+
const pkgInfoLines = [
117+
`pkgname = ${PACKAGE_NAME}`,
118+
`pkgbase = ${PACKAGE_NAME}`,
119+
`pkgver = ${version}-${PACKAGE_RELEASE}`,
120+
`pkgdesc = ${PACKAGE_DESCRIPTION}`,
121+
`url = ${PACKAGE_URL}`,
122+
`builddate = ${Math.floor(Date.now() / 1000)}`,
123+
'packager = Agent Workspace Release Automation',
124+
`size = ${installedSize}`,
125+
`arch = ${PACKAGE_ARCH}`,
126+
`license = ${PACKAGE_LICENSE}`,
127+
...PACKAGE_DEPENDS.map((dependency) => `depend = ${dependency}`)
128+
];
129+
fs.writeFileSync(path.join(pkgRoot, '.PKGINFO'), `${pkgInfoLines.join('\n')}\n`);
130+
}
131+
132+
function createPackageArchive({ pkgRoot, outFile }) {
133+
ensureDir(path.dirname(outFile));
134+
removeIfExists(outFile);
135+
run('bsdtar', [
136+
'--uid', '0',
137+
'--gid', '0',
138+
'--numeric-owner',
139+
'--format', 'gnutar',
140+
'--zstd',
141+
'-cf', outFile,
142+
'.PKGINFO',
143+
'usr'
144+
], { cwd: pkgRoot });
145+
}
146+
147+
function main() {
148+
const repoRoot = getProjectRoot();
149+
const { profile, targetDir, version } = parseArgs(process.argv);
150+
const expectedVersion = version || process.env.RELEASE_VERSION || readPackageVersion(repoRoot);
151+
if (!expectedVersion) {
152+
throw new Error('Unable to determine package version for Arch package build');
153+
}
154+
155+
const resolvedTargetDir = resolveTargetDir(repoRoot, targetDir);
156+
const bundleRoot = resolveBundleRoot(resolvedTargetDir, profile);
157+
const debDataDir = findDebDataDir(bundleRoot, expectedVersion);
158+
if (!debDataDir) {
159+
throw new Error(`Unable to locate extracted deb bundle for version ${expectedVersion} under ${bundleRoot}`);
160+
}
161+
162+
const pacmanRoot = path.join(bundleRoot, 'pacman');
163+
const pkgStem = `${PACKAGE_NAME}-${expectedVersion}-${PACKAGE_RELEASE}-${PACKAGE_ARCH}`;
164+
const pkgRoot = path.join(pacmanRoot, pkgStem);
165+
const outFile = path.join(pacmanRoot, `${pkgStem}.pkg.tar.zst`);
166+
167+
removeIfExists(pkgRoot);
168+
ensureDir(pkgRoot);
169+
fs.cpSync(debDataDir, pkgRoot, { recursive: true });
170+
writePkgInfo(pkgRoot, expectedVersion);
171+
createPackageArchive({ pkgRoot, outFile });
172+
173+
console.log(`[arch] Built package: ${outFile}`);
174+
}
175+
176+
if (require.main === module) {
177+
main();
178+
}
179+
180+
module.exports = {
181+
PACKAGE_ARCH,
182+
PACKAGE_DEPENDS,
183+
PACKAGE_NAME,
184+
PACKAGE_RELEASE,
185+
createPackageArchive,
186+
findDebDataDir,
187+
parseArgs,
188+
resolveBundleRoot,
189+
resolveTargetDir,
190+
sumFileSizes,
191+
writePkgInfo
192+
};

scripts/release/verify-bundle-version.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { getProjectRoot, readPackageVersion } = require('./version-utils');
77
const BUNDLE_FILE_RULES = {
88
appimage: { directory: 'appimage', extensions: ['.AppImage'] },
99
deb: { directory: 'deb', extensions: ['.deb'] },
10+
pacman: { directory: 'pacman', extensions: ['.pkg.tar.zst'] },
1011
rpm: { directory: 'rpm', extensions: ['.rpm'] },
1112
nsis: { directory: 'nsis', extensions: ['.exe'] },
1213
msi: { directory: 'msi', extensions: ['.msi'] },
@@ -93,7 +94,7 @@ function listBundleFiles(bundleTypeDir, extensions) {
9394
}
9495

9596
return fs.readdirSync(bundleTypeDir)
96-
.filter((entry) => extensions.includes(path.extname(entry).toLowerCase()))
97+
.filter((entry) => extensions.some((extension) => entry.toLowerCase().endsWith(extension.toLowerCase())))
9798
.map((entry) => path.join(bundleTypeDir, entry));
9899
}
99100

scripts/tauri/run-tauri-build.js

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,26 @@ function parseBundleList(value) {
112112
.filter(Boolean);
113113
}
114114

115+
function splitBundleTargets(bundleTargets) {
116+
const normalized = Array.isArray(bundleTargets) ? bundleTargets : [];
117+
const postprocessTargets = [];
118+
const tauriTargets = [];
119+
120+
for (const bundleTarget of normalized) {
121+
if (bundleTarget === 'pacman') {
122+
postprocessTargets.push(bundleTarget);
123+
continue;
124+
}
125+
tauriTargets.push(bundleTarget);
126+
}
127+
128+
if (postprocessTargets.includes('pacman') && !tauriTargets.includes('deb')) {
129+
tauriTargets.unshift('deb');
130+
}
131+
132+
return { postprocessTargets, tauriTargets };
133+
}
134+
115135
function resolveBundleTargets({
116136
profile,
117137
explicitBundles,
@@ -142,8 +162,8 @@ function resolveBundleTargets({
142162

143163
if (platform === 'linux' && profile === 'fast' && !env.CI && isArchBasedLinux({ env, platform })) {
144164
return {
145-
bundleTargets: ['deb'],
146-
reason: 'local-arch-fast-skip-appimage'
165+
bundleTargets: ['pacman'],
166+
reason: 'local-arch-fast-native-package'
147167
};
148168
}
149169

@@ -178,6 +198,7 @@ function main() {
178198
profile,
179199
explicitBundles: bundles
180200
});
201+
const { tauriTargets, postprocessTargets } = splitBundleTargets(bundleTargets);
181202

182203
const env = { ...process.env };
183204
if (targetDir) {
@@ -224,8 +245,8 @@ function main() {
224245
}
225246

226247
const tauriArgs = [tauriCliPath, 'build'];
227-
if (bundleTargets && bundleTargets.length > 0) {
228-
tauriArgs.push('--bundles', bundleTargets.join(','));
248+
if (tauriTargets && tauriTargets.length > 0) {
249+
tauriArgs.push('--bundles', tauriTargets.join(','));
229250
}
230251
tauriArgs.push('--', '--profile', profile);
231252

@@ -234,6 +255,19 @@ function main() {
234255
env
235256
});
236257

258+
if (postprocessTargets.includes('pacman')) {
259+
run(process.execPath, [
260+
path.join(repoRoot, 'scripts', 'release', 'build-arch-package.js'),
261+
'--profile',
262+
profile,
263+
'--target-dir',
264+
resolveTargetRoot({ repoRoot, targetDir })
265+
], {
266+
cwd: repoRoot,
267+
env
268+
});
269+
}
270+
237271
const verifyArgs = [
238272
path.join(repoRoot, 'scripts', 'release', 'verify-bundle-version.js'),
239273
'--profile',
@@ -262,6 +296,7 @@ module.exports = {
262296
parseBundleList,
263297
isArchBasedLinux,
264298
readOsRelease,
299+
splitBundleTargets,
265300
resolveBundleTargets,
266301
resolveCargoTargetDir,
267302
resolveTargetRoot

0 commit comments

Comments
 (0)