feat(updater): Alpha version (#643)
Co-authored-by: Rajiv Shah <rajivshah1@icloud.com> Co-authored-by: Lucas Nogueira <lucas@tauri.studio> Co-authored-by: nothingismagick <denjell@mailscript.com> Co-authored-by: Laegel <valentin.chouaf@laposte.net>
@ -185,8 +185,7 @@
|
||||
},
|
||||
"tauri-updater": {
|
||||
"path": "./tauri-updater",
|
||||
"manager": "rust",
|
||||
"publish": false
|
||||
"manager": "rust"
|
||||
},
|
||||
"tauri": {
|
||||
"path": "./tauri",
|
||||
|
8
.changes/updater-alpha.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
"tauri-updater": minor
|
||||
"tauri-cli": minor
|
||||
"tauri-bundler": minor
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Alpha version of tauri-updater. Please refer to the `README` for more details.
|
62
.github/workflows/artifacts-updater.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
name: updater test artifacts
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/artifacts-updater.yml'
|
||||
- 'tauri/**'
|
||||
- 'tauri-updater/**'
|
||||
- 'cli/core/**'
|
||||
- 'cli/tauri-bundler/**'
|
||||
- 'examples/updater/**'
|
||||
|
||||
jobs:
|
||||
build-artifacs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: install stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: install webkit2gtk (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y webkit2gtk-4.0
|
||||
- run: cargo install --path ./cli/core --force
|
||||
- name: install cli deps via yarn
|
||||
working-directory: ./cli/tauri.js
|
||||
run: yarn
|
||||
- name: build cli
|
||||
working-directory: ./cli/tauri.js
|
||||
run: yarn build
|
||||
- name: build sample artifacts (updater)
|
||||
working-directory: ./examples/updater
|
||||
run: |
|
||||
yarn install
|
||||
node ../../cli/tauri.js/bin/tauri build
|
||||
env:
|
||||
TAURI_PRIVATE_KEY: dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg==
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
with:
|
||||
name: linux-updater-artifacts
|
||||
path: ./target/release/bundle/appimage/updater-example_*.AppImage.*
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: matrix.platform == 'windows-latest'
|
||||
with:
|
||||
name: windows-updater-artifacts
|
||||
path: ./target/release/bundle/msi/*
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: matrix.platform == 'macos-latest'
|
||||
with:
|
||||
name: macos-updater-artifacts
|
||||
path: ./target/release/bundle/macos/updater-example_*.app.tar.*
|
10
.github/workflows/test-bundler.yml
vendored
@ -23,6 +23,11 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: install webkit2gtk (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y webkit2gtk-4.0
|
||||
- name: test
|
||||
run: |
|
||||
cd ./cli/tauri-bundler
|
||||
@ -34,6 +39,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: rustup component add clippy
|
||||
- name: install webkit2gtk (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y webkit2gtk-4.0
|
||||
- name: clippy check
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
|
@ -13,6 +13,8 @@ members = [
|
||||
"examples/api/src-tauri",
|
||||
"examples/helloworld/src-tauri",
|
||||
"examples/multiwindow/src-tauri",
|
||||
# used to build updater artifacts
|
||||
"examples/updater/src-tauri",
|
||||
]
|
||||
|
||||
# default to small, optimized workspace release binaries
|
||||
|
@ -8,6 +8,7 @@
|
||||
"./cli": "./dist/cli.js",
|
||||
"./dialog": "./dist/dialog.js",
|
||||
"./event": "./dist/event.js",
|
||||
"./updater": "./dist/updater.js",
|
||||
"./fs": "./dist/fs.js",
|
||||
"./path": "./dist/path.js",
|
||||
"./http": "./dist/http.js",
|
||||
|
@ -14,6 +14,7 @@ export default [
|
||||
path: './src/path.ts',
|
||||
dialog: './src/dialog.ts',
|
||||
event: './src/event.ts',
|
||||
updater: './src/updater.ts',
|
||||
http: './src/http.ts',
|
||||
index: './src/index.ts',
|
||||
shell: './src/shell.ts',
|
||||
|
@ -2,6 +2,7 @@ import 'regenerator-runtime/runtime'
|
||||
import * as cli from './cli'
|
||||
import * as dialog from './dialog'
|
||||
import * as event from './event'
|
||||
import * as updater from './updater'
|
||||
import * as fs from './fs'
|
||||
import * as path from './path'
|
||||
import * as http from './http'
|
||||
@ -15,6 +16,7 @@ export {
|
||||
cli,
|
||||
dialog,
|
||||
event,
|
||||
updater,
|
||||
fs,
|
||||
path,
|
||||
http,
|
||||
|
130
api/src/updater.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { once, listen, emit, UnlistenFn } from './helpers/event'
|
||||
|
||||
export type UpdateStatus = 'PENDING' | 'ERROR' | 'DONE' | 'UPTODATE'
|
||||
|
||||
export interface UpdateStatusResult {
|
||||
error?: string
|
||||
status: UpdateStatus
|
||||
}
|
||||
|
||||
export interface UpdateManifest {
|
||||
version: string
|
||||
date: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export interface UpdateResult {
|
||||
manifest?: UpdateManifest
|
||||
shouldUpdate: boolean
|
||||
}
|
||||
|
||||
export async function installUpdate(): Promise<void> {
|
||||
let unlistenerFn: UnlistenFn | undefined
|
||||
|
||||
function cleanListener(): void {
|
||||
if (unlistenerFn) {
|
||||
unlistenerFn()
|
||||
}
|
||||
unlistenerFn = undefined
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
function onStatusChange(statusResult: UpdateStatusResult): void {
|
||||
if (statusResult.error) {
|
||||
cleanListener()
|
||||
return reject(statusResult.error)
|
||||
}
|
||||
|
||||
// install complete
|
||||
if (statusResult.status === 'DONE') {
|
||||
cleanListener()
|
||||
return resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// listen status change
|
||||
listen('tauri://update-status', (data: { payload: any }) => {
|
||||
onStatusChange(data?.payload as UpdateStatusResult)
|
||||
})
|
||||
.then((fn) => {
|
||||
unlistenerFn = fn
|
||||
})
|
||||
.catch((e) => {
|
||||
cleanListener()
|
||||
// dispatch the error to our checkUpdate
|
||||
throw e
|
||||
})
|
||||
|
||||
// start the process we dont require much security as it's
|
||||
// handled by rust
|
||||
emit('tauri://update-install').catch((e) => {
|
||||
cleanListener()
|
||||
// dispatch the error to our checkUpdate
|
||||
throw e
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkUpdate(): Promise<UpdateResult> {
|
||||
let unlistenerFn: UnlistenFn | undefined
|
||||
|
||||
function cleanListener(): void {
|
||||
if (unlistenerFn) {
|
||||
unlistenerFn()
|
||||
}
|
||||
unlistenerFn = undefined
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
function onUpdateAvailable(manifest: UpdateManifest): void {
|
||||
cleanListener()
|
||||
return resolve({
|
||||
manifest,
|
||||
shouldUpdate: true
|
||||
})
|
||||
}
|
||||
|
||||
function onStatusChange(statusResult: UpdateStatusResult): void {
|
||||
if (statusResult.error) {
|
||||
cleanListener()
|
||||
return reject(statusResult.error)
|
||||
}
|
||||
|
||||
if (statusResult.status === 'UPTODATE') {
|
||||
cleanListener()
|
||||
return resolve({
|
||||
shouldUpdate: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// wait to receive the latest update
|
||||
once('tauri://update-available', (data: { payload: any }) => {
|
||||
onUpdateAvailable(data?.payload as UpdateManifest)
|
||||
}).catch((e) => {
|
||||
cleanListener()
|
||||
// dispatch the error to our checkUpdate
|
||||
throw e
|
||||
})
|
||||
|
||||
// listen status change
|
||||
listen('tauri://update-status', (data: { payload: any }) => {
|
||||
onStatusChange(data?.payload as UpdateStatusResult)
|
||||
})
|
||||
.then((fn) => {
|
||||
unlistenerFn = fn
|
||||
})
|
||||
.catch((e) => {
|
||||
cleanListener()
|
||||
// dispatch the error to our checkUpdate
|
||||
throw e
|
||||
})
|
||||
|
||||
// start the process
|
||||
emit('tauri://update').catch((e) => {
|
||||
cleanListener()
|
||||
// dispatch the error to our checkUpdate
|
||||
throw e
|
||||
})
|
||||
})
|
||||
}
|
88
cli/core/Cargo.lock
generated
@ -236,6 +236,15 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
|
||||
dependencies = [
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.0.0-beta.2"
|
||||
@ -390,6 +399,16 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-mac"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6"
|
||||
dependencies = [
|
||||
"generic-array 0.14.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.12.2"
|
||||
@ -729,6 +748,16 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
|
||||
dependencies = [
|
||||
"crypto-mac",
|
||||
"digest 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.3"
|
||||
@ -997,6 +1026,17 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minisign"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59eb3626cabfaad94cca37c4b4f424705fd2567c16b4e000d61039405b384dda"
|
||||
dependencies = [
|
||||
"getrandom 0.2.2",
|
||||
"rpassword",
|
||||
"scrypt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.3.7"
|
||||
@ -1231,6 +1271,15 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "309c95c5f738c85920eb7062a2de29f3840d4f96974453fc9ac1ba078da9c627"
|
||||
dependencies = [
|
||||
"crypto-mac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
@ -1588,6 +1637,16 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac"
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.18"
|
||||
@ -1619,6 +1678,15 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "399f290ffc409596022fce5ea5d4138184be4784f2b28c62c59f0d8389059a15"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@ -1674,6 +1742,18 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25cba843abb27147110e2e9aa82b3be81eb386a9c6e3b35504c8c2ee305fd643"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"pbkdf2",
|
||||
"salsa20",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.6.0"
|
||||
@ -1855,6 +1935,12 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.65"
|
||||
@ -1941,6 +2027,7 @@ name = "tauri-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"clap",
|
||||
"colored",
|
||||
"dialoguer",
|
||||
@ -1948,6 +2035,7 @@ dependencies = [
|
||||
"heck",
|
||||
"include_dir",
|
||||
"json-patch",
|
||||
"minisign",
|
||||
"notify",
|
||||
"once_cell",
|
||||
"os_info",
|
||||
|
@ -29,6 +29,8 @@ valico = "3.6"
|
||||
handlebars = "3.5"
|
||||
include_dir = "0.6"
|
||||
dialoguer = "0.8"
|
||||
minisign = "0.6"
|
||||
base64 = "0.13.0"
|
||||
ureq = "2.1"
|
||||
os_info = "3.0"
|
||||
semver = "0.11"
|
||||
|
@ -502,7 +502,6 @@ impl Allowlist for AllowlistConfig {
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
|
||||
pub struct TauriConfig {
|
||||
/// The windows configuration.
|
||||
#[serde(default)]
|
||||
@ -515,6 +514,9 @@ pub struct TauriConfig {
|
||||
#[serde(default)]
|
||||
allowlist: AllowlistConfig,
|
||||
pub security: Option<SecurityConfig>,
|
||||
/// The updater configuration.
|
||||
#[serde(default = "default_updater")]
|
||||
pub updater: UpdaterConfig,
|
||||
}
|
||||
|
||||
impl TauriConfig {
|
||||
@ -524,6 +526,29 @@ impl TauriConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct UpdaterConfig {
|
||||
/// Whether the updater is active or not.
|
||||
pub active: bool,
|
||||
/// Display built-in dialog or use event system if disabled.
|
||||
#[serde(default = "default_dialog")]
|
||||
pub dialog: Option<bool>,
|
||||
/// The updater endpoints.
|
||||
pub endpoints: Option<Vec<String>>,
|
||||
/// Optional pubkey.
|
||||
pub pubkey: Option<String>,
|
||||
}
|
||||
|
||||
// We enable the unnecessary_wraps because we need
|
||||
// to use an Option for dialog otherwise the CLI schema will mark
|
||||
// the dialog as a required field which is not as we default it to true.
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn default_dialog() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
/// The Build configuration object.
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
|
||||
@ -582,3 +607,12 @@ fn default_build() -> BuildConfig {
|
||||
with_global_tauri: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_updater() -> UpdaterConfig {
|
||||
UpdaterConfig {
|
||||
active: false,
|
||||
dialog: Some(true),
|
||||
endpoints: None,
|
||||
pubkey: None,
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,9 @@
|
||||
"timestampUrl": null
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"windows": []
|
||||
},
|
||||
"allOf": [
|
||||
@ -938,6 +941,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"updater": {
|
||||
"description": "The updater configuration.",
|
||||
"default": {
|
||||
"active": false,
|
||||
"dialog": true
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/UpdaterConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"windows": {
|
||||
"description": "The windows configuration.",
|
||||
"default": [],
|
||||
@ -949,6 +964,44 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"UpdaterConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"active"
|
||||
],
|
||||
"properties": {
|
||||
"active": {
|
||||
"description": "Whether the updater is active or not.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"dialog": {
|
||||
"description": "Display built-in dialog or use event system if disabled.",
|
||||
"default": true,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"endpoints": {
|
||||
"description": "The updater endpoints.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"pubkey": {
|
||||
"description": "Optional pubkey.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"WindowAllowlistConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1,10 +1,13 @@
|
||||
use tauri_bundler::bundle::{bundle_project, PackageType, SettingsBuilder};
|
||||
use tauri_bundler::bundle::{
|
||||
bundle_project, common::print_signed_updater_archive, PackageType, SettingsBuilder,
|
||||
};
|
||||
|
||||
use crate::helpers::{
|
||||
app_paths::{app_dir, tauri_dir},
|
||||
config::get as get_config,
|
||||
execute_with_output,
|
||||
manifest::rewrite_manifest,
|
||||
updater_signature::sign_file_from_env_variables,
|
||||
Logger,
|
||||
};
|
||||
|
||||
@ -129,6 +132,7 @@ impl Build {
|
||||
if self.verbose {
|
||||
settings_builder = settings_builder.verbose();
|
||||
}
|
||||
|
||||
if let Some(names) = self.targets {
|
||||
let mut types = vec![];
|
||||
for name in names {
|
||||
@ -147,10 +151,35 @@ impl Build {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings_builder = settings_builder.package_types(types);
|
||||
}
|
||||
|
||||
// Bundle the project
|
||||
let settings = settings_builder.build()?;
|
||||
bundle_project(settings)?;
|
||||
|
||||
let bundles = bundle_project(settings)?;
|
||||
|
||||
// If updater is active and pubkey is available
|
||||
if config_.tauri.updater.active && config_.tauri.updater.pubkey.is_some() {
|
||||
// make sure we have our package builts
|
||||
let mut signed_paths = Vec::new();
|
||||
for elem in bundles
|
||||
.iter()
|
||||
.filter(|bundle| bundle.package_type == PackageType::Updater)
|
||||
{
|
||||
// we expect to have only one path in the vec but we iter if we add
|
||||
// another type of updater package who require multiple file signature
|
||||
for path in elem.bundle_paths.iter() {
|
||||
// sign our path from environment variables
|
||||
let (signature_path, _signature) = sign_file_from_env_variables(path)?;
|
||||
signed_paths.append(&mut vec![signature_path]);
|
||||
}
|
||||
}
|
||||
if !signed_paths.is_empty() {
|
||||
print_signed_updater_archive(&signed_paths)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -7,6 +7,7 @@ use crate::helpers::{app_paths::tauri_dir, config::Config};
|
||||
use tauri_bundler::WindowsSettings;
|
||||
use tauri_bundler::{
|
||||
AppCategory, BundleBinary, BundleSettings, DebianSettings, MacOSSettings, PackageSettings,
|
||||
UpdaterSettings,
|
||||
};
|
||||
|
||||
/// The `workspace` section of the app configuration (read from Cargo.toml).
|
||||
@ -141,7 +142,7 @@ impl AppSettings {
|
||||
}
|
||||
|
||||
pub fn get_bundle_settings(&self, config: &Config) -> crate::Result<BundleSettings> {
|
||||
tauri_config_to_bundle_settings(config.tauri.bundle.clone())
|
||||
tauri_config_to_bundle_settings(config.tauri.bundle.clone(), config.tauri.updater.clone())
|
||||
}
|
||||
|
||||
pub fn get_out_dir(&self, debug: bool) -> crate::Result<PathBuf> {
|
||||
@ -306,6 +307,7 @@ pub fn get_workspace_dir(current_dir: &PathBuf) -> PathBuf {
|
||||
|
||||
fn tauri_config_to_bundle_settings(
|
||||
config: crate::helpers::config::BundleConfig,
|
||||
updater_config: crate::helpers::config::UpdaterConfig,
|
||||
) -> crate::Result<BundleSettings> {
|
||||
Ok(BundleSettings {
|
||||
identifier: config.identifier,
|
||||
@ -341,6 +343,14 @@ fn tauri_config_to_bundle_settings(
|
||||
digest_algorithm: config.windows.digest_algorithm,
|
||||
certificate_thumbprint: config.windows.certificate_thumbprint,
|
||||
},
|
||||
updater: Some(UpdaterSettings {
|
||||
active: updater_config.active,
|
||||
// we set it to true by default we shouldn't have to use
|
||||
// unwrap_or as we have a default value but used to prevent any failing
|
||||
dialog: updater_config.dialog.unwrap_or(true),
|
||||
pubkey: updater_config.pubkey,
|
||||
endpoints: updater_config.endpoints,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
@ -45,6 +45,51 @@ subcommands:
|
||||
long: config
|
||||
about: config JSON to merge with tauri.conf.json
|
||||
takes_value: true
|
||||
- sign:
|
||||
about: Tauri updates signer.
|
||||
args:
|
||||
- generate:
|
||||
short: g
|
||||
long: generate
|
||||
about: Generate keypair to sign files
|
||||
- sign-file:
|
||||
long: sign-file
|
||||
about: Sign the specified file
|
||||
takes_value: true
|
||||
- private-key-path:
|
||||
short: f
|
||||
long: private-key-path
|
||||
about: Load the private key from a file
|
||||
takes_value: true
|
||||
conflicts_with: private-key
|
||||
- private-key:
|
||||
short: k
|
||||
long: private-key
|
||||
about: Load the private key from a string
|
||||
takes_value: true
|
||||
conflicts_with: private-key-path
|
||||
requires: sign-file
|
||||
- write-keys:
|
||||
short: w
|
||||
long: write-keys
|
||||
about: Write private key to a file
|
||||
takes_value: true
|
||||
requires: generate
|
||||
- password:
|
||||
short: p
|
||||
long: password
|
||||
about: Set private key password when signing
|
||||
takes_value: true
|
||||
conflicts_with: no-password
|
||||
- no-password:
|
||||
long: no-password
|
||||
about: Set empty password for your private key
|
||||
conflicts_with: password
|
||||
- force:
|
||||
long: force
|
||||
about: Overwrite private key even if it exists on the specified path
|
||||
requires: generate
|
||||
|
||||
- info:
|
||||
about: Shows information about Tauri dependencies
|
||||
- init:
|
||||
|
@ -2,6 +2,7 @@ pub mod app_paths;
|
||||
pub mod config;
|
||||
mod logger;
|
||||
pub mod manifest;
|
||||
pub mod updater_signature;
|
||||
|
||||
pub use logger::Logger;
|
||||
|
||||
|
186
cli/core/src/helpers/updater_signature.rs
Normal file
@ -0,0 +1,186 @@
|
||||
extern crate minisign;
|
||||
|
||||
use base64::{decode, encode};
|
||||
use minisign::{sign, KeyPair as KP, SecretKeyBox};
|
||||
use std::{
|
||||
env::var_os,
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri_bundler::bundle::common::create_file;
|
||||
|
||||
/// A key pair (`PublicKey` and `SecretKey`).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyPair {
|
||||
pub pk: String,
|
||||
pub sk: String,
|
||||
}
|
||||
|
||||
/// Generate base64 encoded keypair
|
||||
pub fn generate_key(password: Option<String>) -> crate::Result<KeyPair> {
|
||||
let KP { pk, sk } = KP::generate_encrypted_keypair(password).unwrap();
|
||||
|
||||
let pk_box_str = pk.to_box().unwrap().to_string();
|
||||
let sk_box_str = sk.to_box(None).unwrap().to_string();
|
||||
|
||||
let encoded_pk = encode(&pk_box_str);
|
||||
let encoded_sk = encode(&sk_box_str);
|
||||
|
||||
Ok(KeyPair {
|
||||
pk: encoded_pk,
|
||||
sk: encoded_sk,
|
||||
})
|
||||
}
|
||||
|
||||
/// Transform a base64 String to readable string for the main signer
|
||||
pub fn decode_key(base64_key: String) -> crate::Result<String> {
|
||||
let decoded_str = &decode(&base64_key)?[..];
|
||||
Ok(String::from(str::from_utf8(decoded_str)?))
|
||||
}
|
||||
|
||||
/// Save KeyPair to disk
|
||||
pub fn save_keypair<P>(
|
||||
force: bool,
|
||||
sk_path: P,
|
||||
key: &str,
|
||||
pubkey: &str,
|
||||
) -> crate::Result<(PathBuf, PathBuf)>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let sk_path = sk_path.as_ref();
|
||||
|
||||
let pubkey_path = format!("{}.pub", sk_path.display());
|
||||
let pk_path = Path::new(&pubkey_path);
|
||||
|
||||
if sk_path.exists() {
|
||||
if !force {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Key generation aborted:\n{} already exists\nIf you really want to overwrite the existing key pair, add the --force switch to force this operation.",
|
||||
sk_path.display()
|
||||
));
|
||||
} else {
|
||||
std::fs::remove_file(&sk_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
if pk_path.exists() {
|
||||
std::fs::remove_file(&pk_path)?;
|
||||
}
|
||||
|
||||
let mut sk_writer = create_file(&sk_path)?;
|
||||
write!(sk_writer, "{:}", key)?;
|
||||
sk_writer.flush()?;
|
||||
|
||||
let mut pk_writer = create_file(&pk_path)?;
|
||||
write!(pk_writer, "{:}", pubkey)?;
|
||||
pk_writer.flush()?;
|
||||
|
||||
Ok((fs::canonicalize(&sk_path)?, fs::canonicalize(&pk_path)?))
|
||||
}
|
||||
|
||||
/// Read key from file
|
||||
pub fn read_key_from_file<P>(sk_path: P) -> crate::Result<String>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
Ok(fs::read_to_string(sk_path)?)
|
||||
}
|
||||
|
||||
/// Sign files
|
||||
pub fn sign_file<P>(
|
||||
private_key: String,
|
||||
password: String,
|
||||
bin_path: P,
|
||||
prehashed: bool,
|
||||
) -> crate::Result<(PathBuf, String)>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let decoded_secret = decode_key(private_key)?;
|
||||
let sk_box = SecretKeyBox::from_string(&decoded_secret).unwrap();
|
||||
let sk = sk_box.into_secret_key(Some(password)).unwrap();
|
||||
|
||||
// We need to append .sig at the end it's where the signature will be stored
|
||||
let signature_path_string = format!("{}.sig", bin_path.as_ref().display());
|
||||
let signature_path = Path::new(&signature_path_string);
|
||||
|
||||
let mut signature_box_writer = create_file(&signature_path)?;
|
||||
|
||||
let trusted_comment = format!(
|
||||
"timestamp:{}\tfile:{}",
|
||||
unix_timestamp(),
|
||||
bin_path.as_ref().display()
|
||||
);
|
||||
|
||||
let (data_reader, should_be_prehashed) = open_data_file(bin_path)?;
|
||||
|
||||
let signature_box = sign(
|
||||
None,
|
||||
&sk,
|
||||
data_reader,
|
||||
prehashed | should_be_prehashed,
|
||||
Some(trusted_comment.as_str()),
|
||||
Some("signature from tauri secret key"),
|
||||
)?;
|
||||
|
||||
let encoded_signature = encode(&signature_box.to_string());
|
||||
signature_box_writer.write_all(&encoded_signature.as_bytes())?;
|
||||
signature_box_writer.flush()?;
|
||||
Ok((fs::canonicalize(&signature_path)?, encoded_signature))
|
||||
}
|
||||
|
||||
/// Sign files using the TAURI_KEY_PASSWORD and TAURI_PRIVATE_KEY environment variables
|
||||
pub fn sign_file_from_env_variables<P>(path_to_sign: P) -> crate::Result<(PathBuf, String)>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
// if no password provided we set empty string
|
||||
let password_string = match var_os("TAURI_KEY_PASSWORD") {
|
||||
Some(value) => String::from(value.to_str().unwrap()),
|
||||
None => "".into(),
|
||||
};
|
||||
// get the private key
|
||||
if let Some(private_key) = var_os("TAURI_PRIVATE_KEY") {
|
||||
// check if this file exist..
|
||||
let mut private_key_string = String::from(private_key.to_str().unwrap());
|
||||
let pk_dir = Path::new(&private_key_string);
|
||||
// Check if user provided a path or a key
|
||||
// We validate if the path exist or no.
|
||||
if pk_dir.exists() {
|
||||
// read file content as use it as private key
|
||||
private_key_string = read_key_from_file(pk_dir)?;
|
||||
}
|
||||
// sign our file
|
||||
return sign_file(private_key_string, password_string, path_to_sign, false);
|
||||
}
|
||||
// reject if we don't have the private key
|
||||
Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_PRIVATE_KEY` environment variable."))
|
||||
}
|
||||
|
||||
fn unix_timestamp() -> u64 {
|
||||
let start = SystemTime::now();
|
||||
let since_the_epoch = start
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock is incorrect");
|
||||
since_the_epoch.as_secs()
|
||||
}
|
||||
|
||||
fn open_data_file<P>(data_path: P) -> crate::Result<(BufReader<File>, bool)>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let data_path = data_path.as_ref();
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(data_path)
|
||||
.map_err(|e| minisign::PError::new(minisign::ErrorKind::Io, e))?;
|
||||
let should_be_hashed = match file.metadata() {
|
||||
Ok(metadata) => metadata.len() > (1u64 << 30),
|
||||
Err(_) => true,
|
||||
};
|
||||
Ok((BufReader::new(file), should_be_hashed))
|
||||
}
|
@ -7,6 +7,7 @@ mod dev;
|
||||
mod helpers;
|
||||
mod info;
|
||||
mod init;
|
||||
mod sign;
|
||||
|
||||
pub use helpers::Logger;
|
||||
|
||||
@ -121,6 +122,63 @@ fn info_command() -> Result<()> {
|
||||
info::Info::new().run()
|
||||
}
|
||||
|
||||
fn sign_command(matches: &ArgMatches) -> Result<()> {
|
||||
let private_key = matches.value_of("private-key");
|
||||
let private_key_path = matches.value_of("private-key-path");
|
||||
let file = matches.value_of("sign-file");
|
||||
let password = matches.value_of("password");
|
||||
let no_password = matches.is_present("no-password");
|
||||
let write_keys = matches.value_of("write-keys");
|
||||
let force = matches.is_present("force");
|
||||
|
||||
// generate keypair
|
||||
if matches.is_present("generate") {
|
||||
let mut keygen_runner = sign::KeyGenerator::new();
|
||||
|
||||
if no_password {
|
||||
keygen_runner = keygen_runner.empty_password();
|
||||
}
|
||||
|
||||
if force {
|
||||
keygen_runner = keygen_runner.force();
|
||||
}
|
||||
|
||||
if let Some(write_keys) = write_keys {
|
||||
keygen_runner = keygen_runner.output_path(write_keys);
|
||||
}
|
||||
|
||||
if let Some(password) = password {
|
||||
keygen_runner = keygen_runner.password(password);
|
||||
}
|
||||
|
||||
return keygen_runner.generate_keys();
|
||||
}
|
||||
|
||||
// sign our binary / archive
|
||||
let mut sign_runner = sign::Signer::new();
|
||||
if let Some(private_key) = private_key {
|
||||
sign_runner = sign_runner.private_key(private_key);
|
||||
}
|
||||
|
||||
if let Some(private_key_path) = private_key_path {
|
||||
sign_runner = sign_runner.private_key_path(private_key_path);
|
||||
}
|
||||
|
||||
if let Some(file) = file {
|
||||
sign_runner = sign_runner.file_to_sign(file);
|
||||
}
|
||||
|
||||
if let Some(password) = password {
|
||||
sign_runner = sign_runner.password(password);
|
||||
}
|
||||
|
||||
if no_password {
|
||||
sign_runner = sign_runner.empty_password();
|
||||
}
|
||||
|
||||
sign_runner.run()
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let yaml = load_yaml!("cli.yml");
|
||||
let app = App::from(yaml)
|
||||
@ -139,6 +197,8 @@ fn main() -> Result<()> {
|
||||
build_command(&matches)?;
|
||||
} else if matches.subcommand_matches("info").is_some() {
|
||||
info_command()?;
|
||||
} else if let Some(matches) = matches.subcommand_matches("sign") {
|
||||
sign_command(&matches)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
134
cli/core/src/sign.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use crate::helpers::updater_signature::{
|
||||
generate_key, read_key_from_file, save_keypair, sign_file,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Signer {
|
||||
private_key: Option<String>,
|
||||
password: Option<String>,
|
||||
file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Signer {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn private_key(mut self, private_key: &str) -> Self {
|
||||
self.private_key = Some(private_key.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn password(mut self, password: &str) -> Self {
|
||||
self.password = Some(password.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn empty_password(mut self) -> Self {
|
||||
self.password = Some("".to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn file_to_sign(mut self, file_path: &str) -> Self {
|
||||
self.file = Some(Path::new(file_path).to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn private_key_path(mut self, private_key: &str) -> Self {
|
||||
self.private_key =
|
||||
Some(read_key_from_file(Path::new(private_key)).expect("Unable to extract private key"));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn run(self) -> crate::Result<()> {
|
||||
if self.private_key.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Key generation aborted: Unable to find the private key".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if self.password.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Please use --no-password to set empty password or add --password <password> if your private key have a password.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let (manifest_dir, signature) = sign_file(
|
||||
self.private_key.unwrap(),
|
||||
self.password.unwrap(),
|
||||
self.file.unwrap(),
|
||||
false,
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"\nYour file was signed successfully, You can find the signature here:\n{}\n\nPublic signature:\n{}\n\nMake sure to include this into the signature field of your update server.",
|
||||
manifest_dir.display(),
|
||||
signature
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct KeyGenerator {
|
||||
password: Option<String>,
|
||||
output_path: Option<PathBuf>,
|
||||
force: bool,
|
||||
}
|
||||
|
||||
impl KeyGenerator {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn empty_password(mut self) -> Self {
|
||||
self.password = Some("".to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn force(mut self) -> Self {
|
||||
self.force = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn password(mut self, password: &str) -> Self {
|
||||
self.password = Some(password.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn output_path(mut self, output_path: &str) -> Self {
|
||||
self.output_path = Some(Path::new(output_path).to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn generate_keys(self) -> crate::Result<()> {
|
||||
let keypair = generate_key(self.password).expect("Failed to generate key");
|
||||
|
||||
if let Some(output_path) = self.output_path {
|
||||
let (secret_path, public_path) =
|
||||
save_keypair(self.force, output_path, &keypair.sk, &keypair.pk)
|
||||
.expect("Unable to write keypair");
|
||||
|
||||
println!(
|
||||
"\nYour keypair was generated successfully\nPrivate: {} (Keep it secret!)\nPublic: {}\n---------------------------",
|
||||
secret_path.display(),
|
||||
public_path.display()
|
||||
)
|
||||
} else {
|
||||
println!(
|
||||
"\nYour secret key was generated successfully - Keep it secret!\n{}\n\n",
|
||||
keypair.sk
|
||||
);
|
||||
println!(
|
||||
"Your public key was generated successfully:\n{}\n\nAdd the public key in your tauri.conf.json\n---------------------------\n",
|
||||
keypair.pk
|
||||
);
|
||||
}
|
||||
|
||||
println!("\nEnvironment variabled used to sign:\n`TAURI_PRIVATE_KEY` Path or String of your private key\n`TAURI_KEY_PASSWORD` Your private key password (optional)\n\nATTENTION: If you lose your private key OR password, you'll not be able to sign your update package and updates will not works.\n---------------------------\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -6,5 +6,6 @@
|
||||
fn main() {
|
||||
tauri::AppBuilder::default()
|
||||
.build(tauri::generate_context!())
|
||||
.run();
|
||||
.run()
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
@ -45,6 +45,9 @@
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"allowlist": {
|
||||
"all": true
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
mod appimage_bundle;
|
||||
mod category;
|
||||
mod common;
|
||||
pub mod common;
|
||||
mod deb_bundle;
|
||||
mod dmg_bundle;
|
||||
mod ios_bundle;
|
||||
@ -11,6 +11,7 @@ mod path_utils;
|
||||
mod platform;
|
||||
mod rpm_bundle;
|
||||
mod settings;
|
||||
mod updater_bundle;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod wix;
|
||||
|
||||
@ -19,7 +20,7 @@ pub use self::{
|
||||
common::{print_error, print_info},
|
||||
settings::{
|
||||
BundleBinary, BundleSettings, DebianSettings, MacOSSettings, PackageSettings, PackageType,
|
||||
Settings, SettingsBuilder,
|
||||
Settings, SettingsBuilder, UpdaterSettings,
|
||||
},
|
||||
};
|
||||
#[cfg(windows)]
|
||||
@ -29,37 +30,46 @@ use common::print_finished;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct Bundle {
|
||||
// the package type
|
||||
pub package_type: PackageType,
|
||||
/// all paths for this package
|
||||
pub bundle_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns the list of paths where the bundles can be found.
|
||||
pub fn bundle_project(settings: Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let mut paths = Vec::new();
|
||||
pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
|
||||
let mut bundles = Vec::new();
|
||||
let package_types = settings.package_types()?;
|
||||
|
||||
for package_type in &package_types {
|
||||
let mut bundle_paths = match package_type {
|
||||
PackageType::MacOSBundle => {
|
||||
if package_types.clone().iter().any(|&t| t == PackageType::Dmg) {
|
||||
vec![]
|
||||
} else {
|
||||
macos_bundle::bundle_project(&settings)?
|
||||
}
|
||||
}
|
||||
let bundle_paths = match package_type {
|
||||
PackageType::MacOSBundle => macos_bundle::bundle_project(&settings)?,
|
||||
PackageType::IosBundle => ios_bundle::bundle_project(&settings)?,
|
||||
#[cfg(target_os = "windows")]
|
||||
PackageType::WindowsMsi => msi_bundle::bundle_project(&settings)?,
|
||||
PackageType::Deb => deb_bundle::bundle_project(&settings)?,
|
||||
PackageType::Rpm => rpm_bundle::bundle_project(&settings)?,
|
||||
PackageType::AppImage => appimage_bundle::bundle_project(&settings)?,
|
||||
PackageType::Dmg => dmg_bundle::bundle_project(&settings)?,
|
||||
// dmg is dependant of MacOSBundle, we send our bundles to prevent rebuilding
|
||||
PackageType::Dmg => dmg_bundle::bundle_project(&settings, &bundles)?,
|
||||
// updater is dependant of multiple bundle, we send our bundles to prevent rebuilding
|
||||
PackageType::Updater => updater_bundle::bundle_project(&settings, &bundles)?,
|
||||
};
|
||||
paths.append(&mut bundle_paths);
|
||||
|
||||
bundles.push(Bundle {
|
||||
package_type: package_type.to_owned(),
|
||||
bundle_paths,
|
||||
});
|
||||
}
|
||||
|
||||
settings.copy_resources(settings.project_out_directory())?;
|
||||
settings.copy_binaries(settings.project_out_directory())?;
|
||||
|
||||
print_finished(&paths)?;
|
||||
print_finished(&bundles)?;
|
||||
|
||||
Ok(paths)
|
||||
Ok(bundles)
|
||||
}
|
||||
|
||||
/// Check to see if there are icons in the settings struct
|
||||
|
@ -150,14 +150,36 @@ pub fn print_bundling(filename: &str) -> crate::Result<()> {
|
||||
|
||||
/// Prints a message to stderr, in the same format that `cargo` uses,
|
||||
/// indicating that we have finished the the given bundles.
|
||||
pub fn print_finished(output_paths: &[PathBuf]) -> crate::Result<()> {
|
||||
let pluralised = if output_paths.len() == 1 {
|
||||
pub fn print_finished(bundles: &[crate::bundle::Bundle]) -> crate::Result<()> {
|
||||
let pluralised = if bundles.len() == 1 {
|
||||
"bundle"
|
||||
} else {
|
||||
"bundles"
|
||||
};
|
||||
let msg = format!("{} {} at:", output_paths.len(), pluralised);
|
||||
let msg = format!("{} {} at:", bundles.len(), pluralised);
|
||||
print_progress("Finished", &msg)?;
|
||||
for bundle in bundles {
|
||||
for path in &bundle.bundle_paths {
|
||||
let mut note = "";
|
||||
if bundle.package_type == crate::PackageType::Updater {
|
||||
note = " (updater)";
|
||||
}
|
||||
println!(" {}{}", path.display(), note,);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prints a message to stderr, in the same format that `cargo` uses,
|
||||
/// indicating that we have finished the the given signatures.
|
||||
pub fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
|
||||
let pluralised = if output_paths.len() == 1 {
|
||||
"updater archive"
|
||||
} else {
|
||||
"updater archives"
|
||||
};
|
||||
let msg = format!("{} {} at:", output_paths.len(), pluralised);
|
||||
print_progress("Signed", &msg)?;
|
||||
for path in output_paths {
|
||||
println!(" {}", path.display());
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::{common, macos_bundle};
|
||||
use crate::Settings;
|
||||
use crate::{bundle::Bundle, PackageType::MacOSBundle, Settings};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
@ -12,9 +12,16 @@ use std::{
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the DMG was created.
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
// generate the .app bundle
|
||||
macos_bundle::bundle_project(settings)?;
|
||||
pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
// generate the .app bundle if needed
|
||||
if bundles
|
||||
.iter()
|
||||
.filter(|bundle| bundle.package_type == MacOSBundle)
|
||||
.count()
|
||||
== 0
|
||||
{
|
||||
macos_bundle::bundle_project(settings)?;
|
||||
}
|
||||
|
||||
// get the target path
|
||||
let output_path = settings.project_out_directory().join("bundle/dmg");
|
||||
@ -32,7 +39,6 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
let product_name = &format!("{}.app", &package_base_name);
|
||||
let bundle_dir = settings.project_out_directory().join("bundle/macos");
|
||||
let bundle_path = bundle_dir.join(&product_name.clone());
|
||||
|
||||
let support_directory_path = output_path.join("support");
|
||||
if output_path.exists() {
|
||||
@ -80,8 +86,11 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let mut args = vec![
|
||||
"--volname",
|
||||
&package_base_name,
|
||||
"--volicon",
|
||||
"../../../../icons/icon.icns",
|
||||
// todo: volume icon
|
||||
// make sure this is a valid path?
|
||||
|
||||
//"--volicon",
|
||||
//"../../../../icons/icon.icns",
|
||||
"--icon",
|
||||
&product_name,
|
||||
"180",
|
||||
@ -134,5 +143,5 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
if let Some(identity) = &settings.macos().signing_identity {
|
||||
crate::bundle::macos_bundle::sign(dmg_path.clone(), identity, &settings, false)?;
|
||||
}
|
||||
Ok(vec![bundle_path, dmg_path])
|
||||
Ok(vec![dmg_path])
|
||||
}
|
||||
|
@ -26,11 +26,13 @@ pub enum PackageType {
|
||||
AppImage,
|
||||
/// The macOS DMG bundle (.dmg).
|
||||
Dmg,
|
||||
/// The Updater bundle.
|
||||
Updater,
|
||||
}
|
||||
|
||||
impl PackageType {
|
||||
/// Maps a short name to a PackageType.
|
||||
/// Possible values are "deb", "ios", "msi", "app", "rpm", "appimage", "dmg".
|
||||
/// Possible values are "deb", "ios", "msi", "app", "rpm", "appimage", "dmg", "updater".
|
||||
pub fn from_short_name(name: &str) -> Option<PackageType> {
|
||||
// Other types we may eventually want to support: apk.
|
||||
match name {
|
||||
@ -42,6 +44,7 @@ impl PackageType {
|
||||
"rpm" => Some(PackageType::Rpm),
|
||||
"appimage" => Some(PackageType::AppImage),
|
||||
"dmg" => Some(PackageType::Dmg),
|
||||
"updater" => Some(PackageType::Updater),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -58,6 +61,7 @@ impl PackageType {
|
||||
PackageType::Rpm => "rpm",
|
||||
PackageType::AppImage => "appimage",
|
||||
PackageType::Dmg => "dmg",
|
||||
PackageType::Updater => "updater",
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +80,7 @@ const ALL_PACKAGE_TYPES: &[PackageType] = &[
|
||||
PackageType::Rpm,
|
||||
PackageType::Dmg,
|
||||
PackageType::AppImage,
|
||||
PackageType::Updater,
|
||||
];
|
||||
|
||||
/// The package settings.
|
||||
@ -95,6 +100,19 @@ pub struct PackageSettings {
|
||||
pub default_run: Option<String>,
|
||||
}
|
||||
|
||||
/// The updater settings.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct UpdaterSettings {
|
||||
/// Whether the updater is active or not.
|
||||
pub active: bool,
|
||||
/// The updater endpoints.
|
||||
pub endpoints: Option<Vec<String>>,
|
||||
/// Optional pubkey.
|
||||
pub pubkey: Option<String>,
|
||||
/// Display built-in dialog or use event system if disabled.
|
||||
pub dialog: bool,
|
||||
}
|
||||
|
||||
/// The Linux debian bundle settings.
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct DebianSettings {
|
||||
@ -189,6 +207,8 @@ pub struct BundleSettings {
|
||||
pub deb: DebianSettings,
|
||||
/// MacOS-specific settings.
|
||||
pub macos: MacOSSettings,
|
||||
// Updater configuration
|
||||
pub updater: Option<UpdaterSettings>,
|
||||
/// Windows-specific settings.
|
||||
#[cfg(windows)]
|
||||
pub windows: WindowsSettings,
|
||||
@ -372,7 +392,7 @@ impl Settings {
|
||||
/// Fails if the host/target's native package type is not supported.
|
||||
pub fn package_types(&self) -> crate::Result<Vec<PackageType>> {
|
||||
let target_os = std::env::consts::OS;
|
||||
let platform_types = match target_os {
|
||||
let mut platform_types = match target_os {
|
||||
"macos" => vec![PackageType::MacOSBundle, PackageType::Dmg],
|
||||
"ios" => vec![PackageType::IosBundle],
|
||||
"linux" => vec![PackageType::Deb, PackageType::AppImage],
|
||||
@ -385,6 +405,12 @@ impl Settings {
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// add updater if needed
|
||||
if self.is_update_enabled() {
|
||||
platform_types.push(PackageType::Updater)
|
||||
}
|
||||
|
||||
if let Some(package_types) = &self.package_types {
|
||||
let mut types = vec![];
|
||||
for package_type in package_types {
|
||||
@ -535,6 +561,34 @@ impl Settings {
|
||||
pub fn windows(&self) -> &WindowsSettings {
|
||||
&self.bundle_settings.windows
|
||||
}
|
||||
|
||||
/// Is update enabled
|
||||
pub fn is_update_enabled(&self) -> bool {
|
||||
match &self.bundle_settings.updater {
|
||||
Some(val) => val.active,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is pubkey provided?
|
||||
pub fn is_updater_pubkey(&self) -> bool {
|
||||
match &self.bundle_settings.updater {
|
||||
Some(val) => val.pubkey.is_some(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get pubkey (mainly for testing)
|
||||
#[cfg(test)]
|
||||
pub fn updater_pubkey(&self) -> Option<&str> {
|
||||
self
|
||||
.bundle_settings
|
||||
.updater
|
||||
.as_ref()
|
||||
.expect("Updater is not defined")
|
||||
.pubkey
|
||||
.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the external binaries to bundle, adding the target triple suffix to each of them.
|
||||
|
234
cli/tauri-bundler/src/bundle/updater_bundle.rs
Normal file
@ -0,0 +1,234 @@
|
||||
use super::common;
|
||||
use libflate::gzip;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::macos_bundle;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use super::appimage_bundle;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use super::msi_bundle;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::fs::File;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::io::prelude::*;
|
||||
#[cfg(target_os = "windows")]
|
||||
use zip::write::FileOptions;
|
||||
|
||||
use crate::{bundle::Bundle, Settings};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs::{self},
|
||||
io::Write,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// Build update
|
||||
pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
if cfg!(unix) || cfg!(windows) || cfg!(macos) {
|
||||
// Create our archive bundle
|
||||
let bundle_result = bundle_update(settings, bundles)?;
|
||||
Ok(bundle_result)
|
||||
} else {
|
||||
common::print_info("Current platform do not support updates")?;
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
// Create simple update-macos.tar.gz
|
||||
// This is the Mac OS App packaged
|
||||
#[cfg(target_os = "macos")]
|
||||
fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
// find our .app or rebuild our bundle
|
||||
let bundle_path = match bundles
|
||||
.iter()
|
||||
.filter(|bundle| bundle.package_type == crate::PackageType::MacOSBundle)
|
||||
.find_map(|bundle| {
|
||||
bundle
|
||||
.bundle_paths
|
||||
.iter()
|
||||
.find(|path| path.extension() == Some(OsStr::new("app")))
|
||||
}) {
|
||||
Some(path) => vec![path.clone()],
|
||||
None => macos_bundle::bundle_project(settings)?,
|
||||
};
|
||||
|
||||
// we expect our .app to be on bundle_path[0]
|
||||
if bundle_path.is_empty() {
|
||||
return Err(crate::Error::UnableToFindProject);
|
||||
}
|
||||
|
||||
let source_path = &bundle_path[0];
|
||||
|
||||
// add .tar.gz to our path
|
||||
let osx_archived = format!("{}.tar.gz", source_path.display());
|
||||
let osx_archived_path = PathBuf::from(&osx_archived);
|
||||
|
||||
// safe unwrap
|
||||
//let tar_source = &source_path.parent().unwrap().to_path_buf();
|
||||
|
||||
// Create our gzip file (need to send parent)
|
||||
// as we walk the source directory (source isnt added)
|
||||
create_tar(&source_path, &osx_archived_path)
|
||||
.with_context(|| "Failed to tar.gz update directory")?;
|
||||
|
||||
common::print_bundling(format!("{:?}", &osx_archived_path).as_str())?;
|
||||
Ok(vec![osx_archived_path])
|
||||
}
|
||||
|
||||
// Create simple update-linux_<arch>.tar.gz
|
||||
// Including the AppImage
|
||||
// Right now in linux we hot replace the bin and request a restart
|
||||
// No assets are replaced
|
||||
#[cfg(target_os = "linux")]
|
||||
fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
// build our app actually we support only appimage on linux
|
||||
let bundle_path = match bundles
|
||||
.iter()
|
||||
.filter(|bundle| bundle.package_type == crate::PackageType::AppImage)
|
||||
.find_map(|bundle| {
|
||||
bundle
|
||||
.bundle_paths
|
||||
.iter()
|
||||
.find(|path| path.extension() == Some(OsStr::new("AppImage")))
|
||||
}) {
|
||||
Some(path) => vec![path.clone()],
|
||||
None => appimage_bundle::bundle_project(settings)?,
|
||||
};
|
||||
|
||||
// we expect our .app to be on bundle[0]
|
||||
if bundle_path.is_empty() {
|
||||
return Err(crate::Error::UnableToFindProject);
|
||||
}
|
||||
|
||||
let source_path = &bundle_path[0];
|
||||
|
||||
// add .tar.gz to our path
|
||||
let appimage_archived = format!("{}.tar.gz", source_path.display());
|
||||
let appimage_archived_path = PathBuf::from(&appimage_archived);
|
||||
|
||||
// Create our gzip file
|
||||
create_tar(&source_path, &appimage_archived_path)
|
||||
.with_context(|| "Failed to tar.gz update directory")?;
|
||||
|
||||
common::print_bundling(format!("{:?}", &appimage_archived_path).as_str())?;
|
||||
Ok(vec![appimage_archived_path])
|
||||
}
|
||||
|
||||
// Create simple update-win_<arch>.zip
|
||||
// Including the binary as root
|
||||
// Right now in windows we hot replace the bin and request a restart
|
||||
// No assets are replaced
|
||||
#[cfg(target_os = "windows")]
|
||||
fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
// find our .msi or rebuild
|
||||
let bundle_path = match bundles
|
||||
.iter()
|
||||
.filter(|bundle| bundle.package_type == crate::PackageType::WindowsMsi)
|
||||
.find_map(|bundle| {
|
||||
bundle
|
||||
.bundle_paths
|
||||
.iter()
|
||||
.find(|path| path.extension() == Some(OsStr::new("msi")))
|
||||
}) {
|
||||
Some(path) => vec![path.clone()],
|
||||
None => msi_bundle::bundle_project(settings)?,
|
||||
};
|
||||
|
||||
// we expect our .msi to be on bundle_path[0]
|
||||
if bundle_path.is_empty() {
|
||||
return Err(crate::Error::UnableToFindProject);
|
||||
}
|
||||
|
||||
let source_path = &bundle_path[0];
|
||||
|
||||
// add .tar.gz to our path
|
||||
let msi_archived = format!("{}.zip", source_path.display());
|
||||
let msi_archived_path = PathBuf::from(&msi_archived);
|
||||
|
||||
// Create our gzip file
|
||||
create_zip(&source_path, &msi_archived_path).with_context(|| "Failed to zip update MSI")?;
|
||||
|
||||
common::print_bundling(format!("{:?}", &msi_archived_path).as_str())?;
|
||||
Ok(vec![msi_archived_path])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn create_zip(src_file: &PathBuf, dst_file: &PathBuf) -> crate::Result<PathBuf> {
|
||||
let parent_dir = dst_file.parent().expect("No data in parent");
|
||||
fs::create_dir_all(parent_dir)?;
|
||||
let writer = common::create_file(dst_file)?;
|
||||
|
||||
let file_name = src_file
|
||||
.file_name()
|
||||
.expect("Can't extract file name from path");
|
||||
|
||||
let mut zip = zip::ZipWriter::new(writer);
|
||||
let options = FileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Stored)
|
||||
.unix_permissions(0o755);
|
||||
|
||||
zip.start_file(file_name.to_string_lossy(), options)?;
|
||||
let mut f = File::open(src_file)?;
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer)?;
|
||||
zip.write_all(&*buffer)?;
|
||||
buffer.clear();
|
||||
|
||||
Ok(dst_file.to_owned())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn create_tar(src_dir: &PathBuf, dest_path: &PathBuf) -> crate::Result<PathBuf> {
|
||||
let dest_file = common::create_file(&dest_path)?;
|
||||
let gzip_encoder = gzip::Encoder::new(dest_file)?;
|
||||
|
||||
let gzip_encoder = create_tar_from_src(src_dir, gzip_encoder)?;
|
||||
let mut dest_file = gzip_encoder.finish().into_result()?;
|
||||
dest_file.flush()?;
|
||||
Ok(dest_path.to_owned())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn create_tar_from_src<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let mut tar_builder = tar::Builder::new(dest_file);
|
||||
|
||||
// validate source type
|
||||
let file_type = fs::metadata(src_dir).expect("Can't read source directory");
|
||||
// if it's a file don't need to walkdir
|
||||
if file_type.is_file() {
|
||||
let mut src_file = fs::File::open(src_dir)?;
|
||||
let file_name = src_dir
|
||||
.file_name()
|
||||
.expect("Can't extract file name from path");
|
||||
|
||||
tar_builder.append_file(file_name, &mut src_file)?;
|
||||
} else {
|
||||
for entry in WalkDir::new(&src_dir) {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
if src_path == src_dir {
|
||||
continue;
|
||||
}
|
||||
|
||||
// todo(lemarier): better error catching
|
||||
// We add the .parent() because example if we send a path
|
||||
// /dev/src-tauri/target/debug/bundle/osx/app.app
|
||||
// We need a tar with app.app/<...> (source root folder should be included)
|
||||
let dest_path = src_path.strip_prefix(&src_dir.parent().expect(""))?;
|
||||
if entry.file_type().is_dir() {
|
||||
tar_builder.append_dir(dest_path, src_path)?;
|
||||
} else {
|
||||
let mut src_file = fs::File::open(src_path)?;
|
||||
tar_builder.append_file(dest_path, &mut src_file)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let dest_file = tar_builder.into_inner()?;
|
||||
Ok(dest_file)
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
use std::{io, num, path};
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
pub enum Error {
|
||||
@ -52,6 +51,9 @@ pub enum Error {
|
||||
ShellScriptError(String),
|
||||
#[error("`{0}`")]
|
||||
GenericError(String),
|
||||
/// No bundled project found for the updater.
|
||||
#[error("Unable to find a bundled project for the updater")]
|
||||
UnableToFindProject,
|
||||
#[error("string is not UTF-8")]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
/// Windows SignTool not found.
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const cmds = ['icon', 'deps']
|
||||
const rustCliCmds = ['dev', 'build', 'init', 'info']
|
||||
const rustCliCmds = ['dev', 'build', 'init', 'info', 'sign']
|
||||
|
||||
const cmd = process.argv[2]
|
||||
|
||||
|
@ -37,5 +37,9 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
||||
config: #config,
|
||||
assets: #assets,
|
||||
default_window_icon: #default_window_icon,
|
||||
package_info: ::tauri::api::PackageInfo {
|
||||
name: env!("CARGO_PKG_NAME"),
|
||||
version: env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
@ -179,4 +179,8 @@ main{
|
||||
|
||||
.just-around{
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
@ -57,6 +57,14 @@
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
|
||||
"endpoints": [
|
||||
"https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}"
|
||||
]
|
||||
},
|
||||
"allowlist": {
|
||||
"all": true
|
||||
},
|
||||
|
@ -12,6 +12,7 @@
|
||||
import Window from "./components/Window.svelte";
|
||||
import Shortcuts from "./components/Shortcuts.svelte";
|
||||
import Shell from "./components/Shell.svelte";
|
||||
import Updater from "./components/Updater.svelte";
|
||||
|
||||
const views = [
|
||||
{
|
||||
@ -53,7 +54,11 @@
|
||||
{
|
||||
label: "Shell",
|
||||
component: Shell,
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Updater",
|
||||
component: Updater,
|
||||
},
|
||||
];
|
||||
|
||||
let selected = views[0];
|
||||
|
46
examples/api/src/components/Updater.svelte
Normal file
@ -0,0 +1,46 @@
|
||||
<script>
|
||||
|
||||
// This example show how updater events work when dialog is disabled.
|
||||
// This allow you to use custom dialog for the updater.
|
||||
// This is your responsability to restart the application after you receive the STATUS: DONE.
|
||||
|
||||
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
||||
|
||||
export let onMessage;
|
||||
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
document.getElementById("check_update").classList.add("hidden");
|
||||
|
||||
const {shouldUpdate, manifest} = await checkUpdate();
|
||||
onMessage(`Should update: ${shouldUpdate}`);
|
||||
onMessage(manifest);
|
||||
|
||||
if (shouldUpdate) {
|
||||
document.getElementById("start_update").classList.remove("hidden");
|
||||
}
|
||||
} catch(e) {
|
||||
onMessage(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function install() {
|
||||
try {
|
||||
document.getElementById("start_update").classList.add("hidden");
|
||||
|
||||
await installUpdate();
|
||||
onMessage("Installation complete, restart required.");
|
||||
|
||||
} catch(e) {
|
||||
onMessage(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<button class="button" id="check_update" on:click={check}>Check update</button>
|
||||
<button class="button hidden" id="start_update" on:click={install}>Install update</button>
|
||||
</div>
|
@ -48,6 +48,9 @@
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,9 @@
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
examples/updater/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "updater",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"tauri": "node ../../cli/tauri.js/bin/tauri"
|
||||
}
|
||||
}
|
329
examples/updater/public/__tauri.js
Normal file
@ -0,0 +1,329 @@
|
||||
// polyfills
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function (searchString, position) {
|
||||
position = position || 0;
|
||||
return this.substr(position, searchString.length) === searchString;
|
||||
};
|
||||
}
|
||||
|
||||
(function () {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
|
||||
var uid = function () {
|
||||
return (
|
||||
s4() +
|
||||
s4() +
|
||||
"-" +
|
||||
s4() +
|
||||
"-" +
|
||||
s4() +
|
||||
"-" +
|
||||
s4() +
|
||||
"-" +
|
||||
s4() +
|
||||
s4() +
|
||||
s4()
|
||||
);
|
||||
};
|
||||
|
||||
function ownKeys(object, enumerableOnly) {
|
||||
var keys = Object.keys(object);
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(object);
|
||||
if (enumerableOnly)
|
||||
symbols = symbols.filter(function (sym) {
|
||||
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
||||
});
|
||||
keys.push.apply(keys, symbols);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function _objectSpread(target) {
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var source = arguments[i] != null ? arguments[i] : {};
|
||||
if (i % 2) {
|
||||
ownKeys(source, true).forEach(function (key) {
|
||||
_defineProperty(target, key, source[key]);
|
||||
});
|
||||
} else if (Object.getOwnPropertyDescriptors) {
|
||||
Object.defineProperties(
|
||||
target,
|
||||
Object.getOwnPropertyDescriptors(source)
|
||||
);
|
||||
} else {
|
||||
ownKeys(source).forEach(function (key) {
|
||||
Object.defineProperty(
|
||||
target,
|
||||
key,
|
||||
Object.getOwnPropertyDescriptor(source, key)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function _defineProperty(obj, key, value) {
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (!window.__TAURI__) {
|
||||
window.__TAURI__ = {};
|
||||
}
|
||||
|
||||
window.__TAURI__.transformCallback = function transformCallback(
|
||||
callback,
|
||||
once
|
||||
) {
|
||||
var identifier = uid();
|
||||
|
||||
window[identifier] = function (result) {
|
||||
if (once) {
|
||||
delete window[identifier];
|
||||
}
|
||||
|
||||
return callback && callback(result);
|
||||
};
|
||||
|
||||
return identifier;
|
||||
};
|
||||
|
||||
window.__TAURI__.invoke = function invoke(cmd, args = {}) {
|
||||
var _this = this;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
var callback = _this.transformCallback(function (r) {
|
||||
resolve(r);
|
||||
delete window[error];
|
||||
}, true);
|
||||
var error = _this.transformCallback(function (e) {
|
||||
reject(e);
|
||||
delete window[callback];
|
||||
}, true);
|
||||
|
||||
if (typeof cmd === "string") {
|
||||
args.cmd = cmd;
|
||||
} else if (typeof cmd === "object") {
|
||||
args = cmd;
|
||||
} else {
|
||||
return reject(new Error("Invalid argument type."));
|
||||
}
|
||||
|
||||
if (window.rpc) {
|
||||
window.rpc.notify(
|
||||
cmd,
|
||||
_objectSpread(
|
||||
{
|
||||
callback: callback,
|
||||
error: error,
|
||||
},
|
||||
args
|
||||
)
|
||||
);
|
||||
} else {
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
window.rpc.notify(
|
||||
cmd,
|
||||
_objectSpread(
|
||||
{
|
||||
callback: callback,
|
||||
error: error,
|
||||
},
|
||||
args
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// open <a href="..."> links with the Tauri API
|
||||
function __openLinks() {
|
||||
document.querySelector("body").addEventListener(
|
||||
"click",
|
||||
function (e) {
|
||||
var target = e.target;
|
||||
while (target != null) {
|
||||
if (
|
||||
target.matches ? target.matches("a") : target.msMatchesSelector("a")
|
||||
) {
|
||||
if (
|
||||
target.href &&
|
||||
target.href.startsWith("http") &&
|
||||
target.target === "_blank"
|
||||
) {
|
||||
window.__TAURI__.invoke('tauri', {
|
||||
__tauriModule: "Shell",
|
||||
message: {
|
||||
cmd: "open",
|
||||
uri: target.href,
|
||||
},
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
target = target.parentElement;
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
__openLinks();
|
||||
} else {
|
||||
window.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
function () {
|
||||
__openLinks();
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
window.__TAURI__.invoke('tauri', {
|
||||
__tauriModule: "Event",
|
||||
message: {
|
||||
cmd: "listen",
|
||||
event: "tauri://window-created",
|
||||
handler: window.__TAURI__.transformCallback(function (event) {
|
||||
if (event.payload) {
|
||||
var windowLabel = event.payload.label;
|
||||
window.__TAURI__.__windows.push({ label: windowLabel });
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
let permissionSettable = false;
|
||||
let permissionValue = "default";
|
||||
|
||||
function isPermissionGranted() {
|
||||
if (window.Notification.permission !== "default") {
|
||||
return Promise.resolve(window.Notification.permission === "granted");
|
||||
}
|
||||
return window.__TAURI__.invoke('tauri', {
|
||||
__tauriModule: "Notification",
|
||||
message: {
|
||||
cmd: "isNotificationPermissionGranted",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setNotificationPermission(value) {
|
||||
permissionSettable = true;
|
||||
window.Notification.permission = value;
|
||||
permissionSettable = false;
|
||||
}
|
||||
|
||||
function requestPermission() {
|
||||
return window.__TAURI__
|
||||
.invoke('tauri', {
|
||||
__tauriModule: "Notification",
|
||||
mainThread: true,
|
||||
message: {
|
||||
cmd: "requestNotificationPermission",
|
||||
},
|
||||
})
|
||||
.then(function (permission) {
|
||||
setNotificationPermission(permission);
|
||||
return permission;
|
||||
});
|
||||
}
|
||||
|
||||
function sendNotification(options) {
|
||||
if (typeof options === "object") {
|
||||
Object.freeze(options);
|
||||
}
|
||||
|
||||
isPermissionGranted().then(function (permission) {
|
||||
if (permission) {
|
||||
return window.__TAURI__.invoke('tauri', {
|
||||
__tauriModule: "Notification",
|
||||
message: {
|
||||
cmd: "notification",
|
||||
options:
|
||||
typeof options === "string"
|
||||
? {
|
||||
title: options,
|
||||
}
|
||||
: options,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.Notification = function (title, options) {
|
||||
var opts = options || {};
|
||||
sendNotification(
|
||||
Object.assign(opts, {
|
||||
title: title,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
window.Notification.requestPermission = requestPermission;
|
||||
|
||||
Object.defineProperty(window.Notification, "permission", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return permissionValue;
|
||||
},
|
||||
set: function (v) {
|
||||
if (!permissionSettable) {
|
||||
throw new Error("Readonly property");
|
||||
}
|
||||
permissionValue = v;
|
||||
},
|
||||
});
|
||||
|
||||
isPermissionGranted().then(function (response) {
|
||||
if (response === null) {
|
||||
setNotificationPermission("default");
|
||||
} else {
|
||||
setNotificationPermission(response ? "granted" : "denied");
|
||||
}
|
||||
});
|
||||
|
||||
window.alert = function (message) {
|
||||
window.__TAURI__.invoke('tauri', {
|
||||
__tauriModule: "Dialog",
|
||||
mainThread: true,
|
||||
message: {
|
||||
cmd: "messageDialog",
|
||||
message: message,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
window.confirm = function (message) {
|
||||
return window.__TAURI__.invoke('tauri', {
|
||||
__tauriModule: "Dialog",
|
||||
mainThread: true,
|
||||
message: {
|
||||
cmd: "askDialog",
|
||||
message: message,
|
||||
},
|
||||
});
|
||||
};
|
||||
})();
|
12
examples/updater/public/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to Tauri!</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to Tauri!</h1>
|
||||
</body>
|
||||
</html>
|
10
examples/updater/src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
WixTools
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
config.json
|
||||
bundle.json
|
3791
examples/updater/src-tauri/Cargo.lock
generated
Normal file
24
examples/updater/src-tauri/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "updater-example"
|
||||
version = "0.1.0"
|
||||
description = "A very simple Tauri Appplication"
|
||||
edition = "2018"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { path = "../../../core/tauri-build", features = [ "codegen" ]}
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
tauri = { path = "../../../tauri", features =["api-all"]}
|
||||
|
||||
[target."cfg(windows)".build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[features]
|
||||
default = [ "custom-protocol" ]
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
|
||||
[[bin]]
|
||||
name = "updater-example"
|
||||
path = "src/main.rs"
|
3
examples/updater/src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
BIN
examples/updater/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
examples/updater/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
examples/updater/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
examples/updater/src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
examples/updater/src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
examples/updater/src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
examples/updater/src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
examples/updater/src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
examples/updater/src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
examples/updater/src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
examples/updater/src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
examples/updater/src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
examples/updater/src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
examples/updater/src-tauri/icons/icon.icns
Normal file
BIN
examples/updater/src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
examples/updater/src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 88 KiB |
17
examples/updater/src-tauri/src/main.rs
Normal file
@ -0,0 +1,17 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
#[tauri::command]
|
||||
fn my_custom_command(argument: String) {
|
||||
println!("{}", argument);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::AppBuilder::default()
|
||||
.invoke_handler(tauri::generate_handler![my_custom_command])
|
||||
.build(tauri::generate_context!())
|
||||
.run()
|
||||
.expect("error while running tauri application");
|
||||
}
|
61
examples/updater/src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"build": {
|
||||
"distDir": "../public",
|
||||
"devPath": "../public",
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "com.tauri.dev",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "",
|
||||
"longDescription": "",
|
||||
"deb": {
|
||||
"depends": [],
|
||||
"useBootstrapper": false
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"useBootstrapper": false,
|
||||
"exceptionDomain": ""
|
||||
}
|
||||
},
|
||||
"allowlist": {
|
||||
"all": true
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Welcome to Tauri!",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
|
||||
"endpoints": [
|
||||
"https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
4
examples/updater/yarn.lock
Normal file
@ -0,0 +1,4 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
@ -21,6 +21,7 @@ serde_repr = "0.1"
|
||||
dirs-next = "2.0.0"
|
||||
zip = "0.5.11"
|
||||
semver = "0.11"
|
||||
ignore = "^0.4.16"
|
||||
tempfile = "3"
|
||||
either = "1.6.1"
|
||||
tar = "0.4"
|
||||
|
@ -7,6 +7,9 @@ pub enum Error {
|
||||
/// The path operation error.
|
||||
#[error("Path Error: {0}")]
|
||||
Path(String),
|
||||
/// The path StripPrefixError error.
|
||||
#[error("Path Error: {0}")]
|
||||
PathPrefix(#[from] std::path::StripPrefixError),
|
||||
/// Error showing the dialog.
|
||||
#[error("Dialog Error: {0}")]
|
||||
Dialog(String),
|
||||
@ -37,6 +40,9 @@ pub enum Error {
|
||||
/// IO error.
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Ignore error.
|
||||
#[error("failed to walkdir: {0}")]
|
||||
Ignore(#[from] ignore::Error),
|
||||
/// ZIP error.
|
||||
#[error("{0}")]
|
||||
Zip(#[from] zip::result::ZipError),
|
||||
|
@ -1,3 +1,4 @@
|
||||
use ignore::WalkBuilder;
|
||||
use std::{fs, path};
|
||||
|
||||
/// Moves a file from the given path to the specified destination.
|
||||
@ -33,16 +34,13 @@ impl<'a> Move<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Move source file to specified destination
|
||||
/// Move source file to specified destination (replace whole directory)
|
||||
pub fn to_dest(&self, dest: &path::Path) -> crate::Result<()> {
|
||||
match self.temp {
|
||||
None => {
|
||||
fs::rename(self.source, dest)?;
|
||||
}
|
||||
Some(temp) => {
|
||||
println!("dest {}", dest.to_str().unwrap());
|
||||
println!("temp {}", temp.to_str().unwrap());
|
||||
println!("source {}", self.source.to_str().unwrap());
|
||||
if dest.exists() {
|
||||
fs::rename(dest, temp)?;
|
||||
if let Err(e) = fs::rename(self.source, dest) {
|
||||
@ -56,4 +54,57 @@ impl<'a> Move<'a> {
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk in the source and copy all files and create directories if needed by
|
||||
/// replacing existing elements. (equivalent to a cp -R)
|
||||
pub fn walk_to_dest(&self, dest: &path::Path) -> crate::Result<()> {
|
||||
match self.temp {
|
||||
None => {
|
||||
// got no temp -- no need to backup
|
||||
walkdir_and_copy(self.source, dest)?;
|
||||
}
|
||||
Some(temp) => {
|
||||
if dest.exists() {
|
||||
// we got temp and our dest exist, lets make a backup
|
||||
// of current files
|
||||
walkdir_and_copy(dest, temp)?;
|
||||
|
||||
if let Err(e) = walkdir_and_copy(self.source, dest) {
|
||||
// if we got something wrong we reset the dest with our backup
|
||||
fs::rename(temp, dest)?;
|
||||
return Err(e);
|
||||
}
|
||||
} else {
|
||||
// got temp but dest didnt exist
|
||||
walkdir_and_copy(self.source, dest)?;
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Walk into the source and create directories, and copy files
|
||||
// Overwriting existing items but keeping untouched the files in the dest
|
||||
// not provided in the source.
|
||||
fn walkdir_and_copy(source: &path::Path, dest: &path::Path) -> crate::Result<()> {
|
||||
let walkdir = WalkBuilder::new(source).hidden(false).build();
|
||||
|
||||
for entry in walkdir {
|
||||
// Check if it's a file
|
||||
|
||||
let element = entry?;
|
||||
let metadata = element.metadata()?;
|
||||
let destination = dest.join(element.path().strip_prefix(&source)?);
|
||||
|
||||
// we make sure it's a directory and destination doesnt exist
|
||||
if metadata.is_dir() && !&destination.exists() {
|
||||
fs::create_dir_all(&destination)?;
|
||||
}
|
||||
|
||||
// we make sure it's a file
|
||||
if metadata.is_file() {
|
||||
fs::copy(element.path(), destination)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -49,6 +49,15 @@ pub use error::Error;
|
||||
/// Tauri API result type.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// `App` package information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PackageInfo {
|
||||
/// App name.
|
||||
pub name: &'static str,
|
||||
/// App version.
|
||||
pub version: &'static str,
|
||||
}
|
||||
|
||||
// Not public API
|
||||
#[doc(hidden)]
|
||||
pub mod private {
|
||||
@ -85,5 +94,6 @@ pub mod private {
|
||||
fn config() -> &'static crate::config::Config;
|
||||
fn assets() -> &'static crate::assets::EmbeddedAssets;
|
||||
fn default_window_icon() -> Option<&'static [u8]>;
|
||||
fn package_info() -> crate::PackageInfo;
|
||||
}
|
||||
}
|
||||
|
@ -47,3 +47,8 @@ pub fn is_patch(current: &str, other: &str) -> crate::Result<bool> {
|
||||
let other = Version::parse(other)?;
|
||||
Ok(current.major == other.major && current.minor == other.minor && other.patch > current.patch)
|
||||
}
|
||||
|
||||
/// Check if a version is greater than the current
|
||||
pub fn is_greater(current: &str, other: &str) -> crate::Result<bool> {
|
||||
Ok(Version::parse(other)? > Version::parse(current)?)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "tauri-updater"
|
||||
version = "0.4.2"
|
||||
authors = ["Lucas Fernandes Gonçalves Nogueira <lucas@tauri.studio>", "Daniel Thompson-Yvetot <denjell@sfosc.org>", "Tensor Programming <tensordeveloper@gmail.com>"]
|
||||
authors = ["Lucas Fernandes Gonçalves Nogueira <lucas@tauri.studio>", "Daniel Thompson-Yvetot <denjell@sfosc.org>", "Tensor Programming <tensordeveloper@gmail.com>", "David Lemarier <david@lemarier.ca>"]
|
||||
license = "MIT"
|
||||
homepage = "https://tauri.studio"
|
||||
repository = "https://github.com/tauri-apps/tauri"
|
||||
@ -10,13 +10,21 @@ edition = "2018"
|
||||
exclude = ["test/fixture/**"]
|
||||
|
||||
[dependencies]
|
||||
attohttpc = {version = "0.10.1", features=["json", "compress" ]}
|
||||
# pbr = "1"
|
||||
serde_json = "1.0"
|
||||
tempfile = "3"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = "1.0"
|
||||
zip = "0.5.3"
|
||||
tempdir = "0.3"
|
||||
tauri-api = { version = "0.5", path = "../tauri-api" }
|
||||
tauri-api = { version = "0.7.2", path = "../tauri-api" }
|
||||
tauri-utils = { version = "0.5", path = "../tauri-utils" }
|
||||
anyhow = "1.0.31"
|
||||
thiserror = "1.0.19"
|
||||
semver = "0.11"
|
||||
minisign-verify = "0.1.8"
|
||||
base64 = "0.13.0"
|
||||
# error handling
|
||||
thiserror = "1.0.24"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "*"
|
||||
mockito = "0.29"
|
||||
|
||||
[features]
|
||||
default = ["reqwest/default-tls"]
|
||||
|
328
tauri-updater/README.md
Normal file
@ -0,0 +1,328 @@
|
||||
# Tauri Updater
|
||||
---
|
||||
> ⚠️ This project is a working project. Expect breaking changes.
|
||||
---
|
||||
|
||||
The updater is focused on making Tauri's application updates **as safe and transparent as updates to a website**.
|
||||
|
||||
Instead of publishing a feed of versions from which your app must select, Tauri updates to the version your server tells it to. This allows you to intelligently update your clients based on the request you give to Tauri.
|
||||
|
||||
The server can remotely drive behaviors like rolling back or phased rollouts.
|
||||
|
||||
The update JSON Tauri requests should be dynamically generated based on criteria in the request, and whether an update is required.
|
||||
|
||||
Tauri's installer is also designed to be fault-tolerant, and ensure that any updates installed are valid and safe.
|
||||
|
||||
# Configuration
|
||||
|
||||
Once you have your Tauri project ready, you need to configure the updater.
|
||||
|
||||
Add this in tauri.conf.json
|
||||
```json
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://releases.myapp.com/{target}}/{current_version}}"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": ""
|
||||
}
|
||||
```
|
||||
|
||||
The required keys are "active" and "endpoints", others are optional.
|
||||
|
||||
"active" must be a boolean. By default, it's set to false.
|
||||
|
||||
"endpoints" must be an array. The string `{{target}}` and `{{current_version}}` are automatically replaced in the URL allowing you determine [server-side](#update-server-json-format) if an update is available. If multiple endpoints are specified, the updater will fallback if a server is not responding within the pre-defined timeout.
|
||||
|
||||
"dialog" if present must be a boolean. By default, it's set to true. If enabled, [events](#events) are turned-off as the updater will handle everything. If you need the custom events, you MUST turn off the built-in dialog.
|
||||
|
||||
"pubkey" if present must be a valid public-key generated with Tauri cli. See [Signing updates](#signing-updates).
|
||||
|
||||
## Update Requests
|
||||
|
||||
Tauri is indifferent to the request the client application provides for update checking.
|
||||
|
||||
`Accept: application/json` is added to the request headers because Tauri is responsible for parsing the response.
|
||||
|
||||
For the requirements imposed on the responses and the body format of an update, response see [Server Support](#server-support).
|
||||
|
||||
Your update request must *at least* include a version identifier so that the server can determine whether an update for this specific version is required.
|
||||
|
||||
It may also include other identifying criteria such as operating system version, to allow the server to deliver as fine-grained an update as you would like.
|
||||
|
||||
How you include the version identifier or other criteria is specific to the server that you are requesting updates from. A common approach is to use query parameters, [Configuration](#configuration) shows an example of this.
|
||||
|
||||
## Built-in dialog
|
||||
|
||||
By default, updater uses a built-in dialog API from Tauri.
|
||||
|
||||
![New Update](https://i.imgur.com/UMilB5A.png)
|
||||
|
||||
The dialog release notes is represented by the update `note` provided by the [server](#server-support).
|
||||
|
||||
If the user accepts, the download and install are initialized. The user will be then prompted to restart the application.
|
||||
|
||||
## Javascript API
|
||||
|
||||
**Attention, you need to _disable built-in dialog_ in your [tauri configuration](#configuration), otherwise, events aren't emitted and the javascript API will NOT work.**
|
||||
|
||||
|
||||
```
|
||||
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
||||
|
||||
try {
|
||||
const {shouldUpdate, manifest} = await checkUpdate();
|
||||
|
||||
if (shouldUpdate) {
|
||||
// display dialog
|
||||
await installUpdate();
|
||||
// install complete, ask to restart
|
||||
}
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
**Attention, you need to _disable built-in dialog_ in your [tauri configuration](#configuration), otherwise, events aren't emitted.**
|
||||
|
||||
To know when an update is ready to be installed, you can subscribe to these events:
|
||||
|
||||
### Initialize updater and check if a new version is available
|
||||
|
||||
#### If a new version is available, the event `tauri://update-available` is emitted.
|
||||
|
||||
Event : `tauri://update`
|
||||
|
||||
### Rust
|
||||
```rust
|
||||
dispatcher.emit("tauri://update", None);
|
||||
```
|
||||
|
||||
### Javascript
|
||||
```js
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
emit("tauri://update");
|
||||
```
|
||||
|
||||
### Listen New Update Available
|
||||
|
||||
Event : `tauri://update-available`
|
||||
|
||||
Emitted data:
|
||||
```
|
||||
version Version announced by the server
|
||||
date Date announced by the server
|
||||
body Note announced by the server
|
||||
```
|
||||
|
||||
### Rust
|
||||
```rust
|
||||
dispatcher.listen("tauri://update-available", move |msg| {
|
||||
println("New version available: {:?}", msg);
|
||||
})
|
||||
```
|
||||
|
||||
### Javascript
|
||||
```js
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
listen("tauri://update-available", function (res) {
|
||||
console.log("New version available: ", res);
|
||||
});
|
||||
```
|
||||
|
||||
### Emit Install and Download
|
||||
|
||||
You need to emit this event to initialize the download and listen to the [install progress](#listen-install-progress).
|
||||
|
||||
Event : `tauri://update-install`
|
||||
|
||||
### Rust
|
||||
```rust
|
||||
dispatcher.emit("tauri://update-install", None);
|
||||
```
|
||||
|
||||
### Javascript
|
||||
```js
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
emit("tauri://update-install");
|
||||
```
|
||||
|
||||
### Listen Install Progress
|
||||
|
||||
Event : `tauri://update-status`
|
||||
|
||||
Emitted data:
|
||||
```
|
||||
status [ERROR/PENDING/DONE]
|
||||
error String/null
|
||||
```
|
||||
|
||||
PENDING is emitted when the download is started and DONE when the install is complete. You can then ask to restart the application.
|
||||
|
||||
ERROR is emitted when there is an error with the updater. We suggest to listen to this event even if the dialog is enabled.
|
||||
|
||||
### Rust
|
||||
```rust
|
||||
dispatcher.listen("tauri://update-status", move |msg| {
|
||||
println("New status: {:?}", msg);
|
||||
})
|
||||
```
|
||||
|
||||
### Javascript
|
||||
```js
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
listen("tauri://update-status", function (res) {
|
||||
console.log("New status: ", res);
|
||||
});
|
||||
```
|
||||
|
||||
# Server Support
|
||||
|
||||
Your server should determine whether an update is required based on the [Update Request](#update-requests) your client issues.
|
||||
|
||||
If an update is required your server should respond with a status code of [200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the [update JSON](#update-server-json-format) in the body. To save redundantly downloading the same version multiple times your server must not inform the client to update.
|
||||
|
||||
If no update is required your server must respond with a status code of [204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5).
|
||||
|
||||
## Update Server JSON Format
|
||||
|
||||
When an update is available, Tauri expects the following schema in response to the update request provided:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://mycompany.example.com/myapp/releases/myrelease.tar.gz",
|
||||
"version": "0.0.1",
|
||||
"notes": "Theses are some release notes",
|
||||
"pub_date": "2020-09-18T12:29:53+01:00",
|
||||
"signature": ""
|
||||
}
|
||||
```
|
||||
|
||||
The only required keys are "url" and "version", the others are optional.
|
||||
|
||||
"pub_date" if present must be formatted according to ISO 8601.
|
||||
|
||||
"signature" if present must be a valid signature generated with Tauri cli. See [Signing updates](#signing-updates).
|
||||
|
||||
## Update File JSON Format
|
||||
|
||||
The alternate update technique uses a plain JSON file meaning you can store your update metadata on S3, gist, or another static file store. Tauri will check against the name/version field and if the version is smaller than the current one and the platform is available, the update will be triggered. The format of this file is detailed below:
|
||||
|
||||
```json
|
||||
{
|
||||
"name":"v1.0.0",
|
||||
"notes":"Test version",
|
||||
"pub_date":"2020-06-22T19:25:57Z",
|
||||
"platforms": {
|
||||
"darwin": {
|
||||
"signature":"",
|
||||
"url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.app.tar.gz"
|
||||
},
|
||||
"linux": {
|
||||
"signature":"",
|
||||
"url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz"
|
||||
},
|
||||
"win64": {
|
||||
"signature":"",
|
||||
"url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Bundler (Artifacts)
|
||||
|
||||
The Tauri bundler will automatically generate update artifacts if the updater is enabled in `tauri.conf.json`
|
||||
|
||||
If the bundler can locate your private and pubkey, your update artifacts will be automatically signed.
|
||||
|
||||
The signature can be found in the `sig` file. The signature can be uploaded to GitHub safely or made public as long as your private key is secure.
|
||||
|
||||
You can see how it's [bundled with the CI](https://github.com/tauri-apps/tauri/blob/feature/new_updater/.github/workflows/artifacts-updater.yml#L44) and a [sample tauri.conf.json](https://github.com/tauri-apps/tauri/blob/feature/new_updater/examples/updater/src-tauri/tauri.conf.json#L52)
|
||||
|
||||
## macOS
|
||||
|
||||
On MACOS we create a .tar.gz from the whole application. (.app)
|
||||
|
||||
```
|
||||
target/release/bundle
|
||||
└── osx
|
||||
└── app.app
|
||||
└── app.app.tar.gz (update bundle)
|
||||
└── app.app.tar.gz.sig (if signature enabled)
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
On Windows we create a .zip from the MSI, when downloaded and validated, we run the MSI install.
|
||||
|
||||
```
|
||||
target/release
|
||||
└── app.x64.msi
|
||||
└── app.x64.msi.zip (update bundle)
|
||||
└── app.x64.msi.zip.sig (if signature enabled)
|
||||
```
|
||||
|
||||
## Linux
|
||||
|
||||
On Linux, we create a .tar.gz from the AppImage.
|
||||
|
||||
```
|
||||
target/release/bundle
|
||||
└── appimage
|
||||
└── app.AppImage
|
||||
└── app.AppImage.tar.gz (update bundle)
|
||||
└── app.AppImage.tar.gz.sig (if signature enabled)
|
||||
```
|
||||
|
||||
# Signing updates
|
||||
|
||||
We offer a built-in signature to ensure your update is safe to be installed.
|
||||
|
||||
To sign your updates, you need two things.
|
||||
|
||||
The *Public-key* (pubkey) should be added inside your `tauri.conf.json` to validate the update archive before installing.
|
||||
|
||||
The *Private key* (privkey) is used to sign your update and should NEVER be shared with anyone. Also, if you lost this key, you'll NOT be able to publish a new update to the current user base (if pubkey is set in tauri.conf.json). It's important to save it at a safe place and you can always access it.
|
||||
|
||||
To generate your keys you need to use the Tauri cli.
|
||||
|
||||
```bash
|
||||
tauri sign -g -w ~/.tauri/myapp.key
|
||||
```
|
||||
|
||||
You have multiple options available
|
||||
```bash
|
||||
Tauri updates signer.
|
||||
|
||||
USAGE:
|
||||
tauri sign [FLAGS] [OPTIONS]
|
||||
|
||||
FLAGS:
|
||||
--force Overwrite private key even if it exists on the specified path
|
||||
-g, --generate Generate keypair to sign files
|
||||
-h, --help Prints help information
|
||||
--no-password Set empty password for your private key
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-p, --password <password> Set private key password when signing
|
||||
-k, --private-key <private-key> Load the private key from a string
|
||||
-f, --private-key-path <private-key-path> Load the private key from a file
|
||||
--sign-file <sign-file> Sign the specified file
|
||||
-w, --write-keys <write-keys> Write private key to a file
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
Environment variables used to sign with `tauri-bundler`:
|
||||
If they are set, and `tauri.conf.json` expose the public key, the bundler will automatically generate and sign the updater artifacts.
|
||||
|
||||
`TAURI_PRIVATE_KEY` Path or String of your private key
|
||||
|
||||
`TAURI_KEY_PASSWORD` Your private key password (optional)
|
||||
|
50
tauri-updater/src/error.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
pub enum Error {
|
||||
/// IO Errors.
|
||||
#[error("`{0}`")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Reqwest Errors.
|
||||
#[error("Request error: {0}")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
/// Semver Errors.
|
||||
#[error("Unable to compare version: {0}")]
|
||||
Semver(#[from] semver::SemVerError),
|
||||
/// JSON (Serde) Errors.
|
||||
#[error("JSON error: {0}")]
|
||||
SerdeJson(#[from] serde_json::Error),
|
||||
/// Minisign is used for signature validation.
|
||||
#[error("Verify signature error: {0}")]
|
||||
Minisign(#[from] minisign_verify::Error),
|
||||
/// Error with Minisign base64 decoding.
|
||||
#[error("Signature decoding error: {0}")]
|
||||
Base64(#[from] base64::DecodeError),
|
||||
/// UTF8 Errors in signature.
|
||||
#[error("Signature encoding error: {0}")]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
/// Tauri utils, mainly extract and file move.
|
||||
#[error("Tauri API error: {0}")]
|
||||
TauriApi(#[from] tauri_api::Error),
|
||||
/// Network error.
|
||||
#[error("Network error: {0}")]
|
||||
Network(String),
|
||||
/// Metadata (JSON) error.
|
||||
#[error("Remote JSON error: {0}")]
|
||||
RemoteMetadata(String),
|
||||
/// Error building updater.
|
||||
#[error("Unable to prepare the updater: {0}")]
|
||||
Builder(String),
|
||||
/// Updater is not supported for current operating system or platform.
|
||||
#[error("Unsuported operating system or platform")]
|
||||
UnsupportedPlatform,
|
||||
/// Public key found in `tauri.conf.json` but no signature announced remotely.
|
||||
#[error("Signature not available but public key provided, skipping update")]
|
||||
PubkeyButNoSignature,
|
||||
/// Triggered when there is NO error and the two versions are equals.
|
||||
/// On client side, it's important to catch this error.
|
||||
#[error("No updates available")]
|
||||
UpToDate,
|
||||
}
|
||||
|
||||
pub type Result<T = ()> = std::result::Result<T, Error>;
|
@ -1,32 +0,0 @@
|
||||
use attohttpc;
|
||||
use serde::Serialize;
|
||||
|
||||
use std::io::{BufWriter, Write};
|
||||
|
||||
pub(crate) mod link_value;
|
||||
|
||||
pub fn get(url: String) -> crate::Result<attohttpc::Response> {
|
||||
let response = attohttpc::get(url).send()?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn post_as_json<T: Serialize>(url: String, payload: &T) -> crate::Result<attohttpc::Response> {
|
||||
let response = attohttpc::post(url).json(payload)?.send()?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn download<T: Write>(url: String, dest: T, _display_progress: bool) -> crate::Result<()> {
|
||||
set_ssl_vars!();
|
||||
|
||||
let resp = get(url)?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(crate::Error::Download(resp.status()).into());
|
||||
}
|
||||
|
||||
let file = BufWriter::new(dest);
|
||||
resp.write_to(file)?;
|
||||
Ok(())
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct LinkValue {
|
||||
/// Target IRI: `link-value`.
|
||||
link: Cow<'static, str>,
|
||||
|
||||
/// Forward Relation Types: `rel`.
|
||||
rel: Option<Vec<RelationType>>,
|
||||
}
|
||||
|
||||
impl LinkValue {
|
||||
pub fn new<T>(uri: T) -> LinkValue
|
||||
where
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
LinkValue {
|
||||
link: uri.into(),
|
||||
rel: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rel(&self) -> Option<&[RelationType]> {
|
||||
self.rel.as_ref().map(AsRef::as_ref)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum RelationType {
|
||||
/// next.
|
||||
Next,
|
||||
/// ext-rel-type.
|
||||
#[allow(dead_code)]
|
||||
ExtRelType(String),
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/// Helper to `print!` and immediately `flush` `stdout`
|
||||
macro_rules! print_flush {
|
||||
($literal:expr) => {
|
||||
print!($literal);
|
||||
::std::io::Write::flush(&mut ::std::io::stdout())?;
|
||||
};
|
||||
($literal:expr, $($arg:expr),*) => {
|
||||
print!($literal, $($arg),*);
|
||||
::std::io::Write::flush(&mut ::std::io::stdout())?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set ssl cert env. vars to make sure openssl can find required files
|
||||
macro_rules! set_ssl_vars {
|
||||
() => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if ::std::env::var_os("SSL_CERT_FILE").is_none() {
|
||||
::std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
|
||||
}
|
||||
if ::std::env::var_os("SSL_CERT_DIR").is_none() {
|
||||
::std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::http;
|
||||
|
||||
use tauri_api::file::{Extract, Move};
|
||||
|
||||
pub mod github;
|
||||
|
||||
/// Status returned after updating
|
||||
///
|
||||
/// Wrapped `String`s are version tags
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Status {
|
||||
UpToDate(String),
|
||||
Updated(String),
|
||||
}
|
||||
impl Status {
|
||||
/// Return the version tag
|
||||
pub fn version(&self) -> &str {
|
||||
use Status::*;
|
||||
match *self {
|
||||
UpToDate(ref s) => s,
|
||||
Updated(ref s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `Status::UpToDate`
|
||||
pub fn uptodate(&self) -> bool {
|
||||
match *self {
|
||||
Status::UpToDate(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `Status::Updated`
|
||||
pub fn updated(&self) -> bool {
|
||||
match *self {
|
||||
Status::Updated(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Release {
|
||||
pub version: String,
|
||||
pub asset_name: String,
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateBuilder {
|
||||
release: Option<Release>,
|
||||
bin_name: Option<String>,
|
||||
bin_install_path: Option<PathBuf>,
|
||||
bin_path_in_archive: Option<PathBuf>,
|
||||
show_download_progress: bool,
|
||||
show_output: bool,
|
||||
current_version: Option<String>,
|
||||
}
|
||||
impl UpdateBuilder {
|
||||
/// Initialize a new builder, defaulting the `bin_install_path` to the current
|
||||
/// executable's path
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Io - Determining current exe path
|
||||
pub fn new() -> crate::Result<UpdateBuilder> {
|
||||
Ok(Self {
|
||||
release: None,
|
||||
bin_name: None,
|
||||
bin_install_path: Some(env::current_exe()?),
|
||||
bin_path_in_archive: None,
|
||||
show_download_progress: false,
|
||||
show_output: true,
|
||||
current_version: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn release(&mut self, release: Release) -> &mut Self {
|
||||
self.release = Some(release);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the current app version, used to compare against the latest available version.
|
||||
/// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml`
|
||||
pub fn current_version(&mut self, ver: &str) -> &mut Self {
|
||||
self.current_version = Some(ver.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the exe's name. Also sets `bin_path_in_archive` if it hasn't already been set.
|
||||
pub fn bin_name(&mut self, name: &str) -> &mut Self {
|
||||
self.bin_name = Some(name.to_owned());
|
||||
if self.bin_path_in_archive.is_none() {
|
||||
self.bin_path_in_archive = Some(PathBuf::from(name));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the installation path for the new exe, defaults to the current
|
||||
/// executable's path
|
||||
pub fn bin_install_path(&mut self, bin_install_path: &str) -> &mut Self {
|
||||
self.bin_install_path = Some(PathBuf::from(bin_install_path));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path of the exe inside the release tarball. This is the location
|
||||
/// of the executable relative to the base of the tar'd directory and is the
|
||||
/// path that will be copied to the `bin_install_path`. If not specified, this
|
||||
/// will default to the value of `bin_name`. This only needs to be specified if
|
||||
/// the path to the binary (from the root of the tarball) is not equal to just
|
||||
/// the `bin_name`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// For a tarball `myapp.tar.gz` with the contents:
|
||||
///
|
||||
/// ```shell
|
||||
/// myapp.tar/
|
||||
/// |------- bin/
|
||||
/// | |--- myapp # <-- executable
|
||||
/// ```
|
||||
///
|
||||
/// The path provided should be:
|
||||
///
|
||||
/// ```
|
||||
/// # use tauri_updater::updater::Update;
|
||||
/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// Update::configure()?
|
||||
/// .bin_path_in_archive("bin/myapp")
|
||||
/// # .build()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self {
|
||||
self.bin_path_in_archive = Some(PathBuf::from(bin_path));
|
||||
self
|
||||
}
|
||||
|
||||
/// Toggle download progress bar, defaults to `off`.
|
||||
pub fn show_download_progress(&mut self, show: bool) -> &mut Self {
|
||||
self.show_download_progress = show;
|
||||
self
|
||||
}
|
||||
|
||||
/// Toggle update output information, defaults to `true`.
|
||||
pub fn show_output(&mut self, show: bool) -> &mut Self {
|
||||
self.show_output = show;
|
||||
self
|
||||
}
|
||||
|
||||
/// Confirm config and create a ready-to-use `Update`
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Config - Invalid `Update` configuration
|
||||
pub fn build(&self) -> crate::Result<Update> {
|
||||
Ok(Update {
|
||||
release: if let Some(ref release) = self.release {
|
||||
release.to_owned()
|
||||
} else {
|
||||
return Err(crate::Error::Config("`release`".into()).into());
|
||||
},
|
||||
bin_name: if let Some(ref name) = self.bin_name {
|
||||
name.to_owned()
|
||||
} else {
|
||||
return Err(crate::Error::Config("`bin_name`".into()).into());
|
||||
},
|
||||
bin_install_path: if let Some(ref path) = self.bin_install_path {
|
||||
path.to_owned()
|
||||
} else {
|
||||
return Err(crate::Error::Config("`bin_install_path`".into()).into());
|
||||
},
|
||||
bin_path_in_archive: if let Some(ref path) = self.bin_path_in_archive {
|
||||
path.to_owned()
|
||||
} else {
|
||||
return Err(crate::Error::Config("`bin_path_in_archive`".into()).into());
|
||||
},
|
||||
current_version: if let Some(ref ver) = self.current_version {
|
||||
ver.to_owned()
|
||||
} else {
|
||||
return Err(crate::Error::Config("`current_version`".into()).into());
|
||||
},
|
||||
show_download_progress: self.show_download_progress,
|
||||
show_output: self.show_output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates to a specified or latest release distributed
|
||||
#[derive(Debug)]
|
||||
pub struct Update {
|
||||
release: Release,
|
||||
current_version: String,
|
||||
bin_name: String,
|
||||
bin_install_path: PathBuf,
|
||||
bin_path_in_archive: PathBuf,
|
||||
show_download_progress: bool,
|
||||
show_output: bool,
|
||||
}
|
||||
impl Update {
|
||||
/// Initialize a new `Update` builder
|
||||
pub fn configure() -> crate::Result<UpdateBuilder> {
|
||||
UpdateBuilder::new()
|
||||
}
|
||||
|
||||
fn print_flush(&self, msg: &str) -> crate::Result<()> {
|
||||
if self.show_output {
|
||||
print_flush!("{}", msg);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn println(&self, msg: &str) {
|
||||
if self.show_output {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(self) -> crate::Result<Status> {
|
||||
self.println(&format!(
|
||||
"Checking current version... v{}",
|
||||
self.current_version
|
||||
));
|
||||
|
||||
if self.show_output {
|
||||
println!("\n{} release status:", self.bin_name);
|
||||
println!(" * Current exe: {:?}", self.bin_install_path);
|
||||
println!(" * New exe download url: {:?}", self.release.download_url);
|
||||
println!(
|
||||
"\nThe new release will be downloaded/extracted and the existing binary will be replaced."
|
||||
);
|
||||
}
|
||||
|
||||
let tmp_dir_parent = self
|
||||
.bin_install_path
|
||||
.parent()
|
||||
.ok_or_else(|| crate::Error::Updater)?;
|
||||
let tmp_dir =
|
||||
tempdir::TempDir::new_in(&tmp_dir_parent, &format!("{}_download", self.bin_name))?;
|
||||
let tmp_archive_path = tmp_dir.path().join(&self.release.asset_name);
|
||||
let mut tmp_archive = fs::File::create(&tmp_archive_path)?;
|
||||
|
||||
self.println("Downloading...");
|
||||
http::download(
|
||||
self.release.download_url.clone(),
|
||||
&mut tmp_archive,
|
||||
self.show_download_progress,
|
||||
)?;
|
||||
|
||||
self.print_flush("Extracting archive... ")?;
|
||||
Extract::from_source(&tmp_archive_path)
|
||||
.extract_file(&tmp_dir.path(), &self.bin_path_in_archive)?;
|
||||
let new_exe = tmp_dir.path().join(&self.bin_path_in_archive);
|
||||
self.println("Done");
|
||||
|
||||
self.print_flush("Replacing binary file... ")?;
|
||||
let tmp_file = tmp_dir.path().join(&format!("__{}_backup", self.bin_name));
|
||||
Move::from_source(&new_exe)
|
||||
.replace_using_temp(&tmp_file)
|
||||
.to_dest(&self.bin_install_path)?;
|
||||
self.println("Done");
|
||||
Ok(Status::Updated(self.release.version))
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
mod release;
|
||||
|
||||
pub use release::*;
|
||||
|
||||
use crate::http;
|
||||
|
||||
pub fn get_latest_release(repo_owner: &str, repo_name: &str) -> crate::Result<Release> {
|
||||
set_ssl_vars!();
|
||||
let api_url = format!(
|
||||
"https://api.github.com/repos/{}/{}/releases/latest",
|
||||
repo_owner, repo_name
|
||||
);
|
||||
let resp = http::get(api_url.clone())?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(
|
||||
crate::Error::Network(format!(
|
||||
"api request failed with status: {:?} - for: {:?}",
|
||||
resp.status(),
|
||||
api_url
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let json = resp.json::<serde_json::Value>()?;
|
||||
Ok(Release::parse(&json)?)
|
||||
}
|
||||
|
||||
pub fn get_release_version(repo_owner: &str, repo_name: &str, ver: &str) -> crate::Result<Release> {
|
||||
set_ssl_vars!();
|
||||
let api_url = format!(
|
||||
"https://api.github.com/repos/{}/{}/releases/tags/{}",
|
||||
repo_owner, repo_name, ver
|
||||
);
|
||||
let resp = http::get(api_url.clone())?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(
|
||||
crate::Error::Network(format!(
|
||||
"api request failed with status: {:?} - for: {:?}",
|
||||
resp.status(),
|
||||
api_url
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let json = resp.json::<serde_json::Value>()?;
|
||||
Ok(Release::parse(&json)?)
|
||||
}
|
@ -1,217 +0,0 @@
|
||||
use crate::http::link_value::{LinkValue, RelationType};
|
||||
|
||||
use serde_json;
|
||||
|
||||
/// GitHub release-asset information
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReleaseAsset {
|
||||
pub download_url: String,
|
||||
pub name: String,
|
||||
}
|
||||
impl ReleaseAsset {
|
||||
/// Parse a release-asset json object
|
||||
///
|
||||
/// Errors:
|
||||
/// * Missing required name & browser_download_url keys
|
||||
fn from_asset(asset: &serde_json::Value) -> crate::Result<ReleaseAsset> {
|
||||
let download_url = asset["browser_download_url"]
|
||||
.as_str()
|
||||
.ok_or_else(|| crate::Error::Network("Asset missing `browser_download_url`".into()))?;
|
||||
let name = asset["name"]
|
||||
.as_str()
|
||||
.ok_or_else(|| crate::Error::Network("Asset missing `name`".into()))?;
|
||||
Ok(ReleaseAsset {
|
||||
download_url: download_url.to_owned(),
|
||||
name: name.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Release {
|
||||
pub name: String,
|
||||
pub body: String,
|
||||
pub tag: String,
|
||||
pub date_created: String,
|
||||
pub assets: Vec<ReleaseAsset>,
|
||||
}
|
||||
impl Release {
|
||||
pub fn parse(release: &serde_json::Value) -> crate::Result<Release> {
|
||||
let tag = release["tag_name"]
|
||||
.as_str()
|
||||
.ok_or_else(|| crate::Error::Network("Release missing `tag_name`".into()))?;
|
||||
let date_created = release["created_at"]
|
||||
.as_str()
|
||||
.ok_or_else(|| crate::Error::Network("Release missing `created_at`".into()))?;
|
||||
let name = release["name"].as_str().unwrap_or(tag);
|
||||
let body = release["body"].as_str().unwrap_or("");
|
||||
let assets = release["assets"]
|
||||
.as_array()
|
||||
.ok_or_else(|| crate::Error::Network("No assets found".into()))?;
|
||||
let assets = assets
|
||||
.iter()
|
||||
.map(ReleaseAsset::from_asset)
|
||||
.collect::<crate::Result<Vec<ReleaseAsset>>>()?;
|
||||
Ok(Release {
|
||||
name: name.to_owned(),
|
||||
body: body.to_owned(),
|
||||
tag: tag.to_owned(),
|
||||
date_created: date_created.to_owned(),
|
||||
assets,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if release has an asset who's name contains the specified `target`
|
||||
pub fn has_target_asset(&self, target: &str) -> bool {
|
||||
self.assets.iter().any(|asset| asset.name.contains(target))
|
||||
}
|
||||
|
||||
/// Return the first `ReleaseAsset` for the current release who's name
|
||||
/// contains the specified `target`
|
||||
pub fn asset_for(&self, target: &str) -> Option<ReleaseAsset> {
|
||||
self
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.name.contains(target))
|
||||
.cloned()
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
pub fn version(&self) -> &str {
|
||||
self.tag.trim_start_matches('v')
|
||||
}
|
||||
}
|
||||
|
||||
/// `ReleaseList` Builder
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReleaseListBuilder {
|
||||
repo_owner: Option<String>,
|
||||
repo_name: Option<String>,
|
||||
target: Option<String>,
|
||||
}
|
||||
impl ReleaseListBuilder {
|
||||
/// Set the repo owner, used to build a github api url
|
||||
pub fn repo_owner(&mut self, owner: &str) -> &mut Self {
|
||||
self.repo_owner = Some(owner.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the repo name, used to build a github api url
|
||||
pub fn repo_name(&mut self, name: &str) -> &mut Self {
|
||||
self.repo_name = Some(name.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the optional arch `target` name, used to filter available releases
|
||||
pub fn target(&mut self, target: &str) -> &mut Self {
|
||||
self.target = Some(target.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Verify builder args, returning a `ReleaseList`
|
||||
pub fn build(&self) -> crate::Result<ReleaseList> {
|
||||
Ok(ReleaseList {
|
||||
repo_owner: if let Some(ref owner) = self.repo_owner {
|
||||
owner.to_owned()
|
||||
} else {
|
||||
return Err(crate::Error::Config("`repo_owner`".into()).into());
|
||||
},
|
||||
repo_name: if let Some(ref name) = self.repo_name {
|
||||
name.to_owned()
|
||||
} else {
|
||||
return Err(crate::Error::Config("`repo_name`".into()).into());
|
||||
},
|
||||
target: self.target.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `ReleaseList` provides a builder api for querying a GitHub repo,
|
||||
/// returning a `Vec` of available `Release`s
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReleaseList {
|
||||
repo_owner: String,
|
||||
repo_name: String,
|
||||
target: Option<String>,
|
||||
}
|
||||
impl ReleaseList {
|
||||
/// Initialize a ReleaseListBuilder
|
||||
pub fn configure() -> ReleaseListBuilder {
|
||||
ReleaseListBuilder {
|
||||
repo_owner: None,
|
||||
repo_name: None,
|
||||
target: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve a list of `Release`s.
|
||||
/// If specified, filter for those containing a specified `target`
|
||||
pub fn fetch(self) -> crate::Result<Vec<Release>> {
|
||||
set_ssl_vars!();
|
||||
let api_url = format!(
|
||||
"https://api.github.com/repos/{}/{}/releases",
|
||||
self.repo_owner, self.repo_name
|
||||
);
|
||||
let releases = Self::fetch_releases(&api_url)?;
|
||||
let releases = match self.target {
|
||||
None => releases,
|
||||
Some(ref target) => releases
|
||||
.into_iter()
|
||||
.filter(|r| r.has_target_asset(target))
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
Ok(releases)
|
||||
}
|
||||
|
||||
fn fetch_releases(url: &str) -> crate::Result<Vec<Release>> {
|
||||
let (status, headers, reader) = attohttpc::get(url).send()?.split();
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(
|
||||
crate::Error::Network(format!(
|
||||
"api request failed with status: {:?} - for: {:?}",
|
||||
status, url
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let releases = reader.json::<serde_json::Value>()?;
|
||||
let releases = releases
|
||||
.as_array()
|
||||
.ok_or_else(|| crate::Error::Network("No releases found".into()))?;
|
||||
let mut releases = releases
|
||||
.iter()
|
||||
.map(Release::parse)
|
||||
.collect::<crate::Result<Vec<Release>>>()?;
|
||||
|
||||
// handle paged responses containing `Link` header:
|
||||
// `Link: <https://api.github.com/resource?page=2>; rel="next"`
|
||||
let links = headers.get_all(attohttpc::header::LINK);
|
||||
|
||||
let next_link = links
|
||||
.iter()
|
||||
.filter_map(|link| {
|
||||
if let Ok(link) = link.to_str() {
|
||||
let lv = LinkValue::new(link.to_owned());
|
||||
if let Some(rels) = lv.rel() {
|
||||
if rels.contains(&RelationType::Next) {
|
||||
return Some(link);
|
||||
}
|
||||
}
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.nth(0);
|
||||
|
||||
Ok(match next_link {
|
||||
None => releases,
|
||||
Some(link) => {
|
||||
releases.extend(Self::fetch_releases(link)?);
|
||||
releases
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
BIN
tauri-updater/test/fixture/archives/archive.tar.gz
Normal file
@ -0,0 +1 @@
|
||||
dW90cnVzdGVkIGIvbW1lbnQ6IHNpZ25hdHVyZSBmcm1tIHRhdXJpIHNlY3JldCBrZXkKUldUTE3QzWxkQolZOVVDaC92ZnhXN0IrVm4rVW9GKzdoSFF6NEtFc3J3c004YUhQTFR0Njg5MGtuZkZqeVh1cTlwZ1dmWG9aSkx5d0t1WTBkS04wK1RBeEI2K2pka2tsT3drPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkxODg2NjU2CWZpbGU6L1VzZXJzL2RhdmlkL2Rldi90YXVyaV91cGRhdGVyL3RhdXJpLXVwZGF0ZXIvdGVzdC9maXh0dXJlL2FyY2hpdmVzL2FyY2hpdmUudGFyLmd6CjNmMC9XdmYyzDtCM3ZoaWhEbHVVL08vV2tLejQ0Wlg5SkNGTys2N1ZMTHQvUENrK0svMlgzNE22UkQ0OG1sZ0RqTGZXNzA0OGxocmg4ODljM3BGOEJnPT0K
|
1
tauri-updater/test/fixture/archives/archive.tar.gz.sig
Normal file
@ -0,0 +1 @@
|
||||
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldSWTRKaFRZQmJER1VrQzlhaWFLOExCM3VoV1gxTW1IU0ZQQTZKTXN6U0MwMDEzcThyc3R3a0pmVkJrdWFhN1JCOTNpaEg5c1JhSGY0QWxROENlbTBxbFhpVTNWUWViWVFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjE1Mzk5ODg3CWZpbGU6Li4vLi4vdGF1cmktdXBkYXRlci90ZXN0L2ZpeHR1cmUvYXJjaGl2ZXMvYXJjaGl2ZS50YXIuZ3oKczI0cUxjM2YvMGdpMDI1ejE3ZnREQTNtdlBwR2xydW5rVFBvcUxVcUt2dVpxZkpPd1ZLT1Z1K0hZS0hjRVk3Ylg4UVQxWEtWMHJ4ZUwwSXcvZjlaQmc9PQo=
|
BIN
tauri-updater/test/fixture/archives/archive.zip
Normal file
1
tauri-updater/test/fixture/archives/archive.zip.sig
Normal file
@ -0,0 +1 @@
|
||||
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVVZN3dSRm5KTmZPMlovQjAxdGtCN3YxYWJybFM2RzlxRm1rMnNkc20rbWRrS2d4ZlNJT0F4RUdrTFVGYWlUQVMxUE1VRk1uNW5IS2tucHNYNnRLZGdFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkxODg2NjIzCWZpbGU6L1VzZXJzL2RhdmlkL2Rldi90YXVyaV91cGRhdGVyL3RhdXJpLXVwZGF0ZXIvdGVzdC9maXh0dXJlL2FyY2hpdmVzL2FyY2hpdmUuemlwCkNiNkJBKzNkR2M3WFhGTDgwSnB6NnpSYUl0VUFKVFNyRTJGVEdFbVkrNXZPTHBOQU1UeWIxbTB5QVpLRTBoM1NHeGs0RGxvTFRKY0FoWmVIS2psNkJBPT0K
|
1
tauri-updater/test/fixture/bad_signature/update.key
Normal file
@ -0,0 +1 @@
|
||||
dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5dGlHbTEvRFhRRis2STdlTzF3eWhOVk9LNjdGRENJMnFSREE3R2V3b3Rwb0FBQkFBQUFBQUFBQUFBQUlBQUFBQWFNZEJTNXFuVjk0bmdJMENRRXVYNG5QVzBDd1NMOWN4Q2RKRXZxRDZNakw3Y241Vkt3aTg2WGtoajJGS1owV0ZuSmo4ZXJ0ZCtyaWF0RWJObFpnd1EveDB4NzBTU2RweG9ZaUpuc3hnQ3BYVG9HNnBXUW5SZ2Q3b3dvZ3Y2UnhQZ1BQZDU3bXl6d3M9Cg==
|
1
tauri-updater/test/fixture/bad_signature/update.key.pub
Normal file
@ -0,0 +1 @@
|
||||
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEY1OTgwQzc0UjVGNjM0Q0IKUldUTE5QWWxkQnlZOWFBK21kekU4OGgzdStleEtkeStHaFR5NjEyRHovRnlUdzAwWGJxWEU2aGYK
|
1
tauri-updater/test/fixture/good_signature/update.key
Normal file
@ -0,0 +1 @@
|
||||
dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg==
|
1
tauri-updater/test/fixture/good_signature/update.key.pub
Normal file
@ -0,0 +1 @@
|
||||
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK
|
@ -155,6 +155,39 @@ impl Default for WindowConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Updater configuration object.
|
||||
#[derive(PartialEq, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "updater", rename_all = "camelCase")]
|
||||
pub struct UpdaterConfig {
|
||||
/// Whether the updater is active or not.
|
||||
#[serde(default)]
|
||||
pub active: bool,
|
||||
/// Display built-in dialog or use event system if disabled.
|
||||
#[serde(default = "default_updater_dialog")]
|
||||
pub dialog: bool,
|
||||
/// The updater endpoints.
|
||||
#[serde(default)]
|
||||
pub endpoints: Option<Vec<String>>,
|
||||
/// Optional pubkey.
|
||||
#[serde(default)]
|
||||
pub pubkey: Option<String>,
|
||||
}
|
||||
|
||||
fn default_updater_dialog() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for UpdaterConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: false,
|
||||
dialog: true,
|
||||
endpoints: None,
|
||||
pubkey: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A CLI argument definition
|
||||
#[derive(PartialEq, Deserialize, Debug, Default, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -324,6 +357,9 @@ pub struct TauriConfig {
|
||||
/// The bundler configuration.
|
||||
#[serde(default)]
|
||||
pub bundle: BundleConfig,
|
||||
/// The updater configuration.
|
||||
#[serde(default)]
|
||||
pub updater: UpdaterConfig,
|
||||
}
|
||||
|
||||
impl Default for TauriConfig {
|
||||
@ -332,6 +368,7 @@ impl Default for TauriConfig {
|
||||
windows: default_window_config(),
|
||||
cli: None,
|
||||
bundle: BundleConfig::default(),
|
||||
updater: UpdaterConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -711,13 +748,25 @@ mod build {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for UpdaterConfig {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let active = self.active;
|
||||
let dialog = self.dialog;
|
||||
let pubkey = opt_str_lit(self.pubkey.as_ref());
|
||||
let endpoints = opt_vec_str_lit(self.endpoints.as_ref());
|
||||
|
||||
literal_struct!(tokens, UpdaterConfig, active, dialog, pubkey, endpoints);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for TauriConfig {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let windows = vec_lit(&self.windows, identity);
|
||||
let cli = opt_lit(self.cli.as_ref());
|
||||
let bundle = &self.bundle;
|
||||
let updater = &self.updater;
|
||||
|
||||
literal_struct!(tokens, TauriConfig, windows, cli, bundle);
|
||||
literal_struct!(tokens, TauriConfig, windows, cli, bundle, updater);
|
||||
}
|
||||
}
|
||||
|
||||
@ -765,6 +814,8 @@ mod test {
|
||||
let d_title = default_title();
|
||||
// get default bundle
|
||||
let d_bundle = BundleConfig::default();
|
||||
// get default updater
|
||||
let d_updater = UpdaterConfig::default();
|
||||
|
||||
// create a tauri config.
|
||||
let tauri = TauriConfig {
|
||||
@ -792,6 +843,12 @@ mod test {
|
||||
identifier: String::from(""),
|
||||
},
|
||||
cli: None,
|
||||
updater: UpdaterConfig {
|
||||
active: false,
|
||||
dialog: true,
|
||||
pubkey: None,
|
||||
endpoints: None,
|
||||
},
|
||||
};
|
||||
|
||||
// create a build config
|
||||
@ -805,6 +862,7 @@ mod test {
|
||||
assert_eq!(t_config, tauri);
|
||||
assert_eq!(b_config, build);
|
||||
assert_eq!(d_bundle, tauri.bundle);
|
||||
assert_eq!(d_updater, tauri.updater);
|
||||
assert_eq!(d_path, String::from("http://localhost:8080"));
|
||||
assert_eq!(d_title, tauri.windows[0].title);
|
||||
assert_eq!(d_windows, tauri.windows);
|
||||
|
@ -27,6 +27,7 @@ uuid = { version = "0.8.2", features = [ "v4" ] }
|
||||
thiserror = "1.0.24"
|
||||
once_cell = "1.7.2"
|
||||
tauri-api = { version = "0.7.5", path = "../tauri-api" }
|
||||
tauri-updater = { version = "0.4.2", optional = true, path = "../tauri-updater" }
|
||||
tauri-macros = { version = "0.1", path = "../tauri-macros" }
|
||||
wry = "0.7"
|
||||
rand = "0.8"
|
||||
@ -43,8 +44,8 @@ serde = { version = "1.0", features = [ "derive" ] }
|
||||
[features]
|
||||
cli = [ "tauri-api/cli" ]
|
||||
custom-protocol = [ ]
|
||||
api-all = [ "tauri-api/notification", "tauri-api/global-shortcut" ]
|
||||
updater = [ ]
|
||||
api-all = [ "tauri-api/notification", "tauri-api/global-shortcut", "updater" ]
|
||||
updater = [ "tauri-updater" ]
|
||||
|
||||
# FS
|
||||
fs-all = [ ]
|
||||
|
@ -49,6 +49,9 @@ pub enum Error {
|
||||
/// Encountered an error in the setup hook,
|
||||
#[error("error encountered during setup hood: {0}")]
|
||||
Setup(#[from] Box<dyn std::error::Error>),
|
||||
/// Tauri updater error.
|
||||
#[error("Updater: {0}")]
|
||||
TauriUpdater(#[from] tauri_updater::Error),
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
|
@ -7,6 +7,7 @@ use crate::{
|
||||
manager::WindowManager,
|
||||
sealed::ManagerPrivate,
|
||||
tag::Tag,
|
||||
updater,
|
||||
webview::{Attributes, WindowConfig},
|
||||
window::{PendingWindow, Window},
|
||||
Context, Dispatch, Manager, Params, Runtime, RuntimeOrDispatch,
|
||||
@ -19,6 +20,65 @@ pub struct App<M: Params> {
|
||||
manager: M,
|
||||
}
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
impl<M: Params> App<M> {
|
||||
/// Runs the updater hook with built-in dialog.
|
||||
fn run_updater_dialog(&self, window: Window<M>) {
|
||||
let updater_config = self.manager.config().tauri.updater.clone();
|
||||
let package_info = self.manager.package_info().clone();
|
||||
crate::async_runtime::spawn(async move {
|
||||
updater::check_update_with_dialog(updater_config, package_info, window).await
|
||||
});
|
||||
}
|
||||
|
||||
/// Listen updater events when dialog are disabled.
|
||||
fn listen_updater_events(&self, window: Window<M>) {
|
||||
let updater_config = self.manager.config().tauri.updater.clone();
|
||||
updater::listener(updater_config, self.manager.package_info().clone(), &window);
|
||||
}
|
||||
|
||||
fn run_updater(&self, main_window: Option<Window<M>>) {
|
||||
if let Some(main_window) = main_window {
|
||||
let event_window = main_window.clone();
|
||||
let updater_config = self.manager.config().tauri.updater.clone();
|
||||
// check if updater is active or not
|
||||
if updater_config.dialog && updater_config.active {
|
||||
// if updater dialog is enabled spawn a new task
|
||||
self.run_updater_dialog(main_window.clone());
|
||||
let config = self.manager.config().tauri.updater.clone();
|
||||
let package_info = self.manager.package_info().clone();
|
||||
// When dialog is enabled, if user want to recheck
|
||||
// if an update is available after first start
|
||||
// invoke the Event `tauri://update` from JS or rust side.
|
||||
main_window.listen(
|
||||
updater::EVENT_CHECK_UPDATE
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("bad label")),
|
||||
move |_msg| {
|
||||
let window = event_window.clone();
|
||||
let package_info = package_info.clone();
|
||||
let config = config.clone();
|
||||
// re-spawn task inside tokyo to launch the download
|
||||
// we don't need to emit anything as everything is handled
|
||||
// by the process (user is asked to restart at the end)
|
||||
// and it's handled by the updater
|
||||
crate::async_runtime::spawn(async move {
|
||||
updater::check_update_with_dialog(config, package_info, window).await
|
||||
});
|
||||
},
|
||||
);
|
||||
} else if updater_config.active {
|
||||
// we only listen for `tauri://update`
|
||||
// once we receive the call, we check if an update is available or not
|
||||
// if there is a new update we emit `tauri://update-available` with details
|
||||
// this is the user responsabilities to display dialog and ask if user want to install
|
||||
// to install the update you need to invoke the Event `tauri://update-install`
|
||||
self.listen_updater_events(main_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Params> Manager<M> for App<M> {}
|
||||
impl<M: Params> ManagerPrivate<M> for App<M> {
|
||||
fn manager(&self) -> &M {
|
||||
@ -66,12 +126,22 @@ impl<M: Params> Runner<M> {
|
||||
};
|
||||
|
||||
let pending_windows = self.pending_windows;
|
||||
#[cfg(feature = "updater")]
|
||||
let mut main_window = None;
|
||||
|
||||
for pending in pending_windows {
|
||||
let pending = app.manager.prepare_window(pending, &labels)?;
|
||||
let detached = app.runtime.create_window(pending)?;
|
||||
app.manager.attach_window(detached);
|
||||
let window = app.manager.attach_window(detached);
|
||||
#[cfg(feature = "updater")]
|
||||
if main_window.is_none() {
|
||||
main_window = Some(window);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
app.run_updater(main_window);
|
||||
|
||||
(self.setup)(&mut app)?;
|
||||
app.runtime.run();
|
||||
Ok(())
|
||||
|
@ -2,6 +2,7 @@ use crate::{
|
||||
api::{
|
||||
assets::Assets,
|
||||
config::{Config, WindowUrl},
|
||||
PackageInfo,
|
||||
},
|
||||
event::{Event, EventHandler, Listeners},
|
||||
hooks::{InvokeHandler, InvokeMessage, InvokePayload, OnPageLoad, PageLoadPayload},
|
||||
@ -44,6 +45,7 @@ pub struct InnerWindowManager<M: Params> {
|
||||
|
||||
/// A list of salts that are valid for the current application.
|
||||
salts: Mutex<HashSet<Uuid>>,
|
||||
package_info: PackageInfo,
|
||||
}
|
||||
|
||||
pub struct WindowManager<E, L, A, R>
|
||||
@ -93,6 +95,7 @@ where
|
||||
assets: Arc::new(context.assets),
|
||||
default_window_icon: context.default_window_icon,
|
||||
salts: Mutex::default(),
|
||||
package_info: context.package_info,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -159,6 +162,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// If we are on windows use App Data Local as webview temp dir
|
||||
// to prevent any bundled application to failed.
|
||||
// Fix: https://github.com/tauri-apps/tauri/issues/1365
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Should return a path similar to C:\Users\<User>\AppData\Local\<AppName>
|
||||
let local_app_data = tauri_api::path::resolve_path(
|
||||
self.inner.package_info.name,
|
||||
Some(tauri_api::path::BaseDirectory::LocalData),
|
||||
);
|
||||
// Make sure the directory exist without panic
|
||||
if let Ok(user_data_dir) = local_app_data {
|
||||
if let Ok(()) = std::fs::create_dir_all(&user_data_dir) {
|
||||
attributes = attributes.user_data_path(Some(user_data_dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
@ -484,6 +505,10 @@ where
|
||||
&self.inner.config
|
||||
}
|
||||
|
||||
fn package_info(&self) -> &PackageInfo {
|
||||
&self.inner.package_info
|
||||
}
|
||||
|
||||
fn unlisten(&self, handler_id: EventHandler) {
|
||||
self.inner.listeners.unlisten(handler_id)
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ pub(crate) mod app;
|
||||
pub mod flavor;
|
||||
pub(crate) mod manager;
|
||||
pub(crate) mod tag;
|
||||
#[cfg(feature = "updater")]
|
||||
pub(crate) mod updater;
|
||||
pub(crate) mod webview;
|
||||
pub(crate) mod window;
|
||||
|
||||
@ -28,6 +30,9 @@ pub struct Context<A: Assets> {
|
||||
|
||||
/// The default window icon Tauri should use when creating windows.
|
||||
pub default_window_icon: Option<Vec<u8>>,
|
||||
|
||||
/// Package information.
|
||||
pub package_info: tauri_api::PackageInfo,
|
||||
}
|
||||
|
||||
/// The webview runtime interface.
|
||||
@ -140,7 +145,7 @@ pub trait Dispatch: Clone + Send + Sized + 'static {
|
||||
pub(crate) mod sealed {
|
||||
use super::Params;
|
||||
use crate::{
|
||||
api::config::Config,
|
||||
api::{config::Config, PackageInfo},
|
||||
event::{Event, EventHandler},
|
||||
hooks::{InvokeMessage, PageLoadPayload},
|
||||
runtime::{
|
||||
@ -202,6 +207,9 @@ pub(crate) mod sealed {
|
||||
/// The configuration the [`Manager`] was built with.
|
||||
fn config(&self) -> &Config;
|
||||
|
||||
/// App package information.
|
||||
fn package_info(&self) -> &PackageInfo;
|
||||
|
||||
/// Remove the specified event handler.
|
||||
fn unlisten(&self, handler_id: EventHandler);
|
||||
|
||||
|
285
tauri/src/runtime/updater.rs
Normal file
@ -0,0 +1,285 @@
|
||||
use crate::{
|
||||
api::{
|
||||
config::UpdaterConfig,
|
||||
dialog::{ask, AskResponse},
|
||||
},
|
||||
runtime::{window::Window, Params},
|
||||
};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
process::{exit, Command},
|
||||
};
|
||||
|
||||
// Check for new updates
|
||||
pub const EVENT_CHECK_UPDATE: &str = "tauri://update";
|
||||
// New update available
|
||||
pub const EVENT_UPDATE_AVAILABLE: &str = "tauri://update-available";
|
||||
// Used to intialize an update *should run check-update first (once you received the update available event)*
|
||||
pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install";
|
||||
// Send updater status or error even if dialog is enabled, you should
|
||||
// always listen for this event. It'll send you the install progress
|
||||
// and any error triggered during update check and install
|
||||
pub const EVENT_STATUS_UPDATE: &str = "tauri://update-status";
|
||||
// this is the status emitted when the download start
|
||||
pub const EVENT_STATUS_PENDING: &str = "PENDING";
|
||||
// When you got this status, something went wrong
|
||||
// you can find the error message inside the `error` field.
|
||||
pub const EVENT_STATUS_ERROR: &str = "ERROR";
|
||||
// When you receive this status, you should ask the user to restart
|
||||
pub const EVENT_STATUS_SUCCESS: &str = "DONE";
|
||||
// When you receive this status, this is because the application is runniing last version
|
||||
pub const EVENT_STATUS_UPTODATE: &str = "UPTODATE";
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct StatusEvent {
|
||||
status: String,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct UpdateManifest {
|
||||
version: String,
|
||||
date: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
/// Check if there is any new update with builtin dialog.
|
||||
pub(crate) async fn check_update_with_dialog<M: Params>(
|
||||
updater_config: UpdaterConfig,
|
||||
package_info: crate::api::PackageInfo,
|
||||
window: Window<M>,
|
||||
) {
|
||||
if !updater_config.active || updater_config.endpoints.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// prepare our endpoints
|
||||
let endpoints = updater_config
|
||||
.endpoints
|
||||
.as_ref()
|
||||
// this expect can lead to a panic
|
||||
// we should have a better handling here
|
||||
.expect("Something wrong with endpoints")
|
||||
.clone();
|
||||
|
||||
// check updates
|
||||
match tauri_updater::builder()
|
||||
.urls(&endpoints[..])
|
||||
.current_version(package_info.version)
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(updater) => {
|
||||
let pubkey = updater_config.pubkey.clone();
|
||||
|
||||
// if dialog enabled only
|
||||
if updater.should_update && updater_config.dialog {
|
||||
let body = updater.body.clone().unwrap_or_else(|| String::from(""));
|
||||
let dialog =
|
||||
prompt_for_install(&updater.clone(), package_info.name, &body.clone(), pubkey).await;
|
||||
|
||||
if dialog.is_err() {
|
||||
send_status_update(
|
||||
window.clone(),
|
||||
EVENT_STATUS_ERROR,
|
||||
Some(dialog.err().unwrap().to_string()),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
send_status_update(window.clone(), EVENT_STATUS_ERROR, Some(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Experimental listener
|
||||
/// This function should be run on the main thread once.
|
||||
pub(crate) fn listener<M: Params>(
|
||||
updater_config: UpdaterConfig,
|
||||
package_info: crate::api::PackageInfo,
|
||||
window: &Window<M>,
|
||||
) {
|
||||
let isolated_window = window.clone();
|
||||
|
||||
// Wait to receive the event `"tauri://update"`
|
||||
window.listen(
|
||||
EVENT_CHECK_UPDATE
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("bad label")),
|
||||
move |_msg| {
|
||||
let window = isolated_window.clone();
|
||||
let package_info = package_info.clone();
|
||||
|
||||
// prepare our endpoints
|
||||
let endpoints = updater_config
|
||||
.endpoints
|
||||
.as_ref()
|
||||
.expect("Something wrong with endpoints")
|
||||
.clone();
|
||||
|
||||
let pubkey = updater_config.pubkey.clone();
|
||||
|
||||
// check updates
|
||||
crate::async_runtime::spawn(async move {
|
||||
let window = window.clone();
|
||||
let window_isolation = window.clone();
|
||||
let pubkey = pubkey.clone();
|
||||
|
||||
match tauri_updater::builder()
|
||||
.urls(&endpoints[..])
|
||||
.current_version(package_info.version)
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(updater) => {
|
||||
// send notification if we need to update
|
||||
if updater.should_update {
|
||||
let body = updater.body.clone().unwrap_or_else(|| String::from(""));
|
||||
|
||||
// Emit `tauri://update-available`
|
||||
let _ = window.emit(
|
||||
&EVENT_UPDATE_AVAILABLE
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("bad label")),
|
||||
Some(UpdateManifest {
|
||||
body,
|
||||
date: updater.date.clone(),
|
||||
version: updater.version.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
// Listen for `tauri://update-install`
|
||||
window.once(
|
||||
EVENT_INSTALL_UPDATE
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("bad label")),
|
||||
move |_msg| {
|
||||
let window = window_isolation.clone();
|
||||
let updater = updater.clone();
|
||||
let pubkey = pubkey.clone();
|
||||
|
||||
// Start installation
|
||||
crate::async_runtime::spawn(async move {
|
||||
// emit {"status": "PENDING"}
|
||||
send_status_update(window.clone(), EVENT_STATUS_PENDING, None);
|
||||
|
||||
// Launch updater download process
|
||||
// macOS we display the `Ready to restart dialog` asking to restart
|
||||
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
|
||||
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
|
||||
let update_result = updater.clone().download_and_install(pubkey.clone()).await;
|
||||
|
||||
if update_result.is_err() {
|
||||
// emit {"status": "ERROR", "error": "The error message"}
|
||||
send_status_update(
|
||||
window.clone(),
|
||||
EVENT_STATUS_ERROR,
|
||||
Some(update_result.err().unwrap().to_string()),
|
||||
);
|
||||
} else {
|
||||
// emit {"status": "DONE"}
|
||||
// todo(lemarier): maybe we should emit the
|
||||
// path of the current EXE so they can restart it
|
||||
send_status_update(window.clone(), EVENT_STATUS_SUCCESS, None);
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
} else {
|
||||
send_status_update(window.clone(), EVENT_STATUS_UPTODATE, None);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
send_status_update(window.clone(), EVENT_STATUS_ERROR, Some(e.to_string()));
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Send a status update via `tauri://update-status` event.
|
||||
fn send_status_update<M: Params>(window: Window<M>, status: &str, error: Option<String>) {
|
||||
let _ = window.emit_internal(
|
||||
EVENT_STATUS_UPDATE.to_string(),
|
||||
Some(StatusEvent {
|
||||
error,
|
||||
status: String::from(status),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Prompt a dialog asking if the user want to install the new version
|
||||
// Maybe we should add an option to customize it in future versions.
|
||||
async fn prompt_for_install(
|
||||
updater: &tauri_updater::Update,
|
||||
app_name: &str,
|
||||
body: &str,
|
||||
pubkey: Option<String>,
|
||||
) -> crate::Result<()> {
|
||||
// remove single & double quote
|
||||
let escaped_body = body.replace(&['\"', '\''][..], "");
|
||||
|
||||
// todo(lemarier): We should review this and make sure we have
|
||||
// something more conventional.
|
||||
let should_install = ask(
|
||||
format!(r#"A new version of {} is available! "#, app_name),
|
||||
format!(
|
||||
r#"{} {} is now available -- you have {}.
|
||||
|
||||
Would you like to install it now?
|
||||
|
||||
Release Notes:
|
||||
{}"#,
|
||||
app_name, updater.version, updater.current_version, escaped_body,
|
||||
),
|
||||
);
|
||||
|
||||
match should_install {
|
||||
AskResponse::Yes => {
|
||||
// Launch updater download process
|
||||
// macOS we display the `Ready to restart dialog` asking to restart
|
||||
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
|
||||
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
|
||||
updater.download_and_install(pubkey.clone()).await?;
|
||||
|
||||
// Ask user if we need to restart the application
|
||||
let should_exit = ask(
|
||||
"Ready to Restart",
|
||||
"The installation was successful, do you want to restart the application now?",
|
||||
);
|
||||
match should_exit {
|
||||
AskResponse::Yes => {
|
||||
restart_application(updater.current_binary.as_ref());
|
||||
// safely exit even if the process
|
||||
// should be killed
|
||||
return Ok(());
|
||||
}
|
||||
AskResponse::No => {
|
||||
// Do nothing -- maybe we can emit some event here
|
||||
}
|
||||
}
|
||||
}
|
||||
AskResponse::No => {
|
||||
// Do nothing -- maybe we can emit some event here
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tested on macOS and Linux. Windows will not trigger the dialog
|
||||
// as it'll exit before, to launch the MSI installation.
|
||||
fn restart_application(binary_to_start: Option<&PathBuf>) {
|
||||
// spawn new process
|
||||
if let Some(path) = binary_to_start {
|
||||
Command::new(path)
|
||||
.spawn()
|
||||
.expect("application failed to start");
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
@ -19,6 +19,9 @@
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|