feat(fpagent): pick the installer bundle for the host architecture
The hosted installer only ever chose the x86_64 bundle, so a 32-bit field laptop running the public one-liner failed with "no compatible bundle" even though fingerprint-agent v1.5.0 publishes fpagent-<version>-linux-i386.tar.gz. Resolve the host architecture from uname -m (x86_64 or i386, else fail before any download), then prefer -ubuntu-<codename>-<arch>.tar.gz and fall back to -linux-<arch>.tar.gz. The version regex now derives the git hash for every supported architecture, so a 32-bit install records and reports it too. The help text matches the bundle installer: the systemd service and the self-update timer are on by default, with --no-service, --no-updates and --update-base documented.
This commit is contained in:
@@ -37,6 +37,17 @@ const RELEASE = {
|
||||
],
|
||||
};
|
||||
|
||||
// A directory holding a fake `uname` that reports the given machine, so the
|
||||
// installer's architecture-sensitive picker runs on any CI host. PATH is
|
||||
// prepended with it; everything else resolves as usual.
|
||||
function fakeUname(machine) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fpagent-uname-'));
|
||||
fs.writeFileSync(path.join(dir, 'uname'), `#!/bin/sh\nprintf '%s\\n' '${machine}'\n`, {
|
||||
mode: 0o755,
|
||||
});
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Fake Gitea: refuses without the token, serves the release and its assets.
|
||||
async function startFakeGitea({ releaseStatus = 200, downloadStatus = 200 } = {}) {
|
||||
const calls = [];
|
||||
@@ -231,6 +242,98 @@ test('install.sh is served publicly at /fpagent/install.sh', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('install.sh picks the bundle for the host architecture', async () => {
|
||||
const calls = [];
|
||||
const gateway = await startServer((req, res) => {
|
||||
calls.push(req.url);
|
||||
if (req.url === '/releases/latest') {
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
tag: 'v9.9.9',
|
||||
assets: [
|
||||
{ name: 'fpagent-deadbee-linux-x86_64.tar.gz' },
|
||||
{ name: 'fpagent-deadbee-linux-x86_64.tar.gz.sha256' },
|
||||
{ name: 'fpagent-cafebabe-linux-i386.tar.gz' },
|
||||
{ name: 'fpagent-cafebabe-linux-i386.tar.gz.sha256' },
|
||||
],
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
const unameDir = fakeUname('i686');
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'fpagent-arch-test-'));
|
||||
try {
|
||||
let out = '';
|
||||
try {
|
||||
// Async so the in-process gateway can answer while the child runs.
|
||||
const r = await execFileAsync('bash', [SCRIPT], {
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${unameDir}:${process.env.PATH}`,
|
||||
TMPDIR: tmp,
|
||||
FPAGENT_BASE_URL: urlOf(gateway),
|
||||
},
|
||||
});
|
||||
out = r.stdout + r.stderr;
|
||||
} catch (e) {
|
||||
out = `${e.stdout ?? ''}${e.stderr ?? ''}`; // the asset download 404s
|
||||
}
|
||||
assert.match(out, /cafebabe/, 'the i386 version should come from the asset name');
|
||||
assert.ok(
|
||||
calls.some((u) => u.includes('fpagent-cafebabe-linux-i386.tar.gz')),
|
||||
'should request the i386 bundle'
|
||||
);
|
||||
assert.ok(
|
||||
!calls.some((u) => u.includes('x86_64')),
|
||||
'should never request another architecture\u2019s bundle'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
fs.rmSync(unameDir, { recursive: true, force: true });
|
||||
await stopServer(gateway);
|
||||
}
|
||||
});
|
||||
|
||||
test('install.sh refuses an unsupported architecture before fetching', async () => {
|
||||
const calls = [];
|
||||
const gateway = await startServer((req, res) => {
|
||||
calls.push(req.url);
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
const unameDir = fakeUname('aarch64');
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'fpagent-arch-test-'));
|
||||
try {
|
||||
let out = '';
|
||||
let failed = false;
|
||||
try {
|
||||
const r = await execFileAsync('bash', [SCRIPT], {
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${unameDir}:${process.env.PATH}`,
|
||||
TMPDIR: tmp,
|
||||
FPAGENT_BASE_URL: urlOf(gateway),
|
||||
},
|
||||
});
|
||||
out = r.stdout + r.stderr;
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
out = `${e.stdout ?? ''}${e.stderr ?? ''}`;
|
||||
}
|
||||
assert.equal(failed, true, 'the installer should reject an unsupported host');
|
||||
assert.match(out, /unsupported architecture: aarch64/);
|
||||
assert.equal(calls.length, 0, 'should fail before touching the network');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
fs.rmSync(unameDir, { recursive: true, force: true });
|
||||
await stopServer(gateway);
|
||||
}
|
||||
});
|
||||
|
||||
test('install.sh is valid bash', () => {
|
||||
execFileSync('bash', ['-n', SCRIPT], { stdio: 'pipe' });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user