Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/desktop-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,11 @@ jobs:
needs.publish-relay-image.result == 'success'
runs-on: ubuntu-latest
env:
REQUIRED_UPDATER_PLATFORMS: windows-x86_64,darwin-x86_64,darwin-aarch64,linux-x86_64,linux-aarch64
# Bundle-type Linux keys are load-bearing: tauri-plugin-updater on a deb
# (or rpm) install rejects every non-deb (non-rpm) payload with
# "invalid updater binary format", so the manifest MUST carry
# linux-*-deb / linux-*-rpm entries alongside the AppImage keys.
REQUIRED_UPDATER_PLATFORMS: windows-x86_64,darwin-x86_64,darwin-aarch64,linux-x86_64,linux-aarch64,linux-x86_64-deb,linux-aarch64-deb,linux-x86_64-rpm,linux-aarch64-rpm

steps:
- name: Checkout
Expand Down
22 changes: 21 additions & 1 deletion scripts/collect-tauri-updater-assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ function isUpdaterBundle(file) {
lower.endsWith('.app.tar.gz') ||
lower.endsWith('.tar.gz') ||
lower.endsWith('.zip') ||
lower.endsWith('.exe')
lower.endsWith('.exe') ||
lower.endsWith('.deb') ||
lower.endsWith('.rpm')
);
}

