diff --git a/.changes/config.json b/.changes/config.json index 1ae0cbc6f..9a9f48262 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -185,8 +185,7 @@ }, "tauri-updater": { "path": "./tauri-updater", - "manager": "rust", - "publish": false + "manager": "rust" }, "tauri": { "path": "./tauri", diff --git a/.changes/updater-alpha.md b/.changes/updater-alpha.md new file mode 100644 index 000000000..c551e4258 --- /dev/null +++ b/.changes/updater-alpha.md @@ -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. diff --git a/.github/workflows/artifacts-updater.yml b/.github/workflows/artifacts-updater.yml new file mode 100644 index 000000000..1ca19d88a --- /dev/null +++ b/.github/workflows/artifacts-updater.yml @@ -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.* diff --git a/.github/workflows/test-bundler.yml b/.github/workflows/test-bundler.yml index 4669205aa..f6057ef4e 100644 --- a/.github/workflows/test-bundler.yml +++ b/.github/workflows/test-bundler.yml @@ -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: diff --git a/Cargo.toml b/Cargo.toml index beba431d3..d95bd64b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/api/package.json b/api/package.json index aea913d2a..851b665bc 100644 --- a/api/package.json +++ b/api/package.json @@ -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", diff --git a/api/rollup.config.js b/api/rollup.config.js index a5e00f9c0..b82c96871 100644 --- a/api/rollup.config.js +++ b/api/rollup.config.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', diff --git a/api/src/bundle.ts b/api/src/bundle.ts index f232b3b74..40073c2f1 100644 --- a/api/src/bundle.ts +++ b/api/src/bundle.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, diff --git a/api/src/updater.ts b/api/src/updater.ts new file mode 100644 index 000000000..48bdfc68c --- /dev/null +++ b/api/src/updater.ts @@ -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 { + 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 { + 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 + }) + }) +} diff --git a/cli/core/Cargo.lock b/cli/core/Cargo.lock index 3fbc02264..3ec2a7644 100755 --- a/cli/core/Cargo.lock +++ b/cli/core/Cargo.lock @@ -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", diff --git a/cli/core/Cargo.toml b/cli/core/Cargo.toml index d1f80250f..b1ac4dedc 100644 --- a/cli/core/Cargo.toml +++ b/cli/core/Cargo.toml @@ -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" diff --git a/cli/core/config_definition.rs b/cli/core/config_definition.rs index 83cfabfad..9c431ad0a 100644 --- a/cli/core/config_definition.rs +++ b/cli/core/config_definition.rs @@ -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, + /// 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, + /// The updater endpoints. + pub endpoints: Option>, + /// Optional pubkey. + pub pubkey: Option, +} + +// 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 { + 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, + } +} diff --git a/cli/core/schema.json b/cli/core/schema.json index 40685e2f7..7e0d85d1d 100644 --- a/cli/core/schema.json +++ b/cli/core/schema.json @@ -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": { diff --git a/cli/core/src/build.rs b/cli/core/src/build.rs index 907a81ad3..9c0c33755 100644 --- a/cli/core/src/build.rs +++ b/cli/core/src/build.rs @@ -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(()) diff --git a/cli/core/src/build/rust.rs b/cli/core/src/build/rust.rs index 6c6d5ed6a..ee163f18c 100644 --- a/cli/core/src/build/rust.rs +++ b/cli/core/src/build/rust.rs @@ -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 { - 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 { @@ -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 { 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() }) } diff --git a/cli/core/src/cli.yml b/cli/core/src/cli.yml index 55760959e..8ef19c316 100644 --- a/cli/core/src/cli.yml +++ b/cli/core/src/cli.yml @@ -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: diff --git a/cli/core/src/helpers/mod.rs b/cli/core/src/helpers/mod.rs index 5ab803d9d..5cf76ba33 100644 --- a/cli/core/src/helpers/mod.rs +++ b/cli/core/src/helpers/mod.rs @@ -2,6 +2,7 @@ pub mod app_paths; pub mod config; mod logger; pub mod manifest; +pub mod updater_signature; pub use logger::Logger; diff --git a/cli/core/src/helpers/updater_signature.rs b/cli/core/src/helpers/updater_signature.rs new file mode 100644 index 000000000..f151ad875 --- /dev/null +++ b/cli/core/src/helpers/updater_signature.rs @@ -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) -> crate::Result { + 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 { + let decoded_str = &decode(&base64_key)?[..]; + Ok(String::from(str::from_utf8(decoded_str)?)) +} + +/// Save KeyPair to disk +pub fn save_keypair

( + force: bool, + sk_path: P, + key: &str, + pubkey: &str, +) -> crate::Result<(PathBuf, PathBuf)> +where + P: AsRef, +{ + 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

(sk_path: P) -> crate::Result +where + P: AsRef, +{ + Ok(fs::read_to_string(sk_path)?) +} + +/// Sign files +pub fn sign_file

( + private_key: String, + password: String, + bin_path: P, + prehashed: bool, +) -> crate::Result<(PathBuf, String)> +where + P: AsRef, +{ + 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

(path_to_sign: P) -> crate::Result<(PathBuf, String)> +where + P: AsRef, +{ + // 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

(data_path: P) -> crate::Result<(BufReader, bool)> +where + P: AsRef, +{ + 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)) +} diff --git a/cli/core/src/main.rs b/cli/core/src/main.rs index 5e12eac77..7a6dc9ef6 100644 --- a/cli/core/src/main.rs +++ b/cli/core/src/main.rs @@ -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(()) diff --git a/cli/core/src/sign.rs b/cli/core/src/sign.rs new file mode 100644 index 000000000..f8c3cc22c --- /dev/null +++ b/cli/core/src/sign.rs @@ -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, + password: Option, + file: Option, +} + +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 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, + output_path: Option, + 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(()) + } +} diff --git a/cli/core/templates/src-tauri/src/main.rs b/cli/core/templates/src-tauri/src/main.rs index ab60afb77..ca183591b 100755 --- a/cli/core/templates/src-tauri/src/main.rs +++ b/cli/core/templates/src-tauri/src/main.rs @@ -6,5 +6,6 @@ fn main() { tauri::AppBuilder::default() .build(tauri::generate_context!()) - .run(); + .run() + .expect("error while running tauri application"); } diff --git a/cli/core/templates/src-tauri/tauri.conf.json b/cli/core/templates/src-tauri/tauri.conf.json index c785275a9..94f632b34 100644 --- a/cli/core/templates/src-tauri/tauri.conf.json +++ b/cli/core/templates/src-tauri/tauri.conf.json @@ -45,6 +45,9 @@ "timestampUrl": "" } }, + "updater": { + "active": false + }, "allowlist": { "all": true }, diff --git a/cli/tauri-bundler/src/bundle.rs b/cli/tauri-bundler/src/bundle.rs index a0d31fa89..3b83b0888 100644 --- a/cli/tauri-bundler/src/bundle.rs +++ b/cli/tauri-bundler/src/bundle.rs @@ -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, +} + /// Bundles the project. /// Returns the list of paths where the bundles can be found. -pub fn bundle_project(settings: Settings) -> crate::Result> { - let mut paths = Vec::new(); +pub fn bundle_project(settings: Settings) -> crate::Result> { + 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 diff --git a/cli/tauri-bundler/src/bundle/common.rs b/cli/tauri-bundler/src/bundle/common.rs index f7adcce89..6cab2b6dc 100644 --- a/cli/tauri-bundler/src/bundle/common.rs +++ b/cli/tauri-bundler/src/bundle/common.rs @@ -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()); } diff --git a/cli/tauri-bundler/src/bundle/dmg_bundle.rs b/cli/tauri-bundler/src/bundle/dmg_bundle.rs index 65fdb0a00..ccbc831b5 100644 --- a/cli/tauri-bundler/src/bundle/dmg_bundle.rs +++ b/cli/tauri-bundler/src/bundle/dmg_bundle.rs @@ -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> { - // generate the .app bundle - macos_bundle::bundle_project(settings)?; +pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result> { + // 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> { 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> { 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> { 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]) } diff --git a/cli/tauri-bundler/src/bundle/settings.rs b/cli/tauri-bundler/src/bundle/settings.rs index c2a63886b..dd91a0630 100644 --- a/cli/tauri-bundler/src/bundle/settings.rs +++ b/cli/tauri-bundler/src/bundle/settings.rs @@ -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 { // 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, } +/// 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>, + /// Optional pubkey. + pub pubkey: Option, + /// 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, /// 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> { 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. diff --git a/cli/tauri-bundler/src/bundle/updater_bundle.rs b/cli/tauri-bundler/src/bundle/updater_bundle.rs new file mode 100644 index 000000000..5becdc9a9 --- /dev/null +++ b/cli/tauri-bundler/src/bundle/updater_bundle.rs @@ -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> { + 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> { + // 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_.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> { + // 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_.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> { + // 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 { + 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 { + 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, W: Write>(src_dir: P, dest_file: W) -> crate::Result { + 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) +} diff --git a/cli/tauri-bundler/src/error.rs b/cli/tauri-bundler/src/error.rs index 2da5f2a3c..081f8f66c 100644 --- a/cli/tauri-bundler/src/error.rs +++ b/cli/tauri-bundler/src/error.rs @@ -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. diff --git a/cli/tauri.js/bin/tauri.js b/cli/tauri.js/bin/tauri.js index dc6aab562..9aba84c90 100755 --- a/cli/tauri.js/bin/tauri.js +++ b/cli/tauri.js/bin/tauri.js @@ -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] diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index 95a2232f7..b4ae7af3d 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -37,5 +37,9 @@ pub fn context_codegen(data: ContextData) -> Result + + // 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); + } + } + + + + +

