Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,52 @@ 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)
* Install the nightly toolchain: `rustup install nightly`
* 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`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think macOS only has libicucore, and not uc/i18n... hmm

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it also exports all of them as C++ symbols, i.e. with underscore prefix.

* 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`:
For instance, `libicuuc.so.76`.
* `EDIT_CFG_ICUI18N_SONAME`:
For instance, `libicui18n.so.76`.

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`.

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
cargo test -- --ignored
```
89 changes: 88 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,86 @@
// 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_opt("CARGO_CFG_TARGET_OS").as_str() {
"windows" => TargetOs::Windows,
"macos" | "ios" => TargetOs::MacOS,
_ => TargetOs::Unix,
};
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");

// 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::<bool>().unwrap()
} else {
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"
);
}

let icuuc_soname = if !icuuc_soname.is_empty() {
&icuuc_soname
} else {
match target_os {
TargetOs::Windows => "icuuc.dll",
TargetOs::MacOS => "libicucore.dylib",
TargetOs::Unix => "libicuuc.so",
}
};
let icui18n_soname = if !icui18n_soname.is_empty() {
&icui18n_soname
} else {
match target_os {
TargetOs::Windows => "icuin.dll",
TargetOs::MacOS => "libicucore.dylib",
TargetOs::Unix => "libicui18n.so",
}
};
let icu_export_prefix =
if !cpp_exports.is_empty() && cpp_exports.parse::<bool>().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_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 {
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")
Expand All @@ -13,3 +90,13 @@ fn main() {
.unwrap();
}
}

fn env_opt(name: &str) -> String {
match std::env::var(name) {
Ok(value) => value,
Err(VarError::NotPresent) => String::new(),
Err(VarError::NotUnicode(_)) => {
panic!("Environment variable `{name}` is not valid Unicode")
}
}
}
6 changes: 5 additions & 1 deletion src/arena/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ impl Arena {
})
}

pub fn is_empty(&self) -> bool {
self.base == NonNull::dangling()
}

pub fn offset(&self) -> usize {
self.offset.get()
}
Expand Down Expand Up @@ -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) };
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/arena/scratch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
93 changes: 52 additions & 41 deletions src/icu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -922,33 +922,40 @@ struct LibraryFunctions {
uregex_end64: icu_ffi::uregex_end64,
}

macro_rules! proc_name {
($s:literal) => {
concat!(env!("EDIT_CFG_ICU_EXPORT_PREFIX"), $s, env!("EDIT_CFG_ICU_EXPORT_SUFFIX"), "\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 {
Expand All @@ -971,10 +978,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;
};

Expand All @@ -998,25 +1002,26 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> {
let mut funcs = MaybeUninit::<LibraryFunctions>::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;
};
Expand Down Expand Up @@ -1314,6 +1319,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
Expand Down
Loading