add pacman maker for Arch Linux packages (#604)

Custom Electron Forge maker that wraps makepkg to produce a
.pkg.tar.zst with /opt/<app>, /usr/bin wrapper, .desktop entry,
and hicolor icon. Only activates on Linux when makepkg is present,
so other platforms are unaffected.
This commit is contained in:
Harshvardhan Vatsa 2026-06-10 14:55:15 +05:30 committed by GitHub
parent 80fef06da0
commit 0aec665220
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 153 additions and 0 deletions

View file

@ -86,6 +86,21 @@ module.exports = {
}
}
},
{
name: require.resolve('./makers/maker-pacman.cjs'),
platforms: ['linux'],
config: {
name: 'rowboat',
bin: 'rowboat',
executableName: 'rowboat',
description: 'AI coworker with memory',
maintainer: 'rowboatlabs',
homepage: 'https://rowboatlabs.com',
license: 'Apache',
icon: path.join(__dirname, 'icons/icon.png'),
mimeType: ['x-scheme-handler/rowboat'],
}
},
{
name: '@electron-forge/maker-zip',
platform: ["darwin", "win32", "linux"],

View file

@ -0,0 +1,134 @@
// Custom Electron Forge maker that produces Arch Linux .pkg.tar.zst packages
// via makepkg. Runs only on Linux with makepkg available (i.e. an Arch host).
//
// CJS on purpose: forge.config.cjs require()s us.
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const MakerBase = require('@electron-forge/maker-base').default;
const ARCH_MAP = { x64: 'x86_64', arm64: 'aarch64', ia32: 'i686', armv7l: 'armv7h' };
class MakerPacman extends MakerBase {
name = 'pacman';
defaultPlatforms = ['linux'];
isSupportedOnCurrentPlatform() {
if (process.platform !== 'linux') return false;
try {
execSync('command -v makepkg', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
async make({ dir, makeDir, targetArch, packageJSON, appName }) {
const pkgArch = ARCH_MAP[targetArch] || targetArch;
const cfg = this.config || {};
const pkgName = (cfg.name || appName || packageJSON.name).toLowerCase();
// pacman pkgver disallows '-'; map prerelease tags through.
const pkgVersion = String(packageJSON.version || '0.0.0').replace(/-/g, '_');
const pkgDesc = (cfg.description || packageJSON.description || '').replace(/"/g, '\\"');
const maintainer = cfg.maintainer || 'unknown';
const homepage = cfg.homepage || packageJSON.homepage || '';
const license = cfg.license || 'custom';
const bin = cfg.bin || pkgName;
const execName = cfg.executableName || appName || pkgName;
const mimeTypes = cfg.mimeType || [];
const depends = cfg.depends || [];
const iconSrc = cfg.icon;
const outDir = path.resolve(path.join(makeDir, 'pacman', targetArch));
await this.ensureDirectory(outDir);
// Clean prior contents so makepkg starts fresh each run.
for (const f of fs.readdirSync(outDir)) {
fs.rmSync(path.join(outDir, f), { recursive: true, force: true });
}
// Wrapper script — execs the packaged Electron binary, forwards args (incl. rowboat:// URLs).
fs.writeFileSync(
path.join(outDir, bin),
`#!/bin/sh\nexec "/opt/${pkgName}/${execName}" "$@"\n`,
{ mode: 0o755 },
);
const desktop = [
'[Desktop Entry]',
`Name=${appName || pkgName}`,
`Comment=${pkgDesc}`,
`Exec=${bin} %U`,
`Icon=${pkgName}`,
'Type=Application',
'Categories=Utility;',
'Terminal=false',
mimeTypes.length ? `MimeType=${mimeTypes.join(';')};` : null,
'',
].filter(Boolean).join('\n');
fs.writeFileSync(path.join(outDir, `${pkgName}.desktop`), desktop);
const sources = [bin, `${pkgName}.desktop`];
let iconInstall = '';
if (iconSrc && fs.existsSync(iconSrc)) {
fs.copyFileSync(iconSrc, path.join(outDir, 'icon.png'));
sources.push('icon.png');
iconInstall = ` install -Dm644 "$srcdir/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/${pkgName}.png"`;
}
const sumsLine = sources.map(() => "'SKIP'").join(' ');
const sourceLine = sources.map((s) => `'${s}'`).join(' ');
const dependsLine = depends.map((d) => `'${d}'`).join(' ');
// Embed the packager output dir as a bash-safe literal.
const appDirEscaped = dir.replace(/'/g, `'\\''`);
const pkgbuild = `# Maintainer: ${maintainer}
# Auto-generated by maker-pacman.cjs do not edit by hand.
pkgname=${pkgName}
pkgver=${pkgVersion}
pkgrel=1
pkgdesc="${pkgDesc}"
arch=('${pkgArch}')
url="${homepage}"
license=('${license}')
depends=(${dependsLine})
options=('!strip' '!debug')
source=(${sourceLine})
sha256sums=(${sumsLine})
_appdir='${appDirEscaped}'
package() {
install -dm755 "$pkgdir/opt/$pkgname"
cp -a "$_appdir/." "$pkgdir/opt/$pkgname/"
# Electron's sandbox helper needs setuid root for the namespace sandbox.
if [ -f "$pkgdir/opt/$pkgname/chrome-sandbox" ]; then
chmod 4755 "$pkgdir/opt/$pkgname/chrome-sandbox"
fi
install -Dm755 "$srcdir/${bin}" "$pkgdir/usr/bin/${bin}"
install -Dm644 "$srcdir/${pkgName}.desktop" "$pkgdir/usr/share/applications/${pkgName}.desktop"
${iconInstall}
}
`;
fs.writeFileSync(path.join(outDir, 'PKGBUILD'), pkgbuild);
execSync('makepkg -f --noconfirm --nodeps', {
cwd: outDir,
stdio: 'inherit',
env: { ...process.env, PKGEXT: '.pkg.tar.zst', CARCH: pkgArch },
});
return fs
.readdirSync(outDir)
.filter((f) => f.endsWith('.pkg.tar.zst'))
.map((f) => path.join(outDir, f));
}
}
module.exports = MakerPacman;
module.exports.default = MakerPacman;

View file

@ -29,6 +29,7 @@
},
"devDependencies": {
"@electron-forge/cli": "^7.10.2",
"@electron-forge/maker-base": "^7.11.1",
"@electron-forge/maker-deb": "^7.11.1",
"@electron-forge/maker-dmg": "^7.10.2",
"@electron-forge/maker-rpm": "^7.11.1",

3
apps/x/pnpm-lock.yaml generated
View file

@ -95,6 +95,9 @@ importers:
'@electron-forge/cli':
specifier: ^7.10.2
version: 7.11.1(encoding@0.1.13)(esbuild@0.24.2)
'@electron-forge/maker-base':
specifier: ^7.11.1
version: 7.11.1
'@electron-forge/maker-deb':
specifier: ^7.11.1
version: 7.11.1