diff --git a/crates/tauri-bundler/src/bundle/linux/appimage.rs b/crates/tauri-bundler/src/bundle/linux/appimage.rs new file mode 100644 index 000000000..fd2d1e582 --- /dev/null +++ b/crates/tauri-bundler/src/bundle/linux/appimage.rs @@ -0,0 +1,265 @@ +// Copyright 2016-2019 Cargo-Bundle developers +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::debian; +use crate::{ + bundle::settings::Arch, + utils::{fs_utils, http_utils::download, CommandExt}, + Settings, +}; +use anyhow::Context; +use std::{ + fs, + io::Write, + path::{Path, PathBuf}, + process::Command, +}; + +/// Bundles the project. +/// Returns a vector of PathBuf that shows where the AppImage was created. +pub fn bundle_project(settings: &Settings) -> crate::Result> { + // generate the deb binary name + let appimage_arch: &str = match settings.binary_arch() { + Arch::X86_64 => "amd64", + Arch::X86 => "i386", + Arch::AArch64 => "aarch64", + Arch::Armhf => "armhf", + target => { + return Err(crate::Error::ArchError(format!( + "Unsupported architecture: {:?}", + target + ))); + } + }; + + let tools_arch = settings.target().split('-').next().unwrap(); + let output_path = settings.project_out_directory().join("bundle/appimage"); + if output_path.exists() { + fs::remove_dir_all(&output_path)?; + } + + let tools_path = settings + .local_tools_directory() + .map(|d| d.join(".tauri")) + .unwrap_or_else(|| { + dirs::cache_dir().map_or_else(|| output_path.to_path_buf(), |p| p.join("tauri")) + }); + + let linuxdeploy_path = prepare_tools(&tools_path, tools_arch)?; + + let package_dir = settings.project_out_directory().join("bundle/appimage_deb"); + + let main_binary = settings.main_binary()?; + let product_name = settings.product_name(); + + let mut settings = settings.clone(); + if main_binary.name().contains(" ") { + let main_binary_path = settings.binary_path(main_binary); + let project_out_directory = settings.project_out_directory(); + + let main_binary_name_kebab = heck::AsKebabCase(main_binary.name()).to_string(); + let new_path = project_out_directory.join(&main_binary_name_kebab); + fs::copy(main_binary_path, new_path)?; + + let main_binary = settings.main_binary_mut()?; + main_binary.set_name(main_binary_name_kebab); + } + + // generate deb_folder structure + let (data_dir, icons) = debian::generate_data(&settings, &package_dir) + .with_context(|| "Failed to build data folders and files")?; + fs_utils::copy_custom_files(&settings.appimage().files, &data_dir) + .with_context(|| "Failed to copy custom files")?; + + fs::create_dir_all(&output_path)?; + let app_dir_path = output_path.join(format!("{}.AppDir", settings.product_name())); + let appimage_filename = format!( + "{}_{}_{}.AppImage", + settings.product_name(), + settings.version_string(), + appimage_arch + ); + let appimage_path = output_path.join(&appimage_filename); + fs_utils::create_dir(&app_dir_path, true)?; + + fs::create_dir_all(&tools_path)?; + let larger_icon = icons + .iter() + .filter(|i| i.width == i.height) + .max_by_key(|i| i.width) + .expect("couldn't find a square icon to use as AppImage icon"); + let larger_icon_path = larger_icon + .path + .strip_prefix(package_dir.join("data")) + .unwrap() + .to_string_lossy() + .to_string(); + + log::info!(action = "Bundling"; "{} ({})", appimage_filename, appimage_path.display()); + + let app_dir_usr = app_dir_path.join("usr/"); + let app_dir_usr_bin = app_dir_usr.join("bin/"); + let app_dir_usr_lib = app_dir_usr.join("lib/"); + + fs_utils::copy_dir(&data_dir.join("usr/"), &app_dir_usr)?; + + // Using create_dir_all for a single dir so we don't get errors if the path already exists + fs::create_dir_all(&app_dir_usr_bin)?; + fs::create_dir_all(&app_dir_usr_lib)?; + + // Copy bins and libs that linuxdeploy doesn't know about + + // we also check if the user may have provided their own copy already + // xdg-open will be handled by the `files` config instead + if settings.deep_link_protocols().is_some() && !app_dir_usr_bin.join("xdg-open").exists() { + fs::copy("/usr/bin/xdg-mime", app_dir_usr_bin.join("xdg-mime")) + .context("xdg-mime binary not found")?; + } + + // we also check if the user may have provided their own copy already + if settings.appimage().bundle_xdg_open && !app_dir_usr_bin.join("xdg-open").exists() { + fs::copy("/usr/bin/xdg-open", app_dir_usr_bin.join("xdg-open")) + .context("xdg-open binary not found")?; + } + + let search_dirs = [ + match settings.binary_arch() { + Arch::X86_64 => "/usr/lib/x86_64-linux-gnu/", + Arch::X86 => "/usr/lib/i386-linux-gnu/", + Arch::AArch64 => "/usr/lib/aarch64-linux-gnu/", + Arch::Armhf => "/usr/lib/arm-linux-gnueabihf/", + _ => unreachable!(), + }, + "/usr/lib64", + "/usr/lib", + "/usr/libexec", + ]; + + for file in [ + "WebKitNetworkProcess", + "WebKitWebProcess", + "libwebkit2gtkinjectedbundle.so", + ] { + for source in search_dirs.map(PathBuf::from) { + // TODO: Check if it's the same dir name on all systems + let source = source.join("webkit2gtk-4.1").join(file); + if source.exists() { + fs_utils::copy_file( + &source, + &app_dir_path.join(source.strip_prefix("/").unwrap()), + )?; + } + } + } + + fs::copy( + tools_path.join(format!("AppRun-{tools_arch}")), + app_dir_path.join("AppRun"), + )?; + fs::copy( + app_dir_path.join(larger_icon_path), + app_dir_path.join(format!("{product_name}.png")), + )?; + std::os::unix::fs::symlink( + app_dir_path.join(format!("{product_name}.png")), + app_dir_path.join(".DirIcon"), + )?; + std::os::unix::fs::symlink( + app_dir_path.join(format!("usr/share/applications/{product_name}.desktop")), + app_dir_path.join(format!("{product_name}.desktop")), + )?; + + let log_level = match settings.log_level() { + log::Level::Error => "3", + log::Level::Warn => "2", + log::Level::Info => "1", + _ => "0", + }; + + let mut cmd = Command::new(linuxdeploy_path); + cmd.env("OUTPUT", &appimage_path); + cmd.args([ + "--appimage-extract-and-run", + "--verbosity", + log_level, + "--appdir", + &app_dir_path.display().to_string(), + "--plugin", + "gtk", + ]); + if settings.appimage().bundle_media_framework { + cmd.args(["--plugin", "gstreamer"]); + } + cmd.args(["--output", "appimage"]); + + // Linuxdeploy logs everything into stderr so we have to ignore the output ourselves here + if settings.log_level() == log::Level::Error { + log::debug!(action = "Running"; "Command `linuxdeploy {}`", cmd.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}"))); + if !cmd.output()?.status.success() { + return Err(crate::Error::GenericError( + "failed to run linuxdeploy".to_string(), + )); + } + } else { + cmd.output_ok()?; + } + + fs::remove_dir_all(&package_dir)?; + Ok(vec![appimage_path]) +} + +// returns the linuxdeploy path to keep linuxdeploy_arch contained +fn prepare_tools(tools_path: &Path, arch: &str) -> crate::Result { + let apprun = tools_path.join(format!("AppRun-{arch}")); + if !apprun.exists() { + let data = download(&format!( + "https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-{arch}" + ))?; + write_and_make_executable(&apprun, data)?; + } + + let linuxdeploy_arch = if arch == "i686" { "i383" } else { arch }; + let linuxdeploy = tools_path.join(format!("linuxdeploy-{linuxdeploy_arch}.AppImage")); + if !linuxdeploy.exists() { + let data = download(&format!("https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-{linuxdeploy_arch}.AppImage"))?; + write_and_make_executable(&linuxdeploy, data)?; + } + + let gtk = tools_path.join("linuxdeploy-plugin-gtk.sh"); + if !gtk.exists() { + let data = download("https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh")?; + write_and_make_executable(>k, data)?; + } + + let gstreamer = tools_path.join("linuxdeploy-plugin-gstreamer.sh"); + if !gstreamer.exists() { + let data = download("https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh")?; + write_and_make_executable(&gstreamer, data)?; + } + + // This should prevent linuxdeploy to be detected by appimage integration tools + let _ = Command::new("dd") + .args([ + "if=/dev/zero", + "bs=1", + "count=3", + "seek=8", + "conv=notrunc", + &format!("of={}", linuxdeploy.display()), + ]) + .output(); + + Ok(linuxdeploy) +} + +fn write_and_make_executable(path: &Path, data: Vec) -> std::io::Result<()> { + use std::os::unix::fs::PermissionsExt; + + let mut file = fs::File::create(path)?; + file.write_all(&data)?; + let mut perms = file.metadata()?.permissions(); + perms.set_mode(0o770); + Ok(()) +} diff --git a/crates/tauri-bundler/src/bundle/linux/appimage/appimage b/crates/tauri-bundler/src/bundle/linux/appimage/appimage deleted file mode 100644 index 8e447a004..000000000 --- a/crates/tauri-bundler/src/bundle/linux/appimage/appimage +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2019-2024 Tauri Programme within The Commons Conservancy -# SPDX-License-Identifier: Apache-2.0 -# SPDX-License-Identifier: MIT - -set -euxo pipefail - -export ARCH={{arch}} -APPIMAGE_BUNDLE_XDG_OPEN=${APPIMAGE_BUNDLE_XDG_OPEN-0} -APPIMAGE_BUNDLE_XDG_MIME=${APPIMAGE_BUNDLE_XDG_MIME-0} -APPIMAGE_BUNDLE_GSTREAMER=${APPIMAGE_BUNDLE_GSTREAMER-0} -TAURI_TRAY_LIBRARY_PATH=${TAURI_TRAY_LIBRARY_PATH-0} - -if [ "$ARCH" == "i686" ]; then - linuxdeploy_arch="i386" -else - linuxdeploy_arch="$ARCH" -fi - -mkdir -p "{{product_name}}.AppDir" -cp -r ../appimage_deb/data/usr "{{product_name}}.AppDir" - -cd "{{product_name}}.AppDir" -mkdir -p "usr/bin" -mkdir -p "usr/lib" - -if [[ "$APPIMAGE_BUNDLE_XDG_OPEN" != "0" ]] && [[ -f "/usr/bin/xdg-open" ]]; then - echo "Copying /usr/bin/xdg-open" - cp /usr/bin/xdg-open usr/bin -fi - -if [[ "$APPIMAGE_BUNDLE_XDG_MIME" != "0" ]] && [[ -f "/usr/bin/xdg-mime" ]]; then - echo "Copying /usr/bin/xdg-mime" - cp /usr/bin/xdg-mime usr/bin -fi - -if [[ "$TAURI_TRAY_LIBRARY_PATH" != "0" ]]; then - echo "Copying appindicator library ${TAURI_TRAY_LIBRARY_PATH}" - cp ${TAURI_TRAY_LIBRARY_PATH} usr/lib - # It looks like we're practicing good hygiene by adding the ABI version. - # But for compatibility we'll symlink this file to what we did before. - # Specifically this prevents breaking libappindicator-sys v0.7.1 and v0.7.2. - if [[ "$TAURI_TRAY_LIBRARY_PATH" == *.so.1 ]]; then - readonly soname=$(basename "$TAURI_TRAY_LIBRARY_PATH") - readonly old_name=$(basename "$TAURI_TRAY_LIBRARY_PATH" .1) - echo "Adding compatibility symlink ${old_name} -> ${soname}" - ln -s ${soname} usr/lib/${old_name} - fi -fi - -# Copy WebKit files. Follow symlinks in case `/usr/lib64` is a symlink to `/usr/lib` -find -L /usr/lib* -name WebKitNetworkProcess -exec mkdir -p "$(dirname '{}')" \; -exec cp --parents '{}' "." \; || true -find -L /usr/lib* -name WebKitWebProcess -exec mkdir -p "$(dirname '{}')" \; -exec cp --parents '{}' "." \; || true -find -L /usr/lib* -name libwebkit2gtkinjectedbundle.so -exec mkdir -p "$(dirname '{}')" \; -exec cp --parents '{}' "." \; || true - -( cd "{{tauri_tools_path}}" && ( wget -q -4 -N https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${ARCH} || wget -q -4 -N https://github.com/AppImage/AppImageKit/releases/download/12/AppRun-${ARCH} ) ) -chmod +x "{{tauri_tools_path}}/AppRun-${ARCH}" - -# We need AppRun to be installed as {{product_name}}.AppDir/AppRun. -# Otherwise the linuxdeploy scripts will default to symlinking our main bin instead and will crash on trying to launch. -cp "{{tauri_tools_path}}/AppRun-${ARCH}" AppRun - -cp "{{icon_path}}" .DirIcon -ln -sf "{{icon_path}}" "{{product_name}}.png" - -ln -sf "usr/share/applications/{{product_name}}.desktop" "{{product_name}}.desktop" - -cd .. - -if [[ "$APPIMAGE_BUNDLE_GSTREAMER" != "0" ]]; then - gst_plugin="--plugin gstreamer" - wget -q -4 -N "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh" - chmod +x linuxdeploy-plugin-gstreamer.sh -else - gst_plugin="" -fi - -( cd "{{tauri_tools_path}}" && wget -q -4 -N https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh ) -( cd "{{tauri_tools_path}}" && wget -q -4 -N https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-${linuxdeploy_arch}.AppImage ) - -chmod +x "{{tauri_tools_path}}/linuxdeploy-plugin-gtk.sh" -chmod +x "{{tauri_tools_path}}/linuxdeploy-${linuxdeploy_arch}.AppImage" - -dd if=/dev/zero bs=1 count=3 seek=8 conv=notrunc of="{{tauri_tools_path}}/linuxdeploy-${linuxdeploy_arch}.AppImage" - -OUTPUT="{{appimage_filename}}" "{{tauri_tools_path}}/linuxdeploy-${linuxdeploy_arch}.AppImage" --appimage-extract-and-run --appdir "{{product_name}}.AppDir" --plugin gtk ${gst_plugin} --output appimage diff --git a/crates/tauri-bundler/src/bundle/linux/appimage/mod.rs b/crates/tauri-bundler/src/bundle/linux/appimage/mod.rs deleted file mode 100644 index 70d39e570..000000000 --- a/crates/tauri-bundler/src/bundle/linux/appimage/mod.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2016-2019 Cargo-Bundle developers -// Copyright 2019-2024 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use super::debian; -use crate::{ - bundle::settings::Arch, - utils::{fs_utils, CommandExt}, - Settings, -}; -use anyhow::Context; -use handlebars::Handlebars; -use std::{ - collections::BTreeMap, - fs, - path::PathBuf, - process::{Command, Stdio}, -}; - -/// Bundles the project. -/// Returns a vector of PathBuf that shows where the AppImage was created. -pub fn bundle_project(settings: &Settings) -> crate::Result> { - // generate the deb binary name - let arch: &str = match settings.binary_arch() { - Arch::X86_64 => "amd64", - Arch::X86 => "i386", - Arch::AArch64 => "aarch64", - Arch::Armhf => "armhf", - target => { - return Err(crate::Error::ArchError(format!( - "Unsupported architecture: {:?}", - target - ))); - } - }; - let package_dir = settings.project_out_directory().join("bundle/appimage_deb"); - - let main_binary = settings.main_binary()?; - - let mut settings = settings.clone(); - if main_binary.name().contains(" ") { - let main_binary_path = settings.binary_path(main_binary); - let project_out_directory = settings.project_out_directory(); - - let main_binary_name_kebab = heck::AsKebabCase(main_binary.name()).to_string(); - let new_path = project_out_directory.join(&main_binary_name_kebab); - fs::copy(main_binary_path, new_path)?; - - let main_binary = settings.main_binary_mut()?; - main_binary.set_name(main_binary_name_kebab); - } - - // generate deb_folder structure - let (data_dir, icons) = debian::generate_data(&settings, &package_dir) - .with_context(|| "Failed to build data folders and files")?; - fs_utils::copy_custom_files(&settings.appimage().files, &data_dir) - .with_context(|| "Failed to copy custom files")?; - - let output_path = settings.project_out_directory().join("bundle/appimage"); - if output_path.exists() { - fs::remove_dir_all(&output_path)?; - } - fs::create_dir_all(output_path.clone())?; - let app_dir_path = output_path.join(format!("{}.AppDir", settings.product_name())); - let appimage_filename = format!( - "{}_{}_{}.AppImage", - settings.product_name(), - settings.version_string(), - arch - ); - let appimage_path = output_path.join(&appimage_filename); - fs_utils::create_dir(&app_dir_path, true)?; - - // setup data to insert into shell script - let mut sh_map = BTreeMap::new(); - sh_map.insert("arch", settings.target().split('-').next().unwrap()); - sh_map.insert("product_name", settings.product_name()); - sh_map.insert("appimage_filename", &appimage_filename); - - let tauri_tools_path = settings - .local_tools_directory() - .map(|d| d.join(".tauri")) - .unwrap_or_else(|| { - dirs::cache_dir().map_or_else(|| output_path.to_path_buf(), |p| p.join("tauri")) - }); - - std::fs::create_dir_all(&tauri_tools_path)?; - let tauri_tools_path_str = tauri_tools_path.to_string_lossy(); - sh_map.insert("tauri_tools_path", &tauri_tools_path_str); - let larger_icon = icons - .iter() - .filter(|i| i.width == i.height) - .max_by_key(|i| i.width) - .expect("couldn't find a square icon to use as AppImage icon"); - let larger_icon_path = larger_icon - .path - .strip_prefix(package_dir.join("data")) - .unwrap() - .to_string_lossy() - .to_string(); - sh_map.insert("icon_path", &larger_icon_path); - - // initialize shell script template. - let mut handlebars = Handlebars::new(); - handlebars.register_escape_fn(handlebars::no_escape); - let temp = handlebars.render_template(include_str!("./appimage"), &sh_map)?; - - // create the shell script file in the target/ folder. - let sh_file = output_path.join("build_appimage.sh"); - - log::info!(action = "Bundling"; "{} ({})", appimage_filename, appimage_path.display()); - - fs::write(&sh_file, temp)?; - - // chmod script for execution - Command::new("chmod") - .arg("777") - .arg(&sh_file) - .current_dir(output_path.clone()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .expect("Failed to chmod script"); - - // execute the shell script to build the appimage. - Command::new(&sh_file) - .current_dir(output_path) - .output_ok() - .context("error running build_appimage.sh")?; - - fs::remove_dir_all(&package_dir)?; - Ok(vec![appimage_path]) -} diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index 7ee51102d..b7d39aa68 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -220,6 +220,10 @@ pub struct DebianSettings { pub struct AppImageSettings { /// The files to include in the Appimage Binary. pub files: HashMap, + /// Whether to include gstreamer plugins for audio/media support. + pub bundle_media_framework: bool, + /// Whether to include the `xdg-open` binary. + pub bundle_xdg_open: bool, } /// The RPM bundle settings. diff --git a/crates/tauri-cli/src/bundle.rs b/crates/tauri-cli/src/bundle.rs index 5473c4b46..20118b224 100644 --- a/crates/tauri-cli/src/bundle.rs +++ b/crates/tauri-cli/src/bundle.rs @@ -181,24 +181,6 @@ pub fn bundle( _ => log::Level::Trace, }); - // set env vars used by the bundler - #[cfg(target_os = "linux")] - { - if config.bundle.linux.appimage.bundle_media_framework { - std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1"); - } - - if let Some(open) = config.plugins.0.get("shell").and_then(|v| v.get("open")) { - if open.as_bool().is_some_and(|x| x) || open.is_string() { - std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1"); - } - } - - if settings.deep_link_protocols().is_some() { - std::env::set_var("APPIMAGE_BUNDLE_XDG_MIME", "1"); - } - } - let bundles = tauri_bundler::bundle_project(&settings) .map_err(|e| match e { tauri_bundler::Error::BundlerError(e) => e, diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index 8a407f8ee..04906b1b0 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -867,6 +867,26 @@ impl AppSettings for RustAppSettings { }); } + if let Some(open) = config.plugins.0.get("shell").and_then(|v| v.get("open")) { + if open.as_bool().is_some_and(|x| x) || open.is_string() { + settings.appimage.bundle_xdg_open = true; + } + } + + if let Some(deps) = self + .manifest + .lock() + .unwrap() + .inner + .as_table() + .get("dependencies") + .and_then(|f| f.as_table()) + { + if deps.contains_key("tauri-plugin-opener") { + settings.appimage.bundle_xdg_open = true; + }; + } + Ok(settings) } @@ -1234,6 +1254,9 @@ fn tauri_config_to_bundle_settings( #[allow(unused_mut)] let mut depends_rpm = config.linux.rpm.depends.unwrap_or_default(); + #[allow(unused_mut)] + let mut appimage_files = config.linux.appimage.files; + // set env vars used by the bundler and inject dependencies #[cfg(target_os = "linux")] { @@ -1276,7 +1299,12 @@ fn tauri_config_to_bundle_settings( } } - std::env::set_var("TAURI_TRAY_LIBRARY_PATH", path); + // conditionally setting it in case the user provided its own version for some reason + let path = PathBuf::from(path); + if !appimage_files.contains_key(&path) { + // manually construct target path, just in case the source path is something unexpected + appimage_files.insert(Path::new("/usr/lib/").join(path.file_name().unwrap()), path); + } } depends_deb.push("libwebkit2gtk-4.1-0".to_string()); @@ -1368,7 +1396,9 @@ fn tauri_config_to_bundle_settings( post_remove_script: config.linux.deb.post_remove_script, }, appimage: AppImageSettings { - files: config.linux.appimage.files, + files: appimage_files, + bundle_media_framework: config.linux.appimage.bundle_media_framework, + bundle_xdg_open: false, }, rpm: RpmSettings { depends: if depends_rpm.is_empty() {