fix(linux): install cli launcher in packages

This commit is contained in:
777genius 2026-05-21 12:57:29 +03:00
parent 384446e83c
commit 8511d6af6e
6 changed files with 214 additions and 2 deletions

View file

@ -642,6 +642,9 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm pack:linux --publish never
- name: Validate Linux installers
run: node ./scripts/electron-builder/verifyLinuxInstallers.cjs release
- name: Validate packaged bundle (Linux)
run: node ./scripts/electron-builder/verifyBundle.cjs "release/linux-unpacked" linux x64

View file

@ -327,7 +327,22 @@
"artifactName": "Agent.Teams.AI-${version}.${ext}"
},
"deb": {
"afterInstall": "resources/afterInstall.sh"
"afterInstall": "resources/afterInstall.sh",
"fpm": [
"resources/linux/bin/agent-teams-ai=/usr/bin/agent-teams-ai"
]
},
"rpm": {
"afterInstall": "resources/afterInstall.sh",
"fpm": [
"resources/linux/bin/agent-teams-ai=/usr/bin/agent-teams-ai"
]
},
"pacman": {
"afterInstall": "resources/afterInstall.sh",
"fpm": [
"resources/linux/bin/agent-teams-ai=/usr/bin/agent-teams-ai"
]
},
"nsis": {
"artifactName": "Agent.Teams.AI.Setup.${version}.${ext}",

View file

@ -1,8 +1,10 @@
#!/bin/bash
set -e
# Fix chrome-sandbox permissions for SUID sandbox on Linux
# See: https://github.com/electron/electron/issues/17972
SANDBOX_PATH="/opt/${productFilename}/chrome-sandbox"
SANDBOX_PATH="/opt/${sanitizedProductName}/chrome-sandbox"
if [ -f "$SANDBOX_PATH" ]; then
chown root:root "$SANDBOX_PATH"

View file

@ -0,0 +1,18 @@
#!/bin/sh
set -eu
if [ "${AGENT_TEAMS_AI_BIN:-}" != "" ]; then
exec "$AGENT_TEAMS_AI_BIN" "$@"
fi
for executable in \
"/opt/Agent-Teams-AI/agent-teams-ai" \
"/opt/Agent Teams AI/agent-teams-ai"
do
if [ -x "$executable" ]; then
exec "$executable" "$@"
fi
done
echo "agent-teams-ai: installed app executable was not found under /opt/Agent-Teams-AI" >&2
exit 127

View file

@ -0,0 +1,138 @@
#!/usr/bin/env node
/* global Buffer, console, module, process, require */
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('fs');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
function fail(message) {
console.error(`[verifyLinuxInstallers] ${message}`);
process.exit(1);
}
function run(command, args, input) {
const result = spawnSync(command, args, {
input,
encoding: input ? undefined : 'utf8',
maxBuffer: 128 * 1024 * 1024,
});
if (result.status !== 0) {
const stderr = Buffer.isBuffer(result.stderr)
? result.stderr.toString('utf8')
: result.stderr || '';
throw new Error(`${command} ${args.join(' ')} failed: ${stderr}`);
}
return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout || '', 'utf8');
}
function readArMember(archivePath, memberName) {
return execFileSync('ar', ['p', archivePath, memberName], {
maxBuffer: 128 * 1024 * 1024,
});
}
function getTarCompressionFlag(memberName) {
if (memberName.endsWith('.tar.xz')) return 'J';
if (memberName.endsWith('.tar.gz')) return 'z';
if (memberName.endsWith('.tar.bz2')) return 'j';
fail(`Unsupported deb tar member compression: ${memberName}`);
}
function getDebMember(archivePath, prefix) {
const members = execFileSync('ar', ['t', archivePath], { encoding: 'utf8' })
.split(/\r?\n/)
.filter(Boolean);
const member = members.find((entry) => entry.startsWith(prefix));
if (!member) {
fail(`Missing ${prefix} member in ${archivePath}`);
}
return member;
}
function listTar(tarBuffer, memberName, verbose = false) {
const flag = getTarCompressionFlag(memberName);
const mode = verbose ? `-tv${flag}f` : `-t${flag}f`;
return run('tar', [mode, '-'], tarBuffer).toString('utf8');
}
function extractTarFile(tarBuffer, memberName, filePath) {
const flag = getTarCompressionFlag(memberName);
return run('tar', [`-xO${flag}f`, '-', filePath], tarBuffer).toString('utf8');
}
function assertContains(haystack, needle, label) {
if (!haystack.includes(needle)) {
fail(`${label} does not contain ${needle}`);
}
}
function verifyDeb(debPath) {
const dataMember = getDebMember(debPath, 'data.tar.');
const controlMember = getDebMember(debPath, 'control.tar.');
const data = readArMember(debPath, dataMember);
const control = readArMember(debPath, controlMember);
const dataList = listTar(data, dataMember);
const dataVerboseList = listTar(data, dataMember, true);
assertContains(dataList, './usr/bin/agent-teams-ai\n', path.basename(debPath));
assertContains(dataList, './opt/Agent-Teams-AI/agent-teams-ai\n', path.basename(debPath));
assertContains(dataList, './opt/Agent-Teams-AI/chrome-sandbox\n', path.basename(debPath));
const launcherLine = dataVerboseList
.split(/\r?\n/)
.find((line) => line.endsWith('./usr/bin/agent-teams-ai'));
if (!launcherLine || !launcherLine.startsWith('-rwx')) {
fail(`/usr/bin/agent-teams-ai is not executable in ${debPath}`);
}
const launcher = extractTarFile(data, dataMember, './usr/bin/agent-teams-ai');
assertContains(launcher, '/opt/Agent-Teams-AI/agent-teams-ai', 'CLI launcher');
if (launcher.includes('--no-sandbox')) {
fail('CLI launcher must not force --no-sandbox');
}
const postinst = extractTarFile(control, controlMember, './postinst');
assertContains(
postinst,
'SANDBOX_PATH="/opt/Agent-Teams-AI/chrome-sandbox"',
'deb postinst'
);
assertContains(postinst, 'chmod 4755 "$SANDBOX_PATH"', 'deb postinst');
}
function main() {
const releaseDir = path.resolve(process.argv[2] || 'release');
if (!fs.existsSync(releaseDir)) {
fail(`Release directory does not exist: ${releaseDir}`);
}
const debs = fs
.readdirSync(releaseDir)
.filter((entry) => entry.endsWith('.deb'))
.map((entry) => path.join(releaseDir, entry));
if (debs.length === 0) {
fail(`No .deb packages found in ${releaseDir}`);
}
for (const deb of debs) {
verifyDeb(deb);
console.log(`[verifyLinuxInstallers] OK ${path.basename(deb)}`);
}
}
if (require.main === module) {
try {
main();
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
}
module.exports = {
_internal: {
extractTarFile,
getDebMember,
listTar,
verifyDeb,
},
};

View file

@ -0,0 +1,36 @@
// @vitest-environment node
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
const repoRoot = path.resolve(__dirname, '../../..');
describe('electron-builder Linux packaging', () => {
it('installs a package-owned CLI launcher into PATH for fpm Linux packages', () => {
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
const launcherPath = path.join(repoRoot, 'resources/linux/bin/agent-teams-ai');
const launcher = fs.readFileSync(launcherPath, 'utf8');
const launcherMode = fs.statSync(launcherPath).mode;
for (const target of ['deb', 'rpm', 'pacman'] as const) {
expect(packageJson.build[target].afterInstall).toBe('resources/afterInstall.sh');
expect(packageJson.build[target].fpm).toContain(
'resources/linux/bin/agent-teams-ai=/usr/bin/agent-teams-ai'
);
}
expect(launcher).toContain('#!/bin/sh');
expect(launcher).toContain('/opt/Agent-Teams-AI/agent-teams-ai');
expect(launcher).not.toContain('--no-sandbox');
if (process.platform !== 'win32') {
expect(launcherMode & 0o111).not.toBe(0);
}
});
it('fixes chrome-sandbox permissions at the actual fpm install directory', () => {
const afterInstall = fs.readFileSync(path.join(repoRoot, 'resources/afterInstall.sh'), 'utf8');
expect(afterInstall).toContain('/opt/${sanitizedProductName}/chrome-sandbox');
expect(afterInstall).not.toContain('/opt/${productFilename}/chrome-sandbox');
});
});