You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
470 lines
15 KiB
470 lines
15 KiB
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#![allow(dead_code, unused_imports)]
|
|
|
|
use std::{
|
|
collections::HashMap,
|
|
fs::File,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
sync::Arc,
|
|
};
|
|
|
|
use serde::Serialize;
|
|
use tauri::utils::config::{Updater, V1Compatible};
|
|
|
|
const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg==";
|
|
const UPDATED_EXIT_CODE: i32 = 0;
|
|
const UP_TO_DATE_EXIT_CODE: i32 = 2;
|
|
|
|
fn npm_command() -> Command {
|
|
#[cfg(target_os = "windows")]
|
|
let cmd = {
|
|
let mut cmd = Command::new("cmd");
|
|
cmd.arg("/c").arg("npm");
|
|
cmd
|
|
};
|
|
#[cfg(not(target_os = "windows"))]
|
|
let cmd = Command::new("npm");
|
|
cmd
|
|
}
|
|
|
|
mod v1 {
|
|
use super::{npm_command, BundleTarget, UPDATER_PRIVATE_KEY};
|
|
use serde::Serialize;
|
|
use std::{
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
};
|
|
|
|
#[derive(Serialize)]
|
|
pub struct PackageConfig {
|
|
pub version: &'static str,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct Config {
|
|
pub package: PackageConfig,
|
|
}
|
|
|
|
pub fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) {
|
|
let mut command = npm_command();
|
|
command
|
|
.args(["run", "tauri", "--", "build", "--debug", "--verbose"])
|
|
.arg("--config")
|
|
.arg(serde_json::to_string(config).unwrap())
|
|
.env("TAURI_PRIVATE_KEY", UPDATER_PRIVATE_KEY)
|
|
.env("TAURI_KEY_PASSWORD", "")
|
|
.current_dir(cwd);
|
|
|
|
#[cfg(target_os = "linux")]
|
|
command.args(["--bundles", target.name()]);
|
|
#[cfg(target_os = "macos")]
|
|
command.args(["--bundles", target.name()]);
|
|
|
|
if bundle_updater {
|
|
#[cfg(windows)]
|
|
command.args(["--bundles", "msi", "nsis"]);
|
|
} else {
|
|
#[cfg(windows)]
|
|
command.args(["--bundles", target.name()]);
|
|
}
|
|
|
|
let status = command
|
|
.status()
|
|
.expect("failed to run Tauri CLI to bundle v1 app");
|
|
|
|
if !status.success() {
|
|
panic!("failed to bundle v1 app {:?}", status.code());
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
|
vec![(
|
|
BundleTarget::AppImage,
|
|
root_dir.join(format!(
|
|
"target/debug/bundle/appimage/app-updater-v1_{version}_amd64.AppImage",
|
|
)),
|
|
)]
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
|
vec![(
|
|
BundleTarget::App,
|
|
root_dir.join("target/debug/bundle/macos/app-updater-v1.app"),
|
|
)]
|
|
}
|
|
|
|
#[cfg(target_os = "ios")]
|
|
pub fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
|
vec![(
|
|
BundleTarget::App,
|
|
root_dir.join("target/debug/bundle/ios/app-updater-v1.ipa"),
|
|
)]
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
pub fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
|
|
root_dir.join("target/debug/bundle/android/app-updater-v1.apk")
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
|
vec![
|
|
(
|
|
BundleTarget::Nsis,
|
|
root_dir.join(format!(
|
|
"target/debug/bundle/nsis/app-updater-v1_{version}_x64-setup.exe"
|
|
)),
|
|
),
|
|
(
|
|
BundleTarget::Msi,
|
|
root_dir.join(format!(
|
|
"target/debug/bundle/msi/app-updater-v1_{version}_x64_en-US.msi"
|
|
)),
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
mod v2 {
|
|
|
|
use super::{BundleTarget, UPDATER_PRIVATE_KEY};
|
|
use serde::Serialize;
|
|
use std::{
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
};
|
|
use tauri::utils::config::Updater;
|
|
|
|
#[derive(Serialize)]
|
|
pub struct Config {
|
|
pub version: &'static str,
|
|
pub bundle: BundleConfig,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BundleConfig {
|
|
pub create_updater_artifacts: Updater,
|
|
}
|
|
|
|
pub fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) {
|
|
let mut command = Command::new("cargo");
|
|
command
|
|
.args(["tauri", "build", "--debug", "--verbose"])
|
|
.arg("--config")
|
|
.arg(serde_json::to_string(config).unwrap())
|
|
.env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY)
|
|
.env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "")
|
|
.current_dir(cwd);
|
|
|
|
#[cfg(target_os = "linux")]
|
|
command.args(["--bundles", target.name()]);
|
|
#[cfg(target_os = "macos")]
|
|
command.args(["--bundles", target.name()]);
|
|
|
|
if bundle_updater {
|
|
#[cfg(windows)]
|
|
command.args(["--bundles", "msi", "nsis"]);
|
|
} else {
|
|
#[cfg(windows)]
|
|
command.args(["--bundles", target.name()]);
|
|
}
|
|
|
|
let status = command
|
|
.status()
|
|
.expect("failed to run Tauri CLI to bundle v2 app");
|
|
|
|
if !status.success() {
|
|
panic!("failed to bundle v2 app {:?}", status.code());
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
|
vec![(
|
|
BundleTarget::AppImage,
|
|
root_dir.join(format!(
|
|
"target/debug/bundle/appimage/app-updater-v2_{version}_amd64.AppImage",
|
|
)),
|
|
)]
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
|
vec![(
|
|
BundleTarget::App,
|
|
root_dir.join("target/debug/bundle/macos/app-updater-v2.app"),
|
|
)]
|
|
}
|
|
|
|
#[cfg(target_os = "ios")]
|
|
pub fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
|
vec![(
|
|
BundleTarget::App,
|
|
root_dir.join("target/debug/bundle/ios/app-updater-v2.ipa"),
|
|
)]
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
pub fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
|
|
root_dir.join("target/debug/bundle/android/app-updater-v2.apk")
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
|
vec![
|
|
(
|
|
BundleTarget::Nsis,
|
|
root_dir.join(format!(
|
|
"target/debug/bundle/nsis/app-updater-v2_{version}_x64-setup.exe"
|
|
)),
|
|
),
|
|
(
|
|
BundleTarget::Msi,
|
|
root_dir.join(format!(
|
|
"target/debug/bundle/msi/app-updater-v2_{version}_x64_en-US.msi"
|
|
)),
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct PlatformUpdate {
|
|
signature: String,
|
|
url: &'static str,
|
|
with_elevated_task: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct Update {
|
|
version: &'static str,
|
|
date: String,
|
|
platforms: HashMap<String, PlatformUpdate>,
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
enum BundleTarget {
|
|
AppImage,
|
|
|
|
App,
|
|
|
|
Msi,
|
|
Nsis,
|
|
}
|
|
|
|
impl BundleTarget {
|
|
fn name(self) -> &'static str {
|
|
match self {
|
|
Self::AppImage => "appimage",
|
|
Self::App => "app",
|
|
Self::Msi => "msi",
|
|
Self::Nsis => "nsis",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for BundleTarget {
|
|
fn default() -> Self {
|
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
|
return Self::App;
|
|
#[cfg(target_os = "linux")]
|
|
return Self::AppImage;
|
|
#[cfg(windows)]
|
|
return Self::Nsis;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn update_app() {
|
|
let target =
|
|
tauri_plugin_updater::target().expect("running updater test in an unsupported platform");
|
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
let root_dir = manifest_dir.join("../../../..");
|
|
let v1_root_dir = manifest_dir.join("v1-app");
|
|
let v2_root_dir = manifest_dir.join("v2-app");
|
|
|
|
let status = npm_command()
|
|
.arg("install")
|
|
.current_dir(&v1_root_dir)
|
|
.status()
|
|
.expect("failed to run npm install");
|
|
if !status.success() {
|
|
panic!("failed to run npm install");
|
|
}
|
|
|
|
let v2_config = v2::Config {
|
|
version: "1.0.0",
|
|
bundle: v2::BundleConfig {
|
|
create_updater_artifacts: Updater::String(V1Compatible::V1Compatible),
|
|
},
|
|
};
|
|
|
|
// bundle app update (v2)
|
|
v2::build_app(&v2_root_dir, &v2_config, true, Default::default());
|
|
|
|
let updater_zip_ext = if cfg!(windows) { "zip" } else { "tar.gz" };
|
|
|
|
for (bundle_target, out_bundle_path) in v2::bundle_paths(&root_dir, "1.0.0") {
|
|
let bundle_updater_ext = out_bundle_path
|
|
.extension()
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.replace("exe", "nsis");
|
|
let updater_extension = format!("{bundle_updater_ext}.{updater_zip_ext}");
|
|
let signature_extension = format!("{updater_extension}.sig");
|
|
let signature_path = out_bundle_path.with_extension(signature_extension);
|
|
let signature = std::fs::read_to_string(&signature_path).unwrap_or_else(|_| {
|
|
panic!("failed to read signature file {}", signature_path.display())
|
|
});
|
|
let out_updater_path = out_bundle_path.with_extension(updater_extension);
|
|
let updater_path = root_dir.join(format!(
|
|
"target/debug/{}",
|
|
out_updater_path.file_name().unwrap().to_str().unwrap()
|
|
));
|
|
std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
|
|
|
|
let target = target.clone();
|
|
|
|
// start the updater server
|
|
let server = Arc::new(
|
|
tiny_http::Server::http("localhost:3007").expect("failed to start updater server"),
|
|
);
|
|
|
|
let server_ = server.clone();
|
|
std::thread::spawn(move || {
|
|
for request in server_.incoming_requests() {
|
|
match request.url() {
|
|
"/" => {
|
|
let mut platforms = HashMap::new();
|
|
|
|
platforms.insert(
|
|
target.clone(),
|
|
PlatformUpdate {
|
|
signature: signature.clone(),
|
|
url: "http://localhost:3007/download",
|
|
with_elevated_task: false,
|
|
},
|
|
);
|
|
let body = serde_json::to_vec(&Update {
|
|
version: "1.0.0",
|
|
date: time::OffsetDateTime::now_utc()
|
|
.format(&time::format_description::well_known::Rfc3339)
|
|
.unwrap(),
|
|
platforms,
|
|
})
|
|
.unwrap();
|
|
let len = body.len();
|
|
let response = tiny_http::Response::new(
|
|
tiny_http::StatusCode(200),
|
|
Vec::new(),
|
|
std::io::Cursor::new(body),
|
|
Some(len),
|
|
None,
|
|
);
|
|
let _ = request.respond(response);
|
|
}
|
|
"/download" => {
|
|
let _ = request.respond(tiny_http::Response::from_file(
|
|
File::open(&updater_path).unwrap_or_else(|_| {
|
|
panic!("failed to open updater bundle {}", updater_path.display())
|
|
}),
|
|
));
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
});
|
|
|
|
let v1_config = v1::Config {
|
|
package: v1::PackageConfig { version: "0.1.0" },
|
|
};
|
|
|
|
// bundle initial app version (tauri v1)
|
|
v1::build_app(&v1_root_dir, &v1_config, false, bundle_target);
|
|
|
|
let status_checks = if matches!(bundle_target, BundleTarget::Msi) {
|
|
// for msi we can't really check if the app was updated, because we can't change the install path
|
|
vec![(UPDATED_EXIT_CODE, 1)]
|
|
} else {
|
|
vec![(UPDATED_EXIT_CODE, 1), (UP_TO_DATE_EXIT_CODE, 2)]
|
|
};
|
|
|
|
for (expected_exit_code, expected_tauri_version) in status_checks {
|
|
let (expected_app_version, bundle_paths_fn, app_name_suffix) =
|
|
match expected_tauri_version {
|
|
1 => (
|
|
v1_config.package.version,
|
|
Box::new(|| v1::bundle_paths(&v1_root_dir, v1_config.package.version))
|
|
as Box<dyn Fn() -> Vec<(BundleTarget, PathBuf)>>,
|
|
"-v1",
|
|
),
|
|
2 => (
|
|
v2_config.version,
|
|
Box::new(|| v2::bundle_paths(&root_dir, v2_config.version))
|
|
as Box<dyn Fn() -> Vec<(BundleTarget, PathBuf)>>,
|
|
"-v2",
|
|
),
|
|
_ => panic!("unknown tauri version"),
|
|
};
|
|
let mut binary_cmd = if cfg!(windows) {
|
|
let app_root_dir = match expected_tauri_version {
|
|
1 => &v1_root_dir,
|
|
2 => &root_dir,
|
|
_ => panic!("unknown tauri version"),
|
|
};
|
|
Command::new(
|
|
app_root_dir.join(format!("target/debug/app-updater{app_name_suffix}.exe")),
|
|
)
|
|
} else if cfg!(target_os = "macos") {
|
|
Command::new(
|
|
bundle_paths_fn()
|
|
.first()
|
|
.unwrap()
|
|
.1
|
|
.join(format!("Contents/MacOS/app-updater{app_name_suffix}")),
|
|
)
|
|
} else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
|
|
let mut c = Command::new("xvfb-run");
|
|
c.arg("--auto-servernum")
|
|
.arg(&bundle_paths_fn().first().unwrap().1);
|
|
c
|
|
} else {
|
|
Command::new(&bundle_paths_fn().first().unwrap().1)
|
|
};
|
|
|
|
binary_cmd.env("TARGET", bundle_target.name());
|
|
|
|
let output = binary_cmd.output().expect("failed to run app");
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
println!("{stdout}");
|
|
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
|
|
|
|
let code = output.status.code().unwrap_or(-1);
|
|
|
|
if code != expected_exit_code {
|
|
panic!("failed to run app, expected exit code {expected_exit_code}, got {code}");
|
|
}
|
|
if !stdout.contains(&format!("version={expected_app_version}")) {
|
|
panic!("app version does not match {expected_app_version}");
|
|
}
|
|
#[cfg(windows)]
|
|
if code == UPDATED_EXIT_CODE {
|
|
// wait for the update to finish
|
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
|
}
|
|
}
|
|
|
|
server.unblock();
|
|
}
|
|
}
|