feat(seeder): arch-aware macOS .pkg selection for auto-update

The seeder update checker treated a macOS .pkg as arch-universal and
returned the first .pkg it saw. But build-macos-remote.sh emits BOTH
`PearCalSeeder-<v>-arm64.pkg` and `-x64.pkg`, so a release carrying both
would hand ~half of x64 Macs the arm64 build (or vice versa).

Make selectAsset match the running arch for darwin the same way it
already does for Linux: prefer an arch-matching .pkg, fall back to an
arch-universal (un-suffixed) .pkg, and never return the wrong arch's
suffixed build — a wrong-arch install is worse than none. This is the
last correctness gap before wiring the seeder installers into the
release flow so self-managed installs can actually self-update.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hccVdQLiqJjE35qhUs3Rb
This commit is contained in:
Your Name
2026-07-18 00:19:09 -05:00
co-authored by Claude Opus 4.8
parent d655ab0a13
commit 38352bc8fb
2 changed files with 42 additions and 9 deletions
+21 -8
View File
@@ -55,14 +55,21 @@ function archTokens (arch) {
return arch ? [String(arch).toLowerCase()] : []
}
// Every arch token that may appear in a seeder installer name, across all
// platforms. Used to tell an arch-suffixed asset from an arch-universal one.
const ALL_ARCH_TOKENS = ['x86_64', 'amd64', 'x64', 'aarch64', 'arm64']
// Pick this platform+arch's installer asset from a GitHub release's `assets`
// array. platform is process.platform ('darwin' | 'win32' | 'linux'), arch is
// process.arch ('x64' | 'arm64'). macOS `.pkg` and Windows `.exe` ship as a
// single (universal) asset, so arch is not required there. On Linux the
// process.arch ('x64' | 'arm64'). Windows `.exe` ships as a single (universal)
// asset, so arch is not required there. macOS `.pkg` and Linux `.AppImage`/`.deb`
// are built per-arch (the .pkg name carries `-arm64`/`-x64`), so we match the
// running arch and never hand back a wrong-arch binary — a wrong-arch install is
// worse than none, so we fall back to null. A macOS `.pkg` with no arch token in
// its name is treated as arch-universal (a single fat build). On Linux the
// `installKind` hint decides which artifact a running seeder gets so the apply
// path matches how it was installed. Only an arch-matching asset is returned - a
// wrong-arch binary is worse than none, so we fall back to null. .sha256
// sidecars are never returned as the primary asset.
// path matches how it was installed. .sha256 sidecars are never returned as the
// primary asset.
function selectAsset (assets, platform, arch, installKind) {
if (!Array.isArray(assets)) return null
// Match ONLY seeder-named assets. Every seeder installer carries "seeder" in
@@ -76,11 +83,17 @@ function selectAsset (assets, platform, arch, installKind) {
!a.name.endsWith('.sha256') && a.name.toLowerCase().includes('seeder'))
const lower = (a) => a.name.toLowerCase()
const bySuffix = (suffix) => named.filter((a) => lower(a).endsWith(suffix))
if (platform === 'darwin') return bySuffix('.pkg')[0] || null
const toks = archTokens(arch)
const matchArch = (list) => list.find((a) => toks.some((t) => lower(a).includes(t))) || null
if (platform === 'darwin') {
const pkgs = bySuffix('.pkg')
// Prefer an arch-matching build; otherwise accept an arch-universal pkg (no
// arch token in the name). Never return the wrong arch's suffixed pkg.
const universal = pkgs.find((a) => !ALL_ARCH_TOKENS.some((t) => lower(a).includes(t))) || null
return matchArch(pkgs) || universal
}
if (platform === 'win32') return bySuffix('.exe')[0] || null
if (platform === 'linux') {
const toks = archTokens(arch)
const matchArch = (list) => list.find((a) => toks.some((t) => lower(a).includes(t))) || null
const appimage = () => matchArch(bySuffix('.appimage'))
const deb = () => matchArch(bySuffix('.deb'))
return installKind === 'deb'
+21 -1
View File
@@ -48,12 +48,32 @@ const ASSETS = [
{ name: 'pearcal-v1.0.10.apk', browser_download_url: 'u/apk' }, // mobile, never picked
]
test('selectAsset: macOS pkg + Windows exe are arch-universal', () => {
test('selectAsset: an arch-universal macOS pkg + Windows exe serve any arch', () => {
// An un-suffixed .pkg (a single fat build) is offered to both Mac arches.
assert.equal(selectAsset(ASSETS, 'darwin', 'x64').browser_download_url, 'u/pkg')
assert.equal(selectAsset(ASSETS, 'darwin', 'arm64').browser_download_url, 'u/pkg')
assert.equal(selectAsset(ASSETS, 'win32', 'x64').browser_download_url, 'u/exe')
})
test('selectAsset: arch-suffixed macOS pkgs match the running arch, never cross', () => {
// The real build (build-macos-remote.sh) emits BOTH -arm64.pkg and -x64.pkg.
// Each Mac must get its own arch; a wrong-arch .pkg is worse than none.
const pkgs = [
{ name: 'PearCalSeeder-1.0.34-arm64.pkg', browser_download_url: 'u/pkg-arm64' },
{ name: 'PearCalSeeder-1.0.34-arm64.pkg.sha256', browser_download_url: 'u/pkg-arm64.sha' },
{ name: 'PearCalSeeder-1.0.34-x64.pkg', browser_download_url: 'u/pkg-x64' },
{ name: 'PearCalSeeder-1.0.34-x64.pkg.sha256', browser_download_url: 'u/pkg-x64.sha' },
]
assert.equal(selectAsset(pkgs, 'darwin', 'arm64').browser_download_url, 'u/pkg-arm64')
assert.equal(selectAsset(pkgs, 'darwin', 'x64').browser_download_url, 'u/pkg-x64')
// Only the arm64 pkg present → an x64 Mac gets nothing, not the arm64 build.
const armOnly = pkgs.filter((a) => a.name.includes('arm64'))
assert.equal(selectAsset(armOnly, 'darwin', 'x64'), null)
// sha256 sidecar resolves for the arch-matched pkg.
const chosen = selectAsset(pkgs, 'darwin', 'x64')
assert.equal(selectSha256For(pkgs, chosen.name).browser_download_url, 'u/pkg-x64.sha')
})
test('selectAsset: linux prefers the ARCH-matching AppImage, then deb', () => {
assert.equal(selectAsset(ASSETS, 'linux', 'x64').browser_download_url, 'u/app-x64')
assert.equal(selectAsset(ASSETS, 'linux', 'arm64').browser_download_url, 'u/app-arm64')