refactor(bundler): Move appimage bundler script logic into rust (#11758)

This commit is contained in:
Fabian-Lars 2024-11-28 03:52:24 +01:00 committed by GitHub
parent 18bd639f6e
commit d6bed20a0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 301 additions and 240 deletions

View File

@ -0,0 +1,265 @@
// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
// 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<Vec<PathBuf>> {
// 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<PathBuf> {
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(&gtk, 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<u8>) -> 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(())
}

View File

@ -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

View File

@ -1,134 +0,0 @@
// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
// 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<Vec<PathBuf>> {
// 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])
}

View File

@ -220,6 +220,10 @@ pub struct DebianSettings {
pub struct AppImageSettings {
/// The files to include in the Appimage Binary.
pub files: HashMap<PathBuf, PathBuf>,
/// 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.

View File

@ -181,24 +181,6 @@ pub fn bundle<A: AppSettings>(
_ => 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,

View File

@ -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() {