+ + +
diff --git a/examples/helloworld/src-tauri/tauri.conf.json b/examples/helloworld/src-tauri/tauri.conf.json index ebacdc3ea..454c2f339 100644 --- a/examples/helloworld/src-tauri/tauri.conf.json +++ b/examples/helloworld/src-tauri/tauri.conf.json @@ -48,6 +48,9 @@ ], "security": { "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'" + }, + "updater": { + "active": false } } } diff --git a/examples/multiwindow/src-tauri/tauri.conf.json b/examples/multiwindow/src-tauri/tauri.conf.json index ae2ce6ae2..3eabeb07b 100644 --- a/examples/multiwindow/src-tauri/tauri.conf.json +++ b/examples/multiwindow/src-tauri/tauri.conf.json @@ -40,6 +40,9 @@ ], "security": { "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'" + }, + "updater": { + "active": false } } } diff --git a/examples/updater/package.json b/examples/updater/package.json new file mode 100644 index 000000000..c59bdc726 --- /dev/null +++ b/examples/updater/package.json @@ -0,0 +1,7 @@ +{ + "name": "updater", + "version": "1.0.0", + "scripts": { + "tauri": "node ../../cli/tauri.js/bin/tauri" + } +} diff --git a/examples/updater/public/__tauri.js b/examples/updater/public/__tauri.js new file mode 100644 index 000000000..2dbd701df --- /dev/null +++ b/examples/updater/public/__tauri.js @@ -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 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, + }, + }); + }; +})(); diff --git a/examples/updater/public/index.html b/examples/updater/public/index.html new file mode 100644 index 000000000..7035e7f7b --- /dev/null +++ b/examples/updater/public/index.html @@ -0,0 +1,12 @@ + + + + + + + Welcome to Tauri! + + +

Welcome to Tauri!

