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:
@@ -11,7 +11,7 @@
|
|||||||
# own installer, which handles libfprint, the udev rule, plugdev and the
|
# own installer, which handles libfprint, the udev rule, plugdev and the
|
||||||
# systemd service. Every flag is forwarded to that installer, e.g.:
|
# systemd service. Every flag is forwarded to that installer, e.g.:
|
||||||
#
|
#
|
||||||
# curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --enable-service
|
# curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --service-user alice
|
||||||
# curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --check
|
# curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --check
|
||||||
#
|
#
|
||||||
# The private release is reached through the tools app, so no credentials are
|
# The private release is reached through the tools app, so no credentials are
|
||||||
@@ -35,12 +35,18 @@ Options:
|
|||||||
Options below are forwarded to the bundled installer:
|
Options below are forwarded to the bundled installer:
|
||||||
--check preflight and report only; change nothing
|
--check preflight and report only; change nothing
|
||||||
--offline never touch the network; fail if dependencies are missing
|
--offline never touch the network; fail if dependencies are missing
|
||||||
--enable-service install and enable a systemd service (starts at boot)
|
--no-service do not install the systemd service (also disables updates)
|
||||||
|
--no-updates do not install the self-update timer
|
||||||
|
--update-base URL release base for self-update
|
||||||
|
(default: https://tools.yrka.app/fpagent)
|
||||||
--service-user NAME user for the service (default: the invoking user)
|
--service-user NAME user for the service (default: the invoking user)
|
||||||
--install-path PATH binary destination (default: /usr/local/bin/fpagent)
|
--install-path PATH binary destination (default: /usr/local/bin/fpagent)
|
||||||
--uninstall remove the binary, the udev rule and the service
|
--uninstall remove the binary, the udev rule and the service
|
||||||
-h, --help the installer's help
|
-h, --help the installer's help
|
||||||
|
|
||||||
|
The systemd service and the self-update timer are installed and enabled by
|
||||||
|
default; --no-service and --no-updates turn them off.
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
FPAGENT_BASE_URL override the download base (default: ${BASE})
|
FPAGENT_BASE_URL override the download base (default: ${BASE})
|
||||||
EOF
|
EOF
|
||||||
@@ -85,6 +91,15 @@ find "$tmpbase" -maxdepth 1 -mindepth 1 -type d -uid "$(id -u)" \
|
|||||||
# online bundle (whose installer apt-get's the dependency).
|
# online bundle (whose installer apt-get's the dependency).
|
||||||
codename="$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"')"
|
codename="$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"')"
|
||||||
|
|
||||||
|
# Releases are named for the target architecture; only these two are published.
|
||||||
|
# Mirrors the bundled installer's own preflight so both reject the same hosts.
|
||||||
|
host_machine="$(uname -m)"
|
||||||
|
case "$host_machine" in
|
||||||
|
x86_64 | amd64) arch="x86_64" ;;
|
||||||
|
i386 | i486 | i586 | i686) arch="i386" ;;
|
||||||
|
*) die "unsupported architecture: ${host_machine} (x86_64 or i386 required)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
if [ "$RELEASE_TAG" = "latest" ]; then
|
if [ "$RELEASE_TAG" = "latest" ]; then
|
||||||
meta_url="${BASE}/releases/latest"
|
meta_url="${BASE}/releases/latest"
|
||||||
else
|
else
|
||||||
@@ -96,25 +111,25 @@ meta="$(curl -fsSL "$meta_url")" || die "could not fetch the release list from $
|
|||||||
|
|
||||||
readout="$(python3 -c '
|
readout="$(python3 -c '
|
||||||
import json, re, sys
|
import json, re, sys
|
||||||
codename = sys.argv[1]
|
codename, arch = sys.argv[1], sys.argv[2]
|
||||||
release = json.loads(sys.argv[2])
|
release = json.loads(sys.argv[3])
|
||||||
assets = [a["name"] for a in release.get("assets", [])]
|
assets = [a["name"] for a in release.get("assets", [])]
|
||||||
|
|
||||||
def pick(suffix):
|
def pick(suffix):
|
||||||
return next((n for n in assets if n.endswith(suffix) and not n.endswith(".sha256")), "")
|
return next((n for n in assets if n.endswith(suffix) and not n.endswith(".sha256")), "")
|
||||||
|
|
||||||
name = pick("-ubuntu-" + codename + "-x86_64.tar.gz") if codename else ""
|
name = pick("-ubuntu-" + codename + "-" + arch + ".tar.gz") if codename else ""
|
||||||
if not name:
|
if not name:
|
||||||
name = pick("-linux-x86_64.tar.gz")
|
name = pick("-linux-" + arch + ".tar.gz")
|
||||||
# The git hash is baked into the asset name (bundle.sh --version) and into
|
# The git hash is baked into the asset name (bundle.sh --version) and into
|
||||||
# the binary itself; it is the version we record and later compare against.
|
# the binary itself; it is the version we record and later compare against.
|
||||||
m = re.match(r"^fpagent-(.+)-(?:linux-x86_64|ubuntu-[a-z0-9]+-x86_64)\.tar\.gz$", name)
|
m = re.match(r"^fpagent-(.+)-(?:linux|ubuntu-[a-z0-9]+)-" + re.escape(arch) + r"\.tar\.gz$", name)
|
||||||
version = m.group(1) if m else ""
|
version = m.group(1) if m else ""
|
||||||
print(release.get("tag", "") + "\t" + name + "\t" + version)
|
print(release.get("tag", "") + "\t" + name + "\t" + version)
|
||||||
' "$codename" "$meta")" || die "could not parse the release metadata"
|
' "$codename" "$arch" "$meta")" || die "could not parse the release metadata"
|
||||||
|
|
||||||
IFS=$'\t' read -r tag asset version <<<"$readout"
|
IFS=$'\t' read -r tag asset version <<<"$readout"
|
||||||
[ -n "$tag" ] && [ -n "$asset" ] || die "no compatible bundle in release ${RELEASE_TAG} (x86_64 required)"
|
[ -n "$tag" ] && [ -n "$asset" ] || die "no compatible bundle in release ${RELEASE_TAG} (${arch} required)"
|
||||||
|
|
||||||
say " release ${tag}"
|
say " release ${tag}"
|
||||||
say " bundle ${asset}"
|
say " bundle ${asset}"
|
||||||
|
|||||||
@@ -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.
|
// Fake Gitea: refuses without the token, serves the release and its assets.
|
||||||
async function startFakeGitea({ releaseStatus = 200, downloadStatus = 200 } = {}) {
|
async function startFakeGitea({ releaseStatus = 200, downloadStatus = 200 } = {}) {
|
||||||
const calls = [];
|
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', () => {
|
test('install.sh is valid bash', () => {
|
||||||
execFileSync('bash', ['-n', SCRIPT], { stdio: 'pipe' });
|
execFileSync('bash', ['-n', SCRIPT], { stdio: 'pipe' });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user