diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f0d1978..40a1b0c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -25,6 +25,20 @@ jobs: vendor: TelegramMessenger - run: cargo test --workspace --all-features + rlottie-sys-vendor-samsung: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test -p rlottie-sys --lib + + rlottie-sys-vendor-telegram: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test -p rlottie-sys --lib --features vendor-telegram --no-default-features + rustfmt: runs-on: ubuntu-latest steps: diff --git a/rlottie-sys/Cargo.toml b/rlottie-sys/Cargo.toml index 6630546..3464a3a 100644 --- a/rlottie-sys/Cargo.toml +++ b/rlottie-sys/Cargo.toml @@ -11,9 +11,26 @@ license.workspace = true repository.workspace = true edition = "2021" -rust-version = "1.64" # bump version in build.rs as well +rust-version = "1.70" # bump version in build.rs as well links = "rlottie" [build-dependencies] bindgen = { version = "0.71", features = ["prettyplease", "runtime"], default-features = false } +#cmake = { version = "0.1.54", optional = true } pkg-config = "0.3.22" + +[features] +default = ["vendor-samsung"] + +# If rlottie cannot be found on the system, download the samsung version of rlottie and +# compile it. You can force the use of the vendored code by setting `RLOTTIE_NO_PKG_CONFIG`. +# If both `vendor-samsung` and `vendor-telegram` are enabled, samsung has priority. +vendor-samsung = ["__vendor"] + +# If rlottie cannot be found on the system, download the telegram version of rlottie and +# compile it. You can force the use of the vendored code by setting `RLOTTIE_NO_PKG_CONFIG`. +# If both `vendor-samsung` and `vendor-telegram` are enabled, samsung has priority. +vendor-telegram = ["__vendor"] + +# Internal +__vendor = [] diff --git a/rlottie-sys/README.md b/rlottie-sys/README.md index f2b9842..8659457 100644 --- a/rlottie-sys/README.md +++ b/rlottie-sys/README.md @@ -1,3 +1,11 @@ # rlottie-sys [![rlottie-sys on crates.io](https://img.shields.io/crates/v/rlottie-sys.svg)](https://crates.io/crates/rlottie-sys) [![rlottie-sys docs](https://img.shields.io/badge/docs-release-blue)](https://docs.msrd0.de/#rlottie-sys) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://mit-license.org/) Rust bindings to rlottie. + +## Features + + - `vendor-samsung` (enabled by default): If rlottie cannot be found on the system download the samsung version of rlottie and compile it. You can force the use of the vendored code by setting `RLOTTIE_NO_PKG_CONFIG`. + + - `vendor-telegram`: If rlottie cannot be found on the system download the telegram version of rlottie and compile it. You can force the use of the vendored code by setting `RLOTTIE_NO_PKG_CONFIG`. + +If both `vendor-samsung` and `vendor-telegram` are enabled, samsung has priority. diff --git a/rlottie-sys/build.rs b/rlottie-sys/build.rs index 58a624d..347a9b2 100644 --- a/rlottie-sys/build.rs +++ b/rlottie-sys/build.rs @@ -1,24 +1,196 @@ use std::{env, path::PathBuf}; +#[cfg(feature = "vendor-samsung")] +mod source { + pub const VENDOR: &str = "samsung"; + pub const GIT_REPO: &str = "https://github.com/Samsung/rlottie.git"; + pub const GIT_REV: &str = "v0.2"; + pub const GIT_PATCHES: &[&str] = &["2d7b1fa2b005bba3d4b45e8ebfa632060e8a157a"]; +} + +#[cfg(all(not(feature = "vendor-samsung"), feature = "vendor-telegram"))] +mod source { + pub const VENDOR: &str = "telegram"; + pub const GIT_REPO: &str = "https://github.com/TelegramMessenger/rlottie"; + pub const GIT_REV: &str = "67f103bc8b625f2a4a9e94f1d8c7bd84c5a08d1d"; + pub const GIT_PATCHES: &[&str] = &["1dd47cec7eb8e1f657f02dce9c497ae60f7cf8c5"]; +} + +#[cfg(feature = "__vendor")] +fn compile_rlottie() -> Vec { + use crate::source::*; + use std::{ + env::current_dir, + fmt::{self, Display, Formatter}, + fs::create_dir_all, + process::{Command, Stdio} + }; + + struct DisplayCommand<'a>(&'a Command); + + impl Display for DisplayCommand<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0.get_program())?; + for arg in self.0.get_args() { + write!(f, " {arg:?}")?; + } + Ok(()) + } + } + + fn print_cmd(cmd: &Command) { + println!( + "{} $ {}", + cmd.get_current_dir() + .map(PathBuf::from) + .unwrap_or_else(|| current_dir().unwrap()) + .display(), + DisplayCommand(cmd) + ); + } + + fn try_run(cmd: &mut Command) -> bool { + // no user interactions, ever + cmd.stdin(Stdio::null()); + + print_cmd(cmd); + cmd.status().is_ok_and(|status| status.success()) + } + + fn run(cmd: &mut Command) { + // no user interactions, ever + cmd.stdin(Stdio::null()); + + print_cmd(cmd); + match cmd.status() { + Ok(status) if status.success() => {}, + Ok(status) => panic!( + "Failed to run {}: Exit code {:?}", + DisplayCommand(cmd), + status.code() + ), + Err(err) => panic!("Failed to run {}: {err:?}", DisplayCommand(cmd)) + } + } + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let src_dir = out_dir.join(format!("rlottie-{VENDOR}-{GIT_REV}")); + let build_dir = out_dir.join(format!("build-{VENDOR}-{GIT_REV}")); + + if !src_dir.exists() { + run(Command::new("git") + .arg("clone") + .arg(GIT_REPO) + .arg(&src_dir) + .current_dir(&out_dir)); + // since telegram doesn't merge fixes we need to download pull requests + if VENDOR == "telegram" { + run(Command::new("git") + .arg("config") + .arg("--add") + .arg("remote.origin.fetch") + .arg("refs/pull/*/head:refs/remotes/origin/pull/*") + .current_dir(&src_dir)); + run(Command::new("git").arg("fetch").current_dir(&src_dir)); + } + } + run(Command::new("git") + .arg("checkout") + .arg(GIT_REV) + .current_dir(&src_dir)); + for rev in GIT_PATCHES { + run(Command::new("git") + .arg("cherry-pick") + .arg("-x") + .arg(rev) + .current_dir(&src_dir) + .env("GIT_COMMITTER_NAME", "nobody") + .env("GIT_COMMITTER_EMAIL", "nobody")); + } + + // we could revisit this if the `cmake` crate becomes less opinionated + // - `cmake` forces setting of `CMAKE_BUILD_TYPE` which is fine unless you are part + // of an already build-tool-independent configured environment like a linux + // distribution packaging process where they want the same flags for everything + // - `cmake` forces setting of `CMAKE_INSTALL_PREFIX` for no good reason + // - `cmake` has no way to set the build directory, and it doesn't use out_dir + // as its target directory + // - `cmake` prints some weird `cargo:root` line that I only found mentioned here + // https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/cargo/reference/build-scripts.html#outputs-of-the-build-script + // where it is called "user-defined metadata" but cmake is only part of the build + // script so who would use that metadata? + // cmake::Config::new(&src_dir) + // .out_dir(&build_dir) + // .build_target("rlottie") + // .build(); + + // for the time being, I force my own opinions on how to use cmake + create_dir_all(&build_dir).expect("Failed to create directory"); + let mut cmake = Command::new("cmake"); + cmake.arg("-Wno-dev"); + cmake.arg("-DBUILD_SHARED_LIBS=OFF"); + if try_run(Command::new("which").arg("ninja")) { + cmake.arg("-G").arg("Ninja"); + } + if env::var("CFLAGS").is_err() { + let opt_level = env::var("OPT_LEVEL").unwrap(); + let mut build_type = "Release"; + // TODO rust supports debug info in release builds, we should detect that + if opt_level == "0" || opt_level == "g" { + build_type = "RelWithDebInfo"; + } else if opt_level == "s" || opt_level == "z" { + build_type = "MinSizeRel"; + } + cmake.arg(format!("-DCMAKE_BUILD_TYPE={build_type}")); + } + cmake.arg(&src_dir); + cmake.current_dir(&build_dir); + run(&mut cmake); + run(Command::new("cmake") + .arg("--build") + .arg(".") + .arg("--target") + .arg("rlottie") + .current_dir(&build_dir)); + + println!("cargo:rustc-link-search=native={}", build_dir.display()); + println!("cargo:rustc-link-lib=static=rlottie"); + // no idea how platform/target specific this is but it doesn't work without it + println!("cargo:rustc-link-lib=stdc++"); + + vec![src_dir.join("inc")] +} + fn main() { - pkg_config::Config::new() + let include_paths = pkg_config::Config::new() .probe("rlottie") - .expect("Unable to find rlottie"); + .map(|lib| lib.include_paths); + + #[cfg(not(feature = "__vendor"))] + let include_paths = include_path.expect("Unable to find rlottie"); + + #[cfg(feature = "__vendor")] + let include_paths = include_paths.unwrap_or_else(|_| compile_rlottie()); println!("cargo:rerun-if-changed=wrapper.h"); let bindings = bindgen::Builder::default() .formatter(bindgen::Formatter::Prettyplease) + .clang_args( + include_paths + .into_iter() + .map(|path| format!("-I{}", path.display())) + ) .header("wrapper.h") .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .newtype_enum(".*") - .rust_target(bindgen::RustTarget::stable(64, 0).unwrap_or_else(|_| panic!())) + .rust_target(bindgen::RustTarget::stable(70, 0).unwrap_or_else(|_| panic!())) .size_t_is_usize(true) .use_core() .generate() .expect("Unable to generate bindings"); - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); bindings - .write_to_file(out_path.join("bindings.rs")) + .write_to_file(out_dir.join("bindings.rs")) .expect("Couldn't write bindings!"); }