+ + diff --git a/examples/updater/src-tauri/.gitignore b/examples/updater/src-tauri/.gitignore new file mode 100644 index 000000000..270a92d27 --- /dev/null +++ b/examples/updater/src-tauri/.gitignore @@ -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 diff --git a/examples/updater/src-tauri/Cargo.lock b/examples/updater/src-tauri/Cargo.lock new file mode 100644 index 000000000..636f3d7fd --- /dev/null +++ b/examples/updater/src-tauri/Cargo.lock @@ -0,0 +1,3791 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fe5e32de01730eb1f6b7f5b51c17e03e2325bf40a74f754f04f130043affff" + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "andrew" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4afb09dd642feec8408e33f92f3ffc4052946f6b20f32fb99c1f58cd4fa7cf" +dependencies = [ + "bitflags 1.2.1", + "rusttype", + "walkdir", + "xdg", + "xml-rs 0.8.3", +] + +[[package]] +name = "anyhow" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-io" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9315f8f07556761c3e48fec2e6b276004acf426e6dc068b2c2251854d65ee0fd" +dependencies = [ + "concurrent-queue", + "fastrand", + "futures-lite", + "libc", + "log", + "nb-connect", + "once_cell", + "parking", + "polling", + "vec-arena", + "waker-fn", + "winapi 0.3.9", +] + +[[package]] +name = "async-trait" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "atk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812b4911e210bd51b24596244523c856ca749e6223c50a7fbbba3f89ee37c426" +dependencies = [ + "atk-sys", + "bitflags 1.2.1", + "glib", + "glib-sys", + "gobject-sys", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f530e4af131d94cc4fa15c5c9d0348f0ef28bac64ba660b6b2a1cf2605dedfce" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bstr" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" +dependencies = [ + "memchr", +] + +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" + +[[package]] +name = "bytemuck" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58" + +[[package]] +name = "byteorder" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.10+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + +[[package]] +name = "cairo-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c0f2e047e8ca53d0ff249c54ae047931d7a6ebe05d00af73e0ffeb6e34bdb8" +dependencies = [ + "bitflags 1.2.1", + "cairo-sys-rs", + "glib", + "glib-sys", + "gobject-sys", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed2639b9ad5f1d6efa76de95558e11339e7318426d84ac4890b86c03e828ca7" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "calloop" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" +dependencies = [ + "log", + "nix 0.18.0", +] + +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi 0.3.9", +] + +[[package]] +name = "cocoa" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +dependencies = [ + "bitflags 1.2.1", + "block", + "cocoa-foundation", + "core-foundation 0.9.1", + "core-graphics 0.22.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +dependencies = [ + "bitflags 1.2.1", + "block", + "core-foundation 0.9.1", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "com" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a30a2b2a013da986dc5cc3eda3d19c0d59d53f835be1b2356eb8d00f000c793" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7606b05842fea68ddcc89e8053b8860ebcb2a0ba8d6abfe3a148e5d5a8d3f0c1" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.60", +] + +[[package]] +name = "com_macros_support" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97e9a6d20f4ac8830e309a455d7e9416e65c6af5a97c88c55fbb4c2012e107da" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys 0.8.2", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.2.1", + "core-foundation 0.7.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269f35f69b542b80e736a20a89a05215c0ce80c2c03c514abb2e318b78379d86" +dependencies = [ + "bitflags 1.2.1", + "core-foundation 0.9.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +dependencies = [ + "bitflags 1.2.1", + "core-foundation 0.9.1", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "objc", +] + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote 1.0.9", + "strsim", + "syn 1.0.60", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users 0.3.5", + "winapi 0.3.9", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.0", + "winapi 0.3.9", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" +dependencies = [ + "libloading 0.6.7", +] + +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading 0.7.0", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enumflags2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "fastrand" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.5", + "winapi 0.3.9", +] + +[[package]] +name = "flate2" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" +dependencies = [ + "cfg-if 0.1.10", + "crc32fast", + "libc", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.2.1", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" + +[[package]] +name = "futures-executor" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" + +[[package]] +name = "futures-lite" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "futures-sink" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" + +[[package]] +name = "futures-task" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" + +[[package]] +name = "futures-util" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "gdk" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db00839b2a68a7a10af3fa28dfb3febaba3a20c3a9ac2425a33b7df1f84a6b7d" +dependencies = [ + "bitflags 1.2.1", + "cairo-rs", + "cairo-sys-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6dae3cb99dd49b758b88f0132f8d401108e63ae8edd45f432d42cdff99998a" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bfe468a7f43e97b8d193a762b6c5cf67a7d36cacbc0b9291dbcae24bfea1e8f" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9653cfc500fd268015b1ac055ddbc3df7a5c9ea3f4ccef147b3957bd140d69" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "gif" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02efba560f227847cb41463a7395c514d127d4f74fff12ef0137fff1b84b96c4" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gio" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb60242bfff700772dae5d9e3a1f7aa2e4ebccf18b89662a16acb2822568561" +dependencies = [ + "bitflags 1.2.1", + "futures", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e24fb752f8f5d2cf6bbc2c606fd2bc989c81c5e2fe321ab974d54f8b6344eac" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi 0.3.9", +] + +[[package]] +name = "glib" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +dependencies = [ + "bitflags 1.2.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", +] + +[[package]] +name = "glib-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +dependencies = [ + "anyhow", + "heck", + "itertools", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "glib-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "globset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "gobject-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f022f2054072b3af07666341984562c8e626a79daa8be27b955d12d06a5ad6a" +dependencies = [ + "atk", + "bitflags 1.2.1", + "cairo-rs", + "cairo-sys-rs", + "cc", + "gdk", + "gdk-pixbuf", + "gdk-pixbuf-sys", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk-sys", + "libc", + "once_cell", + "pango", + "pango-sys", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89acda6f084863307d948ba64a4b1ef674e8527dddab147ee4cdcc194c880457" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "h2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d832b01df74254fe364568d6ddc294443f61cbec82816b60904303af87efae78" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "httparse" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "hyper" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.23.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "293f07a1875fa7e9c5897b51aa68b2d8ed8271b87e1a44cb64b9c3d98aabbc0d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "infer" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ad0755c42f65a1374dcd0aae07e03dfefc911eceb3f409d2b4a888189447e6" + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "javascriptcore-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc697657edc9cd3d85d5ec6941f74cc9bb2ae84bec320f55c9397c5a8d8722" +dependencies = [ + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46ada8a08dcd75a10afae872fbfb51275df4a8ae0d46b8cc7c708f08dd2998" +dependencies = [ + "libc", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9f84f9b115ce7843d60706df1422a916680bfdfcbdb0447c5614ff9d7e4d78" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" + +[[package]] +name = "libloading" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "mac-notification-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76" +dependencies = [ + "cc", + "chrono", + "dirs", + "objc-foundation", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "memmap2" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minisign-verify" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0507fe8e3c68cd62961cf9f87f6c2b21d884d3515a7150a4a3fa9d014e5c12" + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5dede4e2065b3842b8b0af444119f3aa331cc7cc2dd20388bfb0f5d5a38823a" +dependencies = [ + "libc", + "log", + "miow 0.3.6", + "ntapi", + "winapi 0.3.9", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio 0.6.23", + "slab", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +dependencies = [ + "socket2", + "winapi 0.3.9", +] + +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nb-connect" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670361df1bc2399ee1ff50406a0d422587dd3bb0da596e1978fe8e05dabddf4f" +dependencies = [ + "libc", + "socket2", +] + +[[package]] +name = "ndk" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "ndk-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" +dependencies = [ + "bitflags 1.2.1", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" +dependencies = [ + "bitflags 1.2.1", + "cc", + "cfg-if 0.1.10", + "libc", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags 1.2.1", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "notify-rust" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c16afe17474a42a59062f3409a63160c63d41985b25e9e613400685b839cb6" +dependencies = [ + "mac-notification-sys", + "serde", + "winrt-notification", + "zbus", + "zvariant", + "zvariant_derive", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" + +[[package]] +name = "open" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e9f1bdf15cd1f5a00cc9002a733a6ee6d0ff562491852d59652471c4a389f7" +dependencies = [ + "which", + "winapi 0.3.9", +] + +[[package]] +name = "openssl" +version = "0.10.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" +dependencies = [ + "bitflags 1.2.1", + "cfg-if 1.0.0", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "pango" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9937068580bebd8ced19975938573803273ccbcbd598c58d4906efd4ac87c438" +dependencies = [ + "bitflags 1.2.1", + "glib", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d2650c8b62d116c020abd0cea26a4ed96526afda89b1c4ea567131fdefc890" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.5", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags 1.2.1", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "polling" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a7bc6b2a29e632e45451c941832803a18cce6781db04de8a04696cdca8bde4" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "log", + "wepoll-sys", + "winapi 0.3.9", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid 0.2.1", +] + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha 0.3.0", + "rand_core 0.6.2", + "rand_hc 0.3.0", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.2", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom 0.2.2", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core 0.6.2", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a441a7a6c80ad6473bd4b74ec1c9a4c951794285bf941c2126f607c72e48211" +dependencies = [ + "libc", +] + +[[package]] +name = "rayon" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags 1.2.1", +] + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.2", + "redox_syscall 0.2.5", +] + +[[package]] +name = "regex" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0460542b551950620a3648c6aa23318ac6b3cd779114bd873209e6e8b5eb1c34" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rfd" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3de7c6d5eab0f6b212d1b5a376639d91061bbfbdc2d7c7c5214063bd6ce99581" +dependencies = [ + "block", + "cocoa-foundation", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "objc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rusttype" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfd318104249865096c8da1dfabf09ddbb6d0330ea176812a62ec75e40c4166" +dependencies = [ + "bitflags 1.2.1", + "core-foundation 0.9.1", + "core-foundation-sys 0.8.2", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d" +dependencies = [ + "core-foundation-sys 0.8.2", + "libc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "siphasher" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "smithay-client-toolkit" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "316e13a3eb853ce7bf72ad3530dc186cb2005c57c521ef5f4ada5ee4eed74de6" +dependencies = [ + "andrew", + "bitflags 1.2.1", + "calloop", + "dlib 0.4.2", + "lazy_static", + "log", + "memmap2", + "nix 0.18.0", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "soup-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7adf08565630bbb71f955f11f8a68464817ded2703a3549747c235b58a13e" +dependencies = [ + "bitflags 1.2.1", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "strum" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca6e4730f517e041e547ffe23d29daab8de6b73af4b6ae2a002108169f5e7da" + +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + +[[package]] +name = "strum" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" + +[[package]] +name = "strum_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3384590878eb0cab3b128e844412e2d010821e7e091211b9d87324173ada7db8" +dependencies = [ + "quote 0.3.15", + "syn 0.11.11", +] + +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "strum_macros" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +dependencies = [ + "heck", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid 0.0.4", +] + +[[package]] +name = "syn" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "unicode-xid 0.2.1", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid 0.0.4", +] + +[[package]] +name = "sysinfo" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8834e42be61ae4f6338b216fbb69837c7f33c3d4d3a139fb073735b25af4d9e" +dependencies = [ + "cfg-if 0.1.10", + "doc-comment", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi 0.3.9", +] + +[[package]] +name = "system-deps" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +dependencies = [ + "heck", + "pkg-config", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror", + "toml", + "version-compare", +] + +[[package]] +name = "tar" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bcfbd6a598361fda270d82469fff3d65089dc33e175c9a131f7b4cd395f228" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tauri" +version = "0.11.1" +dependencies = [ + "async-trait", + "base64", + "cfg_aliases", + "futures", + "lazy_static", + "once_cell", + "rand 0.8.3", + "serde", + "serde_json", + "tauri-api", + "tauri-macros", + "tauri-updater", + "thiserror", + "tokio", + "uuid", + "wry", +] + +[[package]] +name = "tauri-api" +version = "0.7.5" +dependencies = [ + "bytes", + "dirs-next", + "either", + "flate2", + "http", + "ignore", + "notify-rust", + "once_cell", + "open", + "rand 0.8.3", + "reqwest", + "rfd", + "semver", + "serde", + "serde_json", + "serde_repr", + "tar", + "tauri-hotkey", + "tauri-utils", + "tempfile", + "thiserror", + "tinyfiledialogs", + "zip", +] + +[[package]] +name = "tauri-codegen" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "serde", + "serde_json", + "tauri-api", + "thiserror", + "walkdir", + "zstd", +] + +[[package]] +name = "tauri-hotkey" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/tauri-hotkey-rs?branch=dev#ccd96947c350d6dd40dc927b49d13599a311fca3" +dependencies = [ + "log", + "once_cell", + "serde", + "strum 0.20.0", + "strum_macros 0.20.1", + "tauri-hotkey-sys", + "thiserror", +] + +[[package]] +name = "tauri-hotkey-sys" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/tauri-hotkey-rs?branch=dev#ccd96947c350d6dd40dc927b49d13599a311fca3" +dependencies = [ + "cc", + "thiserror", + "winapi 0.3.9", + "x11-dl", +] + +[[package]] +name = "tauri-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", + "tauri-codegen", +] + +[[package]] +name = "tauri-updater" +version = "0.4.2" +dependencies = [ + "anyhow", + "base64", + "minisign-verify", + "reqwest", + "semver", + "serde", + "serde_json", + "tauri-api", + "tauri-utils", + "tempfile", + "thiserror", +] + +[[package]] +name = "tauri-utils" +version = "0.5.1" +dependencies = [ + "phf", + "proc-macro2", + "quote 1.0.9", + "serde", + "serde_json", + "sysinfo", + "thiserror", + "zstd", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.3", + "redox_syscall 0.2.5", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "tinyfiledialogs" +version = "3.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c45fb26c3f37d9a8b556e51f6d7f13f685af766017030af56e9247e638aa6194" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tinyvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d56477f6ed99e10225f38f9f75f872f29b8b8bd8c0b946f63345bb144e9eeda" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio 0.7.9", + "num_cpus", + "pin-project-lite", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb7cb2f00c5ae8df755b252306272cd1790d39728363936e01827e11f0b017b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "ttf-parser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "updater-example" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "winres", +] + +[[package]] +name = "url" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.2", +] + +[[package]] +name = "vcpkg" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" + +[[package]] +name = "vec-arena" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d" + +[[package]] +name = "version-compare" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee1280240b7c461d6a0071313e08f34a60b0365f14260362e5a2b17d1d31aa7" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7d8b6942b8bb3a9b0e73fc79b98095a27de6fa247615e59d096754a3bc2aa8" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e67a5806118af01f0d9045915676b22aaebecf4178ae7021bc171dab0b897ab" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ac38da8ef716661f0f36c0d8320b89028efe10c7c0afde65baffb496ce0d3b" +dependencies = [ + "quote 1.0.9", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc053ec74d454df287b9374ee8abb36ffd5acb95ba87da3ba5b7d3fe20eb401e" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f8ec44822dd71f5f221a5847fb34acd9060535c1211b70a05844c0f6383b1" + +[[package]] +name = "wayland-client" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ca44d86554b85cf449f1557edc6cc7da935cc748c8e4bf1c507cbd43bae02c" +dependencies = [ + "bitflags 1.2.1", + "downcast-rs", + "libc", + "nix 0.20.0", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd75ae380325dbcff2707f0cd9869827ea1d2d6d534cff076858d3f0460fd5a" +dependencies = [ + "nix 0.20.0", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b37e5455ec72f5de555ec39b5c3704036ac07c2ecd50d0bffe02d5fe2d4e65ab" +dependencies = [ + "nix 0.20.0", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95df3317872bcf9eec096c864b69aa4769a1d5d6291a5b513f8ba0af0efbd52c" +dependencies = [ + "bitflags 1.2.1", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389d680d7bd67512dc9c37f39560224327038deb0f0e8d33f870900441b68720" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "xml-rs 0.8.3", +] + +[[package]] +name = "wayland-sys" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2907bd297eef464a95ba9349ea771611771aa285b932526c633dc94d5400a8e2" +dependencies = [ + "dlib 0.5.0", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec600b26223b2948cedfde2a0aa6756dcf1fef616f43d7b3097aaf53a6c4d92b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b7e9eb04d30f8423e9c8435f686f42bc497cfcac2cfe4b43ce4139fb1a7cb6" +dependencies = [ + "bitflags 1.2.1", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d10cf73685359cd8611740db241a231f4d74d7e353348dc5332a1a132d6f24" +dependencies = [ + "atk-sys", + "bitflags 1.2.1", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup-sys", +] + +[[package]] +name = "webview2" +version = "0.1.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c0fa7367f4de0e4bca5730a297027f15d2e1f06227809efa47621eb55f08610" +dependencies = [ + "com", + "once_cell", + "webview2-sys", + "widestring", + "winapi 0.3.9", +] + +[[package]] +name = "webview2-sys" +version = "0.1.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53f9c920933ddff6481d525c3e0d14327843c82074c44522b97dcf6da5ad456" +dependencies = [ + "com", + "winapi 0.3.9", +] + +[[package]] +name = "weezl" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a32b378380f4e9869b22f0b5177c68a5519f03b3454fde0b291455ddbae266c" + +[[package]] +name = "wepoll-sys" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb14dea929042224824779fbc82d9fab8d2e6d3cbc0ac404de8edf489e77ff" +dependencies = [ + "cc", +] + +[[package]] +name = "which" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c14ef7e1b8b8ecfc75d5eca37949410046e66f15d185c01d70824f1f8111ef" +dependencies = [ + "libc", + "thiserror", +] + +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winit" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4eda6fce0eb84bd0a33e3c8794eb902e1033d0a1d5a31bc4f19b1b4bbff597" +dependencies = [ + "bitflags 1.2.1", + "cocoa", + "core-foundation 0.9.1", + "core-graphics 0.22.2", + "core-video-sys", + "dispatch", + "instant", + "lazy_static", + "libc", + "log", + "mio 0.6.23", + "mio-extras", + "ndk", + "ndk-glue", + "ndk-sys", + "objc", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "smithay-client-toolkit", + "wayland-client", + "winapi 0.3.9", + "x11-dl", +] + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winres" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4fb510bbfe5b8992ff15f77a2e6fe6cf062878f0eda00c0f44963a807ca5dc" +dependencies = [ + "toml", +] + +[[package]] +name = "winrt" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30cba82e22b083dc5a422c2ee77e20dc7927271a0dc981360c57c1453cb48d" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winrt-notification" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57790eb281688a4682dab44df2a1ba8b78373233bd71cb291c3e75fecb1a01c4" +dependencies = [ + "strum 0.8.0", + "strum_macros 0.8.0", + "winapi 0.3.9", + "winrt", + "xml-rs 0.6.1", +] + +[[package]] +name = "wry" +version = "0.6.0" +source = "git+https://github.com/tauri-apps/wry?rev=39575983dbd128fbbcde933d742b33b691fd1def#39575983dbd128fbbcde933d742b33b691fd1def" +dependencies = [ + "cairo-rs", + "cocoa", + "core-graphics 0.22.2", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk", + "image", + "infer", + "libc", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "thiserror", + "url", + "webkit2gtk", + "webview2", + "winapi 0.3.9", + "winit", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "x11-dl" +version = "2.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8" +dependencies = [ + "lazy_static", + "libc", + "maybe-uninit", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + +[[package]] +name = "xcursor" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9a231574ae78801646617cefd13bfe94be907c0e4fa979cfd8b770aa3c5d08" +dependencies = [ + "nom", +] + +[[package]] +name = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" + +[[package]] +name = "xml-rs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1945e12e16b951721d7976520b0832496ef79c31602c7a29d950de79ba74621" +dependencies = [ + "bitflags 0.9.1", +] + +[[package]] +name = "xml-rs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" + +[[package]] +name = "zbus" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b4d4aa39daed4e32aed75f0c37b969184949a0fdfd5f2e1277abfda61f02a8" +dependencies = [ + "async-io", + "byteorder", + "derivative", + "enumflags2", + "fastrand", + "futures", + "nb-connect", + "nix 0.17.0", + "once_cell", + "polling", + "scoped-tls", + "serde", + "serde_repr", + "zbus_macros", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc141cda72384bef359badf1808e391d3968f9299e8f3c3cbb78dafa1e0930" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "zip" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8264fcea9b7a036a4a5103d7153e988dbc2ebbafb34f68a3c2d404b6b82d74b6" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time", +] + +[[package]] +name = "zstd" +version = "0.6.1+zstd.1.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de55e77f798f205d8561b8fe2ef57abfb6e0ff2abe7fd3c089e119cdb5631a3" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "3.0.1+zstd.1.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1387cabcd938127b30ce78c4bf00b30387dddf704e3f0881dbc4ff62b5566f8c" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.4.20+zstd.1.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd5b733d7cf2d9447e2c3e76a5589b4f5e5ae065c22a2bc0b023cbc331b6c8e" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "zvariant" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc67d552ac18ccd9e440f062f5b32c46776f96073122a8da2fe0c533833a213" +dependencies = [ + "byteorder", + "enumflags2", + "serde", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaee686340b5bff077d52423d8cc4f0f7cb323fe3f31ef676b8a3a2810bc53c5" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote 1.0.9", + "syn 1.0.60", +] diff --git a/examples/updater/src-tauri/Cargo.toml b/examples/updater/src-tauri/Cargo.toml new file mode 100644 index 000000000..3808ab185 --- /dev/null +++ b/examples/updater/src-tauri/Cargo.toml @@ -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" diff --git a/examples/updater/src-tauri/build.rs b/examples/updater/src-tauri/build.rs new file mode 100644 index 000000000..311900de5 --- /dev/null +++ b/examples/updater/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/examples/updater/src-tauri/icons/128x128.png b/examples/updater/src-tauri/icons/128x128.png new file mode 100644 index 000000000..f8d9962cc Binary files /dev/null and b/examples/updater/src-tauri/icons/128x128.png differ diff --git a/examples/updater/src-tauri/icons/128x128@2x.png b/examples/updater/src-tauri/icons/128x128@2x.png new file mode 100644 index 000000000..44d26d04b Binary files /dev/null and b/examples/updater/src-tauri/icons/128x128@2x.png differ diff --git a/examples/updater/src-tauri/icons/32x32.png b/examples/updater/src-tauri/icons/32x32.png new file mode 100644 index 000000000..b6bf7d610 Binary files /dev/null and b/examples/updater/src-tauri/icons/32x32.png differ diff --git a/examples/updater/src-tauri/icons/Square107x107Logo.png b/examples/updater/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 000000000..6146c3354 Binary files /dev/null and b/examples/updater/src-tauri/icons/Square107x107Logo.png differ diff --git a/examples/updater/src-tauri/icons/Square142x142Logo.png b/examples/updater/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 000000000..e24cb2a75 Binary files /dev/null and b/examples/updater/src-tauri/icons/Square142x142Logo.png differ diff --git a/examples/updater/src-tauri/icons/Square150x150Logo.png b/examples/updater/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 000000000..a389fb4a6 Binary files /dev/null and b/examples/updater/src-tauri/icons/Square150x150Logo.png differ diff --git a/examples/updater/src-tauri/icons/Square284x284Logo.png b/examples/updater/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 000000000..c43c42ffd Binary files /dev/null and b/examples/updater/src-tauri/icons/Square284x284Logo.png differ diff --git a/examples/updater/src-tauri/icons/Square30x30Logo.png b/examples/updater/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 000000000..493f155ac Binary files /dev/null and b/examples/updater/src-tauri/icons/Square30x30Logo.png differ diff --git a/examples/updater/src-tauri/icons/Square310x310Logo.png b/examples/updater/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 000000000..6380a98dd Binary files /dev/null and b/examples/updater/src-tauri/icons/Square310x310Logo.png differ diff --git a/examples/updater/src-tauri/icons/Square44x44Logo.png b/examples/updater/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 000000000..61e4eff35 Binary files /dev/null and b/examples/updater/src-tauri/icons/Square44x44Logo.png differ diff --git a/examples/updater/src-tauri/icons/Square71x71Logo.png b/examples/updater/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 000000000..668945bab Binary files /dev/null and b/examples/updater/src-tauri/icons/Square71x71Logo.png differ diff --git a/examples/updater/src-tauri/icons/Square89x89Logo.png b/examples/updater/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 000000000..1e14926fd Binary files /dev/null and b/examples/updater/src-tauri/icons/Square89x89Logo.png differ diff --git a/examples/updater/src-tauri/icons/StoreLogo.png b/examples/updater/src-tauri/icons/StoreLogo.png new file mode 100644 index 000000000..1f20ed7b0 Binary files /dev/null and b/examples/updater/src-tauri/icons/StoreLogo.png differ diff --git a/examples/updater/src-tauri/icons/icon.icns b/examples/updater/src-tauri/icons/icon.icns new file mode 100644 index 000000000..43c73bda9 Binary files /dev/null and b/examples/updater/src-tauri/icons/icon.icns differ diff --git a/examples/updater/src-tauri/icons/icon.ico b/examples/updater/src-tauri/icons/icon.ico new file mode 100644 index 000000000..db7fd9820 Binary files /dev/null and b/examples/updater/src-tauri/icons/icon.ico differ diff --git a/examples/updater/src-tauri/icons/icon.png b/examples/updater/src-tauri/icons/icon.png new file mode 100644 index 000000000..e65ea7e86 Binary files /dev/null and b/examples/updater/src-tauri/icons/icon.png differ diff --git a/examples/updater/src-tauri/src/main.rs b/examples/updater/src-tauri/src/main.rs new file mode 100644 index 000000000..50571a001 --- /dev/null +++ b/examples/updater/src-tauri/src/main.rs @@ -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"); +} diff --git a/examples/updater/src-tauri/tauri.conf.json b/examples/updater/src-tauri/tauri.conf.json new file mode 100644 index 000000000..19ad2c2be --- /dev/null +++ b/examples/updater/src-tauri/tauri.conf.json @@ -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}}" + ] + } + } +} diff --git a/examples/updater/yarn.lock b/examples/updater/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/examples/updater/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/tauri-api/Cargo.toml b/tauri-api/Cargo.toml index 1075bb6ee..e364ea8c0 100644 --- a/tauri-api/Cargo.toml +++ b/tauri-api/Cargo.toml @@ -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" diff --git a/tauri-api/src/error.rs b/tauri-api/src/error.rs index bcaa30cde..1a6b1c4be 100644 --- a/tauri-api/src/error.rs +++ b/tauri-api/src/error.rs @@ -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), diff --git a/tauri-api/src/file/file_move.rs b/tauri-api/src/file/file_move.rs index 71c6f1eea..e583f04fe 100644 --- a/tauri-api/src/file/file_move.rs +++ b/tauri-api/src/file/file_move.rs @@ -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(()) } diff --git a/tauri-api/src/lib.rs b/tauri-api/src/lib.rs index df16a86df..ab64a7cdc 100644 --- a/tauri-api/src/lib.rs +++ b/tauri-api/src/lib.rs @@ -49,6 +49,15 @@ pub use error::Error; /// Tauri API result type. pub type Result = std::result::Result; +/// `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; } } diff --git a/tauri-api/src/version.rs b/tauri-api/src/version.rs index 61f007b4e..d08010c5d 100644 --- a/tauri-api/src/version.rs +++ b/tauri-api/src/version.rs @@ -47,3 +47,8 @@ pub fn is_patch(current: &str, other: &str) -> crate::Result { 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 { + Ok(Version::parse(other)? > Version::parse(current)?) +} diff --git a/tauri-updater/Cargo.toml b/tauri-updater/Cargo.toml index f1367ce27..d69e927f5 100644 --- a/tauri-updater/Cargo.toml +++ b/tauri-updater/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tauri-updater" version = "0.4.2" -authors = ["Lucas Fernandes Gonçalves Nogueira ", "Daniel Thompson-Yvetot ", "Tensor Programming "] +authors = ["Lucas Fernandes Gonçalves Nogueira ", "Daniel Thompson-Yvetot ", "Tensor Programming ", "David Lemarier "] 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"] diff --git a/tauri-updater/README.md b/tauri-updater/README.md new file mode 100644 index 000000000..123e3dcf4 --- /dev/null +++ b/tauri-updater/README.md @@ -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 Set private key password when signing + -k, --private-key Load the private key from a string + -f, --private-key-path Load the private key from a file + --sign-file Sign the specified file + -w, --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) + diff --git a/tauri-updater/src/error.rs b/tauri-updater/src/error.rs new file mode 100644 index 000000000..e7ec8b101 --- /dev/null +++ b/tauri-updater/src/error.rs @@ -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 = std::result::Result; diff --git a/tauri-updater/src/http.rs b/tauri-updater/src/http.rs deleted file mode 100644 index 36d7a3993..000000000 --- a/tauri-updater/src/http.rs +++ /dev/null @@ -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 { - let response = attohttpc::get(url).send()?; - - Ok(response) -} - -pub fn post_as_json(url: String, payload: &T) -> crate::Result { - let response = attohttpc::post(url).json(payload)?.send()?; - - Ok(response) -} - -pub fn download(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(()) -} diff --git a/tauri-updater/src/http/link_value.rs b/tauri-updater/src/http/link_value.rs deleted file mode 100644 index acdb41954..000000000 --- a/tauri-updater/src/http/link_value.rs +++ /dev/null @@ -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>, -} - -impl LinkValue { - pub fn new(uri: T) -> LinkValue - where - T: Into>, - { - 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), -} diff --git a/tauri-updater/src/lib.rs b/tauri-updater/src/lib.rs index 47d159cb9..c0818e1c1 100644 --- a/tauri-updater/src/lib.rs +++ b/tauri-updater/src/lib.rs @@ -1,19 +1,1075 @@ #[macro_use] -pub mod macros; -pub mod http; -pub mod updater; +pub mod error; +use base64::decode; +pub use error::{Error, Result}; +use minisign_verify::{PublicKey, Signature}; +use reqwest::{self, header, StatusCode}; +use std::{ + env, + ffi::OsStr, + fs::{read_dir, remove_file, File, OpenOptions}, + io::{prelude::*, BufReader, Read}, + path::{Path, PathBuf}, + str::from_utf8, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use tauri_api::{file::Extract, version}; -pub use anyhow::Result; -use thiserror::Error; +#[cfg(not(target_os = "macos"))] +use std::process::Command; -#[derive(Error, Debug)] -pub enum Error { - #[error("Download request failed with status:{0}")] - Download(attohttpc::StatusCode), - #[error("Failed to determine parent dir")] - Updater, - #[error("Network Error:{0}")] - Network(String), - #[error("Config Error:{0} required")] - Config(String), +#[cfg(target_os = "macos")] +use tauri_api::file::Move; + +#[cfg(target_os = "windows")] +use std::process::exit; + +#[derive(Debug)] +pub struct RemoteRelease { + /// Version to install + pub version: String, + /// Release date + pub date: String, + /// Download URL for current platform + pub download_url: String, + /// Update short description + pub body: Option, + /// Optional signature for the current platform + pub signature: Option, +} + +impl RemoteRelease { + // Read JSON and confirm this is a valid Schema + fn from_release(release: &serde_json::Value, target: &str) -> Result { + // Version or name is required for static and dynamic JSON + // if `version` is not announced, we fallback to `name` (can be the tag name example v1.0.0) + let version = match release.get("version") { + Some(version) => version + .as_str() + .ok_or_else(|| { + Error::RemoteMetadata("Unable to extract `version` from remote server".into()) + })? + .trim_start_matches('v') + .to_string(), + None => release + .get("name") + .ok_or_else(|| Error::RemoteMetadata("Release missing `name` and `version`".into()))? + .as_str() + .ok_or_else(|| { + Error::RemoteMetadata("Unable to extract `name` from remote server`".into()) + })? + .trim_start_matches('v') + .to_string(), + }; + + // pub_date is required default is: `N/A` if not provided by the remote JSON + let date = match release.get("pub_date") { + Some(pub_date) => pub_date.as_str().unwrap_or("N/A").to_string(), + None => "N/A".to_string(), + }; + + // body is optional to build our update + let body = match release.get("notes") { + Some(notes) => Some(notes.as_str().unwrap_or("").to_string()), + None => None, + }; + + // signature is optional to build our update + let mut signature = match release.get("signature") { + Some(signature) => Some(signature.as_str().unwrap_or("").to_string()), + None => None, + }; + + let download_url; + + match release.get("platforms") { + // + // Did we have a platforms field? + // If we did, that mean it's a static JSON. + // The main difference with STATIC and DYNAMIC is static announce ALL platforms + // and dynamic announce only the current platform. + // + // This could be used if you do NOT want an update server and use + // a GIST, S3 or any static JSON file to announce your updates. + // + // Notes: + // Dynamic help to reduce bandwidth usage or to intelligently update your clients + // based on the request you give. The server can remotely drive behaviors like + // rolling back or phased rollouts. + // + Some(platforms) => { + // make sure we have our target available + if let Some(current_target_data) = platforms.get(target) { + // use provided signature if available + signature = match current_target_data.get("signature") { + Some(found_signature) => Some(found_signature.as_str().unwrap_or("").to_string()), + None => None, + }; + // Download URL is required + download_url = current_target_data + .get("url") + .ok_or_else(|| Error::RemoteMetadata("Release missing `url`".into()))? + .as_str() + .ok_or_else(|| { + Error::RemoteMetadata("Unable to extract `url` from remote server`".into()) + })? + .to_string(); + } else { + // make sure we have an available platform from the static + return Err(Error::RemoteMetadata("Platform not available".into())); + } + } + // We don't have the `platforms` field announced, let's assume our + // download URL is at the root of the JSON. + None => { + download_url = release + .get("url") + .ok_or_else(|| Error::RemoteMetadata("Release missing `url`".into()))? + .as_str() + .ok_or_else(|| { + Error::RemoteMetadata("Unable to extract `url` from remote server`".into()) + })? + .to_string(); + } + } + // Return our formatted release + Ok(RemoteRelease { + version, + download_url, + date, + signature, + body, + }) + } +} + +pub struct UpdateBuilder<'a> { + /// Current version we are running to compare with announced version + pub current_version: &'a str, + /// The URLs to checks updates. We suggest at least one fallback on a different domain. + pub urls: Vec, + /// The platform the updater will check and install the update. Default is from `get_updater_target` + pub target: Option, + /// The current executable path. Default is automatically extracted. + pub executable_path: Option, +} + +impl<'a> Default for UpdateBuilder<'a> { + fn default() -> Self { + UpdateBuilder { + urls: Vec::new(), + target: None, + executable_path: None, + current_version: env!("CARGO_PKG_VERSION"), + } + } +} + +// Create new updater instance and return an Update +impl<'a> UpdateBuilder<'a> { + pub fn new() -> Self { + UpdateBuilder::default() + } + + pub fn url(mut self, url: String) -> Self { + self.urls.push(url); + self + } + + /// Add multiple URLS at once inside a Vec for future reference + pub fn urls(mut self, urls: &[String]) -> Self { + let mut formatted_vec: Vec = Vec::new(); + for url in urls { + formatted_vec.push(url.to_owned()); + } + self.urls = formatted_vec; + 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: &'a str) -> Self { + self.current_version = ver; + self + } + + /// Set the target (os) + /// win32, win64, darwin and linux are currently supported + pub fn target(mut self, target: &str) -> Self { + self.target = Some(target.to_owned()); + self + } + + /// Set the executable path + pub fn executable_path>(mut self, executable_path: A) -> Self { + self.executable_path = Some(PathBuf::from(executable_path.as_ref())); + self + } + + pub async fn build(self) -> Result { + let mut remote_release: Option = None; + + // make sure we have at least one url + if self.urls.is_empty() { + return Err(Error::Builder( + "Unable to check update, `url` is required.".into(), + )); + }; + + // set current version if not set + let current_version = self.current_version; + + // If no executable path provided, we use current_exe from rust + let executable_path = if let Some(v) = &self.executable_path { + v.clone() + } else { + // we expect it to fail if we can't find the executable path + // without this path we can't continue the update process. + env::current_exe().expect("Can't access current executable path.") + }; + + // Did the target is provided by the config? + // Should be: linux, darwin, win32 or win64 + let target = if let Some(t) = &self.target { + t.clone() + } else { + get_updater_target().ok_or(Error::UnsupportedPlatform)? + }; + + // Get the extract_path from the provided executable_path + let extract_path = extract_path_from_executable(&executable_path); + + // current binary used to restart the application + #[cfg(target_os = "windows")] + let current_binary = None; + + #[cfg(not(target_os = "windows"))] + let mut current_binary = None; + + #[cfg(target_os = "linux")] + if let Some(app_image_path) = env::var_os("APPIMAGE") { + current_binary = Some(PathBuf::from(app_image_path)); + } + + #[cfg(target_os = "macos")] + if let Ok(current_process) = std::env::current_exe() { + current_binary = Some(current_process); + } + + // Set SSL certs for linux if they aren't available. + // We do not require to recheck in the download_and_install as we use + // ENV variables, we can expect them to be set for the second call. + #[cfg(target_os = "linux")] + { + if env::var_os("SSL_CERT_FILE").is_none() { + env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); + } + if env::var_os("SSL_CERT_DIR").is_none() { + env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); + } + } + + // Allow fallback if more than 1 urls is provided + let mut last_error: Option = None; + for url in &self.urls { + // replace {{current_version}} and {{target}} in the provided URL + // this is usefull if we need to query example + // https://releases.myapp.com/update/{{target}}/{{current_version}} + // will be transleted into -> + // https://releases.myapp.com/update/darwin/1.0.0 + // The main objective is if the update URL is defined via the Cargo.toml + // the URL will be generated dynamicly + let fixed_link = str::replace( + &str::replace(url, "{{current_version}}", ¤t_version), + "{{target}}", + &target, + ); + + // we want JSON only + let mut headers = header::HeaderMap::new(); + headers.insert(header::ACCEPT, "application/json".parse().unwrap()); + + let resp = reqwest::Client::new() + .get(&fixed_link) + .headers(headers) + // wait 20sec for the firewall + .timeout(Duration::from_secs(20)) + .send() + .await; + + // If we got a success, we stop the loop + // and we set our remote_release variable + if let Ok(ref res) = resp { + // got status code 2XX + if res.status().is_success() { + // if we got 204 + if StatusCode::NO_CONTENT == res.status() { + // return with `UpToDate` error + // we should catch on the client + return Err(Error::UpToDate); + }; + let json = resp?.json::().await?; + // Convert the remote result to our local struct + let built_release = RemoteRelease::from_release(&json, &target); + // make sure all went well and the remote data is compatible + // with what we need locally + match built_release { + Ok(release) => { + last_error = None; + remote_release = Some(release); + break; + } + Err(err) => last_error = Some(err), + } + } // if status code is not 2XX we keep loopin' our urls + } + } + + // Last error is cleaned on success -- shouldn't be triggered if + // we have a successful call + if let Some(error) = last_error { + return Err(Error::Network(error.to_string())); + } + + // Extracted remote metadata + let final_release = remote_release.ok_or_else(|| { + Error::RemoteMetadata("Unable to extract update metadata from the remote server.".into()) + })?; + + // did the announced version is greated than our current one? + let should_update = + version::is_greater(¤t_version, &final_release.version).unwrap_or(false); + + // create our new updater + Ok(Update { + target, + extract_path, + should_update, + version: final_release.version, + date: final_release.date, + current_version: self.current_version.to_owned(), + download_url: final_release.download_url, + body: final_release.body, + signature: final_release.signature, + current_binary, + }) + } +} + +pub fn builder<'a>() -> UpdateBuilder<'a> { + UpdateBuilder::new() +} + +#[derive(Clone)] +pub struct Update { + /// Update description + pub body: Option, + /// Should we update or not + pub should_update: bool, + /// Version announced + pub version: String, + /// Running version + pub current_version: String, + /// Update publish date + pub date: String, + /// If announced, this is the process to start once the + /// update is completed. On linux it return the APPImage Path and macos + /// the current executable path. On windows we exit before this step. + pub current_binary: Option, + /// Target + target: String, + /// Extract path + extract_path: PathBuf, + /// Download URL announced + download_url: String, + /// Signature announced + signature: Option, +} + +impl Update { + // Download and install our update + // @todo(lemarier): Split into download and install (two step) but need to be thread safe + pub async fn download_and_install(&self, pub_key: Option) -> Result { + // download url for selected release + let url = self.download_url.clone(); + // extract path + let extract_path = self.extract_path.clone(); + + // make sure we can install the update on linux + // We fail here because later we can add more linux support + // actually if we use APPIMAGE, our extract path should already + // be set with our APPIMAGE env variable, we don't need to do + // anythin with it yet + #[cfg(target_os = "linux")] + if env::var_os("APPIMAGE").is_none() { + return Err(Error::UnsupportedPlatform); + } + + // used for temp file name + // if we cant extract app name, we use unix epoch duration + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Unable to get Unix Epoch") + .subsec_nanos() + .to_string(); + + // get the current app name + let bin_name = std::env::current_exe() + .ok() + .and_then(|pb| pb.file_name().map(|s| s.to_os_string())) + .and_then(|s| s.into_string().ok()) + .unwrap_or_else(|| current_time.clone()); + + // tmp dir for extraction + let tmp_dir = tempfile::Builder::new() + .prefix(&format!("{}_{}_download", bin_name, current_time)) + .tempdir()?; + + // tmp directories are used to create backup of current application + // if something goes wrong, we can restore to previous state + let tmp_archive_path = tmp_dir.path().join(detect_archive_in_url(&url)); + let mut tmp_archive = File::create(&tmp_archive_path)?; + + // set our headers + let mut headers = header::HeaderMap::new(); + headers.insert(header::ACCEPT, "application/octet-stream".parse().unwrap()); + + // make sure we have a valid agent + if !headers.contains_key(header::USER_AGENT) { + headers.insert( + header::USER_AGENT, + "tauri/updater".parse().expect("invalid user-agent"), + ); + } + + // Create our request + let resp = reqwest::Client::new() + .get(&url) + // wait 20sec for the firewall + .timeout(Duration::from_secs(20)) + .headers(headers) + .send() + .await?; + + // make sure it's success + if !resp.status().is_success() { + return Err(Error::Network(format!( + "Download request failed with status: {}", + resp.status() + ))); + } + + tmp_archive.write_all(&resp.bytes().await?)?; + + // Validate signature ONLY if pubkey is available in tauri.conf.json + if let Some(pub_key) = pub_key { + // We need an announced signature by the server + // if there is no signature, bail out. + if let Some(signature) = self.signature.clone() { + // we make sure the archive is valid and signed with the private key linked with the publickey + verify_signature(&tmp_archive_path, signature, &pub_key)?; + } else { + // We have a public key inside our source file, but not announced by the server, + // we assume this update is NOT valid. + return Err(Error::PubkeyButNoSignature); + } + } + // extract using tauri api inside a tmp path + Extract::from_source(&tmp_archive_path).extract_into(&tmp_dir.path())?; + // Remove archive (not needed anymore) + remove_file(&tmp_archive_path)?; + // we copy the files depending of the operating system + // we run the setup, appimage re-install or overwrite the + // macos .app + copy_files_and_run(tmp_dir, extract_path)?; + // We are done! + Ok(()) + } +} + +// Linux (AppImage) + +// ### Expected structure: +// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler +// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage +// └── ... + +// We should have an AppImage already installed to be able to copy and install +// the extract_path is the current AppImage path +// tmp_dir is where our new AppImage is found + +#[cfg(target_os = "linux")] +fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result { + // we delete our current AppImage (we'll create a new one later) + remove_file(&extract_path)?; + + // In our tempdir we expect 1 directory (should be the .app) + let paths = read_dir(&tmp_dir).unwrap(); + + for path in paths { + let found_path = path.expect("Unable to extract").path(); + // make sure it's our .AppImage + if found_path.extension() == Some(OsStr::new("AppImage")) { + // Simply overwrite our AppImage (we use the command) + // because it prevent failing of bytes stream + Command::new("mv") + .arg("-f") + .arg(&found_path) + .arg(&extract_path) + .status()?; + + // early finish we have everything we need here + return Ok(()); + } + } + + Ok(()) +} + +// Windows + +// ### Expected structure: +// ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler +// │ └──[AppName]_[version]_x64.msi # Application MSI +// └── ... + +// ## MSI +// Update server can provide a MSI for Windows. (Generated with tauri-bundler from *Wix*) +// To replace current version of the application. In later version we'll offer +// incremental update to push specific binaries. + +// ## EXE +// Update server can provide a custom EXE (installer) who can run any task. + +#[cfg(target_os = "windows")] +fn copy_files_and_run(tmp_dir: tempfile::TempDir, _extract_path: PathBuf) -> Result { + let paths = read_dir(&tmp_dir).unwrap(); + // This consumes the TempDir without deleting directory on the filesystem, + // meaning that the directory will no longer be automatically deleted. + tmp_dir.into_path(); + for path in paths { + let found_path = path.expect("Unable to extract").path(); + // we support 2 type of files exe & msi for now + // If it's an `exe` we expect an installer not a runtime. + if found_path.extension() == Some(OsStr::new("exe")) { + // Run the EXE + Command::new(found_path) + .spawn() + .expect("installer failed to start"); + + exit(0); + } else if found_path.extension() == Some(OsStr::new("msi")) { + // restart should be handled by WIX as we exit the process + Command::new("msiexec.exe") + .arg("/i") + .arg(found_path) + // quiet basic UI with prompt at the end + .arg("/qb+") + .spawn() + .expect("installer failed to start"); + + exit(0); + } + } + + Ok(()) +} + +// MacOS + +// ### Expected structure: +// ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler +// │ └──[AppName].app # Main application +// │ └── Contents # Application contents... +// │ └── ... +// └── ... + +#[cfg(target_os = "macos")] +fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result { + // In our tempdir we expect 1 directory (should be the .app) + let paths = read_dir(&tmp_dir).unwrap(); + + for path in paths { + let found_path = path.expect("Unable to extract").path(); + // make sure it's our .app + if found_path.extension() == Some(OsStr::new("app")) { + // Walk the temp dir and copy all files by replacing existing files only + // and creating directories if needed + Move::from_source(&found_path).walk_to_dest(&extract_path)?; + // early finish we have everything we need here + return Ok(()); + } + } + + Ok(()) +} + +/// Returns a target os +/// We do not use a helper function like the target_triple +/// from tauri-utils because this function return `None` if +/// the updater do not support the platform. +/// +/// Available target: `linux, darwin, win32, win64` +pub fn get_updater_target() -> Option { + if cfg!(target_os = "linux") { + Some("linux".into()) + } else if cfg!(target_os = "macos") { + Some("darwin".into()) + } else if cfg!(target_os = "windows") { + if cfg!(target_pointer_width = "32") { + Some("win32".into()) + } else { + Some("win64".into()) + } + } else { + None + } +} + +/// Get the extract_path from the provided executable_path +pub fn extract_path_from_executable(executable_path: &PathBuf) -> PathBuf { + // Return the path of the current executable by default + // Example C:\Program Files\My App\ + let extract_path = executable_path + .parent() + .map(PathBuf::from) + .expect("Can't determine extract path"); + + // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp + // We need to get /Applications/TestApp.app + // todo(lemarier): Need a better way here + // Maybe we could search for <*.app> to get the right path + #[cfg(target_os = "macos")] + if extract_path + .display() + .to_string() + .contains("Contents/MacOS") + { + return extract_path + .parent() + .map(PathBuf::from) + .expect("Unable to find the extract path") + .parent() + .map(PathBuf::from) + .expect("Unable to find the extract path"); + } + + // We should use APPIMAGE exposed env variable + // This is where our APPIMAGE should sit and should be replaced + #[cfg(target_os = "linux")] + if let Some(app_image_path) = env::var_os("APPIMAGE") { + return PathBuf::from(app_image_path); + } + + extract_path +} + +// Return the archive type to save on disk +fn detect_archive_in_url(path: &str) -> String { + path + .split('/') + .next_back() + .unwrap_or(&default_archive_name_by_os()) + .to_string() +} + +// Fallback archive name by os +// The main objective is to provide the right extension based on the target +// if we cant extract the archive type in the url we'll fallback to this value +fn default_archive_name_by_os() -> String { + #[cfg(target_os = "windows")] + { + "update.zip".into() + } + + #[cfg(not(target_os = "windows"))] + { + "update.tar.gz".into() + } +} + +// Convert base64 to string and prevent failing +fn base64_to_string(base64_string: &str) -> Result { + let decoded_string = &decode(base64_string.to_owned())?; + let result = from_utf8(&decoded_string)?.to_string(); + Ok(result) +} + +// Validate signature +// need to be public because its been used +// by our tests in the bundler +pub fn verify_signature( + archive_path: &PathBuf, + release_signature: String, + pub_key: &str, +) -> Result { + // we need to convert the pub key + let pub_key_decoded = &base64_to_string(pub_key)?; + let public_key = PublicKey::decode(pub_key_decoded)?; + let signature_base64_decoded = base64_to_string(&release_signature)?; + + let signature = + Signature::decode(&signature_base64_decoded).expect("Something wrong with the signature"); + + // We need to open the file and extract the datas to make sure its not corrupted + let file_open = OpenOptions::new() + .read(true) + .open(&archive_path) + .expect("Can't open our archive to validate signature"); + + let mut file_buff: BufReader = BufReader::new(file_open); + + // read all bytes since EOF in the buffer + let mut data = vec![]; + file_buff + .read_to_end(&mut data) + .expect("Can't read buffer to validate signature"); + + // Validate signature or bail out + public_key.verify(&data, &signature)?; + Ok(true) +} + +#[cfg(test)] +mod test { + use super::*; + #[cfg(target_os = "macos")] + use std::env::current_exe; + #[cfg(target_os = "macos")] + use std::fs::File; + #[cfg(target_os = "macos")] + use std::path::Path; + + macro_rules! block { + ($e:expr) => { + tokio_test::block_on($e) + }; + } + + fn generate_sample_raw_json() -> String { + r#"{ + "version": "v2.0.0", + "notes": "Test version !", + "pub_date": "2020-06-22T19:25:57Z", + "platforms": { + "darwin": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==", + "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.app.tar.gz" + }, + "linux": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOWZSM29hTFNmUEdXMHRoOC81WDFFVVFRaXdWOUdXUUdwT0NlMldqdXkyaWVieXpoUmdZeXBJaXRqSm1YVmczNXdRL1Brc0tHb1NOTzhrL1hadFcxdmdnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE3MzQzCWZpbGU6L2hvbWUvcnVubmVyL3dvcmsvdGF1cmkvdGF1cmkvdGF1cmkvZXhhbXBsZXMvY29tbXVuaWNhdGlvbi9zcmMtdGF1cmkvdGFyZ2V0L2RlYnVnL2J1bmRsZS9hcHBpbWFnZS9hcHAuQXBwSW1hZ2UudGFyLmd6CmRUTUM2bWxnbEtTbUhOZGtERUtaZnpUMG5qbVo5TGhtZWE1SFNWMk5OOENaVEZHcnAvVW0zc1A2ajJEbWZUbU0yalRHT0FYYjJNVTVHOHdTQlYwQkF3PT0K", + "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz" + }, + "win64": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K", + "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip" + } + } + }"#.into() + } + + fn generate_sample_platform_json( + version: &str, + public_signature: &str, + download_url: &str, + ) -> String { + format!( + r#" + {{ + "name": "v{}", + "notes": "This is the latest version! Once updated you shouldn't see this prompt.", + "pub_date": "2020-06-25T14:14:19Z", + "signature": "{}", + "url": "{}" + }} + "#, + version, public_signature, download_url + ) + } + + fn generate_sample_bad_json() -> String { + r#"{ + "version": "v0.0.3", + "notes": "Blablaa", + "date": "2020-02-20T15:41:00Z", + "download_link": "https://github.com/lemarier/tauri-test/releases/download/v0.0.1/update3.tar.gz" + }"#.into() + } + + #[test] + fn simple_http_updater() { + let _m = mockito::mock("GET", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_raw_json()) + .create(); + + let check_update = block!(builder() + .current_version("0.0.0") + .url(mockito::server_url()) + .build()); + + assert_eq!(check_update.is_ok(), true); + let updater = check_update.expect("Can't check update"); + + assert_eq!(updater.should_update, true); + } + + #[test] + fn simple_http_updater_raw_json() { + let _m = mockito::mock("GET", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_raw_json()) + .create(); + + let check_update = block!(builder() + .current_version("0.0.0") + .url(mockito::server_url()) + .build()); + + assert_eq!(check_update.is_ok(), true); + let updater = check_update.expect("Can't check update"); + + assert_eq!(updater.should_update, true); + } + + #[test] + fn simple_http_updater_raw_json_win64() { + let _m = mockito::mock("GET", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_raw_json()) + .create(); + + let check_update = block!(builder() + .current_version("0.0.0") + .target("win64") + .url(mockito::server_url()) + .build()); + + assert_eq!(check_update.is_ok(), true); + let updater = check_update.expect("Can't check update"); + + assert_eq!(updater.should_update, true); + assert_eq!(updater.version, "2.0.0"); + assert_eq!(updater.signature, Some("dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K".into())); + assert_eq!( + updater.download_url, + "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip" + ); + } + + #[test] + fn simple_http_updater_raw_json_uptodate() { + let _m = mockito::mock("GET", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_raw_json()) + .create(); + + let check_update = block!(builder() + .current_version("10.0.0") + .url(mockito::server_url()) + .build()); + + assert_eq!(check_update.is_ok(), true); + let updater = check_update.expect("Can't check update"); + + assert_eq!(updater.should_update, false); + } + + #[test] + fn simple_http_updater_without_version() { + let _m = mockito::mock("GET", "/darwin/1.0.0") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_platform_json( + "2.0.0", + "SampleTauriKey", + "https://tauri.studio", + )) + .create(); + + let check_update = block!(builder() + .current_version("1.0.0") + .url(format!( + "{}/darwin/{{{{current_version}}}}", + mockito::server_url() + )) + .build()); + + assert_eq!(check_update.is_ok(), true); + let updater = check_update.expect("Can't check update"); + + assert_eq!(updater.should_update, true); + } + + #[test] + fn http_updater_uptodate() { + let _m = mockito::mock("GET", "/darwin/10.0.0") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_platform_json( + "2.0.0", + "SampleTauriKey", + "https://tauri.studio", + )) + .create(); + + let check_update = block!(builder() + .current_version("10.0.0") + .url(format!( + "{}/darwin/{{{{current_version}}}}", + mockito::server_url() + )) + .build()); + + assert_eq!(check_update.is_ok(), true); + let updater = check_update.expect("Can't check update"); + + assert_eq!(updater.should_update, false); + } + + #[test] + fn http_updater_fallback_urls() { + let _m = mockito::mock("GET", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_raw_json()) + .create(); + + let check_update = block!(builder() + .url("http://badurl.www.tld/1".into()) + .url(mockito::server_url()) + .current_version("0.0.1") + .build()); + + assert_eq!(check_update.is_ok(), true); + let updater = check_update.expect("Can't check remote update"); + + assert_eq!(updater.should_update, true); + } + + #[test] + fn http_updater_fallback_urls_withs_array() { + let _m = mockito::mock("GET", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_raw_json()) + .create(); + + let check_update = block!(builder() + .urls(&["http://badurl.www.tld/1".into(), mockito::server_url(),]) + .current_version("0.0.1") + .build()); + + assert_eq!(check_update.is_ok(), true); + let updater = check_update.expect("Can't check remote update"); + + assert_eq!(updater.should_update, true); + } + + #[test] + fn http_updater_missing_remote_data() { + let _m = mockito::mock("GET", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_bad_json()) + .create(); + + let check_update = block!(builder() + .url(mockito::server_url()) + .current_version("0.0.1") + .build()); + + assert_eq!(check_update.is_err(), true); + } + + // run complete process on mac only for now as we don't have + // server (api) that we can use to test + #[cfg(target_os = "macos")] + #[test] + fn http_updater_complete_process() { + let good_archive_url = format!("{}/archive.tar.gz", mockito::server_url()); + + let mut signature_file = + File::open("./test/fixture/archives/archive.tar.gz.sig").expect("Unable to open signature"); + let mut signature = String::new(); + signature_file + .read_to_string(&mut signature) + .expect("Unable to read signature as string"); + + let mut pubkey_file = + File::open("./test/fixture/good_signature/update.key.pub").expect("Unable to open pubkey"); + let mut pubkey = String::new(); + pubkey_file + .read_to_string(&mut pubkey) + .expect("Unable to read signature as string"); + + // add sample file + let _m = mockito::mock("GET", "/archive.tar.gz") + .with_status(200) + .with_header("content-type", "application/octet-stream") + .with_body_from_file("./test/fixture/archives/archive.tar.gz") + .create(); + + // sample mock for update file + let _m = mockito::mock("GET", "/") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(generate_sample_platform_json( + "2.0.1", + signature.as_ref(), + good_archive_url.as_ref(), + )) + .create(); + + // Build a tmpdir so we can test our extraction inside + // We dont want to overwrite our current executable or the directory + // Otherwise tests are failing... + let executable_path = current_exe().expect("Can't extract executable path"); + let parent_path = executable_path + .parent() + .expect("Can't find the parent path"); + + let tmp_dir = tempfile::Builder::new() + .prefix("tauri_updater_test") + .tempdir_in(parent_path); + + assert_eq!(tmp_dir.is_ok(), true); + let tmp_dir_unwrap = tmp_dir.expect("Can't find tmp_dir"); + let tmp_dir_path = tmp_dir_unwrap.path(); + + // configure the updater + let check_update = block!(builder() + .url(mockito::server_url()) + // It should represent the executable path, that's why we add my_app.exe in our + // test path -- in production you shouldn't have to provide it + .executable_path(&tmp_dir_path.join("my_app.exe")) + // make sure we force an update + .current_version("1.0.0") + .build()); + + // make sure the process worked + assert_eq!(check_update.is_ok(), true); + + // unwrap our results + let updater = check_update.expect("Can't check remote update"); + + // make sure we need to update + assert_eq!(updater.should_update, true); + // make sure we can read announced version + assert_eq!(updater.version, "2.0.1"); + + // download, install and validate signature + let install_process = block!(updater.download_and_install(Some(pubkey))); + assert_eq!(install_process.is_ok(), true); + + // make sure the extraction went well (it should have skipped the main app.app folder) + // as we can't extract in /Applications directly + let bin_file = tmp_dir_path.join("Contents").join("MacOS").join("app"); + let bin_file_exist = Path::new(&bin_file).exists(); + assert_eq!(bin_file_exist, true); + } } diff --git a/tauri-updater/src/macros.rs b/tauri-updater/src/macros.rs deleted file mode 100644 index d334f31a7..000000000 --- a/tauri-updater/src/macros.rs +++ /dev/null @@ -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"); - } - } - }; -} diff --git a/tauri-updater/src/updater.rs b/tauri-updater/src/updater.rs deleted file mode 100644 index ad8103054..000000000 --- a/tauri-updater/src/updater.rs +++ /dev/null @@ -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, - bin_name: Option, - bin_install_path: Option, - bin_path_in_archive: Option, - show_download_progress: bool, - show_output: bool, - current_version: Option, -} -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 { - 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> { - /// 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 { - 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::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 { - 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)) - } -} diff --git a/tauri-updater/src/updater/github.rs b/tauri-updater/src/updater/github.rs deleted file mode 100644 index a5ec307fa..000000000 --- a/tauri-updater/src/updater/github.rs +++ /dev/null @@ -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 { - 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::()?; - Ok(Release::parse(&json)?) -} - -pub fn get_release_version(repo_owner: &str, repo_name: &str, ver: &str) -> crate::Result { - 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::()?; - Ok(Release::parse(&json)?) -} diff --git a/tauri-updater/src/updater/github/release.rs b/tauri-updater/src/updater/github/release.rs deleted file mode 100644 index 2d55bdf4e..000000000 --- a/tauri-updater/src/updater/github/release.rs +++ /dev/null @@ -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 { - 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, -} -impl Release { - pub fn parse(release: &serde_json::Value) -> crate::Result { - 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::>>()?; - 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 { - 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, - repo_name: Option, - target: Option, -} -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 { - 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, -} -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> { - 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::>(), - }; - Ok(releases) - } - - fn fetch_releases(url: &str) -> crate::Result> { - 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::()?; - let releases = releases - .as_array() - .ok_or_else(|| crate::Error::Network("No releases found".into()))?; - let mut releases = releases - .iter() - .map(Release::parse) - .collect::>>()?; - - // handle paged responses containing `Link` header: - // `Link: ; 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 - } - }) - } -} diff --git a/tauri-updater/test/fixture/archives/archive.tar.gz b/tauri-updater/test/fixture/archives/archive.tar.gz new file mode 100644 index 000000000..f37d6d0c4 Binary files /dev/null and b/tauri-updater/test/fixture/archives/archive.tar.gz differ diff --git a/tauri-updater/test/fixture/archives/archive.tar.gz.badsig b/tauri-updater/test/fixture/archives/archive.tar.gz.badsig new file mode 100644 index 000000000..cd6483c53 --- /dev/null +++ b/tauri-updater/test/fixture/archives/archive.tar.gz.badsig @@ -0,0 +1 @@ +dW90cnVzdGVkIGIvbW1lbnQ6IHNpZ25hdHVyZSBmcm1tIHRhdXJpIHNlY3JldCBrZXkKUldUTE3QzWxkQolZOVVDaC92ZnhXN0IrVm4rVW9GKzdoSFF6NEtFc3J3c004YUhQTFR0Njg5MGtuZkZqeVh1cTlwZ1dmWG9aSkx5d0t1WTBkS04wK1RBeEI2K2pka2tsT3drPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkxODg2NjU2CWZpbGU6L1VzZXJzL2RhdmlkL2Rldi90YXVyaV91cGRhdGVyL3RhdXJpLXVwZGF0ZXIvdGVzdC9maXh0dXJlL2FyY2hpdmVzL2FyY2hpdmUudGFyLmd6CjNmMC9XdmYyzDtCM3ZoaWhEbHVVL08vV2tLejQ0Wlg5SkNGTys2N1ZMTHQvUENrK0svMlgzNE22UkQ0OG1sZ0RqTGZXNzA0OGxocmg4ODljM3BGOEJnPT0K \ No newline at end of file diff --git a/tauri-updater/test/fixture/archives/archive.tar.gz.sig b/tauri-updater/test/fixture/archives/archive.tar.gz.sig new file mode 100644 index 000000000..4db824d00 --- /dev/null +++ b/tauri-updater/test/fixture/archives/archive.tar.gz.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldSWTRKaFRZQmJER1VrQzlhaWFLOExCM3VoV1gxTW1IU0ZQQTZKTXN6U0MwMDEzcThyc3R3a0pmVkJrdWFhN1JCOTNpaEg5c1JhSGY0QWxROENlbTBxbFhpVTNWUWViWVFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjE1Mzk5ODg3CWZpbGU6Li4vLi4vdGF1cmktdXBkYXRlci90ZXN0L2ZpeHR1cmUvYXJjaGl2ZXMvYXJjaGl2ZS50YXIuZ3oKczI0cUxjM2YvMGdpMDI1ejE3ZnREQTNtdlBwR2xydW5rVFBvcUxVcUt2dVpxZkpPd1ZLT1Z1K0hZS0hjRVk3Ylg4UVQxWEtWMHJ4ZUwwSXcvZjlaQmc9PQo= \ No newline at end of file diff --git a/tauri-updater/test/fixture/archives/archive.zip b/tauri-updater/test/fixture/archives/archive.zip new file mode 100644 index 000000000..bf1ed7245 Binary files /dev/null and b/tauri-updater/test/fixture/archives/archive.zip differ diff --git a/tauri-updater/test/fixture/archives/archive.zip.sig b/tauri-updater/test/fixture/archives/archive.zip.sig new file mode 100644 index 000000000..e40b06410 --- /dev/null +++ b/tauri-updater/test/fixture/archives/archive.zip.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVVZN3dSRm5KTmZPMlovQjAxdGtCN3YxYWJybFM2RzlxRm1rMnNkc20rbWRrS2d4ZlNJT0F4RUdrTFVGYWlUQVMxUE1VRk1uNW5IS2tucHNYNnRLZGdFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkxODg2NjIzCWZpbGU6L1VzZXJzL2RhdmlkL2Rldi90YXVyaV91cGRhdGVyL3RhdXJpLXVwZGF0ZXIvdGVzdC9maXh0dXJlL2FyY2hpdmVzL2FyY2hpdmUuemlwCkNiNkJBKzNkR2M3WFhGTDgwSnB6NnpSYUl0VUFKVFNyRTJGVEdFbVkrNXZPTHBOQU1UeWIxbTB5QVpLRTBoM1NHeGs0RGxvTFRKY0FoWmVIS2psNkJBPT0K \ No newline at end of file diff --git a/tauri-updater/test/fixture/bad_signature/update.key b/tauri-updater/test/fixture/bad_signature/update.key new file mode 100644 index 000000000..c5b60aad8 --- /dev/null +++ b/tauri-updater/test/fixture/bad_signature/update.key @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5dGlHbTEvRFhRRis2STdlTzF3eWhOVk9LNjdGRENJMnFSREE3R2V3b3Rwb0FBQkFBQUFBQUFBQUFBQUlBQUFBQWFNZEJTNXFuVjk0bmdJMENRRXVYNG5QVzBDd1NMOWN4Q2RKRXZxRDZNakw3Y241Vkt3aTg2WGtoajJGS1owV0ZuSmo4ZXJ0ZCtyaWF0RWJObFpnd1EveDB4NzBTU2RweG9ZaUpuc3hnQ3BYVG9HNnBXUW5SZ2Q3b3dvZ3Y2UnhQZ1BQZDU3bXl6d3M9Cg== \ No newline at end of file diff --git a/tauri-updater/test/fixture/bad_signature/update.key.pub b/tauri-updater/test/fixture/bad_signature/update.key.pub new file mode 100644 index 000000000..73c207cb0 --- /dev/null +++ b/tauri-updater/test/fixture/bad_signature/update.key.pub @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEY1OTgwQzc0UjVGNjM0Q0IKUldUTE5QWWxkQnlZOWFBK21kekU4OGgzdStleEtkeStHaFR5NjEyRHovRnlUdzAwWGJxWEU2aGYK \ No newline at end of file diff --git a/tauri-updater/test/fixture/good_signature/update.key b/tauri-updater/test/fixture/good_signature/update.key new file mode 100644 index 000000000..63a69ddf3 --- /dev/null +++ b/tauri-updater/test/fixture/good_signature/update.key @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg== \ No newline at end of file diff --git a/tauri-updater/test/fixture/good_signature/update.key.pub b/tauri-updater/test/fixture/good_signature/update.key.pub new file mode 100644 index 000000000..1669b21ed --- /dev/null +++ b/tauri-updater/test/fixture/good_signature/update.key.pub @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK \ No newline at end of file diff --git a/tauri-utils/src/config.rs b/tauri-utils/src/config.rs index 7d111e383..99b399aa8 100644 --- a/tauri-utils/src/config.rs +++ b/tauri-utils/src/config.rs @@ -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>, + /// Optional pubkey. + #[serde(default)] + pub pubkey: Option, +} + +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); diff --git a/tauri/Cargo.toml b/tauri/Cargo.toml index 5281ccd23..072dd770a 100644 --- a/tauri/Cargo.toml +++ b/tauri/Cargo.toml @@ -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 = [ ] diff --git a/tauri/src/error.rs b/tauri/src/error.rs index 8afc34dec..6750b5602 100644 --- a/tauri/src/error.rs +++ b/tauri/src/error.rs @@ -49,6 +49,9 @@ pub enum Error { /// Encountered an error in the setup hook, #[error("error encountered during setup hood: {0}")] Setup(#[from] Box), + /// Tauri updater error. + #[error("Updater: {0}")] + TauriUpdater(#[from] tauri_updater::Error), } impl From for Error { diff --git a/tauri/src/runtime/app.rs b/tauri/src/runtime/app.rs index 17293f4c7..60441ba82 100644 --- a/tauri/src/runtime/app.rs +++ b/tauri/src/runtime/app.rs @@ -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 { manager: M, } +#[cfg(feature = "updater")] +impl App { + /// Runs the updater hook with built-in dialog. + fn run_updater_dialog(&self, window: Window) { + 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) { + 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>) { + 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 Manager for App {} impl ManagerPrivate for App { fn manager(&self) -> &M { @@ -66,12 +126,22 @@ impl Runner { }; 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(()) diff --git a/tauri/src/runtime/manager.rs b/tauri/src/runtime/manager.rs index 6adf40c6f..eaa5a673a 100644 --- a/tauri/src/runtime/manager.rs +++ b/tauri/src/runtime/manager.rs @@ -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 { /// A list of salts that are valid for the current application. salts: Mutex>, + package_info: PackageInfo, } pub struct WindowManager @@ -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\\AppData\Local\ + 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) } diff --git a/tauri/src/runtime/mod.rs b/tauri/src/runtime/mod.rs index a3db6106a..57aad4e40 100644 --- a/tauri/src/runtime/mod.rs +++ b/tauri/src/runtime/mod.rs @@ -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 { /// The default window icon Tauri should use when creating windows. pub default_window_icon: Option>, + + /// 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); diff --git a/tauri/src/runtime/updater.rs b/tauri/src/runtime/updater.rs new file mode 100644 index 000000000..3e818e805 --- /dev/null +++ b/tauri/src/runtime/updater.rs @@ -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, +} + +#[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( + updater_config: UpdaterConfig, + package_info: crate::api::PackageInfo, + window: Window, +) { + 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( + updater_config: UpdaterConfig, + package_info: crate::api::PackageInfo, + window: &Window, +) { + 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(window: Window, status: &str, error: Option) { + 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, +) -> 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); +} diff --git a/tauri/test/fixture/src-tauri/tauri.conf.json b/tauri/test/fixture/src-tauri/tauri.conf.json index 839091522..f73a99542 100644 --- a/tauri/test/fixture/src-tauri/tauri.conf.json +++ b/tauri/test/fixture/src-tauri/tauri.conf.json @@ -19,6 +19,9 @@ ], "security": { "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'" + }, + "updater": { + "active": false } } }