From 2bffd95cddcd8d879c35fc74054f41417d2b9524 Mon Sep 17 00:00:00 2001 From: JaredTweed Date: Tue, 26 Aug 2025 14:09:38 -0500 Subject: [PATCH 1/4] single line linux installer --- README.md | 13 ++++ tools/install.sh | 190 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 tools/install.sh diff --git a/README.md b/README.md index 3cc9702711f..9b5f27d4c0c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,19 @@ You can install the latest version with WinGet: winget install Microsoft.Edit ``` +### Linux + +You can install the latest version by pasting this into the linux terminal: +```sh +curl -fsSL https://raw.githubusercontent.com/JaredTweed/edit/main/tools/install.sh | bash +``` +or via git cloning: +```sh +git clone git@github.com:microsoft/edit.git +cd edit +curl -fsSL file://"$PWD/tools/install.sh" | bash +``` + ## Build Instructions * [Install Rust](https://www.rust-lang.org/tools/install) diff --git a/tools/install.sh b/tools/install.sh new file mode 100644 index 00000000000..8d8915c9a1c --- /dev/null +++ b/tools/install.sh @@ -0,0 +1,190 @@ +set -euo pipefail + +# Edit (Microsoft.Edit) Linux installer +# - Installs deps (build + ICU), ensures unversioned ICU symlinks exist, +# - builds Edit with nightly, and installs to /usr/local/bin or ~/.local/bin. + +need_cmd() { command -v "$1" >/dev/null 2>&1; } +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m!!\033[0m %s\n' "$*"; } +die() { printf '\033[1;31mxx\033[0m %s\n' "$*"; exit 1; } + +SUDO="" +if [ "${EUID:-$(id -u)}" -ne 0 ]; then + if need_cmd sudo; then SUDO="sudo"; elif need_cmd doas; then SUDO="doas"; else SUDO=""; fi +fi + +PM="" +if need_cmd apt-get; then PM=apt +elif need_cmd dnf; then PM=dnf +elif need_cmd yum; then PM=yum +elif need_cmd zypper; then PM=zypper +elif need_cmd pacman; then PM=pacman +elif need_cmd apk; then PM=apk +elif need_cmd xbps-install; then PM=xbps +else + warn "Unknown distro. Attempting best-effort build if prerequisites exist." +fi + +install_pkgs() { + case "$PM" in + apt) + $SUDO apt-get update -y + $SUDO apt-get install -y --no-install-recommends \ + build-essential pkg-config curl ca-certificates git \ + libicu-dev + ;; + dnf) + $SUDO dnf -y install @development-tools gcc gcc-c++ make \ + pkgconfig curl ca-certificates git libicu-devel + ;; + yum) + $SUDO yum -y groupinstall "Development Tools" || true + $SUDO yum -y install gcc gcc-c++ make pkgconfig curl ca-certificates git libicu-devel + ;; + zypper) + $SUDO zypper --non-interactive ref + $SUDO zypper --non-interactive install -t pattern devel_basis || true + $SUDO zypper --non-interactive install gcc gcc-c++ make \ + pkg-config curl ca-certificates git libicu-devel + ;; + pacman) + $SUDO pacman -Syu --noconfirm --needed base-devel icu curl ca-certificates git + ;; + apk) + $SUDO apk add --no-cache build-base pkgconfig curl ca-certificates git icu-dev + ;; + xbps) + $SUDO xbps-install -Sy gcc clang make pkg-config curl ca-certificates git icu-devel + ;; + *) + warn "Skipping package installation; please ensure build tools + ICU dev libs are installed." + ;; + esac +} + +ensure_unversioned_icu_symlinks() { + # If libicuuc.so / libicui18n.so are missing, create them pointing to the newest versioned .so + local libdirs="/usr/lib /usr/lib64 /lib /lib64 /usr/local/lib" + find_latest() { + local stem="$1" + local best="" + for d in $libdirs; do + [ -d "$d" ] || continue + # shellcheck disable=SC2010 + for f in $(ls "$d"/"$stem".so.* 2>/dev/null | sort -V); do best="$f"; done + done + printf '%s' "$best" + } + + for lib in libicuuc libicui18n; do + local_unver="" + for d in $libdirs; do + if [ -e "$d/$lib.so" ]; then local_unver="$d/$lib.so"; break; fi + done + if [ -z "$local_unver" ]; then + latest="$(find_latest "$lib")" + if [ -n "$latest" ]; then + log "Creating unversioned symlink for $lib → $latest" + $SUDO ln -sf "$latest" "/usr/local/lib/$lib.so" + if need_cmd ldconfig; then $SUDO ldconfig; fi + else + warn "Could not find versioned $lib.so.* — Search/Replace may fail. Install ICU dev/runtime." + fi + fi + done +} + +install_rust() { + # Ensure we can install rustup even if distro rust/cargo exist + export RUSTUP_INIT_SKIP_PATH_CHECK=yes + + # Install rustup if missing + if ! need_cmd rustup; then + log "Installing Rust (rustup)" + curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal + fi + + # Make sure current shell sees $HOME/.cargo/bin first (before /usr/bin cargo) + if [ -f "$HOME/.cargo/env" ]; then + # shellcheck disable=SC1091 + . "$HOME/.cargo/env" + fi + # If shell caches 'cargo' location, refresh it + hash -r 2>/dev/null || true + + # Confirm we're using rustup's cargo; warn if not + if command -v cargo >/dev/null 2>&1 && ! command -v cargo | grep -q "$HOME/.cargo/bin/cargo"; then + warn "Using cargo from: $(command -v cargo) (not rustup). Build will still work, but +nightly may not." + warn "Temporarily preferring rustup cargo for this script run." + if [ -x "$HOME/.cargo/bin/cargo" ]; then + export PATH="$HOME/.cargo/bin:$PATH" + hash -r 2>/dev/null || true + fi + fi + + # Ensure nightly is available (the build uses 'cargo +nightly') + if ! rustup toolchain list 2>/dev/null | grep -q '^nightly'; then + log "Installing Rust nightly toolchain" + rustup toolchain install nightly --no-self-update --profile minimal --component rust-src + fi + + # Optional: set default to stable (not required for build), keep nightly available + if ! rustup default 2>/dev/null | grep -q stable; then + rustup default stable >/dev/null 2>&1 || true + fi + + # Final sanity print + log "Rustup OK: $(rustup --version 2>/dev/null || echo 'not found'), cargo: $(command -v cargo || echo 'missing')" +} + + +build_and_install() { + local SRC_DIR + if [ -d .git ] && [ -f Cargo.toml ]; then + SRC_DIR="$(pwd)" + else + SRC_DIR="$(mktemp -d)" + log "Cloning microsoft/edit into $SRC_DIR" + git clone --depth=1 https://github.com/microsoft/edit.git "$SRC_DIR" + fi + + log "Building Edit (release)" + CARGO_BIN="${HOME}/.cargo/bin/cargo" + if [ ! -x "$CARGO_BIN" ]; then CARGO_BIN="$(command -v cargo)"; fi + (cd "$SRC_DIR" && RUSTC_BOOTSTRAP=1 "$CARGO_BIN" +nightly build --config .cargo/release.toml --release) + + local BIN="$SRC_DIR/target/release/edit" + [ -x "$BIN" ] || die "Build failed: $BIN not found" + + local DEST="/usr/local/bin" + local DEST_USER="$HOME/.local/bin" + if [ -n "$SUDO" ]; then + log "Installing to $DEST" + $SUDO install -Dm755 "$BIN" "$DEST/edit" + $SUDO ln -sf "$DEST/edit" "$DEST/msedit" + else + mkdir -p "$DEST_USER" + log "Installing to $DEST_USER (no sudo)" + install -Dm755 "$BIN" "$DEST_USER/edit" + ln -sf "$DEST_USER/edit" "$DEST_USER/msedit" + if ! printf '%s' "$PATH" | tr ':' '\n' | grep -qx "$DEST_USER"; then + warn "Add $DEST_USER to your PATH to run 'edit' or 'msedit' globally." + fi + fi + + log "Installed: $(command -v edit || true) | Version: $(edit --version 2>/dev/null || true)" +} + +main() { + log "Installing dependencies" + install_pkgs + log "Ensuring ICU unversioned symlinks exist" + ensure_unversioned_icu_symlinks + log "Ensuring Rust toolchain" + install_rust + log "Building and installing Edit" + build_and_install + log "Done. Run: edit (alias: msedit)" +} +main "$@" From 940b8abaecd433886bf4679492c0393dd28a539f Mon Sep 17 00:00:00 2001 From: JaredTweed Date: Tue, 26 Aug 2025 14:12:58 -0500 Subject: [PATCH 2/4] made install line work for microsoft's repo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b5f27d4c0c..c0e463c8801 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ winget install Microsoft.Edit You can install the latest version by pasting this into the linux terminal: ```sh -curl -fsSL https://raw.githubusercontent.com/JaredTweed/edit/main/tools/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/microsoft/edit/main/tools/install.sh | bash ``` or via git cloning: ```sh From a3da1d137ccb14a8b641438acc16698805f8da91 Mon Sep 17 00:00:00 2001 From: JaredTweed Date: Wed, 27 Aug 2025 13:04:58 -0500 Subject: [PATCH 3/4] improved installer and added uninstaller --- README.md | 24 ++- tools/install.sh | 374 ++++++++++++++++++++++++++++++++++----------- tools/uninstall.sh | 99 ++++++++++++ 3 files changed, 404 insertions(+), 93 deletions(-) mode change 100644 => 100755 tools/install.sh create mode 100755 tools/uninstall.sh diff --git a/README.md b/README.md index c0e463c8801..8632a6f6dd1 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,35 @@ You can install the latest version by pasting this into the linux terminal: ```sh curl -fsSL https://raw.githubusercontent.com/microsoft/edit/main/tools/install.sh | bash ``` -or via git cloning: +You can uninstall via: +```sh +curl -fsSL https://raw.githubusercontent.com/microsoft/edit/main/tools/uninstall.sh | bash +``` + + ## Build Instructions * [Install Rust](https://www.rust-lang.org/tools/install) diff --git a/tools/install.sh b/tools/install.sh old mode 100644 new mode 100755 index 8d8915c9a1c..22dbca8533f --- a/tools/install.sh +++ b/tools/install.sh @@ -1,20 +1,58 @@ -set -euo pipefail +#!/usr/bin/env bash + +# guard: ensure Linux + bash +[ "$(uname -s 2>/dev/null)" = "Linux" ] || { + printf '\033[1;31mxx\033[0m This installer targets Linux.\n' >&2; exit 1; } +command -v bash >/dev/null 2>&1 || { + printf '\033[1;31mxx\033[0m bash not found. Please install bash.\n' >&2; exit 1; } + + +set -Eeuo pipefail +umask 022 +export LC_ALL=C + +trap 'code=$?; line=${BASH_LINENO[0]:-}; cmd=${BASH_COMMAND:-}; printf "\033[1;31mxx\033[0m failed (exit %s) at line %s: %s\n" "$code" "$line" "$cmd" >&2' ERR # Edit (Microsoft.Edit) Linux installer -# - Installs deps (build + ICU), ensures unversioned ICU symlinks exist, -# - builds Edit with nightly, and installs to /usr/local/bin or ~/.local/bin. +# - Installs deps (build + ICU) +# - Ensures ICU can be loaded (system-wide symlinks when possible; user wrapper otherwise) +# - Installs rustup + nightly, builds, and installs to /usr/local/bin or ~/.local/bin need_cmd() { command -v "$1" >/dev/null 2>&1; } log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m!!\033[0m %s\n' "$*"; } die() { printf '\033[1;31mxx\033[0m %s\n' "$*"; exit 1; } +is_root() { [ "${EUID:-$(id -u)}" -eq 0 ]; } +run_root() { + if is_root; then + "$@" + elif [ -n "${SUDO:-}" ]; then + $SUDO "$@" + else + die "This step requires root. Re-run as root, with sudo/doas, or set EDIT_SKIP_DEPS=1 after installing dependencies manually." + fi +} SUDO="" if [ "${EUID:-$(id -u)}" -ne 0 ]; then - if need_cmd sudo; then SUDO="sudo"; elif need_cmd doas; then SUDO="doas"; else SUDO=""; fi + if need_cmd sudo; then SUDO="sudo" + elif need_cmd doas; then SUDO="doas" + else SUDO="" + fi fi +HAVE_ROOT=0 +if is_root || [ -n "$SUDO" ]; then HAVE_ROOT=1; fi + PM="" +USE_COLOR=1 +[ -t 1 ] && [ -z "${NO_COLOR:-}" ] || USE_COLOR=0 +if [ "$USE_COLOR" -eq 0 ]; then + log(){ printf '==> %s\n' "$*"; } + warn(){ printf '!! %s\n' "$*"; } + die(){ printf 'xx %s\n' "$*"; exit 1; } +fi + if need_cmd apt-get; then PM=apt elif need_cmd dnf; then PM=dnf elif need_cmd yum; then PM=yum @@ -26,161 +64,313 @@ else warn "Unknown distro. Attempting best-effort build if prerequisites exist." fi +apt_update_if_stale() { + if [ -d /var/lib/apt/lists ]; then + local now=$(date +%s) newest=0 count=0 m + while IFS= read -r -d '' f; do + count=$((count+1)) + m=$(stat -c %Y "$f" 2>/dev/null || echo 0) + [ "$m" -gt "$newest" ] && newest="$m" + done < <(find /var/lib/apt/lists -type f -print0 2>/dev/null || true) + if [ "$count" -eq 0 ] || [ "$newest" -lt $(( now - 21600 )) ]; then + run_root env DEBIAN_FRONTEND=noninteractive apt-get update -y + fi + else + run_root env DEBIAN_FRONTEND=noninteractive apt-get update -y + fi +} + install_pkgs() { case "$PM" in apt) - $SUDO apt-get update -y - $SUDO apt-get install -y --no-install-recommends \ - build-essential pkg-config curl ca-certificates git \ - libicu-dev + apt_update_if_stale + run_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \ + build-essential pkg-config curl ca-certificates git libicu-dev ;; dnf) - $SUDO dnf -y install @development-tools gcc gcc-c++ make \ - pkgconfig curl ca-certificates git libicu-devel + run_root dnf -y install @development-tools gcc gcc-c++ make \ + pkgconf-pkg-config curl ca-certificates git libicu-devel ;; yum) - $SUDO yum -y groupinstall "Development Tools" || true - $SUDO yum -y install gcc gcc-c++ make pkgconfig curl ca-certificates git libicu-devel + run_root yum -y groupinstall "Development Tools" || true + run_root yum -y install gcc gcc-c++ make pkgconfig curl ca-certificates git libicu-devel ;; zypper) - $SUDO zypper --non-interactive ref - $SUDO zypper --non-interactive install -t pattern devel_basis || true - $SUDO zypper --non-interactive install gcc gcc-c++ make \ - pkg-config curl ca-certificates git libicu-devel + run_root zypper --non-interactive ref + run_root zypper --non-interactive install -t pattern devel_basis || true + run_root zypper --non-interactive install --no-recommends \ + gcc gcc-c++ make pkg-config curl ca-certificates git libicu-devel ;; pacman) - $SUDO pacman -Syu --noconfirm --needed base-devel icu curl ca-certificates git + # Full sync to avoid partial upgrades in scripted installs + run_root pacman -Syu --noconfirm --needed --noprogressbar \ + base-devel icu curl ca-certificates git pkgconf ;; apk) - $SUDO apk add --no-cache build-base pkgconfig curl ca-certificates git icu-dev + # Alpine: icu-dev provides unversioned .so symlinks; keep it + run_root apk add --no-cache \ + build-base pkgconf curl ca-certificates git icu-dev ;; xbps) - $SUDO xbps-install -Sy gcc clang make pkg-config curl ca-certificates git icu-devel + run_root xbps-install -Sy -y \ + gcc clang make pkgconf curl ca-certificates git icu-devel ;; *) - warn "Skipping package installation; please ensure build tools + ICU dev libs are installed." + warn "Unknown or unsupported package manager. Skipping dependency installation." + warn "Please ensure build tools, pkg-config, git, curl, and ICU dev/runtime are installed." ;; esac } -ensure_unversioned_icu_symlinks() { - # If libicuuc.so / libicui18n.so are missing, create them pointing to the newest versioned .so - local libdirs="/usr/lib /usr/lib64 /lib /lib64 /usr/local/lib" - find_latest() { - local stem="$1" - local best="" - for d in $libdirs; do - [ -d "$d" ] || continue - # shellcheck disable=SC2010 - for f in $(ls "$d"/"$stem".so.* 2>/dev/null | sort -V); do best="$f"; done - done - printf '%s' "$best" - } - - for lib in libicuuc libicui18n; do - local_unver="" - for d in $libdirs; do - if [ -e "$d/$lib.so" ]; then local_unver="$d/$lib.so"; break; fi - done - if [ -z "$local_unver" ]; then - latest="$(find_latest "$lib")" - if [ -n "$latest" ]; then + +# -------- ICU discovery helpers -------- +# Return the directory containing the newest versioned lib for a given stem, or empty. +find_icu_libdir_for() { + local stem="$1" + if need_cmd ldconfig; then + local p + p="$(ldconfig -p 2>/dev/null | awk '/'"$stem"'\.so\./{print $NF}' | sort -V | tail -1 || true)" + [ -n "$p" ] && { dirname -- "$p"; return 0; } + fi + local d + for d in /usr/local/lib /usr/local/lib64 /usr/lib /usr/lib64 /lib /lib64 /usr/lib/*-linux-gnu /lib/*-linux-gnu /usr/lib32; do + ls "$d/$stem.so."* >/dev/null 2>&1 && { printf '%s' "$d"; return 0; } + done + printf '' +} + +# Build a colon-joined LD_LIBRARY_PATH fragment with unique dirs for uc/i18n/data. +build_icu_ldpath() { + local dirs=() d seen="" + for stem in libicuuc libicui18n libicudata; do + d="$(find_icu_libdir_for "$stem")" + [ -z "$d" ] && continue + case ":$seen:" in *":$d:"*) : ;; *) dirs+=("$d"); seen="$seen:$d";; esac + done + (IFS=:; printf '%s' "${dirs[*]:-}") +} + +# Create unversioned symlinks system-wide if allowed; return 0 on success, 1 otherwise. +ensure_system_icu_symlinks() { + local icudir="$1" ok_all=0 + [ -n "$icudir" ] || return 1 + for lib in libicuuc libicui18n libicudata; do + # Already present? + if [ -e "$icudir/$lib.so" ] || [ -e "/usr/local/lib/$lib.so" ]; then + continue + fi + # Find latest version + local latest + latest="$(ls "$icudir/$lib.so."* 2>/dev/null | sort -V | tail -1 || true)" + if [ -n "$latest" ]; then + if [ "$HAVE_ROOT" -eq 1 ]; then log "Creating unversioned symlink for $lib → $latest" - $SUDO ln -sf "$latest" "/usr/local/lib/$lib.so" - if need_cmd ldconfig; then $SUDO ldconfig; fi + target="$(readlink -f "$latest" 2>/dev/null || echo "$latest")" + run_root install -d -m 0755 /usr/local/lib + run_root ln -sf "$target" "/usr/local/lib/$lib.so" + if need_cmd ldconfig && [ -z "${_EDIT_LDCONFIG_DONE:-}" ]; then + run_root ldconfig; _EDIT_LDCONFIG_DONE=1 + fi else - warn "Could not find versioned $lib.so.* — Search/Replace may fail. Install ICU dev/runtime." + ok_all=1 fi + else + ok_all=1 fi done + return $ok_all } +# Create a user-local wrapper that exports LD_LIBRARY_PATH to ICU dir, then execs the binary +install_user_wrapper() { + local bin="$1" icudir="$2" dst="$3" + mkdir -p "$(dirname "$dst")" + cat > "$dst" </dev/null || true - # Confirm we're using rustup's cargo; warn if not - if command -v cargo >/dev/null 2>&1 && ! command -v cargo | grep -q "$HOME/.cargo/bin/cargo"; then - warn "Using cargo from: $(command -v cargo) (not rustup). Build will still work, but +nightly may not." - warn "Temporarily preferring rustup cargo for this script run." - if [ -x "$HOME/.cargo/bin/cargo" ]; then - export PATH="$HOME/.cargo/bin:$PATH" - hash -r 2>/dev/null || true - fi + # Prefer rustup's cargo for +nightly + if [ -x "$HOME/.cargo/bin/cargo" ] && [ "$(command -v cargo)" != "$HOME/.cargo/bin/cargo" ]; then + export PATH="$HOME/.cargo/bin:$PATH" + hash -r 2>/dev/null || true fi - # Ensure nightly is available (the build uses 'cargo +nightly') if ! rustup toolchain list 2>/dev/null | grep -q '^nightly'; then log "Installing Rust nightly toolchain" rustup toolchain install nightly --no-self-update --profile minimal --component rust-src fi - # Optional: set default to stable (not required for build), keep nightly available - if ! rustup default 2>/dev/null | grep -q stable; then - rustup default stable >/dev/null 2>&1 || true - fi + # Keep stable default (optional) + rustup default stable >/dev/null 2>&1 || true - # Final sanity print - log "Rustup OK: $(rustup --version 2>/dev/null || echo 'not found'), cargo: $(command -v cargo || echo 'missing')" + # final check: ensure '+nightly' actually resolves + if ! "$HOME/.cargo/bin/cargo" +nightly -V >/dev/null 2>&1; then + warn "cargo (+nightly) resolution failed; diagnostics:" + "$HOME/.cargo/bin/rustup" show 2>&1 | sed 's/^/ /' + die "rustup cargo +nightly not usable; check PATH and rustup installation" + fi } - +# -------- Build & install -------- build_and_install() { - local SRC_DIR + : "${EDIT_FORCE_WRAPPER:=0}" # 1 = force user wrapper even with sudo + : "${EDIT_SOURCE_URL:=https://github.com/microsoft/edit.git}" # allow testing forks + + local SRC_DIR CLEANUP=0 + _cleanup() { [ "$CLEANUP" -eq 1 ] && rm -rf "$SRC_DIR"; } + trap _cleanup EXIT + if [ -d .git ] && [ -f Cargo.toml ]; then SRC_DIR="$(pwd)" else SRC_DIR="$(mktemp -d)" + CLEANUP=1 log "Cloning microsoft/edit into $SRC_DIR" - git clone --depth=1 https://github.com/microsoft/edit.git "$SRC_DIR" + : "${EDIT_SOURCE_REF:=}" # can be a tag, branch, or commit SHA + export GIT_TERMINAL_PROMPT=0 + if [ -n "${EDIT_SOURCE_REF:-}" ]; then + git -c http.lowSpeedLimit=1 -c http.lowSpeedTime=30 \ + clone --filter=blob:none --depth=1 --branch "$EDIT_SOURCE_REF" \ + "$EDIT_SOURCE_URL" "$SRC_DIR" || { + log "Ref not a branch/tag; doing full clone to fetch commit" + git -c http.lowSpeedLimit=1 -c http.lowSpeedTime=30 \ + clone --filter=blob:none "$EDIT_SOURCE_URL" "$SRC_DIR" + (cd "$SRC_DIR" && git checkout --detach "$EDIT_SOURCE_REF") + } + else + git -c http.lowSpeedLimit=1 -c http.lowSpeedTime=30 \ + clone --filter=blob:none --depth=1 "$EDIT_SOURCE_URL" "$SRC_DIR" + fi fi log "Building Edit (release)" - CARGO_BIN="${HOME}/.cargo/bin/cargo" - if [ ! -x "$CARGO_BIN" ]; then CARGO_BIN="$(command -v cargo)"; fi - (cd "$SRC_DIR" && RUSTC_BOOTSTRAP=1 "$CARGO_BIN" +nightly build --config .cargo/release.toml --release) + local CARGO_BIN="${HOME}/.cargo/bin/cargo" + [ -x "$CARGO_BIN" ] || CARGO_BIN="$(command -v cargo || true)" + [ -x "$CARGO_BIN" ] || die "cargo not found" + (cd "$SRC_DIR" && RUSTC_BOOTSTRAP=1 "$CARGO_BIN" +nightly \ + build --locked --config .cargo/release.toml --release ${EDIT_CARGO_ARGS:-}) local BIN="$SRC_DIR/target/release/edit" [ -x "$BIN" ] || die "Build failed: $BIN not found" - local DEST="/usr/local/bin" - local DEST_USER="$HOME/.local/bin" - if [ -n "$SUDO" ]; then - log "Installing to $DEST" - $SUDO install -Dm755 "$BIN" "$DEST/edit" - $SUDO ln -sf "$DEST/edit" "$DEST/msedit" + local DEST_SYS="${EDIT_PREFIX:-/usr/local}/bin" + local DEST_USER="${EDIT_USER_PREFIX:-$HOME/.local}/bin" + local OUT_BIN="" WRAPPER_NEEDED=0 ICU_DIR="" ICU_DIR_FIRST="" + + ICU_DIR="$(build_icu_ldpath || true)" + if [ -z "$ICU_DIR" ]; then + warn "ICU libraries not found; install ICU dev/runtime packages. Proceeding; wrapper will not help." + else + log "Detected ICU library dirs: $ICU_DIR" + # First directory (for creating system shims); keep full ICU_DIR for wrappers + ICU_DIR_FIRST="${ICU_DIR%%:*}" + fi + + # Try to make system-wide ICU symlinks if we can + if [ "$HAVE_ROOT" -eq 1 ] && [ -n "$ICU_DIR_FIRST" ]; then + if ensure_system_icu_symlinks "$ICU_DIR_FIRST"; then + log "System ICU symlinks OK." + else + warn "Could not create system ICU symlinks; will use user wrapper if installing locally." + WRAPPER_NEEDED=1 + fi + elif [ "$HAVE_ROOT" -eq 0 ] && [ -n "$ICU_DIR" ]; then + WRAPPER_NEEDED=1 + fi + + if [ "${EDIT_FORCE_WRAPPER:-0}" -eq 1 ] && [ -n "$ICU_DIR" ]; then + WRAPPER_NEEDED=1 + fi + + local DEST_SYS_DIR; DEST_SYS_DIR="$(dirname "$DEST_SYS")" + if [ "$HAVE_ROOT" -eq 1 ] && run_root sh -lc "test -w '$DEST_SYS_DIR' -a -d '$DEST_SYS_DIR'"; then + log "Installing to $DEST_SYS" + if [ "$WRAPPER_NEEDED" -eq 1 ] && [ -n "$ICU_DIR" ]; then + # Could not install the system-wide ICU shim; use a system-wide wrapper + log "System ICU symlinks unavailable; installing wrapper that sets LD_LIBRARY_PATH" + run_root install -Dm755 "$BIN" "${EDIT_PREFIX:-/usr/local}/libexec/edit-real" + run_root bash -lc "cat > '$DEST_SYS/edit' <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +export LD_LIBRARY_PATH='"$ICU_DIR"':\${LD_LIBRARY_PATH:-} +exec -a edit '"${EDIT_PREFIX:-/usr/local}"'/libexec/edit-real "\$@" +EOF +chmod +x '$DEST_SYS/edit'" + run_root ln -sf "$DEST_SYS/edit" "$DEST_SYS/msedit" + OUT_BIN="$DEST_SYS/edit" + else + # Normal case: direct binary install (symlink shim present or not needed) + run_root install -Dm755 "$BIN" "$DEST_SYS/edit" + run_root ln -sf "$DEST_SYS/edit" "$DEST_SYS/msedit" + OUT_BIN="$DEST_SYS/edit" + fi else mkdir -p "$DEST_USER" - log "Installing to $DEST_USER (no sudo)" - install -Dm755 "$BIN" "$DEST_USER/edit" - ln -sf "$DEST_USER/edit" "$DEST_USER/msedit" + if [ "$WRAPPER_NEEDED" -eq 1 ] && [ -n "$ICU_DIR" ]; then + log "Installing user-local wrapper due to missing privileges for ICU shim" + install -Dm755 "$BIN" "$DEST_USER/.edit-real" + install_user_wrapper "$DEST_USER/.edit-real" "$ICU_DIR" "$DEST_USER/edit" + ln -sf "$DEST_USER/edit" "$DEST_USER/msedit" + OUT_BIN="$DEST_USER/edit" + else + log "Installing to $DEST_USER (no sudo)" + install -Dm755 "$BIN" "$DEST_USER/edit" + ln -sf "$DEST_USER/edit" "$DEST_USER/msedit" + OUT_BIN="$DEST_USER/edit" + fi if ! printf '%s' "$PATH" | tr ':' '\n' | grep -qx "$DEST_USER"; then - warn "Add $DEST_USER to your PATH to run 'edit' or 'msedit' globally." + warn "Add $DEST_USER to your PATH to run 'edit' globally." fi fi - log "Installed: $(command -v edit || true) | Version: $(edit --version 2>/dev/null || true)" + CLEANUP=0 + trap - EXIT + + log "Installed: $OUT_BIN" + if [ -n "$OUT_BIN" ]; then + log "Version: $("$OUT_BIN" --version 2>/dev/null || true)" + else + log "Version: $(edit --version 2>/dev/null || true)" + fi + + # PATH check hint + if [ -n "$OUT_BIN" ]; then + case ":$PATH:" in + *":$(dirname "$OUT_BIN"):"*) : ;; + *) warn "Note: $(dirname "$OUT_BIN") is not in PATH for non-login shells." ;; + esac + fi } main() { - log "Installing dependencies" - install_pkgs - log "Ensuring ICU unversioned symlinks exist" - ensure_unversioned_icu_symlinks + if [ "${EDIT_SKIP_DEPS:-0}" != "1" ]; then + log "Installing dependencies" + install_pkgs + else + log "Skipping dependency installation (EDIT_SKIP_DEPS=1)" + need_cmd curl || die "curl is required when EDIT_SKIP_DEPS=1" + if [ ! -d .git ] || [ ! -f Cargo.toml ]; then + need_cmd git || die "git is required to clone the source when EDIT_SKIP_DEPS=1" + fi + fi log "Ensuring Rust toolchain" install_rust log "Building and installing Edit" diff --git a/tools/uninstall.sh b/tools/uninstall.sh new file mode 100755 index 00000000000..451123c282b --- /dev/null +++ b/tools/uninstall.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 022 + +log(){ printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn(){ printf '\033[1;33m!!\033[0m %s\n' "$*"; } +is_root(){ [ "${EUID:-$(id -u)}" -eq 0 ]; } + +# ----- args ----- +MODE="all" # all | user | system +DRYRUN=0 +for a in "$@"; do + case "$a" in + --user-only) MODE="user" ;; + --system-only) MODE="system" ;; + --dry-run) DRYRUN=1 ;; + -h|--help) + cat <<'EOF' +Usage: uninstall.sh [--user-only|--system-only] [--dry-run] + --user-only Remove only ~/.local installs + --system-only Remove only /usr/local installs (requires root/sudo/doas) + --dry-run Show what would be removed, without removing +EOF + exit 0 + ;; + *) warn "Ignoring unknown argument: $a" ;; + esac +done + +# ----- elevation helper (sudo/doas if available) ----- +SUDO="" +if ! is_root; then + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + elif command -v doas >/dev/null 2>&1; then + SUDO="doas" + fi +fi + +run_rm() { + # rm path... (with optional sudo) + if [ "$DRYRUN" -eq 1 ]; then + printf '[dry-run] rm -f %s\n' "$*" ; return 0 + fi + rm -f "$@" 2>/dev/null || true +} + +run_rm_root() { + # rm path... as root (if possible) + if [ "$DRYRUN" -eq 1 ]; then + printf '[dry-run] %s rm -f %s\n' "${SUDO:-(no-sudo)}" "$*" + return 0 + fi + if is_root; then + rm -f "$@" 2>/dev/null || true + elif [ -n "$SUDO" ]; then + $SUDO rm -f "$@" 2>/dev/null || true + else + warn "No sudo/doas; cannot remove: $*" + fi +} + +# ----- user-local ----- +if [ "$MODE" = "all" ] || [ "$MODE" = "user" ]; then + log "Removing user-local binaries" + run_rm "$HOME/.local/bin/edit" \ + "$HOME/.local/bin/msedit" \ + "$HOME/.local/bin/.edit-real" +fi + +# ----- system-wide ----- +if [ "$MODE" = "all" ] || [ "$MODE" = "system" ]; then + if ! is_root && [ -z "$SUDO" ]; then + warn "Skipping system-wide removal: need root, sudo, or doas" + else + log "Removing system-wide binaries" + run_rm_root /usr/local/bin/edit /usr/local/bin/msedit + run_rm_root /usr/local/libexec/edit-real + + log "Removing ICU helper symlinks (if we created them)" + for lib in libicuuc libicui18n libicudata; do + if [ -L "/usr/local/lib/$lib.so" ]; then + run_rm_root "/usr/local/lib/$lib.so" + fi + done + + if command -v ldconfig >/dev/null 2>&1; then + if [ "$DRYRUN" -eq 1 ]; then + printf '[dry-run] %s ldconfig\n' "${SUDO:-(no-sudo)}" + else + if is_root; then ldconfig || true + else $SUDO ldconfig || true + fi + fi + fi + fi +fi + +log "Done." From 0be10209aaf316cac53e8039398445507835497a Mon Sep 17 00:00:00 2001 From: JaredTweed Date: Fri, 29 Aug 2025 13:31:49 -0500 Subject: [PATCH 4/4] move ICU discovery into build.rs and update installer --- Cargo.lock | 1 + Cargo.toml | 1 + build/main.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ tools/install.sh | 28 +++++++++++---- 4 files changed, 117 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b27fd66dea4..b5c06fae258 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,6 +186,7 @@ version = "1.2.1" dependencies = [ "criterion", "libc", + "pkg-config", "serde", "serde_json", "toml-span", diff --git a/Cargo.toml b/Cargo.toml index 792aa41d4fa..465facf81fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ libc = "0.2" # The default toml crate bundles its dependencies with bad compile times. Thanks. # Thankfully toml-span exists. FWIW the alternative is yaml-rust (without the 2 suffix). toml-span = { version = "0.5", default-features = false } +pkg-config = "0.3" [target.'cfg(windows)'.build-dependencies] winresource = { version = "0.1.22", default-features = false } diff --git a/build/main.rs b/build/main.rs index fb1d8d157f9..dde88d572fc 100644 --- a/build/main.rs +++ b/build/main.rs @@ -4,6 +4,9 @@ #![allow(irrefutable_let_patterns)] use crate::helpers::env_opt; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; mod helpers; mod i18n; @@ -15,6 +18,94 @@ enum TargetOs { Unix, } +// ---- ICU discovery for installer & source builds --------------------------- +fn dedup_join(mut v: Vec) -> String { + v.sort(); + v.dedup(); + let parts: Vec = v.into_iter().map(|p| p.display().to_string()).collect(); + parts.join(":") +} + +fn try_pkg_config() -> Vec { + let mut dirs = Vec::new(); + for name in ["icu-uc", "icu-i18n", "icu-data"] { + match pkg_config::Config::new().print_system_libs(false).probe(name) { + Ok(lib) => dirs.extend(lib.link_paths.clone()), + Err(_) => {} + } + } + dirs +} + +fn try_fs_latest_for(stem: &str, roots: &[&str]) -> Option { + // Find lib.so.* and return its parent dir + for d in roots { + let dir = Path::new(d); + if !dir.is_dir() { continue; } + // A simple lexicographic sort is good enough for .so.N versions + let mut candidates: Vec = match fs::read_dir(dir) { + Ok(it) => it.filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| p.file_name() + .and_then(|s| s.to_str()) + .map(|n| n.starts_with(&format!("lib{stem}.so."))) + .unwrap_or(false)) + .collect(), + Err(_) => continue, + }; + candidates.sort(); + if let Some(path) = candidates.last() { + return path.parent().map(|p| p.to_path_buf()); + } + } + None +} + +fn try_fs_scan() -> Vec { + let roots = [ + "/usr/local/lib", "/usr/local/lib64", + "/usr/lib", "/usr/lib64", "/lib", "/lib64", + "/usr/lib32", + "/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu", + "/usr/lib/aarch64-linux-gnu", "/lib/aarch64-linux-gnu", + "/usr/lib/arm-linux-gnueabihf", "/lib/arm-linux-gnueabihf", + ]; + let mut dirs = Vec::new(); + for stem in ["icuuc", "icui18n", "icudata"] { + if let Some(d) = try_fs_latest_for(stem, &roots) { + dirs.push(d); + } + } + dirs +} + +fn write_icu_ldpath_artifact() { + // 1) gather ICU dirs (prefer pkg-config) + let mut dirs = try_pkg_config(); + if dirs.is_empty() { + dirs = try_fs_scan(); + } + + // 2) write ${OUT_DIR}/.edit.ldpath (empty file if not found) + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); + let ldfile = Path::new(&out_dir).join(".edit.ldpath"); + let joined = dedup_join(dirs); + // Create the file regardless (lets the installer detect the “not found” case) + let mut f = fs::File::create(&ldfile).expect("create .edit.ldpath"); + if !joined.is_empty() { + let _ = writeln!(f, "{}", joined); + // Also export for optional runtime hints + println!("cargo:rustc-env=EDIT_BUILD_ICU_LDPATH={}", joined); + println!("cargo:warning=edit: using ICU from {}", joined); + } else { + // Leave it empty; installer will fall back to its own detection + println!("cargo:warning=edit: ICU not found by build script"); + } + // Re-run if we change this file + println!("cargo:rerun-if-changed=build/main.rs"); + println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH"); +} + + fn main() { let target_os = match env_opt("CARGO_CFG_TARGET_OS").as_str() { "windows" => TargetOs::Windows, @@ -22,6 +113,8 @@ fn main() { _ => TargetOs::Unix, }; + // Always produce ICU ldpath artifact for installer & source builds + write_icu_ldpath_artifact(); compile_i18n(); configure_icu(target_os); #[cfg(windows)] diff --git a/tools/install.sh b/tools/install.sh index 22dbca8533f..8ffea0b2960 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -234,8 +234,14 @@ build_and_install() { : "${EDIT_FORCE_WRAPPER:=0}" # 1 = force user wrapper even with sudo : "${EDIT_SOURCE_URL:=https://github.com/microsoft/edit.git}" # allow testing forks - local SRC_DIR CLEANUP=0 - _cleanup() { [ "$CLEANUP" -eq 1 ] && rm -rf "$SRC_DIR"; } + local SRC_DIR + local CLEANUP=0 + _cleanup() { + # safe under `set -u` + if [ "${CLEANUP:-0}" -eq 1 ] && [ -n "${SRC_DIR:-}" ]; then + rm -rf "$SRC_DIR" + fi + } trap _cleanup EXIT if [ -d .git ] && [ -f Cargo.toml ]; then @@ -266,7 +272,7 @@ build_and_install() { [ -x "$CARGO_BIN" ] || CARGO_BIN="$(command -v cargo || true)" [ -x "$CARGO_BIN" ] || die "cargo not found" (cd "$SRC_DIR" && RUSTC_BOOTSTRAP=1 "$CARGO_BIN" +nightly \ - build --locked --config .cargo/release.toml --release ${EDIT_CARGO_ARGS:-}) + build --config .cargo/release.toml --release ${EDIT_CARGO_ARGS:-}) local BIN="$SRC_DIR/target/release/edit" [ -x "$BIN" ] || die "Build failed: $BIN not found" @@ -275,15 +281,25 @@ build_and_install() { local DEST_USER="${EDIT_USER_PREFIX:-$HOME/.local}/bin" local OUT_BIN="" WRAPPER_NEEDED=0 ICU_DIR="" ICU_DIR_FIRST="" - ICU_DIR="$(build_icu_ldpath || true)" + # Prefer build.rs artifact if present, else fall back to shell discovery + local LDPATH_FILE="" + LDPATH_FILE="$(find "$SRC_DIR/target" -type f -name '.edit.ldpath' | head -n1 || true)" + if [ -n "$LDPATH_FILE" ] && [ -s "$LDPATH_FILE" ]; then + ICU_DIR="$(tr -d '\n' < "$LDPATH_FILE" || true)" + log "ICU (from build.rs): ${ICU_DIR:-}" + else + ICU_DIR="$(build_icu_ldpath || true)" + [ -n "$ICU_DIR" ] && log "ICU (shell fallback): $ICU_DIR" + fi + if [ -z "$ICU_DIR" ]; then warn "ICU libraries not found; install ICU dev/runtime packages. Proceeding; wrapper will not help." else - log "Detected ICU library dirs: $ICU_DIR" - # First directory (for creating system shims); keep full ICU_DIR for wrappers + # First dir for symlink shim; keep full list for LD_LIBRARY_PATH wrappers ICU_DIR_FIRST="${ICU_DIR%%:*}" fi + # Try to make system-wide ICU symlinks if we can if [ "$HAVE_ROOT" -eq 1 ] && [ -n "$ICU_DIR_FIRST" ]; then if ensure_system_icu_symlinks "$ICU_DIR_FIRST"; then