fix(linux): install cli launcher in packages
This commit is contained in:
parent
384446e83c
commit
8511d6af6e
6 changed files with 214 additions and 2 deletions
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
17
package.json
17
package.json
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
18
resources/linux/bin/agent-teams-ai
Executable file
18
resources/linux/bin/agent-teams-ai
Executable 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
|
||||
138
scripts/electron-builder/verifyLinuxInstallers.cjs
Normal file
138
scripts/electron-builder/verifyLinuxInstallers.cjs
Normal 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,
|
||||
},
|
||||
};
|
||||
36
test/main/build/electronBuilderLinuxPackaging.test.ts
Normal file
36
test/main/build/electronBuilderLinuxPackaging.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue