From e6aad7c93049f2eb50b0a757a44e0abea2614482 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 18 Jun 2025 16:35:38 +0200 Subject: [PATCH 1/5] Make the ICU SONAME configurable --- README.md | 46 ++++++++++++++++++---- build.rs | 71 +++++++++++++++++++++++++++++++++- src/arena/release.rs | 6 ++- src/arena/scratch.rs | 5 +++ src/icu.rs | 92 ++++++++++++++++++++++++-------------------- src/sys/unix.rs | 52 +++++++++++++++---------- src/sys/windows.rs | 56 ++++++++++++++++++++++----- 7 files changed, 247 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 1cbf9033d8d..527373cc6cb 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,6 @@ You can install the latest version with WinGet: winget install Microsoft.Edit ``` -### Notes to Package Maintainers - -The canonical executable name is "edit" and the alternative name is "msedit". - -We're aware of the potential conflict of "edit" with existing commands and as such recommend naming packages and executables "msedit". -Names such as "ms-edit" should be avoided. -Assigning an "edit" alias is recommended if possible. - ## Build Instructions * [Install Rust](https://www.rust-lang.org/tools/install) @@ -34,3 +26,41 @@ Assigning an "edit" alias is recommended if possible. * Alternatively, set the environment variable `RUSTC_BOOTSTRAP=1` * Clone the repository * For a release build, run: `cargo build --config .cargo/release.toml --release` + +## Notes to Package Maintainers + +### Package Naming + +The canonical executable name is "edit" and the alternative name is "msedit". +We're aware of the potential conflict of "edit" with existing commands and recommend alternatively naming packages and executables "msedit". +Names such as "ms-edit" should be avoided. +Assigning an "edit" alias is recommended, if possible. + +### ICU library name (SONAME) + +This project _optionally_ depends on the ICU library for its Search and Replace functionality. +By default, the project will look for a SONAME without version suffix: +* Windows: `icuuc.dll` +* macOS: `libicuuc.dylib` +* UNIX, and other OS: `libicuuc.so` + +If your installation uses a different SONAME, please set the following environment variable at build time: +* `EDIT_CFG_ICUUC_SONAME`: + Set this to `libicuuc.so.76`, for instance. +* `EDIT_CFG_ICUI18N_SONAME`: + Set this to `libicui18n.so.76`, for instance. + +Additionally, this project assumes that the ICU exports are without version suffix, such as `u_errorName`. +If your installation uses versioned exports, please set: +* `EDIT_CFG_ICU_RENAMING_VERSION`: + If the ICU library exports have a version suffix, such as `u_errorName_76`, set this to the version number, such as `76`. + +Finally, you can set the following environment variables: +* `EDIT_CFG_ICU_RENAMING_AUTO_DETECT`: + If set to `true`, the executable will try to detect the `EDIT_CFG_ICU_RENAMING_VERSION` value at runtime. + The way it does this is not officially supported by ICU and as such is not recommended to be relied upon. + +To test your settings, run `cargo test` again but with the `--ignored` flag. For instance: +```sh +cargo test -- --ignored +``` diff --git a/build.rs b/build.rs index 7a566068131..a0c49c06b78 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,68 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use core::panic; +use std::env::VarError; + +#[derive(PartialEq, Eq)] +enum TargetOs { + Windows, + MacOS, + Unix, +} + fn main() { + let target_os = match env_fallback("CARGO_CFG_TARGET_OS", "").as_str() { + "windows" => TargetOs::Windows, + "macos" | "ios" => TargetOs::MacOS, + _ => TargetOs::Unix, + }; + let icuuc_soname = env_fallback( + "EDIT_CFG_ICUUC_SONAME", + match target_os { + TargetOs::Windows => "icuuc.dll", + TargetOs::MacOS => "libicuuc.dylib", + TargetOs::Unix => "libicuuc.so", + }, + ); + let icui18n_soname = env_fallback( + "EDIT_CFG_ICUI18N_SONAME", + match target_os { + TargetOs::Windows => "icuin.dll", + TargetOs::MacOS => "libicui18n.dylib", + TargetOs::Unix => "libicui18n.so", + }, + ); + let renaming_auto_detect = + env_fallback("EDIT_CFG_ICU_RENAMING_AUTO_DETECT", "false").parse::().unwrap(); + let renaming_version = { + let mut v = env_fallback("EDIT_CFG_ICU_RENAMING_VERSION", ""); + if v.parse::().unwrap_or(0) > 0 { + v.insert(0, '_'); + } + v + }; + + if renaming_auto_detect && !renaming_version.is_empty() { + panic!( + "Either `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` or `EDIT_CFG_ICU_RENAMING_VERSION` must be set, but not both" + ); + } + + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUUC_SONAME"); + println!("cargo::rustc-env=EDIT_CFG_ICUUC_SONAME={icuuc_soname}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUI18N_SONAME"); + println!("cargo::rustc-env=EDIT_CFG_ICUI18N_SONAME={icui18n_soname}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_RENAMING_VERSION"); + println!("cargo::rustc-env=EDIT_CFG_ICU_RENAMING_VERSION={renaming_version}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_RENAMING_AUTO_DETECT"); + println!("cargo::rustc-check-cfg=cfg(edit_icu_renaming_auto_detect)"); + if renaming_auto_detect { + println!("cargo::rustc-cfg=edit_icu_renaming_auto_detect"); + } + #[cfg(windows)] - if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" { + if target_os == TargetOs::Windows { winresource::WindowsResource::new() .set_manifest_file("src/bin/edit/edit.exe.manifest") .set("FileDescription", "Microsoft Edit") @@ -13,3 +72,13 @@ fn main() { .unwrap(); } } + +fn env_fallback(name: &str, fallback: &str) -> String { + match std::env::var(name) { + Ok(value) => value, + Err(VarError::NotPresent) => fallback.to_string(), + Err(VarError::NotUnicode(_)) => { + panic!("Environment variable `{name}` is not valid Unicode") + } + } +} diff --git a/src/arena/release.rs b/src/arena/release.rs index 6e2d32fa09a..7e4ebcf216b 100644 --- a/src/arena/release.rs +++ b/src/arena/release.rs @@ -79,6 +79,10 @@ impl Arena { }) } + pub fn is_empty(&self) -> bool { + self.base == NonNull::dangling() + } + pub fn offset(&self) -> usize { self.offset.get() } @@ -171,7 +175,7 @@ impl Arena { impl Drop for Arena { fn drop(&mut self) { - if self.base != NonNull::dangling() { + if !self.is_empty() { unsafe { sys::virtual_release(self.base, self.capacity) }; } } diff --git a/src/arena/scratch.rs b/src/arena/scratch.rs index 4805bfbb288..bb91ead7ebe 100644 --- a/src/arena/scratch.rs +++ b/src/arena/scratch.rs @@ -46,6 +46,11 @@ pub fn init(capacity: usize) -> apperr::Result<()> { /// If your function takes an [`Arena`] argument, you **MUST** pass it to `scratch_arena` as `Some(&arena)`. pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> { unsafe { + #[cfg(test)] + if S_SCRATCH[0].is_empty() { + init(128 * 1024 * 1024).unwrap(); + } + #[cfg(debug_assertions)] let conflict = conflict.map(|a| a.delegate_target_unchecked()); diff --git a/src/icu.rs b/src/icu.rs index b0261abf94d..80300a6f1a5 100644 --- a/src/icu.rs +++ b/src/icu.rs @@ -4,7 +4,7 @@ //! Bindings to the ICU library. use std::cmp::Ordering; -use std::ffi::CStr; +use std::ffi::{CStr, c_char}; use std::mem; use std::mem::MaybeUninit; use std::ops::Range; @@ -922,33 +922,39 @@ struct LibraryFunctions { uregex_end64: icu_ffi::uregex_end64, } +macro_rules! proc_name { + ($s:literal) => { + concat!($s, env!("EDIT_CFG_ICU_RENAMING_VERSION"), "\0").as_ptr() as *const c_char + }; +} + // Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows. -const LIBICUUC_PROC_NAMES: [&CStr; 10] = [ - c"u_errorName", - c"ucasemap_open", - c"ucasemap_utf8FoldCase", - c"ucnv_getAvailableName", - c"ucnv_getStandardName", - c"ucnv_open", - c"ucnv_close", - c"ucnv_convertEx", - c"utext_setup", - c"utext_close", +const LIBICUUC_PROC_NAMES: [*const c_char; 10] = [ + proc_name!("u_errorName"), + proc_name!("ucasemap_open"), + proc_name!("ucasemap_utf8FoldCase"), + proc_name!("ucnv_getAvailableName"), + proc_name!("ucnv_getStandardName"), + proc_name!("ucnv_open"), + proc_name!("ucnv_close"), + proc_name!("ucnv_convertEx"), + proc_name!("utext_setup"), + proc_name!("utext_close"), ]; // Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows. -const LIBICUI18N_PROC_NAMES: [&CStr; 11] = [ - c"ucol_open", - c"ucol_strcollUTF8", - c"uregex_open", - c"uregex_close", - c"uregex_setTimeLimit", - c"uregex_setUText", - c"uregex_reset64", - c"uregex_findNext", - c"uregex_groupCount", - c"uregex_start64", - c"uregex_end64", +const LIBICUI18N_PROC_NAMES: [*const c_char; 11] = [ + proc_name!("ucol_open"), + proc_name!("ucol_strcollUTF8"), + proc_name!("uregex_open"), + proc_name!("uregex_close"), + proc_name!("uregex_setTimeLimit"), + proc_name!("uregex_setUText"), + proc_name!("uregex_reset64"), + proc_name!("uregex_findNext"), + proc_name!("uregex_groupCount"), + proc_name!("uregex_start64"), + proc_name!("uregex_end64"), ]; enum LibraryFunctionsState { @@ -971,10 +977,7 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> { unsafe { LIBRARY_FUNCTIONS = LibraryFunctionsState::Failed; - let Ok(libicuuc) = sys::load_libicuuc() else { - return; - }; - let Ok(libicui18n) = sys::load_libicui18n() else { + let Ok(icu) = sys::load_icu() else { return; }; @@ -998,25 +1001,26 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> { let mut funcs = MaybeUninit::::uninit(); let mut ptr = funcs.as_mut_ptr() as *mut TransparentFunction; - #[cfg(unix)] + #[cfg(edit_icu_renaming_auto_detect)] let scratch_outer = scratch_arena(None); - #[cfg(unix)] - let suffix = sys::icu_proc_suffix(&scratch_outer, libicuuc); - - for (handle, names) in - [(libicuuc, &LIBICUUC_PROC_NAMES[..]), (libicui18n, &LIBICUI18N_PROC_NAMES[..])] - { - for name in names { - #[cfg(unix)] + #[cfg(edit_icu_renaming_auto_detect)] + let suffix = sys::icu_detect_renaming_suffix(&scratch_outer, icu.libicuuc); + + for (handle, names) in [ + (icu.libicuuc, &LIBICUUC_PROC_NAMES[..]), + (icu.libicui18n, &LIBICUI18N_PROC_NAMES[..]), + ] { + for &name in names { + #[cfg(edit_icu_renaming_auto_detect)] let scratch = scratch_arena(Some(&scratch_outer)); - #[cfg(unix)] - let name = &sys::add_icu_proc_suffix(&scratch, name, &suffix); + #[cfg(edit_icu_renaming_auto_detect)] + let name = sys::icu_add_renaming_suffix(&scratch, name, &suffix); let Ok(func) = sys::get_proc_address(handle, name) else { debug_assert!( false, - "Failed to load ICU function: {}", - name.to_string_lossy() + "Failed to load ICU function: {:?}", + CStr::from_ptr(name) ); return; }; @@ -1314,6 +1318,12 @@ mod icu_ffi { mod tests { use super::*; + #[ignore] + #[test] + fn init() { + assert!(init_if_needed().is_ok()); + } + #[test] fn test_compare_strings_ascii() { // Empty strings diff --git a/src/sys/unix.rs b/src/sys/unix.rs index 4389f30d25c..2ffb0be3a95 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -6,8 +6,8 @@ //! Read the `windows` module for reference. //! TODO: This reminds me that the sys API should probably be a trait. -use std::ffi::{CStr, c_int, c_void}; -use std::fs::{self, File}; +use std::ffi::{CStr, c_char, c_int, c_void}; +use std::fs::File; use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::os::fd::{AsRawFd as _, FromRawFd as _}; use std::path::Path; @@ -433,9 +433,9 @@ pub unsafe fn virtual_commit(base: NonNull, size: usize) -> apperr::Result<( } } -unsafe fn load_library(name: &CStr) -> apperr::Result> { +unsafe fn load_library(name: *const c_char) -> apperr::Result> { unsafe { - NonNull::new(libc::dlopen(name.as_ptr(), libc::RTLD_LAZY)) + NonNull::new(libc::dlopen(name, libc::RTLD_LAZY)) .ok_or_else(|| errno_to_apperr(libc::ENOENT)) } } @@ -448,9 +448,12 @@ unsafe fn load_library(name: &CStr) -> apperr::Result> { /// of the function you're loading. No type checks whatsoever are performed. // // It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. -pub unsafe fn get_proc_address(handle: NonNull, name: &CStr) -> apperr::Result { +pub unsafe fn get_proc_address( + handle: NonNull, + name: *const c_char, +) -> apperr::Result { unsafe { - let sym = libc::dlsym(handle.as_ptr(), name.as_ptr()); + let sym = libc::dlsym(handle.as_ptr(), name); if sym.is_null() { Err(errno_to_apperr(libc::ENOENT)) } else { @@ -459,20 +462,25 @@ pub unsafe fn get_proc_address(handle: NonNull, name: &CStr) -> apper } } -pub fn load_libicuuc() -> apperr::Result> { - unsafe { load_library(c"libicuuc.so") } +pub struct LibIcu { + pub libicuuc: NonNull, + pub libicui18n: NonNull, } -pub fn load_libicui18n() -> apperr::Result> { - unsafe { load_library(c"libicui18n.so") } +pub fn load_icu() -> apperr::Result { + let libicuuc = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0"); + let libicui18n = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0"); + let libicuuc = unsafe { load_library(libicuuc.as_ptr() as *const _)? }; + let libicui18n = unsafe { load_library(libicui18n.as_ptr() as *const _)? }; + Ok(LibIcu { libicuuc, libicui18n }) } /// ICU, by default, adds the major version as a suffix to each exported symbol. /// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`), /// but I found that many (most?) Linux distributions don't do this for some reason. /// This function returns the suffix, if any. -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_> { +#[cfg(edit_icu_renaming_auto_detect)] +pub fn icu_detect_renaming_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_> { unsafe { type T = *const c_void; @@ -480,7 +488,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_ // Check if the ICU library is using unversioned symbols. // Return an empty suffix in that case. - if get_proc_address::(handle, c"u_errorName").is_ok() { + if get_proc_address::(handle, c"u_errorName".as_ptr()).is_ok() { return res; } @@ -488,7 +496,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_ // this symbol seems to be always present. This allows us to call `dladdr`. // It's the `UCaseMap::~UCaseMap()` destructor which for some reason isn't // in a namespace. Thank you ICU maintainers for this oversight. - let proc = match get_proc_address::(handle, c"_ZN8UCaseMapD1Ev") { + let proc = match get_proc_address::(handle, c"_ZN8UCaseMapD1Ev".as_ptr()) { Ok(proc) => proc, Err(_) => return res, }; @@ -506,7 +514,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_ Err(_) => return res, }; - let path = match fs::read_link(path) { + let path = match std::fs::read_link(path) { Ok(path) => path, Err(_) => path.into(), }; @@ -528,7 +536,12 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_ } } -pub fn add_icu_proc_suffix<'a, 'b, 'r>(arena: &'a Arena, name: &'b CStr, suffix: &str) -> &'r CStr +#[cfg(edit_icu_renaming_auto_detect)] +pub fn icu_add_renaming_suffix<'a, 'b, 'r>( + arena: &'a Arena, + name: *const c_char, + suffix: &str, +) -> *const c_char where 'a: 'r, 'b: 'r, @@ -538,16 +551,15 @@ where } else { // SAFETY: In this particular case we know that the string // is valid UTF-8, because it comes from icu.rs. + let name = unsafe { CStr::from_ptr(name) }; let name = unsafe { name.to_str().unwrap_unchecked() }; - let mut res = ArenaString::new_in(arena); + let mut res = ManuallyDrop::new(ArenaString::new_in(arena)); res.reserve(name.len() + suffix.len() + 1); res.push_str(name); res.push_str(suffix); res.push('\0'); - - let bytes: &'a [u8] = unsafe { mem::transmute(res.as_bytes()) }; - unsafe { CStr::from_bytes_with_nul_unchecked(bytes) } + res.as_ptr() as *const c_char } } diff --git a/src/sys/windows.rs b/src/sys/windows.rs index 71d9b517da4..09337655316 100644 --- a/src/sys/windows.rs +++ b/src/sys/windows.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::ffi::{CStr, OsString, c_void}; +use std::ffi::{OsString, c_char, c_void}; use std::fmt::Write as _; use std::fs::{self, File}; use std::mem::MaybeUninit; @@ -20,6 +20,35 @@ use crate::apperr; use crate::arena::{Arena, ArenaString, scratch_arena}; use crate::helpers::*; +macro_rules! w_env { + ($s:literal) => {{ + const INPUT: &[u8] = env!($s).as_bytes(); + const OUTPUT_LEN: usize = windows_sys::core::utf16_len(INPUT) + 1; + const OUTPUT: &[u16; OUTPUT_LEN] = { + let mut buffer = [0; OUTPUT_LEN]; + let mut input_pos = 0; + let mut output_pos = 0; + while let Some((mut code_point, new_pos)) = + windows_sys::core::decode_utf8_char(INPUT, input_pos) + { + input_pos = new_pos; + if code_point <= 0xffff { + buffer[output_pos] = code_point as u16; + output_pos += 1; + } else { + code_point -= 0x10000; + buffer[output_pos] = 0xd800 + (code_point >> 10) as u16; + output_pos += 1; + buffer[output_pos] = 0xdc00 + (code_point & 0x3ff) as u16; + output_pos += 1; + } + } + &{ buffer } + }; + OUTPUT.as_ptr() + }}; +} + type ReadConsoleInputExW = unsafe extern "system" fn( h_console_input: Foundation::HANDLE, lp_buffer: *mut Console::INPUT_RECORD, @@ -109,7 +138,10 @@ pub fn init() -> apperr::Result { } unsafe fn load_read_func(module: *const u16) -> apperr::Result { - unsafe { get_module(module).and_then(|m| get_proc_address(m, c"ReadConsoleInputExW")) } + unsafe { + get_module(module) + .and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr())) + } } // `kernel32.dll` doesn't exist on OneCore variants of Windows. @@ -564,21 +596,25 @@ unsafe fn load_library(name: *const u16) -> apperr::Result> { /// of the function you're loading. No type checks whatsoever are performed. // // It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. -pub unsafe fn get_proc_address(handle: NonNull, name: &CStr) -> apperr::Result { +pub unsafe fn get_proc_address( + handle: NonNull, + name: *const c_char, +) -> apperr::Result { unsafe { - let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name.as_ptr() as *const u8); + let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8); if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) } } } -/// Loads the "common" portion of ICU4C. -pub fn load_libicuuc() -> apperr::Result> { - unsafe { load_library(w!("icuuc.dll")) } +pub struct LibIcu { + pub libicuuc: NonNull, + pub libicui18n: NonNull, } -/// Loads the internationalization portion of ICU4C. -pub fn load_libicui18n() -> apperr::Result> { - unsafe { load_library(w!("icuin.dll")) } +pub fn load_icu() -> apperr::Result { + let libicuuc = unsafe { load_library(w_env!("EDIT_CFG_ICUUC_SONAME"))? }; + let libicui18n = unsafe { load_library(w_env!("EDIT_CFG_ICUI18N_SONAME"))? }; + Ok(LibIcu { libicuuc, libicui18n }) } /// Returns a list of preferred languages for the current user. From 716fca2996bf9a4a6fc807f21c5f54cde9b7a855 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 18 Jun 2025 16:46:03 +0200 Subject: [PATCH 2/5] Make it easier to read --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 527373cc6cb..f87a4457e12 100644 --- a/README.md +++ b/README.md @@ -46,14 +46,14 @@ By default, the project will look for a SONAME without version suffix: If your installation uses a different SONAME, please set the following environment variable at build time: * `EDIT_CFG_ICUUC_SONAME`: - Set this to `libicuuc.so.76`, for instance. + For instance, `libicuuc.so.76`. * `EDIT_CFG_ICUI18N_SONAME`: - Set this to `libicui18n.so.76`, for instance. + For instance, `libicui18n.so.76`. Additionally, this project assumes that the ICU exports are without version suffix, such as `u_errorName`. If your installation uses versioned exports, please set: * `EDIT_CFG_ICU_RENAMING_VERSION`: - If the ICU library exports have a version suffix, such as `u_errorName_76`, set this to the version number, such as `76`. + If set to a version number, such as `76`, it'll look for symbols such as `u_errorName_76`. Finally, you can set the following environment variables: * `EDIT_CFG_ICU_RENAMING_AUTO_DETECT`: From 512d9d523f8993fa5b6af94e25676b732b25da9b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 18 Jun 2025 18:55:39 +0200 Subject: [PATCH 3/5] Improve defaults --- build.rs | 59 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/build.rs b/build.rs index a0c49c06b78..99336f565c6 100644 --- a/build.rs +++ b/build.rs @@ -12,42 +12,53 @@ enum TargetOs { } fn main() { - let target_os = match env_fallback("CARGO_CFG_TARGET_OS", "").as_str() { + let target_os = match env_opt("CARGO_CFG_TARGET_OS").as_str() { "windows" => TargetOs::Windows, "macos" | "ios" => TargetOs::MacOS, _ => TargetOs::Unix, }; - let icuuc_soname = env_fallback( - "EDIT_CFG_ICUUC_SONAME", + let icuuc_soname = env_opt("EDIT_CFG_ICUUC_SONAME"); + let icui18n_soname = env_opt("EDIT_CFG_ICUI18N_SONAME"); + let renaming_version = env_opt("EDIT_CFG_ICU_RENAMING_VERSION"); + let renaming_auto_detect = env_opt("EDIT_CFG_ICU_RENAMING_AUTO_DETECT"); + + // If none of the `EDIT_CFG_ICU*` environment variables are set, + // we default to enabling `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` on UNIX. + // This slightly improves portability at least in the cases where the SONAMEs match our defaults. + let renaming_auto_detect = if !renaming_auto_detect.is_empty() { + renaming_auto_detect.parse::().unwrap() + } else { + target_os == TargetOs::Unix + && icuuc_soname.is_empty() + && icui18n_soname.is_empty() + && renaming_version.is_empty() + }; + if renaming_auto_detect && !renaming_version.is_empty() { + panic!( + "Either `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` or `EDIT_CFG_ICU_RENAMING_VERSION` must be set, but not both" + ); + } + + let icuuc_soname = if !icuuc_soname.is_empty() { + &icuuc_soname + } else { match target_os { TargetOs::Windows => "icuuc.dll", TargetOs::MacOS => "libicuuc.dylib", TargetOs::Unix => "libicuuc.so", - }, - ); - let icui18n_soname = env_fallback( - "EDIT_CFG_ICUI18N_SONAME", + } + }; + let icui18n_soname = if !icui18n_soname.is_empty() { + &icui18n_soname + } else { match target_os { TargetOs::Windows => "icuin.dll", TargetOs::MacOS => "libicui18n.dylib", TargetOs::Unix => "libicui18n.so", - }, - ); - let renaming_auto_detect = - env_fallback("EDIT_CFG_ICU_RENAMING_AUTO_DETECT", "false").parse::().unwrap(); - let renaming_version = { - let mut v = env_fallback("EDIT_CFG_ICU_RENAMING_VERSION", ""); - if v.parse::().unwrap_or(0) > 0 { - v.insert(0, '_'); } - v }; - - if renaming_auto_detect && !renaming_version.is_empty() { - panic!( - "Either `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` or `EDIT_CFG_ICU_RENAMING_VERSION` must be set, but not both" - ); - } + let renaming_version = + if renaming_version.is_empty() { String::new() } else { format!("_{renaming_version}") }; println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUUC_SONAME"); println!("cargo::rustc-env=EDIT_CFG_ICUUC_SONAME={icuuc_soname}"); @@ -73,10 +84,10 @@ fn main() { } } -fn env_fallback(name: &str, fallback: &str) -> String { +fn env_opt(name: &str) -> String { match std::env::var(name) { Ok(value) => value, - Err(VarError::NotPresent) => fallback.to_string(), + Err(VarError::NotPresent) => String::new(), Err(VarError::NotUnicode(_)) => { panic!("Environment variable `{name}` is not valid Unicode") } From c0b9bad389ccba78c0e20b0c9ea6cd09f968d74b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 19 Jun 2025 14:20:10 +0200 Subject: [PATCH 4/5] Stop nagging me --- src/sys/unix.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sys/unix.rs b/src/sys/unix.rs index 2ffb0be3a95..d235fc5ee7f 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -537,6 +537,7 @@ pub fn icu_detect_renaming_suffix(arena: &Arena, handle: NonNull) -> Are } #[cfg(edit_icu_renaming_auto_detect)] +#[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn icu_add_renaming_suffix<'a, 'b, 'r>( arena: &'a Arena, name: *const c_char, From 52dbca9b26b5784afaa441c42054d288a295db60 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 19 Jun 2025 21:48:00 +0200 Subject: [PATCH 5/5] Add EDIT_CFG_ICU_CPP_EXPORTS, libicucore.dylib --- README.md | 6 +++++- build.rs | 19 +++++++++++++------ src/icu.rs | 3 ++- src/sys/unix.rs | 33 +++++++++++++++++++++++++++------ src/sys/windows.rs | 31 ++++++++++++++++++++++++++++--- 5 files changed, 75 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f87a4457e12..021daa584ea 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,11 @@ If your installation uses a different SONAME, please set the following environme * `EDIT_CFG_ICUI18N_SONAME`: For instance, `libicui18n.so.76`. -Additionally, this project assumes that the ICU exports are without version suffix, such as `u_errorName`. +Additionally, this project assumes that the ICU exports are exported without `_` prefix and without version suffix, such as `u_errorName`. If your installation uses versioned exports, please set: +* `EDIT_CFG_ICU_CPP_EXPORTS`: + If set to `true`, it'll look for C++ symbols such as `_u_errorName`. + Enabled by default on macOS. * `EDIT_CFG_ICU_RENAMING_VERSION`: If set to a version number, such as `76`, it'll look for symbols such as `u_errorName_76`. @@ -59,6 +62,7 @@ Finally, you can set the following environment variables: * `EDIT_CFG_ICU_RENAMING_AUTO_DETECT`: If set to `true`, the executable will try to detect the `EDIT_CFG_ICU_RENAMING_VERSION` value at runtime. The way it does this is not officially supported by ICU and as such is not recommended to be relied upon. + Enabled by default on UNIX (excluding macOS) if no other options are set. To test your settings, run `cargo test` again but with the `--ignored` flag. For instance: ```sh diff --git a/build.rs b/build.rs index 99336f565c6..c017bf74c72 100644 --- a/build.rs +++ b/build.rs @@ -19,6 +19,7 @@ fn main() { }; let icuuc_soname = env_opt("EDIT_CFG_ICUUC_SONAME"); let icui18n_soname = env_opt("EDIT_CFG_ICUI18N_SONAME"); + let cpp_exports = env_opt("EDIT_CFG_ICU_CPP_EXPORTS"); let renaming_version = env_opt("EDIT_CFG_ICU_RENAMING_VERSION"); let renaming_auto_detect = env_opt("EDIT_CFG_ICU_RENAMING_AUTO_DETECT"); @@ -31,9 +32,11 @@ fn main() { target_os == TargetOs::Unix && icuuc_soname.is_empty() && icui18n_soname.is_empty() + && cpp_exports.is_empty() && renaming_version.is_empty() }; if renaming_auto_detect && !renaming_version.is_empty() { + // It makes no sense to specify an explicit version and also ask for auto-detection. panic!( "Either `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` or `EDIT_CFG_ICU_RENAMING_VERSION` must be set, but not both" ); @@ -44,7 +47,7 @@ fn main() { } else { match target_os { TargetOs::Windows => "icuuc.dll", - TargetOs::MacOS => "libicuuc.dylib", + TargetOs::MacOS => "libicucore.dylib", TargetOs::Unix => "libicuuc.so", } }; @@ -53,19 +56,23 @@ fn main() { } else { match target_os { TargetOs::Windows => "icuin.dll", - TargetOs::MacOS => "libicui18n.dylib", + TargetOs::MacOS => "libicucore.dylib", TargetOs::Unix => "libicui18n.so", } }; - let renaming_version = - if renaming_version.is_empty() { String::new() } else { format!("_{renaming_version}") }; + let icu_export_prefix = + if !cpp_exports.is_empty() && cpp_exports.parse::().unwrap() { "_" } else { "" }; + let icu_export_suffix = + if !renaming_version.is_empty() { format!("_{renaming_version}") } else { String::new() }; println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUUC_SONAME"); println!("cargo::rustc-env=EDIT_CFG_ICUUC_SONAME={icuuc_soname}"); println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUI18N_SONAME"); println!("cargo::rustc-env=EDIT_CFG_ICUI18N_SONAME={icui18n_soname}"); - println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_RENAMING_VERSION"); - println!("cargo::rustc-env=EDIT_CFG_ICU_RENAMING_VERSION={renaming_version}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_PREFIX"); + println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_PREFIX={icu_export_prefix}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_SUFFIX"); + println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_SUFFIX={icu_export_suffix}"); println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_RENAMING_AUTO_DETECT"); println!("cargo::rustc-check-cfg=cfg(edit_icu_renaming_auto_detect)"); if renaming_auto_detect { diff --git a/src/icu.rs b/src/icu.rs index 80300a6f1a5..9687794a9aa 100644 --- a/src/icu.rs +++ b/src/icu.rs @@ -924,7 +924,8 @@ struct LibraryFunctions { macro_rules! proc_name { ($s:literal) => { - concat!($s, env!("EDIT_CFG_ICU_RENAMING_VERSION"), "\0").as_ptr() as *const c_char + concat!(env!("EDIT_CFG_ICU_EXPORT_PREFIX"), $s, env!("EDIT_CFG_ICU_EXPORT_SUFFIX"), "\0") + .as_ptr() as *const c_char }; } diff --git a/src/sys/unix.rs b/src/sys/unix.rs index d235fc5ee7f..ce7eb20a63b 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -468,13 +468,34 @@ pub struct LibIcu { } pub fn load_icu() -> apperr::Result { - let libicuuc = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0"); - let libicui18n = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0"); - let libicuuc = unsafe { load_library(libicuuc.as_ptr() as *const _)? }; - let libicui18n = unsafe { load_library(libicui18n.as_ptr() as *const _)? }; - Ok(LibIcu { libicuuc, libicui18n }) -} + const fn const_str_eq(a: &str, b: &str) -> bool { + let a = a.as_bytes(); + let b = b.as_bytes(); + let mut i = 0; + loop { + if i >= a.len() || i >= b.len() { + return a.len() == b.len(); + } + if a[i] != b[i] { + return false; + } + i += 1; + } + } + + const LIBICUUC: &str = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0"); + const LIBICUI18N: &str = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0"); + + if const { const_str_eq(LIBICUUC, LIBICUI18N) } { + let icu = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; + Ok(LibIcu { libicuuc: icu, libicui18n: icu }) + } else { + let libicuuc = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; + let libicui18n = unsafe { load_library(LIBICUI18N.as_ptr() as *const _)? }; + Ok(LibIcu { libicuuc, libicui18n }) + } +} /// ICU, by default, adds the major version as a suffix to each exported symbol. /// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`), /// but I found that many (most?) Linux distributions don't do this for some reason. diff --git a/src/sys/windows.rs b/src/sys/windows.rs index 09337655316..be524ea976e 100644 --- a/src/sys/windows.rs +++ b/src/sys/windows.rs @@ -612,9 +612,34 @@ pub struct LibIcu { } pub fn load_icu() -> apperr::Result { - let libicuuc = unsafe { load_library(w_env!("EDIT_CFG_ICUUC_SONAME"))? }; - let libicui18n = unsafe { load_library(w_env!("EDIT_CFG_ICUI18N_SONAME"))? }; - Ok(LibIcu { libicuuc, libicui18n }) + const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool { + unsafe { + let mut a = a; + let mut b = b; + loop { + if *a != *b { + return false; + } + if *a == 0 { + return true; + } + a = a.add(1); + b = b.add(1); + } + } + } + + const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME"); + const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME"); + + if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } { + let icu = unsafe { load_library(LIBICUUC)? }; + Ok(LibIcu { libicuuc: icu, libicui18n: icu }) + } else { + let libicuuc = unsafe { load_library(LIBICUUC)? }; + let libicui18n = unsafe { load_library(LIBICUI18N)? }; + Ok(LibIcu { libicuuc, libicui18n }) + } } /// Returns a list of preferred languages for the current user.