From 2bffd95cddcd8d879c35fc74054f41417d2b9524 Mon Sep 17 00:00:00 2001 From: JaredTweed Date: Tue, 26 Aug 2025 14:09:38 -0500 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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 From 250441de5f5ec721876ef53e405aa1d6cb47551b Mon Sep 17 00:00:00 2001 From: JaredTweed Date: Wed, 25 Mar 2026 16:00:07 -0500 Subject: [PATCH 5/8] fix linux installer reliability and uninstall cleanup --- README.md | 3 +- tools/install.sh | 189 ++++++++++++++++++++++++++++++++++++++++----- tools/uninstall.sh | 45 ++++++++--- 3 files changed, 204 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index f3818eaabed..6e2c8e0b7e7 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ winget install Microsoft.Edit ### Linux -You can install the latest version by pasting this into the linux terminal: +You can install the latest tagged release by pasting this into the Linux terminal: ```sh curl -fsSL https://raw.githubusercontent.com/microsoft/edit/main/tools/install.sh | bash ``` @@ -29,6 +29,7 @@ You can uninstall via: ```sh curl -fsSL https://raw.githubusercontent.com/microsoft/edit/main/tools/uninstall.sh | bash ``` +To target a specific branch, tag, or commit instead, set `EDIT_SOURCE_REF`. -Before this becomes live, these can be used instead. - - -To to this directory: -```sh -git clone git@github.com:microsoft/edit.git -cd edit -``` - -Then from there: - -To install: -```sh -curl -fsSL file://"$PWD/tools/install.sh" | bash -``` - -To uninstall: -```sh -sudo ./tools/uninstall.sh -``` - ---> +The source installer is continuously tested on Ubuntu, Fedora, Arch Linux, and openSUSE Tumbleweed. Other Linux distributions are best-effort and may require manual dependency installation. ## Build Instructions diff --git a/tools/install.sh b/tools/install.sh index 1259a31ba73..179908eaba3 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -133,14 +133,20 @@ try_install_pkgs() { return "$rc" } -find_local_edit_checkout() { - local root - root="$(git rev-parse --show-toplevel 2>/dev/null || true)" +is_edit_source_tree() { + local root="$1" [ -n "$root" ] || return 1 [ -f "$root/Cargo.toml" ] || return 1 [ -f "$root/crates/edit/Cargo.toml" ] || return 1 [ -f "$root/i18n/edit.toml" ] || return 1 - grep -q '^name = "edit"$' "$root/crates/edit/Cargo.toml" || return 1 + grep -q '^name = "edit"$' "$root/crates/edit/Cargo.toml" +} + +find_local_edit_checkout() { + local root + root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + [ -n "$root" ] || return 1 + is_edit_source_tree "$root" || return 1 printf '%s' "$root" } @@ -424,6 +430,7 @@ install_rust() { # -------- Build & install -------- build_and_install() { : "${EDIT_FORCE_WRAPPER:=0}" # 1 = force user wrapper even with sudo + : "${EDIT_SOURCE_DIR:=}" # trusted local source tree override : "${EDIT_SOURCE_URL:=https://github.com/microsoft/edit.git}" # allow testing forks : "${EDIT_SOURCE_REF:=}" @@ -442,8 +449,18 @@ build_and_install() { } trap _cleanup EXIT - LOCAL_CHECKOUT="$(find_local_edit_checkout || true)" + LOCAL_CHECKOUT="${EDIT_SOURCE_DIR:-}" if [ -n "$LOCAL_CHECKOUT" ]; then + is_edit_source_tree "$LOCAL_CHECKOUT" || die "EDIT_SOURCE_DIR is not a valid edit source tree: $LOCAL_CHECKOUT" + SRC_DIR="$LOCAL_CHECKOUT" + log "Using explicit edit source tree at $SRC_DIR" + else + LOCAL_CHECKOUT="$(find_local_edit_checkout || true)" + fi + + if [ -n "${SRC_DIR:-}" ]; then + : + elif [ -n "$LOCAL_CHECKOUT" ]; then SRC_DIR="$LOCAL_CHECKOUT" log "Using existing edit checkout at $SRC_DIR" else diff --git a/tools/test-installer-distro.sh b/tools/test-installer-distro.sh new file mode 100755 index 00000000000..faed57ed993 --- /dev/null +++ b/tools/test-installer-distro.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +prefix="${EDIT_PREFIX:-/usr/local}" +bin_dir="$prefix/bin" +libexec_dir="$prefix/libexec" +manifest="$prefix/share/edit/install-manifest" + +fail() { + printf 'test failure: %s\n' "$*" >&2 + exit 1 +} + +assert_exists() { + [ -e "$1" ] || fail "expected path to exist: $1" +} + +assert_not_exists() { + [ ! -e "$1" ] || fail "expected path to be absent: $1" +} + +assert_manifest_contains() { + local path="$1" + grep -Fxq "$path" "$manifest" || fail "expected manifest $manifest to contain $path" +} + +cleanup() { + bash "$repo_root/tools/uninstall.sh" --system-only >/dev/null 2>&1 || true +} + +main() { + trap cleanup EXIT + + bash -n "$repo_root/tools/install.sh" + bash -n "$repo_root/tools/uninstall.sh" + + EDIT_SOURCE_DIR="$repo_root" bash "$repo_root/tools/install.sh" + + assert_exists "$bin_dir/edit" + assert_exists "$bin_dir/msedit" + assert_exists "$manifest" + assert_manifest_contains "$bin_dir/edit" + assert_manifest_contains "$bin_dir/msedit" + + if [ -e "$libexec_dir/edit-real" ]; then + assert_manifest_contains "$libexec_dir/edit-real" + fi + + bash "$repo_root/tools/uninstall.sh" --system-only + + assert_not_exists "$bin_dir/edit" + assert_not_exists "$bin_dir/msedit" + assert_not_exists "$libexec_dir/edit-real" + assert_not_exists "$manifest" + + trap - EXIT + printf 'distro installer smoke test passed\n' +} + +main "$@" diff --git a/tools/test-installer.sh b/tools/test-installer.sh index 3059e038a4a..66bab03f71a 100644 --- a/tools/test-installer.sh +++ b/tools/test-installer.sh @@ -37,11 +37,15 @@ run_unit_tests() { actual="$(cd "$repo_root" && find_local_edit_checkout)" assert_eq "$actual" "$repo_root" "find_local_edit_checkout should detect the repository root" + is_edit_source_tree "$repo_root" || fail "is_edit_source_tree should accept the repository root" tmp="$(mktemp -d)" if (cd "$tmp" && find_local_edit_checkout >/dev/null 2>&1); then fail "find_local_edit_checkout should reject unrelated repositories" fi + if is_edit_source_tree "$tmp"; then + fail "is_edit_source_tree should reject unrelated repositories" + fi rm -rf "$tmp" tmp="$(mktemp -d)" From 1e91b31b2d36fc83a4ae5e00091b3861cf1c0f6e Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 Apr 2026 19:21:42 +0200 Subject: [PATCH 8/8] Rewrite the install script --- .github/workflows/ci.yml | 44 --- Cargo.lock | 1 - README.md | 34 +- assets/install.sh | 202 +++++++++++ crates/edit/Cargo.toml | 1 - crates/edit/build/main.rs | 92 ----- tools/install.sh | 637 --------------------------------- tools/test-installer-distro.sh | 61 ---- tools/test-installer.sh | 170 --------- tools/uninstall.sh | 144 -------- 10 files changed, 222 insertions(+), 1164 deletions(-) create mode 100755 assets/install.sh delete mode 100755 tools/install.sh delete mode 100755 tools/test-installer-distro.sh delete mode 100644 tools/test-installer.sh delete mode 100755 tools/uninstall.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e97923a014c..c84869c346b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,47 +46,3 @@ jobs: run: cargo test --all-features --all-targets - name: Run clippy run: cargo clippy --all-features --all-targets -- --no-deps --deny warnings - - installer: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install Rust - run: rustup toolchain install nightly --no-self-update --profile minimal --component rust-src - - name: Run installer tests - run: bash tools/test-installer.sh - - installer-distros: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - name: ubuntu - image: ubuntu:24.04 - - name: fedora - image: fedora:42 - - name: archlinux - image: archlinux:latest - - name: opensuse - image: opensuse/tumbleweed:latest - container: - image: ${{ matrix.image }} - env: - CARGO_HOME: /github/home/.cargo - RUSTUP_HOME: /github/home/.rustup - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Run distro installer smoke test - run: bash tools/test-installer-distro.sh diff --git a/Cargo.lock b/Cargo.lock index e9c90531d80..4afe7a208b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,7 +258,6 @@ dependencies = [ "criterion", "libc", "lsh", - "pkg-config", "stdext", "toml-span", "windows-sys", diff --git a/README.md b/README.md index 3290b450452..73c337d8c95 100644 --- a/README.md +++ b/README.md @@ -19,19 +19,23 @@ You can install the latest version with WinGet: winget install Microsoft.Edit ``` -### Linux +### Linux (build from source) -You can install the latest tagged release by pasting this into the Linux terminal: +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 -fsSL https://raw.githubusercontent.com/microsoft/edit/main/tools/install.sh | bash +curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/microsoft/edit/main/assets/install.sh | sh ``` -You can uninstall via: + +Additional flags are `--dev`, to build directly from the main branch, and `--system` to install into `/usr/local/bin`. For instance: ```sh -curl -fsSL https://raw.githubusercontent.com/microsoft/edit/main/tools/uninstall.sh | bash +curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/microsoft/edit/main/assets/install.sh | sh -s -- --dev --system ``` - - -The source installer is continuously tested on Ubuntu, Fedora, Arch Linux, and openSUSE Tumbleweed. Other Linux distributions are best-effort and may require manual dependency installation. ## Build Instructions @@ -51,11 +55,11 @@ The source installer is continuously tested on Ubuntu, Fedora, Arch Linux, and o ### 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 @@ -71,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 diff --git a/crates/edit/Cargo.toml b/crates/edit/Cargo.toml index 77ec15e3589..02becfdb908 100644 --- a/crates/edit/Cargo.toml +++ b/crates/edit/Cargo.toml @@ -28,7 +28,6 @@ libc = "0.2" [build-dependencies] stdext.workspace = true lsh.workspace = true -pkg-config = "0.3" # 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.7", default-features = false } diff --git a/crates/edit/build/main.rs b/crates/edit/build/main.rs index 6b5bb24223d..96ae2f20142 100644 --- a/crates/edit/build/main.rs +++ b/crates/edit/build/main.rs @@ -6,9 +6,6 @@ use stdext::arena::scratch_arena; use crate::helpers::env_opt; -use std::fs; -use std::io::Write; -use std::path::{Path, PathBuf}; mod helpers; mod i18n; @@ -20,94 +17,6 @@ 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() { stdext::arena::init(128 * 1024 * 1024).unwrap(); @@ -117,7 +26,6 @@ fn main() { _ => TargetOs::Unix, }; - write_icu_ldpath_artifact(); compile_lsh(); compile_i18n(); configure_icu(target_os); diff --git a/tools/install.sh b/tools/install.sh deleted file mode 100755 index 179908eaba3..00000000000 --- a/tools/install.sh +++ /dev/null @@ -1,637 +0,0 @@ -#!/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 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 -} - -: "${EDIT_ALLOW_ELEVATION:=1}" -declare -a CREATED_ICU_LINKS=() - -SUDO="" -if [ "${EDIT_ALLOW_ELEVATION}" = "1" ] && [ "${EUID:-$(id -u)}" -ne 0 ]; then - 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 -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 - -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) - 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) - run_root dnf -y install @development-tools gcc gcc-c++ make \ - pkgconf-pkg-config curl ca-certificates git libicu-devel - ;; - yum) - 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) - 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) - # 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) - # 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) - run_root xbps-install -Sy -y \ - gcc clang make pkgconf curl ca-certificates git icu-devel - ;; - *) - 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 -} - -try_install_pkgs() { - set +e - install_pkgs - local rc=$? - set -e - return "$rc" -} - -is_edit_source_tree() { - local root="$1" - [ -n "$root" ] || return 1 - [ -f "$root/Cargo.toml" ] || return 1 - [ -f "$root/crates/edit/Cargo.toml" ] || return 1 - [ -f "$root/i18n/edit.toml" ] || return 1 - grep -q '^name = "edit"$' "$root/crates/edit/Cargo.toml" -} - -find_local_edit_checkout() { - local root - root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - [ -n "$root" ] || return 1 - is_edit_source_tree "$root" || return 1 - printf '%s' "$root" -} - -git_latest_release_tag() { - local source_url="$1" - git ls-remote --tags --refs "$source_url" 'v*' 2>/dev/null \ - | awk '{sub("refs/tags/", "", $2); print $2}' \ - | sort -V \ - | tail -n1 -} - -is_local_git_source() { - local source_url="$1" - case "$source_url" in - file://*) return 0 ;; - esac - [ -e "$source_url" ] -} - -looks_like_commit_ref() { - [[ "$1" =~ ^[0-9a-fA-F]{7,40}$ ]] -} - -git_clone_source() { - local source_url="$1" source_ref="$2" dest="$3" - export GIT_TERMINAL_PROMPT=0 - - if looks_like_commit_ref "$source_ref"; then - git clone "$source_url" "$dest" - (cd "$dest" && git checkout --detach "$source_ref") - return 0 - fi - - if is_local_git_source "$source_url"; then - if [ -n "$source_ref" ]; then - git clone --branch "$source_ref" "$source_url" "$dest" - else - git clone "$source_url" "$dest" - fi - return 0 - fi - - if [ -n "$source_ref" ]; then - git -c http.lowSpeedLimit=1 -c http.lowSpeedTime=30 \ - clone --filter=blob:none --depth=1 --branch "$source_ref" \ - "$source_url" "$dest" - else - git -c http.lowSpeedLimit=1 -c http.lowSpeedTime=30 \ - clone --filter=blob:none --depth=1 "$source_url" "$dest" - fi -} - -nightly_uses_real_immediate_abort() { - local cargo_bin="$1" - local version major rest minor - version="$("$cargo_bin" +nightly -V 2>/dev/null | awk '{print $2}')" - major="${version%%.*}" - rest="${version#*.}" - minor="${rest%%.*}" - - case "$major:$minor" in - ''|*[!0-9:]*) - return 0 - ;; - *) - if [ "$major" -gt 1 ] || { [ "$major" -eq 1 ] && [ "$minor" -ge 91 ]; }; then - return 0 - else - return 1 - fi - ;; - esac -} - -write_release_nightly_compat_config() { - local path="$1" - cat > "$path" <<'EOF' -[profile.release] -panic = "immediate-abort" - -[unstable] -panic-immediate-abort = true -build-std = ["std", "panic_abort"] -EOF -} - -select_release_config() { - local src_dir="$1" cargo_bin="$2" compat_path="$3" - if nightly_uses_real_immediate_abort "$cargo_bin"; then - if [ -f "$src_dir/.cargo/release-nightly.toml" ]; then - printf '%s' ".cargo/release-nightly.toml" - else - write_release_nightly_compat_config "$compat_path" - printf '%s' "$compat_path" - fi - else - printf '%s' ".cargo/release.toml" - fi -} - -check_build_tools() { - local missing=() - if ! need_cmd cc && ! need_cmd gcc; then - missing+=("cc/gcc") - fi - if ! need_cmd pkg-config && ! need_cmd pkgconf; then - missing+=("pkg-config/pkgconf") - fi - if [ "${#missing[@]}" -ne 0 ]; then - die "Missing build prerequisites: ${missing[*]}. Install dependencies first or rerun without EDIT_SKIP_DEPS=1." - fi -} - -write_manifest() { - local manifest_path="$1" use_root="$2" - shift 2 - local tmp - tmp="$(mktemp)" - printf '%s\n' "$@" > "$tmp" - if [ "$use_root" -eq 1 ]; then - run_root install -Dm644 "$tmp" "$manifest_path" - else - install -Dm644 "$tmp" "$manifest_path" - fi - rm -f "$tmp" -} - - -# -------- 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 latest candidate - local -a matches=() - shopt -s nullglob - 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 - for candidate in "$d/$stem.so."*; do - matches+=("$candidate") - done - done - shopt -u nullglob - if [ "${#matches[@]}" -ne 0 ]; then - latest="$(printf '%s\n' "${matches[@]}" | sort -V | tail -1)" - dirname -- "$latest" - return 0 - fi - 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. -find_latest_icu_lib_in_dirs() { - local lib="$1" search_dirs="$2" latest candidate dir - local -a matches=() - [ -n "$search_dirs" ] || return 1 - local old_ifs="$IFS" - IFS=: - for dir in $search_dirs; do - [ -n "$dir" ] || continue - shopt -s nullglob - for candidate in "$dir/$lib.so."*; do - matches+=("$candidate") - done - shopt -u nullglob - done - IFS="$old_ifs" - [ "${#matches[@]}" -ne 0 ] || return 1 - latest="$(printf '%s\n' "${matches[@]}" | sort -V | tail -1)" - printf '%s' "$latest" -} - -icu_has_unversioned_lib_in_dirs() { - local lib="$1" search_dirs="$2" dir - [ -e "/usr/local/lib/$lib.so" ] && return 0 - [ -n "$search_dirs" ] || return 1 - local old_ifs="$IFS" - IFS=: - for dir in $search_dirs; do - if [ -e "$dir/$lib.so" ]; then - IFS="$old_ifs" - return 0 - fi - done - IFS="$old_ifs" - return 1 -} - -# Create unversioned symlinks system-wide if allowed; return 0 on success, 1 otherwise. -ensure_system_icu_symlinks() { - local icu_dirs="$1" ok_all=0 - [ -n "$icu_dirs" ] || return 1 - for lib in libicuuc libicui18n libicudata; do - # Already present? - if icu_has_unversioned_lib_in_dirs "$lib" "$icu_dirs"; then - continue - fi - # Find latest version - local latest target - latest="$(find_latest_icu_lib_in_dirs "$lib" "$icu_dirs" || true)" - if [ -n "$latest" ]; then - if [ "$HAVE_ROOT" -eq 1 ]; then - log "Creating unversioned symlink for $lib → $latest" - 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" - CREATED_ICU_LINKS+=("/usr/local/lib/$lib.so") - if need_cmd ldconfig && [ -z "${_EDIT_LDCONFIG_DONE:-}" ]; then - run_root ldconfig; _EDIT_LDCONFIG_DONE=1 - fi - else - 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 - - # 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 - - 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 - - # 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() { - : "${EDIT_FORCE_WRAPPER:=0}" # 1 = force user wrapper even with sudo - : "${EDIT_SOURCE_DIR:=}" # trusted local source tree override - : "${EDIT_SOURCE_URL:=https://github.com/microsoft/edit.git}" # allow testing forks - : "${EDIT_SOURCE_REF:=}" - - local SRC_DIR - local CLEANUP=0 - local LOCAL_CHECKOUT="" - local TEMP_RELEASE_CONFIG="" - _cleanup() { - # safe under `set -u` - if [ "${CLEANUP:-0}" -eq 1 ] && [ -n "${SRC_DIR:-}" ]; then - rm -rf "$SRC_DIR" - fi - if [ -n "${TEMP_RELEASE_CONFIG:-}" ] && [ -f "${TEMP_RELEASE_CONFIG:-}" ]; then - rm -f "$TEMP_RELEASE_CONFIG" - fi - } - trap _cleanup EXIT - - LOCAL_CHECKOUT="${EDIT_SOURCE_DIR:-}" - if [ -n "$LOCAL_CHECKOUT" ]; then - is_edit_source_tree "$LOCAL_CHECKOUT" || die "EDIT_SOURCE_DIR is not a valid edit source tree: $LOCAL_CHECKOUT" - SRC_DIR="$LOCAL_CHECKOUT" - log "Using explicit edit source tree at $SRC_DIR" - else - LOCAL_CHECKOUT="$(find_local_edit_checkout || true)" - fi - - if [ -n "${SRC_DIR:-}" ]; then - : - elif [ -n "$LOCAL_CHECKOUT" ]; then - SRC_DIR="$LOCAL_CHECKOUT" - log "Using existing edit checkout at $SRC_DIR" - else - local SOURCE_REF="${EDIT_SOURCE_REF:-}" - need_cmd git || die "git is required to clone the source" - SRC_DIR="$(mktemp -d)" - CLEANUP=1 - if [ -z "$SOURCE_REF" ]; then - SOURCE_REF="$(git_latest_release_tag "$EDIT_SOURCE_URL" || true)" - if [ -n "$SOURCE_REF" ]; then - log "Using latest release tag: $SOURCE_REF" - else - SOURCE_REF="main" - warn "Could not determine the latest release tag; falling back to $SOURCE_REF" - fi - fi - log "Cloning $EDIT_SOURCE_URL into $SRC_DIR" - git_clone_source "$EDIT_SOURCE_URL" "$SOURCE_REF" "$SRC_DIR" - fi - - check_build_tools - - log "Building Edit (release)" - local CARGO_BIN="${HOME}/.cargo/bin/cargo" - [ -x "$CARGO_BIN" ] || CARGO_BIN="$(command -v cargo || true)" - [ -x "$CARGO_BIN" ] || die "cargo not found" - TEMP_RELEASE_CONFIG="$(mktemp)" - local RELEASE_CONFIG - RELEASE_CONFIG="$(select_release_config "$SRC_DIR" "$CARGO_BIN" "$TEMP_RELEASE_CONFIG")" - if [ "$RELEASE_CONFIG" = "$TEMP_RELEASE_CONFIG" ]; then - log "Using compatibility release config for current nightly" - else - rm -f "$TEMP_RELEASE_CONFIG" - TEMP_RELEASE_CONFIG="" - log "Using release config: $RELEASE_CONFIG" - fi - local -a BUILD_ARGS=() - if [ -f "$SRC_DIR/crates/edit/Cargo.toml" ]; then - BUILD_ARGS=(-p edit) - fi - (cd "$SRC_DIR" && RUSTC_BOOTSTRAP=1 "$CARGO_BIN" +nightly \ - build "${BUILD_ARGS[@]}" --config "$RELEASE_CONFIG" --release ${EDIT_CARGO_ARGS:-}) - - local BIN="$SRC_DIR/target/release/edit" - [ -x "$BIN" ] || die "Build failed: $BIN not found" - - local DEST_SYS="${EDIT_PREFIX:-/usr/local}/bin" - local DEST_USER="${EDIT_USER_PREFIX:-$HOME/.local}/bin" - local SYSTEM_MANIFEST="${EDIT_PREFIX:-/usr/local}/share/edit/install-manifest" - local USER_MANIFEST="${EDIT_USER_PREFIX:-$HOME/.local}/share/edit/install-manifest" - local OUT_BIN="" WRAPPER_NEEDED=0 ICU_DIR="" - local -a MANIFEST_ENTRIES=() - - # 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." - fi - - - # Try to make system-wide ICU symlinks if we can - if [ "$HAVE_ROOT" -eq 1 ] && [ -n "$ICU_DIR" ]; then - if ensure_system_icu_symlinks "$ICU_DIR"; 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 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" - MANIFEST_ENTRIES+=("${EDIT_PREFIX:-/usr/local}/libexec/edit-real") - 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 - MANIFEST_ENTRIES+=("$DEST_SYS/edit" "$DEST_SYS/msedit") - if [ "${#CREATED_ICU_LINKS[@]}" -ne 0 ]; then - MANIFEST_ENTRIES+=("${CREATED_ICU_LINKS[@]}") - fi - write_manifest "$SYSTEM_MANIFEST" 1 "${MANIFEST_ENTRIES[@]}" - else - mkdir -p "$DEST_USER" - 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" - MANIFEST_ENTRIES+=("$DEST_USER/.edit-real") - 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 - MANIFEST_ENTRIES+=("$DEST_USER/edit" "$DEST_USER/msedit") - write_manifest "$USER_MANIFEST" 0 "${MANIFEST_ENTRIES[@]}" - if ! printf '%s' "$PATH" | tr ':' '\n' | grep -qx "$DEST_USER"; then - warn "Add $DEST_USER to your PATH to run 'edit' globally." - fi - fi - - CLEANUP=0 - trap - EXIT - - log "Installed: $OUT_BIN" - log "Launch it in a terminal with: ${OUT_BIN:-edit}" - - # 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() { - if [ "${EDIT_SKIP_DEPS:-0}" != "1" ]; then - log "Installing dependencies" - if ! try_install_pkgs; then - warn "Dependency installation failed or was skipped." - warn "Continuing with existing tools; the build will fail if prerequisites are still missing." - fi - else - log "Skipping dependency installation (EDIT_SKIP_DEPS=1)" - need_cmd curl || die "curl is required when EDIT_SKIP_DEPS=1" - if [ -z "$(find_local_edit_checkout || true)" ]; 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" - build_and_install - log "Done. Run: edit (alias: msedit)" -} - -if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then - main "$@" -fi diff --git a/tools/test-installer-distro.sh b/tools/test-installer-distro.sh deleted file mode 100755 index faed57ed993..00000000000 --- a/tools/test-installer-distro.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -prefix="${EDIT_PREFIX:-/usr/local}" -bin_dir="$prefix/bin" -libexec_dir="$prefix/libexec" -manifest="$prefix/share/edit/install-manifest" - -fail() { - printf 'test failure: %s\n' "$*" >&2 - exit 1 -} - -assert_exists() { - [ -e "$1" ] || fail "expected path to exist: $1" -} - -assert_not_exists() { - [ ! -e "$1" ] || fail "expected path to be absent: $1" -} - -assert_manifest_contains() { - local path="$1" - grep -Fxq "$path" "$manifest" || fail "expected manifest $manifest to contain $path" -} - -cleanup() { - bash "$repo_root/tools/uninstall.sh" --system-only >/dev/null 2>&1 || true -} - -main() { - trap cleanup EXIT - - bash -n "$repo_root/tools/install.sh" - bash -n "$repo_root/tools/uninstall.sh" - - EDIT_SOURCE_DIR="$repo_root" bash "$repo_root/tools/install.sh" - - assert_exists "$bin_dir/edit" - assert_exists "$bin_dir/msedit" - assert_exists "$manifest" - assert_manifest_contains "$bin_dir/edit" - assert_manifest_contains "$bin_dir/msedit" - - if [ -e "$libexec_dir/edit-real" ]; then - assert_manifest_contains "$libexec_dir/edit-real" - fi - - bash "$repo_root/tools/uninstall.sh" --system-only - - assert_not_exists "$bin_dir/edit" - assert_not_exists "$bin_dir/msedit" - assert_not_exists "$libexec_dir/edit-real" - assert_not_exists "$manifest" - - trap - EXIT - printf 'distro installer smoke test passed\n' -} - -main "$@" diff --git a/tools/test-installer.sh b/tools/test-installer.sh deleted file mode 100644 index 66bab03f71a..00000000000 --- a/tools/test-installer.sh +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -bash -n "$repo_root/tools/install.sh" -bash -n "$repo_root/tools/uninstall.sh" - -source "$repo_root/tools/install.sh" -source "$repo_root/tools/uninstall.sh" - -fail() { - printf 'test failure: %s\n' "$*" >&2 - exit 1 -} - -assert_eq() { - local actual="$1" expected="$2" message="$3" - [ "$actual" = "$expected" ] || fail "$message (expected '$expected', got '$actual')" -} - -assert_exists() { - [ -e "$1" ] || fail "expected path to exist: $1" -} - -assert_not_exists() { - [ ! -e "$1" ] || fail "expected path to be absent: $1" -} - -assert_manifest_contains() { - local manifest="$1" path="$2" - grep -Fxq "$path" "$manifest" || fail "expected manifest $manifest to contain $path" -} - -run_unit_tests() { - local actual tmp compat older release_cfg - - actual="$(cd "$repo_root" && find_local_edit_checkout)" - assert_eq "$actual" "$repo_root" "find_local_edit_checkout should detect the repository root" - is_edit_source_tree "$repo_root" || fail "is_edit_source_tree should accept the repository root" - - tmp="$(mktemp -d)" - if (cd "$tmp" && find_local_edit_checkout >/dev/null 2>&1); then - fail "find_local_edit_checkout should reject unrelated repositories" - fi - if is_edit_source_tree "$tmp"; then - fail "is_edit_source_tree should reject unrelated repositories" - fi - rm -rf "$tmp" - - tmp="$(mktemp -d)" - mkdir -p "$tmp/a" "$tmp/b" - : > "$tmp/a/libicutest.so.1" - : > "$tmp/b/libicutest.so.3" - actual="$(find_latest_icu_lib_in_dirs "libicutest" "$tmp/a:$tmp/b")" - assert_eq "$actual" "$tmp/b/libicutest.so.3" "find_latest_icu_lib_in_dirs should choose the newest candidate" - : > "$tmp/a/libicutest.so" - icu_has_unversioned_lib_in_dirs "libicutest" "$tmp/a:$tmp/b" || fail "icu_has_unversioned_lib_in_dirs should find unversioned libraries" - rm -rf "$tmp" - - compat="$(mktemp)" - older="$(mktemp -d)" - mkdir -p "$older/.cargo" - : > "$older/.cargo/release.toml" - release_cfg="$(select_release_config "$older" "$HOME/.cargo/bin/cargo" "$compat")" - if nightly_uses_real_immediate_abort "$HOME/.cargo/bin/cargo"; then - assert_eq "$release_cfg" "$compat" "older source trees should use a generated compatibility config on newer nightly toolchains" - grep -Fq 'panic = "immediate-abort"' "$compat" || fail "compatibility config should set panic = immediate-abort" - else - assert_eq "$release_cfg" ".cargo/release.toml" "older source trees should keep release.toml on older nightly toolchains" - fi - rm -f "$compat" - rm -rf "$older" - - if [ -f "$repo_root/.cargo/release-nightly.toml" ] && nightly_uses_real_immediate_abort "$HOME/.cargo/bin/cargo"; then - compat="$(mktemp)" - release_cfg="$(select_release_config "$repo_root" "$HOME/.cargo/bin/cargo" "$compat")" - assert_eq "$release_cfg" ".cargo/release-nightly.toml" "modern source trees should prefer release-nightly.toml" - rm -f "$compat" - fi - - tmp="$(mktemp -d)" - mkdir -p "$tmp/prefix/bin" "$tmp/prefix/share/edit" - : > "$tmp/prefix/bin/edit" - ln -s "$tmp/prefix/bin/edit" "$tmp/prefix/bin/msedit" - printf '%s\n%s\n' "$tmp/prefix/bin/edit" "$tmp/prefix/bin/msedit" > "$tmp/prefix/share/edit/install-manifest" - remove_manifest_entries "$tmp/prefix/share/edit/install-manifest" 0 - assert_not_exists "$tmp/prefix/bin/edit" - assert_not_exists "$tmp/prefix/bin/msedit" - assert_not_exists "$tmp/prefix/share/edit/install-manifest" - rm -rf "$tmp" -} - -assert_user_install_tree() { - local prefix="$1" - local manifest="$prefix/share/edit/install-manifest" - - assert_exists "$prefix/bin/edit" - assert_exists "$prefix/bin/msedit" - assert_exists "$manifest" - assert_manifest_contains "$manifest" "$prefix/bin/edit" - assert_manifest_contains "$manifest" "$prefix/bin/msedit" -} - -run_local_checkout_install_cycle() { - local tmp prefix - tmp="$(mktemp -d)" - prefix="$tmp/prefix" - - ( - cd "$repo_root" - EDIT_ALLOW_ELEVATION=0 \ - EDIT_SKIP_DEPS=1 \ - EDIT_USER_PREFIX="$prefix" \ - bash "$repo_root/tools/install.sh" - ) - - assert_user_install_tree "$prefix" - - EDIT_USER_PREFIX="$prefix" bash "$repo_root/tools/uninstall.sh" --user-only - assert_not_exists "$prefix/bin/edit" - assert_not_exists "$prefix/bin/msedit" - assert_not_exists "$prefix/share/edit/install-manifest" - - rm -rf "$tmp" -} - -run_fake_repo_install_cycle() { - local tmp fake prefix source_ref - tmp="$(mktemp -d)" - fake="$tmp/fake" - prefix="$tmp/prefix" - source_ref="$(git -C "$repo_root" rev-parse HEAD)" - - mkdir -p "$fake" - ( - cd "$fake" - git init -q - cat > Cargo.toml <<'EOF' -[package] -name = "fake" -version = "0.1.0" -edition = "2021" -EOF - EDIT_ALLOW_ELEVATION=0 \ - EDIT_SKIP_DEPS=1 \ - EDIT_SOURCE_URL="$repo_root" \ - EDIT_SOURCE_REF="$source_ref" \ - EDIT_USER_PREFIX="$prefix" \ - bash "$repo_root/tools/install.sh" - ) - - assert_user_install_tree "$prefix" - - EDIT_USER_PREFIX="$prefix" bash "$repo_root/tools/uninstall.sh" --user-only - assert_not_exists "$prefix/bin/edit" - assert_not_exists "$prefix/bin/msedit" - assert_not_exists "$prefix/share/edit/install-manifest" - - rm -rf "$tmp" -} - -main() { - run_unit_tests - run_local_checkout_install_cycle - run_fake_repo_install_cycle - printf 'installer tests passed\n' -} - -main "$@" diff --git a/tools/uninstall.sh b/tools/uninstall.sh deleted file mode 100755 index 3d739ff18ed..00000000000 --- a/tools/uninstall.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/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 ]; } -: "${EDIT_PREFIX:=/usr/local}" -: "${EDIT_USER_PREFIX:=$HOME/.local}" - -MODE="all" # all | user | system -DRYRUN=0 - -print_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 -} - -parse_args() { - MODE="all" - DRYRUN=0 - local a - for a in "$@"; do - case "$a" in - --user-only) MODE="user" ;; - --system-only) MODE="system" ;; - --dry-run) DRYRUN=1 ;; - -h|--help) - print_help - 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 -} - -remove_manifest_entries() { - local manifest="$1" use_root="$2" - [ -f "$manifest" ] || return 1 - - while IFS= read -r path; do - [ -n "$path" ] || continue - if [ "$use_root" -eq 1 ]; then - run_rm_root "$path" - else - run_rm "$path" - fi - done < "$manifest" - - if [ "$use_root" -eq 1 ]; then - run_rm_root "$manifest" - else - run_rm "$manifest" - fi -} - -remove_user_install() { - log "Removing user-local binaries" - if ! remove_manifest_entries "$EDIT_USER_PREFIX/share/edit/install-manifest" 0; then - warn "No user install manifest found; falling back to legacy path cleanup" - run_rm "$EDIT_USER_PREFIX/bin/edit" \ - "$EDIT_USER_PREFIX/bin/msedit" \ - "$EDIT_USER_PREFIX/bin/.edit-real" - fi -} - -remove_system_install() { - if ! is_root && [ -z "$SUDO" ]; then - warn "Skipping system-wide removal: need root, sudo, or doas" - return 0 - fi - - log "Removing system-wide binaries" - if ! remove_manifest_entries "$EDIT_PREFIX/share/edit/install-manifest" 1; then - warn "No system install manifest found; falling back to legacy binary cleanup only" - run_rm_root "$EDIT_PREFIX/bin/edit" "$EDIT_PREFIX/bin/msedit" - run_rm_root "$EDIT_PREFIX/libexec/edit-real" - fi - - 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 -} - -main() { - parse_args "$@" - - if [ "$MODE" = "all" ] || [ "$MODE" = "user" ]; then - remove_user_install - fi - - if [ "$MODE" = "all" ] || [ "$MODE" = "system" ]; then - remove_system_install - fi - - log "Done." -} - -if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then - main "$@" -fi