Expand All @@ -140,6 +142,14 @@ function updaterOutputName(file, version, platform) {
if (lower.endsWith('.exe')) {
return `OpenBitFun_${version}_${platform}-setup.exe`;
}
if (lower.endsWith('.deb')) {
// The bundle-type platform key is part of the name, so the renamed copy
// never collides with the raw bundler deb staged from release-assets.
return `OpenBitFun_${version}_${platform}.deb`;
}
if (lower.endsWith('.rpm')) {
return `OpenBitFun_${version}_${platform}.rpm`;
}
if (lower.endsWith('.tar.gz')) {
return `OpenBitFun_${version}_${platform}.tar.gz`;
}
Expand All @@ -162,6 +172,16 @@ function inferPlatform(file) {
if (lower.includes('.appimage.tar.gz')) {
return `linux-${arch}`;
}
if (lower.endsWith('.deb')) {
// Bundle-type key: tauri-plugin-updater installs a deb payload via dpkg only
// when the running install is itself a deb, so these clients must receive a
// dedicated `linux-<arch>-deb` entry instead of the AppImage the bare key
// serves.
return `linux-${arch}-deb`;
}
if (lower.endsWith('.rpm')) {
return `linux-${arch}-rpm`;
}
if (lower.includes('.app.tar.gz')) {
return `darwin-${arch}`;
}
Expand Down
16 changes: 14 additions & 2 deletions scripts/generate-tauri-latest-json.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ for (const sigPath of walkFiles(assetsDir).filter((file) => file.endsWith('.sig'

const platformNames = Object.keys(platforms);
if (platformNames.length === 0) {
fail('No signed updater artifacts were found. Expected .AppImage.sig, .app.tar.gz.sig, .tar.gz.sig, .zip.sig, or .exe.sig files.');
fail('No signed updater artifacts were found. Expected .AppImage.sig, .app.tar.gz.sig, .tar.gz.sig, .zip.sig, .exe.sig, .deb.sig, or .rpm.sig files.');
}

const missingPlatforms = requiredPlatforms.filter((platform) => !platforms[platform]);
Expand Down Expand Up @@ -161,7 +161,9 @@ function isUpdaterBundle(file) {
lower.endsWith('.app.tar.gz') ||
lower.endsWith('.tar.gz') ||
lower.endsWith('.zip') ||
lower.endsWith('.exe')
lower.endsWith('.exe') ||
lower.endsWith('.deb') ||
lower.endsWith('.rpm')
);
}

Expand All @@ -184,6 +186,16 @@ function inferPlatform(file) {
if (lower.includes('.appimage.tar.gz')) {
return `linux-${arch}`;
}
if (lower.endsWith('.deb')) {
// Bundle-type key: tauri-plugin-updater installs a deb payload via dpkg only
// when the running install is itself a deb, so these clients must receive a
// dedicated `linux-<arch>-deb` entry instead of the AppImage the bare key
// serves.
return `linux-${arch}-deb`;
}
if (lower.endsWith('.rpm')) {
return `linux-${arch}-rpm`;
}
if (lower.includes('.app.tar.gz')) {
return `darwin-${arch}`;
}
Expand Down
78 changes: 78 additions & 0 deletions scripts/tauri-release-manifest.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,84 @@ test('manifest declares the signed macOS .dmg installers next to the .app.tar.gz
assert.match(absent.stderr, /Missing macOS installer OpenBitFun_1\.2\.3_x64\.dmg/);
});

// deb/rpm installs reject every payload that is not their own package format
// (tauri-plugin-updater: install_deb/install_rpm -> InvalidUpdaterFormat), so the
// feed must carry bundle-type keys. Regression: the 1.0.1 feed served the
// AppImage under the bare linux-x86_64 key and every deb install failed in-app
// updates with "invalid updater binary format".
test('deb and rpm installs get bundle-type Linux updater keys through collect + generate', () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'openbitfun-deb-updater-'));
const collected = path.join(temp, 'collected');
const out = path.join(temp, 'latest-v1.json');
const assets = [
['OpenBitFun_1.2.3_amd64.AppImage', 'appimage'],
['OpenBitFun_1.2.3_amd64.deb', 'deb'],
['OpenBitFun-1.2.3-1.x86_64.rpm', 'rpm'],
['OpenBitFun_1.2.3_arm64.deb', 'deb-arm64'],
['OpenBitFun-1.2.3-1.aarch64.rpm', 'rpm-arm64'],
];
for (const [name, body] of assets) {
fs.writeFileSync(path.join(temp, name), body);
fs.writeFileSync(path.join(temp, `${name}.sig`), `sig ${name}`);
}

const staging = run('scripts/collect-tauri-updater-assets.mjs', [
'--assets-dir', temp,
'--version', '1.2.3',
'--out-dir', collected,
'--required-platforms', 'linux-x86_64,linux-x86_64-deb,linux-x86_64-rpm,linux-aarch64-deb,linux-aarch64-rpm',
]);
assert.equal(staging.status, 0, staging.stderr);
assert.deepEqual(fs.readdirSync(collected).sort(), [
'OpenBitFun_1.2.3_linux-aarch64-deb.deb',
'OpenBitFun_1.2.3_linux-aarch64-deb.deb.sig',
'OpenBitFun_1.2.3_linux-aarch64-rpm.rpm',
'OpenBitFun_1.2.3_linux-aarch64-rpm.rpm.sig',
'OpenBitFun_1.2.3_linux-x86_64-deb.deb',
'OpenBitFun_1.2.3_linux-x86_64-deb.deb.sig',
'OpenBitFun_1.2.3_linux-x86_64-rpm.rpm',
'OpenBitFun_1.2.3_linux-x86_64-rpm.rpm.sig',
'OpenBitFun_1.2.3_linux-x86_64.AppImage',
'OpenBitFun_1.2.3_linux-x86_64.AppImage.sig',
]);

const generated = run('scripts/generate-tauri-latest-json.mjs', [
'--assets-dir', collected,
'--version', '1.2.3',
'--tag', 'v1.2.3',
'--repo', 'GCWing/OpenBitFun',
'--out', out,
'--required-platforms', 'linux-x86_64,linux-x86_64-deb,linux-x86_64-rpm,linux-aarch64-deb,linux-aarch64-rpm',
]);
assert.equal(generated.status, 0, generated.stderr);

const manifest = JSON.parse(fs.readFileSync(out, 'utf8'));
assert.match(manifest.platforms['linux-x86_64'].url, /OpenBitFun_1\.2\.3_linux-x86_64\.AppImage$/);
assert.match(manifest.platforms['linux-x86_64-deb'].url, /OpenBitFun_1\.2\.3_linux-x86_64-deb\.deb$/);
assert.match(manifest.platforms['linux-x86_64-rpm'].url, /OpenBitFun_1\.2\.3_linux-x86_64-rpm\.rpm$/);
assert.match(manifest.platforms['linux-aarch64-deb'].url, /OpenBitFun_1\.2\.3_linux-aarch64-deb\.deb$/);
assert.match(manifest.platforms['linux-aarch64-rpm'].url, /OpenBitFun_1\.2\.3_linux-aarch64-rpm\.rpm$/);
// collect renames the files but copies signatures byte-for-byte: minisign
// signatures cover the package bytes, not the asset name.
const renamedToRawSignature = {
'linux-x86_64': 'sig OpenBitFun_1.2.3_amd64.AppImage',
'linux-x86_64-deb': 'sig OpenBitFun_1.2.3_amd64.deb',
'linux-x86_64-rpm': 'sig OpenBitFun-1.2.3-1.x86_64.rpm',
'linux-aarch64-deb': 'sig OpenBitFun_1.2.3_arm64.deb',
'linux-aarch64-rpm': 'sig OpenBitFun-1.2.3-1.aarch64.rpm',
};
for (const [key, signature] of Object.entries(renamedToRawSignature)) {
assert.equal(manifest.platforms[key].signature, signature);
}

const verified = run('scripts/verify-tauri-latest-json.mjs', [
'--manifest', out,
'--version', '1.2.3',
'--required-platforms', 'linux-x86_64,linux-x86_64-deb,linux-x86_64-rpm,linux-aarch64-deb,linux-aarch64-rpm',
]);
assert.equal(verified.status, 0, verified.stderr);
});

test('stages GitHub release assets in a flat directory', () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'openbitfun-release-assets-'));
const first = path.join(temp, 'updater', 'latest.json');
Expand Down
70 changes: 68 additions & 2 deletions src/apps/desktop/src/api/system_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,33 @@ async fn updater_endpoints_by_policy() -> Vec<tauri::Url> {

/// Tauri's `latest-v1.json` platform key for this host, e.g. `darwin-aarch64`.
/// Mirrors `scripts/generate-tauri-latest-json.mjs`.
///
/// Linux installs append a bundle-type suffix (`-deb` / `-rpm`) so the manifest
/// hands each install form the package its updater can actually install:
/// tauri-plugin-updater derives `dpkg -i` / `rpm -U` / AppImage rewrite from the
/// same [`tauri::utils::platform::bundle_type`] this key is derived from. A bare
/// `linux-*` key would feed AppImage bytes to a deb install, which the plugin
/// rejects as `invalid updater binary format`.
fn updater_platform_key() -> String {
let os = match std::env::consts::OS {
"macos" => "darwin",
other => other,
};
format!("{os}-{}", std::env::consts::ARCH)
format!(
"{os}-{}{}",
std::env::consts::ARCH,
updater_platform_key_suffix(tauri::utils::platform::bundle_type())
)
}

/// Manifest-key suffix for the running install form, kept in step with
/// `scripts/generate-tauri-latest-json.mjs` (`linux-<arch>-deb` / `linux-<arch>-rpm`).
fn updater_platform_key_suffix(bundle: Option<tauri::utils::config::BundleType>) -> &'static str {
match bundle {
Some(tauri::utils::config::BundleType::Deb) => "-deb",
Some(tauri::utils::config::BundleType::Rpm) => "-rpm",
_ => "",
}
}

