From a4b61d305d9f272f9d5d863d8cec8429f7da5db3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 5 Feb 2026 15:24:41 +0530 Subject: [PATCH] chore: enhance macOS bundling process for TDLib - Updated the package-and-publish.yml workflow to utilize Tauri's native bundling for TDLib on macOS, removing the need for post-build scripts. - Modified build.rs to copy TDLib and its dependencies to the libraries/ directory, ensuring they are included in the app bundle. - Updated tauri.conf.json to reference the newly bundled TDLib and its dependencies. - Added entries to .gitignore for the new libraries directory. --- .github/workflows/package-and-publish.yml | 29 +--- src-tauri/.gitignore | 3 + src-tauri/Cargo.lock | 2 +- src-tauri/build.rs | 177 ++++++++++++++++++++++ src-tauri/tauri.conf.json | 7 +- 5 files changed, 190 insertions(+), 28 deletions(-) diff --git a/.github/workflows/package-and-publish.yml b/.github/workflows/package-and-publish.yml index 4f4a48fdd..2a28a1303 100644 --- a/.github/workflows/package-and-publish.yml +++ b/.github/workflows/package-and-publish.yml @@ -321,35 +321,12 @@ jobs: args: "-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}" includeDebug: ${{ needs.get-version.outputs.should-publish == '' && inputs.forceRelease != 'true' }} includeRelease: ${{ needs.get-version.outputs.should-publish != '' || inputs.forceRelease == 'true' }} - # Don't let tauri-action upload for macOS - we need to bundle TDLib first and recreate the DMG - # For non-macOS platforms, upload directly via tauri-action - releaseId: ${{ matrix.settings.platform != 'macos-latest' && needs.create-release.outputs.releaseId || '' }} + # TDLib dylibs are now bundled natively via build.rs + tauri.conf.json macOS.frameworks + # No post-build scripts needed - Tauri bundler handles copying and signing + releaseId: ${{ needs.create-release.outputs.releaseId }} owner: alphahumanxyz repo: alphahuman - # # Bundle TDLib dylib into macOS app (fixes "Library not loaded" crash) - # - name: Bundle TDLib for macOS - # if: matrix.settings.platform == 'macos-latest' - # run: | - # TARGET="${{ matrix.settings.target }}" - # echo "Bundling TDLib for target: $TARGET" - # chmod +x ./src-tauri/scripts/bundle-tdlib-macos.sh - # ./src-tauri/scripts/bundle-tdlib-macos.sh release "$TARGET" - - # # Re-sign the app after modifying the bundle (required for notarization) - # - name: Re-sign macOS app after TDLib bundling - # if: matrix.settings.platform == 'macos-latest' - # env: - # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} - # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - # APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - # run: bash ./src-tauri/scripts/resign-macos.sh release "${{ matrix.settings.target }}" - - # # Re-create DMG after TDLib bundling (the original DMG was created before bundling) - # - name: Recreate DMG with bundled TDLib - # if: matrix.settings.platform == 'macos-latest' - # run: bash ./src-tauri/scripts/recreate-dmg-macos.sh release "${{ matrix.settings.target }}" - - name: Get file info id: file-info shell: bash diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index b21bd681d..3a30809c8 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -5,3 +5,6 @@ # Generated by Tauri # will have schema files for capabilities auto-completion /gen/schemas + +# TDLib and dependencies copied by build.rs for macOS bundling +/libraries/ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9db94fefd..4960a8fec 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "AlphaHuman" -version = "0.1.0" +version = "0.30.0" dependencies = [ "aes-gcm", "android_logger", diff --git a/src-tauri/build.rs b/src-tauri/build.rs index ddfa71cd8..f34696619 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -2,6 +2,7 @@ fn main() { // Get the target OS from environment variable (set by Cargo during cross-compilation) let target = std::env::var("TARGET").unwrap_or_default(); let is_mobile_target = target.contains("android") || target.contains("ios"); + let is_macos_target = target.contains("apple") && !target.contains("ios"); // TDLib build configuration (desktop only) // The tdlib-rs crate with download-tdlib feature handles downloading and linking @@ -12,7 +13,183 @@ fn main() { // Download and link TDLib library // Pass None to use default download location tdlib_rs::build::build(None); + + // On macOS, copy TDLib and its dependencies to libraries/ so that + // tauri.conf.json > bundle > macOS > frameworks can bundle them into + // the .app's Contents/Frameworks/ directory. + if is_macos_target { + prepare_macos_libraries(); + } + } + + // Add @executable_path/../Frameworks to rpath so the binary can find + // bundled dylibs at runtime (macOS only). + if is_macos_target && !is_mobile_target { + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks"); } tauri_build::build() } + +/// Copy TDLib dylib and its non-system dependencies (OpenSSL) to src-tauri/libraries/ +/// and rewrite their install names to use @rpath so they work inside Contents/Frameworks/. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn prepare_macos_libraries() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + use std::path::{Path, PathBuf}; + + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); + let tdlib_lib_dir = PathBuf::from(&out_dir).join("tdlib").join("lib"); + let manifest_dir = + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set")); + let dest_dir = manifest_dir.join("libraries"); + + // Create libraries/ directory + fs::create_dir_all(&dest_dir).expect("Failed to create libraries directory"); + + let tdlib_dylib = "libtdjson.1.8.29.dylib"; + let tdlib_src = tdlib_lib_dir.join(tdlib_dylib); + + if !tdlib_src.exists() { + println!( + "cargo:warning=TDLib dylib not found at {}, skipping library preparation", + tdlib_src.display() + ); + return; + } + + // Copy TDLib dylib + let tdlib_dest = dest_dir.join(tdlib_dylib); + fs::copy(&tdlib_src, &tdlib_dest).expect("Failed to copy TDLib dylib"); + make_writable(&tdlib_dest); + + // Fix TDLib's install name + run_install_name_tool(&["-id", &format!("@rpath/{tdlib_dylib}"), tdlib_dest.to_str().unwrap()]); + + // Find and bundle non-system dependencies (e.g. OpenSSL from Homebrew) + let deps = get_non_system_deps(&tdlib_dest); + for dep_path in &deps { + let dep_name = Path::new(dep_path) + .file_name() + .unwrap() + .to_str() + .unwrap(); + let dep_dest = dest_dir.join(dep_name); + + if Path::new(dep_path).exists() { + fs::copy(dep_path, &dep_dest).unwrap_or_else(|e| panic!("Failed to copy {dep_name}: {e}")); + make_writable(&dep_dest); + + // Fix the dependency's install name + run_install_name_tool(&[ + "-id", + &format!("@rpath/{dep_name}"), + dep_dest.to_str().unwrap(), + ]); + + // Update TDLib's reference to this dependency + run_install_name_tool(&[ + "-change", + dep_path, + &format!("@rpath/{dep_name}"), + tdlib_dest.to_str().unwrap(), + ]); + + println!("cargo:warning=Bundled dependency: {dep_name}"); + } else { + println!("cargo:warning=Dependency not found: {dep_path}"); + } + } + + // Fix cross-references between dependencies (e.g. libssl -> libcrypto) + let bundled_libs: Vec = fs::read_dir(&dest_dir) + .unwrap() + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| p.extension().is_some_and(|ext| ext == "dylib")) + .collect(); + + for lib in &bundled_libs { + let lib_deps = get_non_system_deps(lib); + for dep_path in &lib_deps { + let dep_name = Path::new(dep_path) + .file_name() + .unwrap() + .to_str() + .unwrap(); + // Only fix if the dep is one we bundled + if dest_dir.join(dep_name).exists() { + run_install_name_tool(&[ + "-change", + dep_path, + &format!("@rpath/{dep_name}"), + lib.to_str().unwrap(), + ]); + } + } + } + + println!("cargo:warning=TDLib libraries prepared in libraries/"); +} + +/// Get non-system dependencies of a dylib using otool +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn get_non_system_deps(dylib_path: &std::path::Path) -> Vec { + use std::process::Command; + + let output = Command::new("otool") + .args(["-L", dylib_path.to_str().unwrap()]) + .output() + .expect("Failed to run otool"); + + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .lines() + .skip(1) // First line is the dylib itself + .filter_map(|line| { + let path = line.trim().split_whitespace().next()?; + // Keep only non-system, non-rpath paths (e.g. Homebrew libs) + if path.starts_with("/usr/lib/") + || path.starts_with("/System/") + || path.starts_with("@rpath/") + || path.starts_with("@executable_path/") + { + None + } else if path.starts_with("/opt/homebrew/") || path.starts_with("/usr/local/") { + Some(path.to_string()) + } else { + None + } + }) + .collect() +} + +/// Make a file writable (Homebrew libs are read-only) +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn make_writable(path: &std::path::Path) { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(path) + .expect("Failed to read file metadata") + .permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(path, perms).expect("Failed to set file permissions"); +} + +/// Run install_name_tool with the given arguments +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn run_install_name_tool(args: &[&str]) { + use std::process::Command; + + let status = Command::new("install_name_tool") + .args(args) + .status() + .expect("Failed to run install_name_tool"); + + if !status.success() { + println!( + "cargo:warning=install_name_tool failed with args: {}", + args.join(" ") + ); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 11c58d4bd..6ebf93b41 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -39,7 +39,12 @@ ], "resources": ["../skills/skills"], "macOS": { - "minimumSystemVersion": "10.15" + "minimumSystemVersion": "10.15", + "frameworks": [ + "./libraries/libtdjson.1.8.29.dylib", + "./libraries/libssl.3.dylib", + "./libraries/libcrypto.3.dylib" + ] } }, "plugins": {