tauri/tooling/cli/src/build.rs
Josh Soref 8fd79b8fc0
Spelling (#4880)
Co-authored-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>
Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
2022-09-03 01:03:02 -03:00

399 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
helpers::{
app_paths::{app_dir, tauri_dir},
command_env,
config::{get as get_config, AppUrl, HookCommand, WindowUrl, MERGE_CONFIG_EXTENSION_NAME},
updater_signature::{read_key_from_file, secret_key as updater_secret_key, sign_file},
},
interface::{AppInterface, AppSettings, Interface},
CommandExt, Result,
};
use anyhow::{bail, Context};
use clap::Parser;
use log::warn;
use log::{error, info};
use std::{
env::{set_current_dir, var_os},
path::{Path, PathBuf},
process::Command,
};
use tauri_bundler::bundle::{bundle_project, Bundle, PackageType};
#[derive(Debug, Clone, Parser)]
#[clap(about = "Tauri build")]
pub struct Options {
/// Binary to use to build the application, defaults to `cargo`
#[clap(short, long)]
pub runner: Option<String>,
/// Builds with the debug flag
#[clap(short, long)]
pub debug: bool,
/// Target triple to build against.
///
/// It must be one of the values outputted by `$rustc --print target-list` or `universal-apple-darwin` for an universal macOS application.
///
/// Note that compiling an universal macOS application requires both `aarch64-apple-darwin` and `x86_64-apple-darwin` targets to be installed.
#[clap(short, long)]
pub target: Option<String>,
/// Space or comma separated list of features to activate
#[clap(short, long, multiple_occurrences(true), multiple_values(true))]
pub features: Option<Vec<String>>,
/// Space or comma separated list of bundles to package.
///
/// Each bundle must be one of `deb`, `appimage`, `msi`, `app` or `dmg` on MacOS and `updater` on all platforms.
/// If `none` is specified, the bundler will be skipped.
///
/// Note that the `updater` bundle is not automatically added so you must specify it if the updater is enabled.
#[clap(short, long, multiple_occurrences(true), multiple_values(true))]
pub bundles: Option<Vec<String>>,
/// JSON string or path to JSON file to merge with tauri.conf.json
#[clap(short, long)]
pub config: Option<String>,
/// Command line arguments passed to the runner
pub args: Vec<String>,
}
pub fn command(mut options: Options) -> Result<()> {
let (merge_config, merge_config_path) = if let Some(config) = &options.config {
if config.starts_with('{') {
(Some(config.to_string()), None)
} else {
(
Some(
std::fs::read_to_string(&config)
.with_context(|| "failed to read custom configuration")?,
),
Some(config.clone()),
)
}
} else {
(None, None)
};
options.config = merge_config;
let tauri_path = tauri_dir();
set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?;
let config = get_config(options.config.as_deref())?;
let config_guard = config.lock().unwrap();
let config_ = config_guard.as_ref().unwrap();
let bundle_identifier_source = match config_.find_bundle_identifier_overwriter() {
Some(source) if source == MERGE_CONFIG_EXTENSION_NAME => merge_config_path.unwrap_or(source),
Some(source) => source,
None => "tauri.conf.json".into(),
};
if config_.tauri.bundle.identifier == "com.tauri.dev" {
error!(
"You must change the bundle identifier in `{} > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
bundle_identifier_source
);
std::process::exit(1);
}
if config_
.tauri
.bundle
.identifier
.chars()
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
{
error!(
"The bundle identifier \"{}\" set in `{} > tauri > bundle > identifier`. The bundle identifier string must contain only alphanumeric characters (AZ, az, and 09), hyphens (-), and periods (.).",
config_.tauri.bundle.identifier,
bundle_identifier_source
);
std::process::exit(1);
}
if let Some(before_build) = config_.build.before_build_command.clone() {
run_hook("beforeBuildCommand", before_build, options.debug)?;
}
if let AppUrl::Url(WindowUrl::App(web_asset_path)) = &config_.build.dist_dir {
if !web_asset_path.exists() {
return Err(anyhow::anyhow!(
"Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
web_asset_path
));
}
if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
return Err(anyhow::anyhow!(
"The configured distDir is the `src-tauri` folder.
Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
));
}
let mut out_folders = Vec::new();
for folder in &["node_modules", "src-tauri", "target"] {
if web_asset_path.join(folder).is_dir() {
out_folders.push(folder.to_string());
}
}
if !out_folders.is_empty() {
return Err(anyhow::anyhow!(
"The configured distDir includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
out_folders,
if out_folders.len() == 1 { "folder" }else { "folders" }
)
);
}
}
if options.runner.is_none() {
options.runner = config_.build.runner.clone();
}
if let Some(list) = options.features.as_mut() {
list.extend(config_.build.features.clone().unwrap_or_default());
}
let mut interface = AppInterface::new(config_)?;
let app_settings = interface.app_settings();
let interface_options = options.clone().into();
let bin_path = app_settings.app_binary_path(&interface_options)?;
let out_dir = bin_path.parent().unwrap();
interface.build(interface_options)?;
let app_settings = interface.app_settings();
if config_.tauri.bundle.active {
let package_types = if let Some(names) = &options.bundles {
let mut types = vec![];
for name in names
.iter()
.flat_map(|n| n.split(',').map(|s| s.to_string()).collect::<Vec<String>>())
{
if name == "none" {
break;
}
match PackageType::from_short_name(&name) {
Some(package_type) => {
types.push(package_type);
}
None => {
return Err(anyhow::anyhow!(format!(
"Unsupported bundle format: {}",
name
)));
}
}
}
Some(types)
} else {
let targets = config_.tauri.bundle.targets.to_vec();
if targets.is_empty() {
None
} else {
Some(targets.into_iter().map(Into::into).collect())
}
};
if let Some(types) = &package_types {
if config_.tauri.updater.active && !types.contains(&PackageType::Updater) {
warn!("The updater is enabled but the bundle target list does not contain `updater`, so the updater artifacts won't be generated.");
}
}
// if we have a package to bundle, let's run the `before_bundle_command`.
if package_types.as_ref().map_or(true, |p| !p.is_empty()) {
if let Some(before_bundle) = config_.build.before_bundle_command.clone() {
run_hook("beforeBundleCommand", before_bundle, options.debug)?;
}
}
let settings = app_settings
.get_bundler_settings(&options.into(), config_, out_dir, package_types)
.with_context(|| "failed to build bundler settings")?;
// set env vars used by the bundler
#[cfg(target_os = "linux")]
{
use crate::helpers::config::ShellAllowlistOpen;
if matches!(
config_.tauri.allowlist.shell.open,
ShellAllowlistOpen::Flag(true) | ShellAllowlistOpen::Validate(_)
) {
std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1");
}
if config_.tauri.system_tray.is_some() {
if let Ok(tray) = std::env::var("TAURI_TRAY") {
std::env::set_var(
"TRAY_LIBRARY_PATH",
if tray == "ayatana" {
format!(
"{}/libayatana-appindicator3.so.1",
pkgconfig_utils::get_library_path("ayatana-appindicator3-0.1")
.expect("failed to get ayatana-appindicator library path using pkg-config.")
)
} else {
format!(
"{}/libappindicator3.so.1",
pkgconfig_utils::get_library_path("appindicator3-0.1")
.expect("failed to get libappindicator-gtk library path using pkg-config.")
)
},
);
} else {
std::env::set_var(
"TRAY_LIBRARY_PATH",
pkgconfig_utils::get_appindicator_library_path(),
);
}
}
if config_.tauri.bundle.appimage.bundle_media_framework {
std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1");
}
}
let bundles = bundle_project(settings).with_context(|| "failed to bundle project")?;
let updater_bundles: Vec<&Bundle> = bundles
.iter()
.filter(|bundle| bundle.package_type == PackageType::Updater)
.collect();
// If updater is active and we bundled it
if config_.tauri.updater.active && !updater_bundles.is_empty() {
// if no password provided we use an empty string
let password = var_os("TAURI_KEY_PASSWORD").map(|v| v.to_str().unwrap().to_string());
// get the private key
let secret_key = if let Some(mut private_key) =
var_os("TAURI_PRIVATE_KEY").map(|v| v.to_str().unwrap().to_string())
{
// check if env var points to a file..
let pk_dir = Path::new(&private_key);
// Check if user provided a path or a key
// We validate if the path exist or not.
if pk_dir.exists() {
// read file content and use it as private key
private_key = read_key_from_file(pk_dir)?;
}
updater_secret_key(private_key, password)
} else {
Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_PRIVATE_KEY` environment variable."))
}?;
let pubkey = base64::decode(&config_.tauri.updater.pubkey)?;
let pub_key_decoded = String::from_utf8_lossy(&pubkey);
let public_key = minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?;
// make sure we have our package built
let mut signed_paths = Vec::new();
for elem in updater_bundles {
// 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(&secret_key, path)?;
if signature.keynum() != public_key.keynum() {
return Err(anyhow::anyhow!(
"The updater secret key from `TAURI_PRIVATE_KEY` does not match the public key defined in `tauri.conf.json > tauri > updater > pubkey`."
));
}
signed_paths.append(&mut vec![signature_path]);
}
}
print_signed_updater_archive(&signed_paths)?;
}
}
Ok(())
}
fn run_hook(name: &str, hook: HookCommand, debug: bool) -> Result<()> {
let (script, script_cwd) = match hook {
HookCommand::Script(s) if s.is_empty() => (None, None),
HookCommand::Script(s) => (Some(s), None),
HookCommand::ScriptWithOptions { script, cwd } => (Some(script), cwd.map(Into::into)),
};
let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
if let Some(script) = script {
info!(action = "Running"; "{} `{}`", name, script);
#[cfg(target_os = "windows")]
let status = Command::new("cmd")
.arg("/S")
.arg("/C")
.arg(&script)
.current_dir(cwd)
.envs(command_env(debug))
.piped()
.with_context(|| format!("failed to run `{}` with `cmd /C`", script))?;
#[cfg(not(target_os = "windows"))]
let status = Command::new("sh")
.arg("-c")
.arg(&script)
.current_dir(cwd)
.envs(command_env(debug))
.piped()
.with_context(|| format!("failed to run `{}` with `sh -c`", script))?;
if !status.success() {
bail!(
"{} `{}` failed with exit code {}",
name,
script,
status.code().unwrap_or_default()
);
}
}
Ok(())
}
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);
info!("{}", msg);
for path in output_paths {
info!(" {}", path.display());
}
Ok(())
}
#[cfg(target_os = "linux")]
mod pkgconfig_utils {
use std::{path::PathBuf, process::Command};
pub fn get_appindicator_library_path() -> PathBuf {
match get_library_path("ayatana-appindicator3-0.1") {
Some(p) => format!("{}/libayatana-appindicator3.so.1", p).into(),
None => match get_library_path("appindicator3-0.1") {
Some(p) => format!("{}/libappindicator3.so.1", p).into(),
None => panic!("Can't detect any appindicator library"),
},
}
}
/// Gets the folder in which a library is located using `pkg-config`.
pub fn get_library_path(name: &str) -> Option<String> {
let mut cmd = Command::new("pkg-config");
cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1");
cmd.arg("--libs-only-L");
cmd.arg(name);
if let Ok(output) = cmd.output() {
if !output.stdout.is_empty() {
// output would be "-L/path/to/library\n"
let word = output.stdout[2..].to_vec();
return Some(String::from_utf8_lossy(&word).trim().to_string());
} else {
None
}
} else {
None
}
}
}