/// Read one updater manifest and return the download URL it advertises for this
Expand Down Expand Up @@ -1115,7 +1136,13 @@ mod tests {
#[test]
fn updater_platform_key_matches_latest_json_convention() {
let key = super::updater_platform_key();
let (os, arch) = key.split_once('-').expect("os-arch shape");
// Optional bundle-type suffix, mirroring the script's linux-<arch>-deb /
// linux-<arch>-rpm keys.
let base = key
.strip_suffix("-deb")
.or_else(|| key.strip_suffix("-rpm"))
.unwrap_or(&key);
let (os, arch) = base.split_once('-').expect("os-arch shape");
assert!(
matches!(os, "darwin" | "linux" | "windows"),
"unexpected updater os segment: {os}"
Expand All @@ -1129,6 +1156,45 @@ mod tests {
key.starts_with("darwin-"),
"macOS must map to darwin, got {key}"
);
#[cfg(target_os = "linux")]
assert!(
key.starts_with("linux-"),
"Linux keys must stay under the linux- prefix, got {key}"
);
}

/// The suffix must mirror the plugin's installer selection exactly: the same
/// `bundle_type()` that makes tauri-plugin-updater run `dpkg -i` / `rpm -U`
/// must ask the manifest for the `-deb` / `-rpm` payload, and every other
/// bundle type must keep the bare `os-arch` key (AppImage rewrite path).
#[test]
fn updater_platform_key_suffix_matches_plugin_installer_selection() {
use tauri::utils::config::BundleType;
assert_eq!(
super::updater_platform_key_suffix(Some(BundleType::Deb)),
"-deb"
);
assert_eq!(
super::updater_platform_key_suffix(Some(BundleType::Rpm)),
"-rpm"
);
assert_eq!(
super::updater_platform_key_suffix(Some(BundleType::AppImage)),
""
);
assert_eq!(
super::updater_platform_key_suffix(Some(BundleType::Msi)),
""
);
assert_eq!(
super::updater_platform_key_suffix(Some(BundleType::Nsis)),
""
);
assert_eq!(
super::updater_platform_key_suffix(Some(BundleType::App)),
""
);
assert_eq!(super::updater_platform_key_suffix(None), "");
}

#[test]
Expand Down
Loading