diff --git a/.changes/fix-dynamic-linking.md b/.changes/fix-dynamic-linking.md new file mode 100644 index 0000000..018201d --- /dev/null +++ b/.changes/fix-dynamic-linking.md @@ -0,0 +1,13 @@ + +--- +"libappindicator": minor +"libappindicator-sys": minor +--- + +Load exclusively using dynamic linking + +This change lets `dlopen` (through `ld.so`) handle what paths to search in for the respective libraries. +Additionally this fixes a mistake with the library filenames. Now using the `SONAME` instead of a symlinked name that happened to work when dev packages are installed. + +**Breaking:** Support for `$APPDIR` based appImage detection is removed. +Though it _should_ still work, because appimages provide an `LD_LIBRARY_PATH` that would be equivalent to what our previous detection method was doing in rust. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e50d125 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +target +Cargo.lock +.tests diff --git a/.tests/Dockerfile b/.tests/Dockerfile new file mode 100644 index 0000000..836ce3f --- /dev/null +++ b/.tests/Dockerfile @@ -0,0 +1,49 @@ +FROM ubuntu:18.04 as build + +# Dependencies +RUN apt-get update && \ + apt-get install --no-install-recommends -y \ + build-essential ca-certificates curl libappindicator3-dev + +# Drop privileges or Rust will have trouble installing +RUN useradd -m --uid 1000 --user-group rust +USER rust +WORKDIR /home/rust/src + +# Install toolchain +ENV RUST_TOOLCHAIN=nightly +ENV RUST_BACKTRACE=1 +ENV CARGO_INCREMENTAL=0 +ENV CARGO_PROFILE_DEV_DEBUG=0 + +RUN curl https://sh.rustup.rs -sSf | \ + sh -s -- --default-toolchain $RUST_TOOLCHAIN -y +ENV PATH=/home/rust/.cargo/bin:$PATH +RUN rustup component add rust-src --toolchain $RUST_TOOLCHAIN + +# Build our app. +COPY --chown=rust:rust . /home/rust/src +ARG build_cmd="cargo build --release --example=hello" +RUN sh -c "${build_cmd}" + + +FROM ubuntu:18.04 + +RUN apt-get update && \ + apt-get install -y libcanberra-gtk3-module binutils + +RUN mkdir -p /app + +ARG with_lib="" +RUN apt-get install -y ${with_lib} + +ENV TRAY_ICON_DIR /app/icons +COPY examples/rust-logo.png /app/icons/rust-logo.png +COPY --from=build /home/rust/src/target/release/examples/hello /app/bin/hello + +# RUN ls -la /usr/lib/x86_64-linux-gnu/ | grep indicator;\ +# ldconfig --print-cache | grep indic;\ +# objdump -p /usr/lib/x86_64-linux-gnu/*indicator* | grep SONAME;\ +# true + +CMD ["/app/bin/hello"] diff --git a/.tests/docker-compose.yml b/.tests/docker-compose.yml new file mode 100644 index 0000000..a4b3f8f --- /dev/null +++ b/.tests/docker-compose.yml @@ -0,0 +1,39 @@ +version: "3.8" +services: + + no-library: + environment: &env + RUST_BACKTRACE: 1 + DISPLAY: ${DISPLAY} + volumes: &vol + - /tmp/.X11-unix:/tmp/.X11-unix + build: &base-build + context: .. + dockerfile: .tests/Dockerfile + args: + build_cmd: cargo build --release --example=hello + with_lib: + + libayatana: + environment: *env + volumes: *vol + build: + <<: *base-build + args: + with_lib: libayatana-appindicator3-1 + + libappindicator: + environment: *env + volumes: *vol + build: + <<: *base-build + args: + with_lib: libappindicator3-1 + + both-libs: + environment: *env + volumes: *vol + build: + <<: *base-build + args: + with_lib: libayatana-appindicator3-1 libappindicator3-1 diff --git a/.tests/hello-examples.sh b/.tests/hello-examples.sh new file mode 100755 index 0000000..4163607 --- /dev/null +++ b/.tests/hello-examples.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +readonly workdir=$(dirname $(dirname $(realpath $0))) +cd ${workdir} + +echo "DISPLAY=${DISPLAY:-""}" +if [ ! -d /tmp/.X11-unix ]; then + echo "Directory /tmp/.X11-unix does not exist." +fi + +xhost + # Note this disable access control on Xorg, letting containers connect. +docker-compose -f .tests/docker-compose.yml up --build || true +xhost - # This reenables access control diff --git a/Cargo.toml b/Cargo.toml index 8906d00..af78c84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,7 @@ glib = "0.15" gtk = "0.15" gtk-sys = "0.15" libappindicator-sys = { version = "0.7", path = "sys/" } + +[features] +default = [ "backcompat" ] +backcompat = [ "libappindicator-sys/backcompat" ] diff --git a/examples/hello.rs b/examples/hello.rs index fea8827..75de4bd 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,5 +1,7 @@ -use std::env; -use std::path::Path; +use std::{ + env, + path::{Path, PathBuf}, +}; use gtk::prelude::*; use libappindicator::{AppIndicator, AppIndicatorStatus}; @@ -8,9 +10,19 @@ fn main() { gtk::init().unwrap(); let mut indicator = AppIndicator::new("libappindicator test application", ""); indicator.set_status(AppIndicatorStatus::Active); - let icon_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples"); + + let icon_path = match env::var("TRAY_ICON_DIR") { + Ok(dir) => PathBuf::from(dir), + _ => Path::new(env!("CARGO_MANIFEST_DIR")).join("examples"), + }; + + let icon_name = match env::var("TRAY_ICON_NAME") { + Ok(name) => name, + _ => "rust-logo".to_string(), + }; + indicator.set_icon_theme_path(icon_path.to_str().unwrap()); - indicator.set_icon_full("rust-logo", "icon"); + indicator.set_icon_full(&icon_name, "icon"); let mut m = gtk::Menu::new(); let mi = gtk::CheckMenuItem::with_label("Hello Rust!"); mi.connect_activate(|_| { diff --git a/examples/rust-logo.png b/examples/rust-logo.png index d1e9f80..14041eb 100644 Binary files a/examples/rust-logo.png and b/examples/rust-logo.png differ diff --git a/sys/Cargo.toml b/sys/Cargo.toml index 08f31aa..24f492a 100644 --- a/sys/Cargo.toml +++ b/sys/Cargo.toml @@ -11,3 +11,7 @@ categories = [ "external-ffi-bindings" ] gtk-sys = "0.15" libloading = "0.7" once_cell = "1.12" + +[features] +default = [ "backcompat" ] +backcompat = [ ] diff --git a/sys/src/lib.rs b/sys/src/lib.rs index 768852c..8a3e337 100644 --- a/sys/src/lib.rs +++ b/sys/src/lib.rs @@ -8,73 +8,51 @@ use gtk_sys::{ }; use libloading::*; use once_cell::sync::Lazy; -use std::{os::raw::*, path::PathBuf, process::Command}; +use std::os::raw::*; pub static LIB: Lazy = Lazy::new(|| { - #[cfg(target_os = "linux")] - { - if let Some(appimage_path) = std::env::var_os("APPDIR") { - // validate that we're actually running on an AppImage - // an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}` - // see https://github.com/AppImage/AppImageKit/blob/1681fd84dbe09c7d9b22e13cdb16ea601aa0ec47/src/runtime.c#L501 - // note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage. - let is_temp = std::env::current_exe() - .map(|p| { - p.display() - .to_string() - .starts_with(&format!("{}/.mount_", std::env::temp_dir().display())) - }) - .unwrap_or(true); + let libayatana = unsafe { Library::new("libayatana-appindicator3.so.1") }; + if let Ok(lib) = libayatana { + return lib; + } - if !is_temp { - panic!("`APPDIR` environment variable found but this application was not detected as an AppImage; this might be a security issue."); - } + let libappindicator = unsafe { Library::new("libappindicator3.so.1") }; + if let Ok(lib) = libappindicator { + return lib; + } - let appimage_path = PathBuf::from(appimage_path); - let ayatana_target_path = appimage_path.join("usr/lib/libayatana-appindicator3.so"); - let gtk_target_path = appimage_path.join("usr/lib/libappindicator3.so"); + // Versions v0.7.1 and v0.7.2 relied exclusively on the .so files without .1 suffix. + // This is 'bad' because by convention that signals we don't care about ABI compatibility. + // However in weird cases (*ahum* Tauri bundled appimages) this .so file is the only one + // available. Using this feature flag allows them some time to fix this problem and bundle + // with the correct filename. + #[cfg(feature = "backcompat")] + { + let libayatana_compat = unsafe { Library::new("libayatana-appindicator3.so") }; + if let Ok(lib) = libayatana_compat { + return lib; + } - if ayatana_target_path.exists() { - return unsafe { Library::new(ayatana_target_path).unwrap() }; - } else if gtk_target_path.exists() { - return unsafe { Library::new(gtk_target_path).unwrap() }; - } + let libappindicator_compat = unsafe { Library::new("libappindicator3.so") }; + if let Ok(lib) = libappindicator_compat { + return lib; } - } - // PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs-only-L ayatana-appindicator3-0.1 - let path = get_appindicator_library_path(); - unsafe { Library::new(path).unwrap() } -}); -/// Gets the path to the target appindicator library file (`.so` extension). -pub fn get_appindicator_library_path() -> PathBuf { - match get_library_path("ayatana-appindicator3-0.1") { - Some(p) => format!("{}/libayatana-appindicator3.so", p).into(), - None => match get_library_path("appindicator3-0.1") { - Some(p) => format!("{}/libappindicator3.so", p).into(), - None => panic!("Can't detect any appindicator library"), - }, + panic!( + "Failed to load ayatana-appindicator3 or appindicator3 dynamic library\n{}\n{}\n{}\n{}", + libayatana.unwrap_err(), + libappindicator.unwrap_err(), + libayatana_compat.unwrap_err(), + libappindicator_compat.unwrap_err(), + ); } -} -/// Gets the folder in which a library is located using `pkg-config`. -pub fn get_library_path(name: &str) -> Option { - 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 { - return None; - } - } else { - return None; - } -} + panic!( + "Failed to load ayatana-appindicator3 or appindicator3 dynamic library\n{}\n{}", + libayatana.unwrap_err(), + libappindicator.unwrap_err() + ); +}); pub type guint32 = c_uint; pub type gint64 = c_long;