diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a2e302c..28b466f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,5 +141,11 @@ not as a reason for mechanical splitting. - Drafts under `docs/temp/` are planning material unless explicitly promoted. - Version changes must go through `tools/bump-version.sh`; keep both package manifests, root `Cargo.lock`, packaging metadata, and tag/release policy aligned. +- Close a user-visible "I don't have that setting / this build" report only after the change is in + a tagged GitHub release. `main` is not what `arch-install.sh`, AUR `wayscriber-bin`, or other + packaged installs ship. `--version` reports the crate version, not the git hash, so bump with + `tools/bump-version.sh` in the same change as a user-visible overlay, settings, or config toggle + (or immediately before tagging that release). Otherwise two binaries can print the same + `wayscriber 0.9.x` and look identical. See [tools/README.md](tools/README.md) for build, install, packaging, version, and release helpers. diff --git a/README.md b/README.md index 7fd82cfe..6a467195 100644 --- a/README.md +++ b/README.md @@ -281,10 +281,16 @@ sh arch-install.sh ``` The dry run verifies the latest stable release, its checksum and install manifest, the -required Arch packages, and package ownership without installing files. The full run -installs allowlisted files under `/usr/local`, refuses to shadow a pacman-owned Wayscriber -installation, and does not start or restart the user service. To update this installation, -download a fresh script, rerun it, then restart `wayscriber.service` when ready. +required Arch packages, and package ownership without installing files. Both the dry run +and the full run refuse another Wayscriber copy that is not this install: an unmanaged +file under `/usr`, `~/.local/bin/wayscriber`, or a user unit / drop-in whose `ExecStart` +is not `/usr/local/bin/wayscriber`. A symlink that already resolves to the dest is not a +conflict. Pass `--replace-other` on those commands to remove the other copy first. If +`wayscriber.service` is already active, the installer restarts it so the overlay daemon +follows `/usr/local/bin/wayscriber`; pass `--no-restart` to leave the running unit alone. +`--version` is the crate version, not a git hash, so inspect the installed path and the +running service `ExecStart` after install. To update this installation, download a fresh +script and rerun it. Remove the direct installation before moving to the AUR. Otherwise, `/usr/local/bin` and `/usr/local/lib/systemd/user` continue to take priority over the package files under @@ -299,10 +305,12 @@ yay -S wayscriber-bin systemctl --user enable --now wayscriber.service ``` -The uninstaller removes only unmanaged files in the direct install manifest, including -the same files from the legacy `/usr` location. It leaves pacman-owned files and your user -configuration and data unchanged, and refuses actual removal while the service is active -or enabled. +The uninstaller removes only unmanaged files in the direct install manifest under +`/usr/local`. It leaves pacman-owned files and your user configuration and data unchanged, +and refuses actual removal while the service is active or enabled. A leftover unmanaged +git build under `/usr` is left in place; pass `--remove-unmanaged-usr` only when that +`/usr` copy should go too. Default uninstall does not remove `~/.local/bin/wayscriber` or +a user unit / drop-in; the script warns if those still point at `/usr/local`. ### NixOS and Nix diff --git a/docs/SETUP.md b/docs/SETUP.md index 2a2d01f3..26cf9daf 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -37,8 +37,13 @@ Run the install script: This will: 1. Build the release binary -2. Copy it to `~/.local/bin/wayscriber` -3. Tell you how to add Hyprland keybind +2. Copy it to `/usr/bin/wayscriber` (or `$WAYSCRIBER_INSTALL_DIR`) +3. Refuse a second copy under `/usr/local/bin` or `~/.local/bin` unless you pass `--replace-other` or confirm +4. Optionally set up the systemd user service or a Hyprland keybind + +If `/usr/local/bin/wayscriber` already exists from the direct Arch installer, keep only one +prefix. Overlay spawn follows the running daemon file, not whichever path you passed to +`--version` or `--about`. ### Manual Install @@ -250,9 +255,15 @@ Press Escape (should hide overlay) - If `wayscriber` is not found from the compositor, use the absolute path from `command -v wayscriber` **Binary not found?** -- Check PATH: `echo $PATH | grep .local/bin` -- Add to PATH if missing (see Manual Install) -- Restart terminal after PATH change +- Check `type -a wayscriber` and the path you installed to (`/usr/bin`, `/usr/local/bin`, or `~/.local/bin`) +- If you used `./tools/install.sh`, the default dest is `/usr/bin/wayscriber` (or `$WAYSCRIBER_INSTALL_DIR`) +- If you copied the binary by hand, add `~/.local/bin` to PATH (see Manual Install) and restart the terminal + +**Settings or `--about` do not match the binary you just built?** +- Two copies can exist: the direct installer uses `/usr/local`, source `tools/install.sh` uses `/usr/bin`, and a manual copy may live in `~/.local/bin`. +- Check `type -a wayscriber`, `systemctl --user show wayscriber.service -p ExecStart,MainPID,FragmentPath`, and `readlink -f /proc/$(systemctl --user show -p MainPID --value wayscriber.service)/exe`. +- `--version` is the crate version and can be identical on both copies. `--about` opens a window; use it on the path you intend to run, not as a substitute for the running daemon path. +- Keep one prefix. `./tools/install.sh --replace-other` removes the other known copies when installing to `/usr/bin`. **Want different key?** - Edit `bindings.lua` or `hyprland.conf`, depending on your Hyprland config format @@ -263,6 +274,16 @@ Press Escape (should hide overlay) ## Uninstall +If you used `./tools/install.sh` (default `/usr/bin`): + +```bash +sudo rm -f /usr/bin/wayscriber +systemctl --user disable --now wayscriber.service +# Remove the keybind from bindings.lua or hyprland.conf +``` + +If you copied the binary by hand to `~/.local/bin`: + ```bash rm ~/.local/bin/wayscriber # Remove the keybind from bindings.lua or hyprland.conf diff --git a/docs/codebase-overview.md b/docs/codebase-overview.md index 3ac017ec..1872c74a 100644 --- a/docs/codebase-overview.md +++ b/docs/codebase-overview.md @@ -41,7 +41,10 @@ backend entry in `src/backend/mod.rs`. action applier. 5. Overlay candidates and runtime helpers are created only by the pre-lock process broker. The daemon owns generation/pidfd decisions while the broker owns wait/reap; overlay readiness is - accepted only after the child wins its lock and publishes matching process identity. + accepted only after the child wins its lock and publishes matching process identity. Startup + also warns when another `wayscriber` file exists on `PATH` or under `/usr`, `/usr/local`, + or `~/.local/bin`, because overlay spawn follows this process file rather than whichever + path the user inspected. 6. Queue renames, producer eventfds, signals, and child pidfds drive the loop without a lifecycle polling tick. Shutdown invalidates readiness, terminates owned work, and joins listeners. diff --git a/src/daemon/AGENTS.md b/src/daemon/AGENTS.md index 1f4e898a..fd19be9e 100644 --- a/src/daemon/AGENTS.md +++ b/src/daemon/AGENTS.md @@ -5,6 +5,7 @@ ## Architecture - `core.rs`, `control.rs`, and `types.rs` own daemon state and toggle/control behavior. +- `binary_conflict.rs` warns when another Wayscriber binary exists besides the running daemon. - `overlay/` owns overlay process spawn/control. - `tray/` owns tray integration and shortcut hint I/O. - `setup.rs` and `global_shortcuts.rs` support daemon setup workflows. diff --git a/src/daemon/binary_conflict.rs b/src/daemon/binary_conflict.rs new file mode 100644 index 00000000..4e06dd5a --- /dev/null +++ b/src/daemon/binary_conflict.rs @@ -0,0 +1,186 @@ +use std::collections::HashSet; +use std::env; +use std::path::{Path, PathBuf}; + +use log::warn; + +use crate::env_vars::PATH_ENV; +use crate::paths::home_dir; + +/// Prefixes this project actually installs to, including ones the packaged +/// user unit PATH (`/usr/local/bin:/usr/bin:/bin`) does not search. +/// +/// `PATH` is scanned separately. Do not add cargo/nix/opt folders here unless +/// a Wayscriber installer writes them; those still show up when they are on +/// `PATH`. +fn well_known_wayscriber_binaries() -> Vec { + let mut paths = vec![ + PathBuf::from("/usr/bin/wayscriber"), + PathBuf::from("/usr/local/bin/wayscriber"), + ]; + if let Some(home) = home_dir() { + paths.push(home.join(".local").join("bin").join("wayscriber")); + } + paths +} + +fn canonicalize_file(path: &Path) -> Option { + if !path.is_file() { + return None; + } + path.canonicalize().ok() +} + +/// Other `wayscriber` files on `PATH` or in well-known prefixes besides this process. +/// +/// Overlay spawn follows `current_exe` first. A second copy is how `--version` or +/// `--about` on a different path can disagree with the running daemon. +pub(crate) fn other_installed_wayscriber_binaries( + current_exe: &Path, + path_env: Option<&str>, + extra_candidates: &[&Path], +) -> Vec { + let Some(current) = canonicalize_file(current_exe) else { + return Vec::new(); + }; + + let mut seen = HashSet::new(); + seen.insert(current); + let mut others = Vec::new(); + + let mut consider = |candidate: &Path| { + let Some(canonical) = canonicalize_file(candidate) else { + return; + }; + if seen.insert(canonical.clone()) { + others.push(canonical); + } + }; + + if let Some(path_env) = path_env { + for dir in env::split_paths(path_env) { + consider(&dir.join("wayscriber")); + } + } + for candidate in extra_candidates { + consider(candidate); + } + + others +} + +pub(super) fn warn_if_other_wayscriber_binaries() { + let Ok(exe) = env::current_exe() else { + return; + }; + let path_env = env::var(PATH_ENV).ok(); + let extras = well_known_wayscriber_binaries(); + let extra_refs: Vec<&Path> = extras.iter().map(PathBuf::as_path).collect(); + let others = other_installed_wayscriber_binaries(&exe, path_env.as_deref(), &extra_refs); + if others.is_empty() { + return; + } + let other_list = others + .iter() + .map(|path| path.display().to_string()) + .collect::>() + .join(", "); + warn!( + "Another wayscriber binary exists besides this daemon ({}). Overlay spawn follows this process file; inspecting a different path with --version or --about can disagree. Other copies: {}", + exe.display(), + other_list + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::os::unix::fs::symlink; + + fn write_file(path: &Path) { + fs::write(path, b"wayscriber-test-binary").unwrap(); + } + + #[test] + fn no_conflict_when_path_and_extras_are_the_same_file() { + let tmp = crate::test_temp::tempdir().unwrap(); + let binary = tmp.path().join("wayscriber"); + write_file(&binary); + let path_env = tmp.path().display().to_string(); + + assert!( + other_installed_wayscriber_binaries(&binary, Some(&path_env), &[&binary]).is_empty() + ); + } + + #[test] + fn reports_a_second_file_on_path() { + let tmp = crate::test_temp::tempdir().unwrap(); + let current_dir = tmp.path().join("current"); + let other_dir = tmp.path().join("other"); + fs::create_dir(¤t_dir).unwrap(); + fs::create_dir(&other_dir).unwrap(); + let current = current_dir.join("wayscriber"); + let other = other_dir.join("wayscriber"); + write_file(¤t); + write_file(&other); + let path_env = format!("{}:{}", other_dir.display(), current_dir.display()); + + let others = other_installed_wayscriber_binaries(¤t, Some(&path_env), &[]); + assert_eq!(others, vec![other.canonicalize().unwrap()]); + } + + #[test] + fn reports_a_well_known_copy_even_when_path_matches_current() { + let tmp = crate::test_temp::tempdir().unwrap(); + let current_dir = tmp.path().join("local"); + let usr_dir = tmp.path().join("usr"); + fs::create_dir(¤t_dir).unwrap(); + fs::create_dir(&usr_dir).unwrap(); + let current = current_dir.join("wayscriber"); + let usr = usr_dir.join("wayscriber"); + write_file(¤t); + write_file(&usr); + let path_env = current_dir.display().to_string(); + + let others = other_installed_wayscriber_binaries(¤t, Some(&path_env), &[&usr]); + assert_eq!(others, vec![usr.canonicalize().unwrap()]); + } + + #[test] + fn symlink_to_the_same_file_is_not_a_conflict() { + let tmp = crate::test_temp::tempdir().unwrap(); + let current = tmp.path().join("wayscriber"); + let alias = tmp.path().join("alias-wayscriber"); + write_file(¤t); + symlink(¤t, &alias).unwrap(); + + assert!(other_installed_wayscriber_binaries(¤t, None, &[&alias]).is_empty()); + } + + #[test] + fn missing_current_exe_yields_no_others() { + let tmp = crate::test_temp::tempdir().unwrap(); + let missing = tmp.path().join("missing"); + let other = tmp.path().join("wayscriber"); + write_file(&other); + + assert!(other_installed_wayscriber_binaries(&missing, None, &[&other]).is_empty()); + } + + #[test] + fn well_known_includes_resolved_user_local_bin() { + let tmp = crate::test_temp::tempdir().unwrap(); + crate::test_env::with_env_var( + crate::env_vars::HOME_ENV, + Some(tmp.path().as_os_str()), + || { + let paths = well_known_wayscriber_binaries(); + assert!(paths.contains(&PathBuf::from("/usr/bin/wayscriber"))); + assert!(paths.contains(&PathBuf::from("/usr/local/bin/wayscriber"))); + assert!(paths.contains(&tmp.path().join(".local").join("bin").join("wayscriber"))); + }, + ); + } +} diff --git a/src/daemon/core.rs b/src/daemon/core.rs index d857feef..26d114e5 100644 --- a/src/daemon/core.rs +++ b/src/daemon/core.rs @@ -245,6 +245,7 @@ impl Daemon { /// Run daemon with signal handling pub fn run(&mut self) -> Result<()> { info!("Starting wayscriber daemon"); + super::binary_conflict::warn_if_other_wayscriber_binaries(); if self.freeze_on_show { info!("Daemon activations will request frozen mode on show"); } diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 2dae3d91..cf79300f 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -1,5 +1,6 @@ //! Daemon mode implementation: background service with toggle activation +mod binary_conflict; mod control; mod core; mod global_shortcuts; diff --git a/tools/AGENTS.md b/tools/AGENTS.md index ccec43b7..4c9d33f9 100644 --- a/tools/AGENTS.md +++ b/tools/AGENTS.md @@ -17,6 +17,8 @@ ## Coupled Changes - Version and packaging scripts must stay aligned with `tools/README.md`, `packaging/`, `.github/`, `Cargo.toml`, and release docs. - Install/reload scripts may affect setup docs and daemon service behavior. +- `install.sh` and the website `arch-install.sh` must refuse a second unmanaged prefix + (`/usr` vs `/usr/local`) unless the operator opts into replacing the other copy. ## Validation - Run changed scripts directly when safe. diff --git a/tools/README.md b/tools/README.md index bdf51181..b734cf97 100644 --- a/tools/README.md +++ b/tools/README.md @@ -41,9 +41,11 @@ Helper scripts for development, installation, packaging, and release workflows. - **install.sh** - Full installation script - Builds and installs binary to `/usr/bin` (or `$WAYSCRIBER_INSTALL_DIR`) + - Refuses a second copy under `/usr/bin`, `/usr/local/bin`, or `~/.local/bin` unless + `--replace-other` is passed or you confirm on a TTY - Sets up config directory with example config - Optionally configures systemd service or Hyprland autostart - - Usage: `./tools/install.sh` + - Usage: `./tools/install.sh [--replace-other]` - **install-configurator.sh** - Install configurator only - Builds and installs wayscriber-configurator, its desktop entry, and icons @@ -60,6 +62,8 @@ Helper scripts for development, installation, packaging, and release workflows. - Updates Cargo.toml, configurator/Cargo.toml, the workspace Cargo.lock, PKGBUILD, and .SRCINFO - flake.nix package version follows Cargo.toml automatically - Auto-increments patch version if no version specified + - Use this in the same change as a user-visible overlay/settings/config toggle, or immediately + before tagging that release, so `--version` is not identical to the last shipped crate - Supports MAJOR.MINOR.PATCH.HOTFIX for packaging-only hotfix releases - Usage: `./tools/bump-version.sh [--dry-run] [new_version]` @@ -100,6 +104,8 @@ Packaging-only hotfix policy: - Strictly parses the installer's static allowlist as data; it never executes the installer, and unsupported manifest syntax fails closed - Requires the archive file set, modes, and service command to match what the installer accepts - Runs against the deployed installer during release packaging + - Live `https://wayscriber.com/arch-install.sh` is what CI fetches. Dual-prefix flags in a + local website checkout are not checked until that file is published. - Usage: `./tools/check-arch-installer-manifest.sh --installer FILE --archive FILE` When the tarball file manifest changes, build and check the new tarball locally, deploy diff --git a/tools/install.sh b/tools/install.sh index d07ed812..1a2dad38 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -9,22 +9,54 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" INSTALL_DIR="${WAYSCRIBER_INSTALL_DIR:-/usr/bin}" +while [ "$INSTALL_DIR" != "/" ] && [ "${INSTALL_DIR%/}" != "$INSTALL_DIR" ]; do + INSTALL_DIR="${INSTALL_DIR%/}" +done +case "$INSTALL_DIR" in + /usr) INSTALL_DIR="/usr/bin" ;; + /usr/local) INSTALL_DIR="/usr/local/bin" ;; +esac BINARY_NAME="wayscriber" INSTALLED_BINARY="$INSTALL_DIR/$BINARY_NAME" BIND_COMMAND="$INSTALL_DIR/$BINARY_NAME --daemon-toggle" CONFIG_DIR="$HOME/.config/wayscriber" HYPR_CONFIG="$HOME/.config/hypr/hyprland.conf" - -echo "================================" -echo " Wayscriber Installation" -echo "================================" -echo "" +REPLACE_OTHER=0 die() { echo "❌ $*" >&2 exit 1 } +usage() { + echo "Usage: $0 [--replace-other]" + echo "Build a source binary and install it to $INSTALL_DIR." + echo "Refuses a second copy under /usr/bin, /usr/local/bin, or ~/.local/bin" + echo "unless --replace-other is passed or you confirm on a TTY." +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --replace-other) + [ "$REPLACE_OTHER" -eq 0 ] || die "--replace-other was specified more than once" + REPLACE_OTHER=1 + ;; + --help|-h) + usage + exit 0 + ;; + *) + die "unexpected argument: $1" + ;; + esac + shift +done + +echo "================================" +echo " Wayscriber Installation" +echo "================================" +echo "" + ensure_replacement() { local file="$1" local search="$2" @@ -42,6 +74,191 @@ ensure_replacement() { fi } +canonical_path() { + readlink -f "$1" 2>/dev/null || printf '%s' "$1" +} + +same_file() { + local left right + { [ -e "$1" ] || [ -L "$1" ]; } || return 1 + left="$(canonical_path "$1")" + right="$(canonical_path "$2")" + [ -n "$left" ] && { [ "$left" = "$right" ] || [ "$left" = "$2" ]; } +} + +systemd_user_dir() { + if [ -n "${XDG_CONFIG_HOME:-}" ]; then + printf '%s\n' "${XDG_CONFIG_HOME}/systemd/user" + elif [ -n "${HOME:-}" ]; then + printf '%s\n' "${HOME}/.config/systemd/user" + fi +} + +known_wayscriber_binaries() { + printf '%s\n' /usr/bin/wayscriber /usr/local/bin/wayscriber + if [ -n "${HOME:-}" ]; then + printf '%s\n' "$HOME/.local/bin/wayscriber" + fi +} + +known_prefix_units() { + printf '%s\n' \ + /usr/lib/systemd/user/wayscriber.service \ + /usr/local/lib/systemd/user/wayscriber.service +} + +user_unit_files() { + local dir conf + dir="$(systemd_user_dir)" + [ -n "$dir" ] || return 0 + [ -f "$dir/wayscriber.service" ] && printf '%s\n' "$dir/wayscriber.service" + if [ -d "$dir/wayscriber.service.d" ]; then + for conf in "$dir/wayscriber.service.d/"*.conf; do + [ -f "$conf" ] || continue + printf '%s\n' "$conf" + done + fi +} + +file_has_conflicting_exec_start() { + local file="$1" dest="$2" dest_canon other + [ -f "$file" ] || return 1 + grep -Eq '^[[:space:]]*ExecStart=' "$file" || return 1 + dest_canon="$(canonical_path "$dest")" + if grep -E '^[[:space:]]*ExecStart=' "$file" | grep -Fq "$dest"; then + return 1 + fi + while IFS= read -r other; do + grep -E '^[[:space:]]*ExecStart=' "$file" | grep -Fq "$other" || continue + if same_file "$other" "$dest"; then + return 1 + fi + return 0 + done < <(known_wayscriber_binaries) + return 0 +} + +unit_paths_for_binary() { + case "$1" in + /usr/local/bin/wayscriber) + printf '%s\n' /usr/local/lib/systemd/user/wayscriber.service + ;; + /usr/bin/wayscriber) + printf '%s\n' /usr/lib/systemd/user/wayscriber.service + ;; + esac +} + +path_is_package_owned() { + command -v pacman >/dev/null 2>&1 || return 1 + pacman -Qoq "$1" >/dev/null 2>&1 +} + +user_unit_conflicts() { + local file + while IFS= read -r file; do + file_has_conflicting_exec_start "$file" "$INSTALLED_BINARY" && return 0 + done < <(user_unit_files) + return 1 +} + +collect_conflicts() { + OTHER_BINARIES=() + OTHER_UNITS=() + local dest other unit + dest="$(canonical_path "$INSTALLED_BINARY")" + while IFS= read -r other; do + [ -e "$other" ] || [ -L "$other" ] || continue + if [ "$other" = "$INSTALLED_BINARY" ] || [ "$(canonical_path "$other")" = "$dest" ]; then + continue + fi + OTHER_BINARIES+=("$other") + done < <(known_wayscriber_binaries) + while IFS= read -r unit; do + [ -e "$unit" ] || [ -L "$unit" ] || continue + file_has_conflicting_exec_start "$unit" "$INSTALLED_BINARY" || continue + OTHER_UNITS+=("$unit") + done < <(known_prefix_units) +} + +removal_paths_for_binary() { + local binary="$1" unit + printf '%s\n' "$binary" + while IFS= read -r unit; do + [ -e "$unit" ] || [ -L "$unit" ] || continue + printf '%s\n' "$unit" + done < <(unit_paths_for_binary "$binary") +} + +remove_path_if_unmanaged() { + local path="$1" + if [ ! -e "$path" ] && [ ! -L "$path" ]; then + return 0 + fi + if path_is_package_owned "$path"; then + if [ "$(basename "$path")" = "wayscriber" ]; then + die "${path} is owned by Arch package $(pacman -Qoq "$path"); remove or update that package instead" + fi + echo "Leaving package-owned $path ($(pacman -Qoq "$path"))" + return 0 + fi + ${SUDO:-} rm -f -- "$path" + echo "Removed $path" +} + +remove_conflicting_other_binary() { + local other="$1" path + while IFS= read -r path; do + remove_path_if_unmanaged "$path" + done < <(removal_paths_for_binary "$other") +} + +remove_conflicting_user_units() { + local file + while IFS= read -r file; do + file_has_conflicting_exec_start "$file" "$INSTALLED_BINARY" || continue + rm -f -- "$file" + echo "Removed user unit override $file" + done < <(user_unit_files) +} + +has_install_conflicts() { + [ "${#OTHER_BINARIES[@]}" -gt 0 ] || [ "${#OTHER_UNITS[@]}" -gt 0 ] || user_unit_conflicts +} + +collect_conflicts +if has_install_conflicts; then + echo "Another wayscriber install would stay beside $INSTALLED_BINARY:" + for other in "${OTHER_BINARIES[@]+"${OTHER_BINARIES[@]}"}"; do + echo " $other" + while IFS= read -r path; do + echo " and $path" + done < <(removal_paths_for_binary "$other" | tail -n +2) + done + for unit in "${OTHER_UNITS[@]+"${OTHER_UNITS[@]}"}"; do + echo " $unit (ExecStart is not $INSTALLED_BINARY)" + done + while IFS= read -r file; do + file_has_conflicting_exec_start "$file" "$INSTALLED_BINARY" || continue + echo " $file (ExecStart is not $INSTALLED_BINARY)" + done < <(user_unit_files) + echo "Leaving both copies makes the overlay daemon follow a different file than the one you inspect." + if [ "$REPLACE_OTHER" -eq 0 ] && [ -t 0 ]; then + read -r -p "Remove those files so only $INSTALLED_BINARY remains? [y/N] " CONFIRM + echo "" + case "$CONFIRM" in + y|Y|yes|YES) + REPLACE_OTHER=1 + ;; + *) + die "Refusing to install beside the other copy. Re-run with --replace-other after removing it." + ;; + esac + elif [ "$REPLACE_OTHER" -eq 0 ]; then + die "Refusing to install beside the other copy. Re-run with --replace-other, or remove it first." + fi +fi + # Ensure required binaries are built echo "Building Wayscriber binary (release, default features)..." (cd "$PROJECT_ROOT" && cargo build --release --bins) @@ -61,6 +278,27 @@ if [ ! -d "$INSTALL_DIR" ] || [ ! -w "$INSTALL_DIR" ]; then fi fi +if has_install_conflicts && [ "$REPLACE_OTHER" -eq 1 ] && [ "$(id -u)" -ne 0 ]; then + if [ -z "${SUDO:-}" ]; then + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + echo "Using sudo to remove the other Wayscriber copy" + else + die "sudo is required to remove the other Wayscriber copy" + fi + fi +fi + +if [ "$REPLACE_OTHER" -eq 1 ]; then + for other in "${OTHER_BINARIES[@]+"${OTHER_BINARIES[@]}"}"; do + remove_conflicting_other_binary "$other" + done + for unit in "${OTHER_UNITS[@]+"${OTHER_UNITS[@]}"}"; do + remove_path_if_unmanaged "$unit" + done + remove_conflicting_user_units +fi + # Ensure install directory exists ${SUDO:-} install -d "$INSTALL_DIR" @@ -98,10 +336,35 @@ echo "Installed: $INSTALLED_BINARY" ls -l "$INSTALLED_BINARY" echo "SHA256: $(sha256sum "$INSTALLED_BINARY" | cut -d' ' -f1)" if command -v "$BINARY_NAME" >/dev/null 2>&1; then - echo "PATH: $(command -v "$BINARY_NAME")" + PATH_BINARY="$(command -v "$BINARY_NAME")" + echo "PATH: $PATH_BINARY" + if [ "$(readlink -f "$PATH_BINARY" 2>/dev/null || printf '%s' "$PATH_BINARY")" != \ + "$(readlink -f "$INSTALLED_BINARY" 2>/dev/null || printf '%s' "$INSTALLED_BINARY")" ]; then + echo "⚠️ PATH resolves $PATH_BINARY, which is not the file just installed." + echo " Overlay spawn follows the running daemon, not this PATH lookup." + fi else echo "PATH: not found in PATH" fi +while IFS= read -r other; do + [ -e "$other" ] || [ -L "$other" ] || continue + if [ "$other" = "$INSTALLED_BINARY" ]; then + continue + fi + if [ "$(canonical_path "$other")" = "$(canonical_path "$INSTALLED_BINARY")" ]; then + continue + fi + echo "⚠️ $other still exists beside $INSTALLED_BINARY" +done < <(known_wayscriber_binaries) +while IFS= read -r unit; do + [ -e "$unit" ] || [ -L "$unit" ] || continue + file_has_conflicting_exec_start "$unit" "$INSTALLED_BINARY" || continue + echo "⚠️ $unit still has ExecStart that is not $INSTALLED_BINARY" +done < <(known_prefix_units) +while IFS= read -r file; do + file_has_conflicting_exec_start "$file" "$INSTALLED_BINARY" || continue + echo "⚠️ $file still has ExecStart that is not $INSTALLED_BINARY" +done < <(user_unit_files) echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " Setup Instructions" @@ -139,7 +402,7 @@ echo "" case $REPLY in 1) - SYSTEMD_USER_DIR="$HOME/.config/systemd/user" + SYSTEMD_USER_DIR="$(systemd_user_dir)" SYSTEMD_SYSTEM_DIR="/usr/lib/systemd/user" USER_SERVICE_FILE="$SYSTEMD_USER_DIR/wayscriber.service" SYSTEM_SERVICE_FILE="$SYSTEMD_SYSTEM_DIR/wayscriber.service" diff --git a/tools/test-release-packaging.sh b/tools/test-release-packaging.sh index 2758159a..7b005ab2 100755 --- a/tools/test-release-packaging.sh +++ b/tools/test-release-packaging.sh @@ -361,6 +361,9 @@ assert_contains "${WORK_DIR}/release-package-job.yml" "https://wayscriber.com/ar assert_contains "${WORK_DIR}/release-package-job.yml" "./tools/check-arch-installer-manifest.sh" assert_contains "${WORK_DIR}/release-package-job.yml" 'wayscriber-v${{ steps.meta.outputs.version }}-linux-x86_64.tar.gz' assert_not_contains "${WORK_DIR}/release-package-job.yml" 'sh "$RUNNER_TEMP/arch-install.sh"' +assert_contains "${REPO_ROOT}/README.md" "--replace-other" +assert_contains "${REPO_ROOT}/README.md" "--no-restart" +assert_contains "${REPO_ROOT}/README.md" "--remove-unmanaged-usr" test -x "${ARCH_INSTALLER_CHECKER}" assert_contains "${ARCH_INSTALLER_CHECKER}" "The installer is parsed as data and is not run."