Compare commits
5
Commits
v1.0.3
..
7e01e599fd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e01e599fd | ||
|
|
6191c7bbc6 | ||
|
|
7d69f2e4b2 | ||
|
|
ba0c99d5b6 | ||
|
|
476e27e253 |
No files matched your search
@@ -26,7 +26,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Format and clippy
|
name: Format and clippy
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-arm64
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-lint
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-lint
|
||||||
steps:
|
steps:
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
# Linux-only runner (ARM64).
|
# Linux-only runner (ARM64).
|
||||||
test:
|
test:
|
||||||
name: Tests (Linux)
|
name: Tests (Linux)
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-arm64
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-test
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-test
|
||||||
steps:
|
steps:
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
audit:
|
audit:
|
||||||
name: Dependency audit
|
name: Dependency audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-arm64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
gui:
|
gui:
|
||||||
name: Check desktop app (Linux)
|
name: Check desktop app (Linux)
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-arm64
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-gui
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-gui
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -48,22 +48,22 @@ jobs:
|
|||||||
cli:
|
cli:
|
||||||
name: CLI (${{ matrix.platform }})
|
name: CLI (${{ matrix.platform }})
|
||||||
needs: release
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.runner }}
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-cli-${{ matrix.platform }}
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-cli-${{ matrix.platform }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- { platform: linux-aarch64, target: aarch64-unknown-linux-gnu, apt: "" }
|
- { platform: linux-aarch64, target: aarch64-unknown-linux-gnu, apt: "", runner: linux-arm64 }
|
||||||
- { platform: linux-x86_64, target: x86_64-unknown-linux-gnu, apt: "gcc-x86-64-linux-gnu" }
|
- { platform: linux-x86_64, target: x86_64-unknown-linux-gnu, apt: "gcc-x86-64-linux-gnu", runner: linux-amd64 }
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
- name: Platform cross-linker
|
- name: Platform cross-linker
|
||||||
if: ${{ matrix.apt != '' }}
|
if: ${{ matrix.apt != '' && matrix.runner != 'linux-amd64' }}
|
||||||
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/cli-x86_64 ${{ matrix.apt }}
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/cli-x86_64 ${{ matrix.apt }}
|
||||||
- name: Build and package
|
- name: Build and package
|
||||||
run: scripts/package-cli.sh "${{ gitea.ref_name }}" "${{ matrix.platform }}" "${{ matrix.target }}" | tee packages.txt
|
run: scripts/package-cli.sh "${{ gitea.ref_name }}" "${{ matrix.platform }}" "${{ matrix.target }}" | tee packages.txt
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
gui:
|
gui:
|
||||||
name: Desktop app (${{ matrix.platform }})
|
name: Desktop app (${{ matrix.platform }})
|
||||||
needs: release
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.runner }}
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-gui-${{ matrix.platform }}
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-gui-${{ matrix.platform }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.SABIA_TAURI_SIGNING_PRIVATE_KEY || secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.SABIA_TAURI_SIGNING_PRIVATE_KEY || secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
@@ -84,7 +84,10 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [linux-aarch64, linux-x86_64, windows]
|
include:
|
||||||
|
- { platform: linux-aarch64, runner: linux-arm64 }
|
||||||
|
- { platform: linux-x86_64, runner: linux-amd64 }
|
||||||
|
- { platform: windows, runner: linux-amd64 }
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
|
|
||||||
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
||||||
|
|
||||||
|
## [1.0.5] - 2026-10-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **One-click desktop update.** The update button actually downloads, verifies (SHA-256) and installs the new version now, instead of only changing its own label. Works for all three desktop packages: AppImage (atomic self-replace), `.deb` (`dpkg -i`, no new password prompt — the app is already running as root by the time you click it), and the Windows installer (silent install). On Linux, the app restarts itself automatically; on Windows, the installer runs silently but the app does not reopen on its own yet — start it again once the install finishes.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **The repository moved to the Ciberfauna organization** (`ciberfauna/sabia`, was `lucasrguerra/sabia`). **Important notice for existing installations:** versions up to 1.0.4 have the old location baked into their update check; they will not find this release automatically. Download 1.0.5 once manually from the new Releases page, and future update checks will work through the new location.
|
||||||
|
|
||||||
## [1.0.3] - 2026-10-30
|
## [1.0.3] - 2026-10-30
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
|
|
||||||
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
||||||
|
|
||||||
|
## [1.0.5] - 2026-10-31
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- **Atualização do app desktop em um clique.** O botão de atualizar agora baixa, confere (SHA-256) e instala a versão nova de verdade, em vez de só mudar o próprio texto. Funciona nos três pacotes do desktop: AppImage (substituição atômica), `.deb` (`dpkg -i`, sem pedir senha de novo — o app já roda como root quando você clica), e o instalador do Windows (instalação silenciosa). No Linux o app reabre sozinho; no Windows o instalador roda silencioso mas o app ainda não reabre por conta própria — abra de novo depois que a instalação terminar.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- **O repositório se mudou pra organização Ciberfauna** (`ciberfauna/sabia`, era `lucasrguerra/sabia`). **Aviso importante pra instalações existentes:** versões até a 1.0.4 têm o endereço antigo fixo na checagem de atualização e não vão encontrar esta versão sozinhas. Baixe a 1.0.5 manualmente uma vez na nova página de Releases, e as próximas verificações voltam a funcionar pelo novo endereço.
|
||||||
|
|
||||||
## [1.0.3] - 2026-10-30
|
## [1.0.3] - 2026-10-30
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|||||||
Generated
+2
-2
@@ -440,7 +440,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-cli"
|
name = "sabia-cli"
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
@@ -451,7 +451,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-core"
|
name = "sabia-core"
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
|||||||
+2
-2
@@ -3,10 +3,10 @@ resolver = "2"
|
|||||||
members = ["crates/sabia-core", "crates/sabia-cli"]
|
members = ["crates/sabia-core", "crates/sabia-cli"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
|
repository = "https://git.lucasrguerra.dev.br/ciberfauna/sabia"
|
||||||
authors = ["Sabiá contributors"]
|
authors = ["Sabiá contributors"]
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -2538,7 +2538,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-core"
|
name = "sabia-core"
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -2555,7 +2555,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-gui"
|
name = "sabia-gui"
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"sabia-core",
|
"sabia-core",
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sabia-gui"
|
name = "sabia-gui"
|
||||||
description = "Sabiá: graphical Wi-Fi security auditing interface"
|
description = "Sabiá: graphical Wi-Fi security auditing interface"
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
authors = ["Sabiá contributors"]
|
authors = ["Sabiá contributors"]
|
||||||
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
|
repository = "https://git.lucasrguerra.dev.br/ciberfauna/sabia"
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
|||||||
@@ -502,8 +502,19 @@ fn update_check(state: tauri::State<'_, State>) -> Result<serde_json::Value, Str
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
|
fn update_apply(app: tauri::AppHandle, state: tauri::State<'_, State>) -> Result<String, String> {
|
||||||
state.0.update_apply().map_err(|e| e.to_string())
|
use tauri::Emitter;
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
let exe = elevated_exe().unwrap_or_else(|_| std::env::current_exe().unwrap_or_default());
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
let exe = std::env::current_exe().unwrap_or_default();
|
||||||
|
|
||||||
|
state
|
||||||
|
.0
|
||||||
|
.update_apply_desktop(&exe, move |phase| {
|
||||||
|
let _ = app.emit("update-progress", serde_json::json!({ "phase": phase }));
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `true` when the process already has sufficient privileges for all
|
/// `true` when the process already has sufficient privileges for all
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Sabiá",
|
"productName": "Sabiá",
|
||||||
"version": "1.0.3",
|
"version": "1.0.5",
|
||||||
"identifier": "dev.sabia.app",
|
"identifier": "dev.sabia.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
|
|||||||
+48
-4
@@ -267,6 +267,8 @@ async function tauriApi(path, opts = {}) {
|
|||||||
return await invoke('export_all_hc22000', { iface: q.get('iface'), out: q.get('out') });
|
return await invoke('export_all_hc22000', { iface: q.get('iface'), out: q.get('out') });
|
||||||
case 'GET /api/update/check':
|
case 'GET /api/update/check':
|
||||||
return await invoke('update_check');
|
return await invoke('update_check');
|
||||||
|
case 'POST /api/update/apply':
|
||||||
|
return await invoke('update_apply');
|
||||||
default:
|
default:
|
||||||
return { error: 'rota desconhecida' };
|
return { error: 'rota desconhecida' };
|
||||||
}
|
}
|
||||||
@@ -371,12 +373,54 @@ els.btnScan.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let updateStatus = null;
|
||||||
|
let updateInProgress = false;
|
||||||
|
|
||||||
els.btnUpdate.addEventListener('click', async () => {
|
els.btnUpdate.addEventListener('click', async () => {
|
||||||
|
if (updateInProgress) return;
|
||||||
|
if (updateStatus?.update_available) {
|
||||||
|
updateInProgress = true;
|
||||||
|
els.btnUpdate.disabled = true;
|
||||||
|
els.btnUpdate.textContent = 'baixando…';
|
||||||
|
|
||||||
|
let unlisten = null;
|
||||||
|
if (tauri?.event?.listen) {
|
||||||
|
try {
|
||||||
|
unlisten = await tauri.event.listen('update-progress', (event) => {
|
||||||
|
const phase = event.payload?.phase;
|
||||||
|
if (phase === 'downloading') els.btnUpdate.textContent = 'baixando…';
|
||||||
|
else if (phase === 'verifying') els.btnUpdate.textContent = 'verificando…';
|
||||||
|
else if (phase === 'installing') els.btnUpdate.textContent = 'instalando…';
|
||||||
|
else if (phase === 'restarting') els.btnUpdate.textContent = 'reiniciando…';
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api('/api/update/apply', { method: 'POST' });
|
||||||
|
els.btnUpdate.textContent = 'já atualizado';
|
||||||
|
} catch (err) {
|
||||||
|
updateInProgress = false;
|
||||||
|
els.btnUpdate.disabled = false;
|
||||||
|
els.btnUpdate.textContent = `atualizar para ${updateStatus.latest_version}`;
|
||||||
|
alert('Falha na atualização: ' + (err?.message || err));
|
||||||
|
} finally {
|
||||||
|
if (unlisten) unlisten();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
els.btnUpdate.textContent = 'checando…';
|
els.btnUpdate.textContent = 'checando…';
|
||||||
const status = await api('/api/update/check');
|
try {
|
||||||
els.btnUpdate.textContent = status.update_available
|
const status = await api('/api/update/check');
|
||||||
? `atualizar para ${status.latest_version}`
|
updateStatus = status;
|
||||||
: 'já atualizado';
|
els.btnUpdate.textContent = status?.update_available
|
||||||
|
? `atualizar para ${status.latest_version}`
|
||||||
|
: 'já atualizado';
|
||||||
|
} catch (err) {
|
||||||
|
els.btnUpdate.textContent = 'checar atualização';
|
||||||
|
alert('Não foi possível verificar atualizações: ' + (err?.message || err));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const SEC_RANK = {
|
const SEC_RANK = {
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export const EN = {
|
|||||||
'checando…': 'checking…',
|
'checando…': 'checking…',
|
||||||
'atualizar para {n}': 'update to {n}',
|
'atualizar para {n}': 'update to {n}',
|
||||||
'já atualizado': 'already up to date',
|
'já atualizado': 'already up to date',
|
||||||
|
'baixando…': 'downloading…',
|
||||||
|
'verificando…': 'verifying…',
|
||||||
|
'instalando…': 'installing…',
|
||||||
|
'reiniciando…': 'restarting…',
|
||||||
|
'Falha na atualização:': 'Update failed:',
|
||||||
|
'Não foi possível verificar atualizações:': 'Could not check for updates:',
|
||||||
|
|
||||||
// topbar
|
// topbar
|
||||||
'Painel de captura': 'Capture dashboard',
|
'Painel de captura': 'Capture dashboard',
|
||||||
|
|||||||
@@ -2979,6 +2979,17 @@ impl Engine {
|
|||||||
update::apply(&exe)
|
update::apply(&exe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_apply_desktop<F>(
|
||||||
|
&self,
|
||||||
|
current_exe: &std::path::Path,
|
||||||
|
on_progress: F,
|
||||||
|
) -> Result<String>
|
||||||
|
where
|
||||||
|
F: Fn(&str),
|
||||||
|
{
|
||||||
|
update::apply_desktop_with_progress(current_exe, on_progress)
|
||||||
|
}
|
||||||
|
|
||||||
fn session_handshake(
|
fn session_handshake(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ use crate::error::{Result, SabiaError};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
/// Gitea `releases/latest` API endpoint (`tag_name`, `assets[].name`, `assets[].browser_download_url`).
|
/// Gitea `releases/latest` API endpoint (`tag_name`, `assets[].name`, `assets[].browser_download_url`).
|
||||||
///
|
///
|
||||||
const LATEST_RELEASE_URL: &str =
|
const LATEST_RELEASE_URL: &str =
|
||||||
"https://git.lucasrguerra.dev.br/api/v1/repos/lucasrguerra/sabia/releases/latest";
|
"https://git.lucasrguerra.dev.br/api/v1/repos/ciberfauna/sabia/releases/latest";
|
||||||
|
|
||||||
/// Human-facing Gitea Releases page for desktop GUI users.
|
/// Human-facing Gitea Releases page for desktop GUI users.
|
||||||
const RELEASES_PAGE_URL: &str = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia/releases";
|
const RELEASES_PAGE_URL: &str = "https://git.lucasrguerra.dev.br/ciberfauna/sabia/releases";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct Release {
|
struct Release {
|
||||||
@@ -56,7 +56,7 @@ fn curl_text(url: &str) -> Result<String> {
|
|||||||
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn curl_download(url: &str, dest: &PathBuf) -> Result<()> {
|
fn curl_download(url: &str, dest: &Path) -> Result<()> {
|
||||||
let status = Command::new("curl")
|
let status = Command::new("curl")
|
||||||
.args(["-fsSL", "-H", "User-Agent: sabia-updater", "-o"])
|
.args(["-fsSL", "-H", "User-Agent: sabia-updater", "-o"])
|
||||||
.arg(dest)
|
.arg(dest)
|
||||||
@@ -246,6 +246,300 @@ fn find_binary(dir: &PathBuf) -> Result<PathBuf> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Desktop GUI installation type detected at runtime.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum DesktopInstall {
|
||||||
|
/// Linux AppImage: real filesystem path of the .AppImage file (from APPIMAGE env var).
|
||||||
|
AppImage(PathBuf),
|
||||||
|
/// Linux Debian package installed via dpkg: `current_exe` is reliable here.
|
||||||
|
Deb,
|
||||||
|
/// Windows NSIS installer: downloaded setup executable run with `/S`
|
||||||
|
/// (silent). Does not relaunch the app on its own — see the comment on
|
||||||
|
/// the `WindowsInstaller` apply path.
|
||||||
|
WindowsInstaller,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detects the desktop installation type given an optional `APPIMAGE` environment
|
||||||
|
/// variable value and the current executable path.
|
||||||
|
pub fn detect_desktop_install_from(
|
||||||
|
appimage_env: Option<&std::ffi::OsStr>,
|
||||||
|
current_exe: &Path,
|
||||||
|
) -> Result<DesktopInstall> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = (appimage_env, current_exe);
|
||||||
|
Ok(DesktopInstall::WindowsInstaller)
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if let Some(val) = appimage_env {
|
||||||
|
let path = PathBuf::from(val);
|
||||||
|
if path.is_file() {
|
||||||
|
return Ok(DesktopInstall::AppImage(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current_exe.is_file()
|
||||||
|
&& current_exe
|
||||||
|
.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.map(|e| e.eq_ignore_ascii_case("appimage"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return Ok(DesktopInstall::AppImage(current_exe.to_path_buf()));
|
||||||
|
}
|
||||||
|
Ok(DesktopInstall::Deb)
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, target_os = "linux")))]
|
||||||
|
{
|
||||||
|
let _ = (appimage_env, current_exe);
|
||||||
|
Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: format!(
|
||||||
|
"automatic self-update is not supported on this platform; please download manually from {RELEASES_PAGE_URL}"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detects the desktop installation type using the live environment.
|
||||||
|
pub fn detect_desktop_install() -> Result<DesktopInstall> {
|
||||||
|
let exe = std::env::current_exe()?;
|
||||||
|
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), &exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the expected release asset suffix for a desktop installation and CPU architecture.
|
||||||
|
pub fn gui_asset_suffix_for(install: &DesktopInstall, arch: &str) -> Result<&'static str> {
|
||||||
|
match install {
|
||||||
|
DesktopInstall::AppImage(_) => match arch {
|
||||||
|
"x86_64" => Ok("_amd64.AppImage"),
|
||||||
|
"aarch64" => Ok("_aarch64.AppImage"),
|
||||||
|
other => Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: format!("unsupported architecture for AppImage: {other}"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
DesktopInstall::Deb => match arch {
|
||||||
|
"x86_64" => Ok("_amd64.deb"),
|
||||||
|
"aarch64" => Ok("_arm64.deb"),
|
||||||
|
other => Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: format!("unsupported architecture for deb package: {other}"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
DesktopInstall::WindowsInstaller => match arch {
|
||||||
|
"x86_64" => Ok("_x64-setup.exe"),
|
||||||
|
other => Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: format!("unsupported architecture for Windows installer: {other}"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the expected release asset suffix for the current architecture.
|
||||||
|
pub fn gui_asset_suffix(install: &DesktopInstall) -> Result<&'static str> {
|
||||||
|
gui_asset_suffix_for(install, std::env::consts::ARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads, verifies, and installs the latest desktop GUI release in place.
|
||||||
|
pub fn apply_desktop(current_exe: &Path) -> Result<String> {
|
||||||
|
apply_desktop_with_progress(current_exe, |_| {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads, verifies, and installs the latest desktop GUI release in place,
|
||||||
|
/// notifying the progress callback of phase transitions (`"downloading"`,
|
||||||
|
/// `"verifying"`, `"installing"`, `"restarting"`).
|
||||||
|
pub fn apply_desktop_with_progress<F>(current_exe: &Path, on_progress: F) -> Result<String>
|
||||||
|
where
|
||||||
|
F: Fn(&str),
|
||||||
|
{
|
||||||
|
let install =
|
||||||
|
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), current_exe)?;
|
||||||
|
let body = curl_text(LATEST_RELEASE_URL)?;
|
||||||
|
let release: Release = serde_json::from_str(&body)?;
|
||||||
|
|
||||||
|
let suffix = gui_asset_suffix(&install)?;
|
||||||
|
let asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.ends_with(suffix))
|
||||||
|
.ok_or_else(|| SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: format!(
|
||||||
|
"no asset ending with `{suffix}` in release {}",
|
||||||
|
release.tag_name
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
let checksum_asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name == format!("{}.sha256", asset.name));
|
||||||
|
|
||||||
|
let tmp_dir = std::env::temp_dir().join("sabia-desktop-update");
|
||||||
|
fs::create_dir_all(&tmp_dir)?;
|
||||||
|
let download_path = tmp_dir.join(&asset.name);
|
||||||
|
|
||||||
|
on_progress("downloading");
|
||||||
|
curl_download(&asset.browser_download_url, &download_path)?;
|
||||||
|
|
||||||
|
if let Some(checksum_asset) = checksum_asset {
|
||||||
|
on_progress("verifying");
|
||||||
|
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||||
|
let expected_hash = expected
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase();
|
||||||
|
let data = fs::read(&download_path)?;
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&data);
|
||||||
|
let actual_hash: String = hasher
|
||||||
|
.finalize()
|
||||||
|
.iter()
|
||||||
|
.map(|b| format!("{b:02x}"))
|
||||||
|
.collect();
|
||||||
|
if actual_hash != expected_hash {
|
||||||
|
return Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: "checksum SHA-256 não confere — atualização descartada".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_progress("installing");
|
||||||
|
match install {
|
||||||
|
DesktopInstall::AppImage(target_appimage) => {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let parent_dir = target_appimage.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
let temp_name = format!(
|
||||||
|
".{}.tmp-{}",
|
||||||
|
target_appimage
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("appimage"),
|
||||||
|
std::process::id()
|
||||||
|
);
|
||||||
|
let temp_file = parent_dir.join(temp_name);
|
||||||
|
fs::copy(&download_path, &temp_file)?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(&temp_file)?.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&temp_file, perms)?;
|
||||||
|
}
|
||||||
|
fs::rename(&temp_file, &target_appimage)?;
|
||||||
|
let _ = fs::remove_file(&download_path);
|
||||||
|
|
||||||
|
on_progress("restarting");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
let err = Command::new(&target_appimage)
|
||||||
|
.args(std::env::args().skip(1))
|
||||||
|
.exec();
|
||||||
|
return Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: format!("failed to relaunch AppImage: {err}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
let _ = (&target_appimage, &download_path);
|
||||||
|
return Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: "AppImage updates are only supported on Linux".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DesktopInstall::Deb => {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let status = Command::new("dpkg")
|
||||||
|
.args(["-i"])
|
||||||
|
.arg(&download_path)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| SabiaError::CommandFailed {
|
||||||
|
cmd: "dpkg -i".into(),
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "dpkg -i".into(),
|
||||||
|
detail: format!("dpkg -i failed with exit code {:?}", status.code()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let _ = fs::remove_file(&download_path);
|
||||||
|
|
||||||
|
on_progress("restarting");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
let err = Command::new(current_exe)
|
||||||
|
.args(std::env::args().skip(1))
|
||||||
|
.exec();
|
||||||
|
return Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: format!("failed to relaunch updated app: {err}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
let _ = (&download_path, current_exe);
|
||||||
|
return Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: "deb updates are only supported on Linux".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DesktopInstall::WindowsInstaller => {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
// `/S` is the one NSIS silent-install switch with a documented,
|
||||||
|
// stable meaning. There is no standard NSIS or Tauri flag that
|
||||||
|
// makes a silent install relaunch the app afterwards — that was
|
||||||
|
// claimed in an earlier draft of this code and could not be
|
||||||
|
// verified against NSIS's or Tauri's documentation; passing an
|
||||||
|
// invented flag to a stock NSIS installer is silently ignored,
|
||||||
|
// not harmful, but also not a relaunch mechanism. Spawning
|
||||||
|
// detached and exiting immediately (instead of waiting on the
|
||||||
|
// child) also avoids a self-deadlock: if the installer's script
|
||||||
|
// waits for this process to close before it can overwrite the
|
||||||
|
// running .exe, blocking here on the installer's exit status
|
||||||
|
// would hang forever. The practical effect: the update installs
|
||||||
|
// silently, but the app does not reopen itself — the user needs
|
||||||
|
// to start it again. This needs real-world confirmation on
|
||||||
|
// Windows before relying on it; if testing shows the bundled
|
||||||
|
// NSIS template does support an explicit relaunch flag or step,
|
||||||
|
// wire it in here instead of this comment.
|
||||||
|
on_progress("restarting");
|
||||||
|
Command::new(&download_path)
|
||||||
|
.arg("/S")
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| SabiaError::CommandFailed {
|
||||||
|
cmd: "installer".into(),
|
||||||
|
detail: format!("failed to launch installer: {e}"),
|
||||||
|
})?;
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let _ = &download_path;
|
||||||
|
return Err(SabiaError::CommandFailed {
|
||||||
|
cmd: "update".into(),
|
||||||
|
detail: "Windows installer updates are only supported on Windows".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
Ok(release.tag_name)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -258,4 +552,70 @@ mod tests {
|
|||||||
let err = apply(&PathBuf::from("C:/Program Files/Sabia/sabia-gui.exe")).unwrap_err();
|
let err = apply(&PathBuf::from("C:/Program Files/Sabia/sabia-gui.exe")).unwrap_err();
|
||||||
assert!(err.to_string().contains(RELEASES_PAGE_URL), "{err}");
|
assert!(err.to_string().contains(RELEASES_PAGE_URL), "{err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_desktop_install_variants() {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let temp_appimage =
|
||||||
|
std::env::temp_dir().join(format!("test-sabia-{}.AppImage", std::process::id()));
|
||||||
|
let _ = fs::write(&temp_appimage, b"dummy appimage");
|
||||||
|
|
||||||
|
let res = detect_desktop_install_from(
|
||||||
|
Some(temp_appimage.as_os_str()),
|
||||||
|
Path::new("/usr/bin/sabia-gui"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res, DesktopInstall::AppImage(temp_appimage.clone()));
|
||||||
|
|
||||||
|
let res_fallback = detect_desktop_install_from(None, &temp_appimage).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
res_fallback,
|
||||||
|
DesktopInstall::AppImage(temp_appimage.clone())
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_file(&temp_appimage);
|
||||||
|
|
||||||
|
let res_deb =
|
||||||
|
detect_desktop_install_from(None, Path::new("/usr/bin/sabia-gui")).unwrap();
|
||||||
|
assert_eq!(res_deb, DesktopInstall::Deb);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let res = detect_desktop_install_from(
|
||||||
|
None,
|
||||||
|
Path::new("C:/Program Files/Sabia/sabia-gui.exe"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res, DesktopInstall::WindowsInstaller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gui_asset_suffix_matches_known_release_naming() {
|
||||||
|
let appimage = DesktopInstall::AppImage(PathBuf::from("/tmp/Sabia.AppImage"));
|
||||||
|
assert_eq!(
|
||||||
|
gui_asset_suffix_for(&appimage, "x86_64").unwrap(),
|
||||||
|
"_amd64.AppImage"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
gui_asset_suffix_for(&appimage, "aarch64").unwrap(),
|
||||||
|
"_aarch64.AppImage"
|
||||||
|
);
|
||||||
|
|
||||||
|
let deb = DesktopInstall::Deb;
|
||||||
|
assert_eq!(gui_asset_suffix_for(&deb, "x86_64").unwrap(), "_amd64.deb");
|
||||||
|
assert_eq!(gui_asset_suffix_for(&deb, "aarch64").unwrap(), "_arm64.deb");
|
||||||
|
|
||||||
|
let win = DesktopInstall::WindowsInstaller;
|
||||||
|
assert_eq!(
|
||||||
|
gui_asset_suffix_for(&win, "x86_64").unwrap(),
|
||||||
|
"_x64-setup.exe"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(gui_asset_suffix_for(&appimage, "mips").is_err());
|
||||||
|
assert!(gui_asset_suffix_for(&deb, "riscv64").is_err());
|
||||||
|
assert!(gui_asset_suffix_for(&win, "arm").is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+13
-4
@@ -76,8 +76,6 @@ case "$platform" in
|
|||||||
rm -rf "$bundle/appimage" "$bundle/deb"
|
rm -rf "$bundle/appimage" "$bundle/deb"
|
||||||
tauri build --bundles appimage,deb
|
tauri build --bundles appimage,deb
|
||||||
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
|
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
|
||||||
# Include .sig alongside the bundle when updater signing is enabled.
|
|
||||||
cp "$bundle"/appimage/*.AppImage.sig ../../../dist-gui/ 2>/dev/null || true
|
|
||||||
;;
|
;;
|
||||||
linux-x86_64)
|
linux-x86_64)
|
||||||
if [ "$(uname -m)" = x86_64 ]; then
|
if [ "$(uname -m)" = x86_64 ]; then
|
||||||
@@ -165,7 +163,6 @@ EOF
|
|||||||
tauri build --target x86_64-unknown-linux-gnu --bundles appimage,deb
|
tauri build --target x86_64-unknown-linux-gnu --bundles appimage,deb
|
||||||
fi
|
fi
|
||||||
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
|
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
|
||||||
cp "$bundle"/appimage/*.AppImage.sig ../../../dist-gui/ 2>/dev/null || true
|
|
||||||
;;
|
;;
|
||||||
windows)
|
windows)
|
||||||
rustup target add x86_64-pc-windows-msvc >/dev/null
|
rustup target add x86_64-pc-windows-msvc >/dev/null
|
||||||
@@ -173,9 +170,21 @@ EOF
|
|||||||
rm -rf "$bundle"
|
rm -rf "$bundle"
|
||||||
tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
|
tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
|
||||||
cp "$bundle"/*-setup.exe ../../../dist-gui/
|
cp "$bundle"/*-setup.exe ../../../dist-gui/
|
||||||
cp "$bundle"/*-setup.exe.sig ../../../dist-gui/ 2>/dev/null || true
|
|
||||||
;;
|
;;
|
||||||
*) echo "unknown platform: $platform" >&2; exit 2 ;;
|
*) echo "unknown platform: $platform" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
(
|
||||||
|
cd ../../../dist-gui
|
||||||
|
for asset in *; do
|
||||||
|
[ -f "$asset" ] || continue
|
||||||
|
case "$asset" in
|
||||||
|
*.sha256) ;;
|
||||||
|
*)
|
||||||
|
sha256sum "$asset" > "$asset.sha256"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
)
|
||||||
|
|
||||||
ls -1 ../../../dist-gui
|
ls -1 ../../../dist-gui
|
||||||
Reference in new issue
Block a user