diff --git a/README.md b/README.md index a7ac26f786f..73c337d8c95 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,24 @@ You can install the latest version with WinGet: winget install Microsoft.Edit ``` +### Linux (build from source) + +If your distribution does not provide binaries, or if you'd like to build your own, you can use our install script, provided you have installed: +* Rust (via `rustup` or similar) +* A C compiler (e.g. `gcc`) +* ICU (e.g. libicu78, libicu, icu) +* curl/wget and tar + +The following command will then install `msedit` into `~/.local/bin`: +```sh +curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/microsoft/edit/main/assets/install.sh | sh +``` + +Additional flags are `--dev`, to build directly from the main branch, and `--system` to install into `/usr/local/bin`. For instance: +```sh +curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/microsoft/edit/main/assets/install.sh | sh -s -- --dev --system +``` + ## Build Instructions * [Install Rust](https://www.rust-lang.org/tools/install) @@ -37,11 +55,11 @@ winget install Microsoft.Edit ### Build Configuration -Uou can set the following environment variables at build-time to configure the build: +You can set the following environment variables at build-time to configure the build: Environment variable | Description --- | --- -`EDIT_CFG_ICU*` | See [ICU library name (SONAME)](#icu-library-name-soname) below for details. This option is particularly important on Linux. +`EDIT_CFG_ICU*` | See [ICU library name (SONAME)](#icu-library-name-soname) below for details. Linux package maintainers are advised to review and configure these options. `EDIT_CFG_LANGUAGES` | A comma-separated list of languages to include in the build. See [i18n/edit.toml](i18n/edit.toml) for available languages. ## Notes to Package Maintainers @@ -57,10 +75,12 @@ Assigning an "edit" alias is recommended, if possible. 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` +By default, the project will look for the following library names: + + Variable | Windows | macOS | Linux / Other +----------|---------|-------|--------------- +`EDIT_CFG_ICUUC_SONAME` | `icuuc.dll` | `libicucore.dylib` | `libicuuc.so` +`EDIT_CFG_ICUI18N_SONAME` | `icuin.dll` | `libicucore.dylib` | `libicui18n.so` If your installation uses a different SONAME, please set the following environment variable at build time: * `EDIT_CFG_ICUUC_SONAME`: diff --git a/assets/install.sh b/assets/install.sh new file mode 100755 index 00000000000..1d2b6aafaa8 --- /dev/null +++ b/assets/install.sh @@ -0,0 +1,202 @@ +#!/bin/sh +# shellcheck shell=dash + +set -eu + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } + +usage() { + cat <<'EOF' +Usage: install.sh [--dev] [--system] + --dev Build from the main branch instead of the latest release + --system Install to /usr/local/bin (requires sudo) + +Without --system, installs to ~/.local/bin. +EOF + exit 1 +} + +#### Parse arguments + +dev=0 +system=0 +for arg in "$@"; do + case "$arg" in + --dev) dev=1 ;; + --system) system=1 ;; + -h|--help) usage ;; + *) usage ;; + esac +done + +if [ "$system" = 1 ]; then + command -v sudo >/dev/null 2>&1 || die "sudo is required for --system installs." +fi + +#### Check prerequisites + +command -v cargo >/dev/null 2>&1 || die "cargo not found. Install Rust via rustup (https://rustup.rs) or your OS package manager." + +if command -v curl >/dev/null 2>&1; then + download() { curl --proto '=https' --tlsv1.2 --retry 3 -fsSL -o "$1" "$2"; } +elif command -v wget >/dev/null 2>&1; then + download() { wget --https-only --secure-protocol=TLSv1_2 -qO "$1" "$2"; } +else + die "curl or wget not found. Install either via your OS package manager." +fi + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +#### Find ICU SONAME + +icuuc_soname="" +icui18n_soname="" +icu_cpp_exports="" +icu_renaming_version="" + +case "$(uname -s)" in + Darwin) + ;; + *) + # Pick the best candidate SONAME + # Preference: libicuuc.so.78 > libicuuc.so > libicuuc.so.78.1 + # (Symbols are usually suffixed with the major version, so that's preferred.) + + icu_ranked_paths=$tmpdir/icu_ranked_paths + + if command -v ldconfig >/dev/null 2>&1; then + ldconfig -p 2>/dev/null | grep -o '/.*libicuuc\.so.*$' + else + find /usr/local/lib /usr/local/lib64 /usr/lib /usr/lib64 /lib /lib64 -maxdepth 2 -name 'libicuuc.so*' 2>/dev/null + fi \ + | while IFS= read -r icuuc_path; do + printf '%s %s\n' "${icuuc_path##*/}" "$icuuc_path" + done \ + | sort -t. -k3,3n -k4,4n > "$icu_ranked_paths" + + major_entry=$(grep -E '^libicuuc\.so\.[0-9]+ ' "$icu_ranked_paths" | tail -n1 || true) + bare_entry=$(grep -E '^libicuuc\.so ' "$icu_ranked_paths" | tail -n1 || true) + full_entry=$(grep -E '^libicuuc\.so\.[0-9]+\.[0-9]+ ' "$icu_ranked_paths" | tail -n1 || true) + + if [ -n "$major_entry" ]; then icu_entry=$major_entry + elif [ -n "$bare_entry" ]; then icu_entry=$bare_entry + elif [ -n "$full_entry" ]; then icu_entry=$full_entry + else die "libicuuc not found. Install ICU via your OS package manager (e.g. libicu78, libicu, icu)." + fi + + icuuc_soname=${icu_entry%% *} + icuuc_path=${icu_entry#* } + icui18n_path="${icuuc_path%/*}/libicui18n.so${icuuc_soname#libicuuc.so}" + [ -e "$icui18n_path" ] || die "libicui18n not found. Install ICU via your OS package manager (e.g. libicu78, libicu, icu)." + icui18n_soname=${icui18n_path##*/} + + # Figure out the symbol naming scheme / renaming version + + if command -v readelf >/dev/null 2>&1; then + icu_probe_symbol=$(readelf -Ws "$icuuc_path" 2>/dev/null | grep -Eo '_?u_errorName(_[0-9]+)?' | tail -n1 || true) + elif command -v nm >/dev/null 2>&1; then + icu_probe_symbol=$(nm -D "$icuuc_path" 2>/dev/null | grep -Eo '_?u_errorName(_[0-9]+)?' | tail -n1 || true) + else + icu_probe_symbol= + fi + + case "$icu_probe_symbol" in + _u_errorName|_u_errorName_[0-9]*) icu_cpp_exports=true ;; + esac + case "$icu_probe_symbol" in + u_errorName_[0-9]*|_u_errorName_[0-9]*) icu_renaming_version=${icu_probe_symbol##*_} ;; + *) ;; + esac + + log_renaming="" + log_cpp="" + if [ -n "$icu_renaming_version" ]; then + log_renaming=", renaming version $icu_renaming_version" + fi + if [ -n "$icu_cpp_exports" ]; then + log_cpp=", C++ symbol exports" + fi + log "Found $icuuc_soname, $icui18n_soname$log_renaming$log_cpp" + ;; +esac + +#### Download source + +if [ "$dev" = 1 ]; then + log "Downloading main branch" + download "$tmpdir/edit.tar.gz" 'https://github.com/microsoft/edit/archive/refs/heads/main.tar.gz' +else + log "Fetching latest release tag" + download "$tmpdir/latest.json" 'https://api.github.com/repos/microsoft/edit/releases/latest' + tag=$(grep -oE '"tag_name": *"[^"]+"' "$tmpdir/latest.json" | grep -oE 'v[^"]+') + [ -n "$tag" ] || die "Could not determine latest release tag." + log "Latest release: $tag" + download "$tmpdir/edit.tar.gz" "https://github.com/microsoft/edit/archive/refs/tags/$tag.tar.gz" +fi + +srcdir="$tmpdir/edit-src" +mkdir -p "$srcdir" +log "Extracting" +tar xf "$tmpdir/edit.tar.gz" -C "$srcdir" --strip-components=1 + +#### Build + +log "Building" +[ -z "$icuuc_soname" ] || export EDIT_CFG_ICUUC_SONAME="$icuuc_soname" +[ -z "$icui18n_soname" ] || export EDIT_CFG_ICUI18N_SONAME="$icui18n_soname" +[ -z "$icu_cpp_exports" ] || export EDIT_CFG_ICU_CPP_EXPORTS="$icu_cpp_exports" +[ -z "$icu_renaming_version" ] || export EDIT_CFG_ICU_RENAMING_VERSION="$icu_renaming_version" + +if rustup component list --installed 2>/dev/null | grep -q rust-src; then + (cd "$srcdir" && RUSTC_BOOTSTRAP=1 cargo build -p edit --release --config .cargo/release.toml) +else + warn "rust-src component not found; building without size optimizations" + (cd "$srcdir" && cargo build -p edit --release) +fi + +bin="$srcdir/target/release/edit" +[ -x "$bin" ] || die "Build failed: binary not found." + +#### Install + +if [ "$system" = 1 ]; then + dest=/usr/local/bin + run="sudo" +else + dest="$HOME/.local/bin" + run="" +fi + +log "Installing to $dest" +$run mkdir -p "$dest" +$run cp "$bin" "$dest/msedit" +$run chmod 755 "$dest/msedit" +if [ ! -e "$dest/edit" ] || [ "$(readlink "$dest/edit" 2>/dev/null)" = "msedit" ]; then + $run ln -sf msedit "$dest/edit" + edit_linked=1 +else + edit_linked=0 +fi + +#### Summary + +case ":$PATH:" in + *":$dest:"*) + if [ "$edit_linked" = 1 ]; then + echo "✅ Done. Run 'edit' or 'msedit' to start." + else + echo "✅ Done. Run 'msedit' to start." + fi + ;; + *) + echo "⚠️ Done. $dest is not in PATH; you may need to add it." + if [ "$edit_linked" = 1 ]; then + echo "Run '$dest/edit' or '$dest/msedit' to start." + else + echo "Run '$dest/msedit' to start." + fi + ;; +esac