mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
fix(installer): stop the release refusing and looping around non-release builds
Two defects made a local `0.10.2 (dev)` build permanent for the person who installed it: the website installer refused to replace it, and `codewhale update` reported "Already up to date" forever because it compared only CARGO_PKG_VERSION. install.sh (website and release archive): - Resolve and validate the install directory before downloading, so a managed or symlinked directory fails before ~100 MB of release assets. - CODEWHALE_FORCE_REINSTALL=1 replaces a differing codewhale/codew in the install directory and keeps each previous file beside it as <name>.bak-<utc-timestamp>. Symlinks and non-regular files are still never replaced, and every other guard is unchanged. - A forced run that fails after moving a file aside puts it back. The publication state and exit trap now install before the preflight checks, so a refusal or a failed publish cannot leave a command missing from the directory while its bytes sit in a .bak file. - Dropped the codewhale-tui compatibility-path guard. It was not a published destination: the installer never writes that name, so any stale legacy file or symlink beside the real commands aborted a correct install. crates/cli/src/update.rs: - `(dev)` in CODEWHALE_BUILD_VERSION is the build script's marker for an unstamped checkout; a release artifact carries its short SHA and a Cargo source package carries the plain version. When the installed build carries that marker and its version equals the latest tag, `update` and `update --check` install the release artifact instead of reporting up to date. Packaged-source and release builds are untouched. - `Current version:` prints the build version, so the output names the actual provenance (`0.10.2 (dev)` vs `0.10.2 (ff466b2ed0)`). docs/INSTALL.md and docs/zh_hans/INSTALL.md describe the force flag, the same-version non-release replacement, and the download-before-refusal wording. Verified on macOS arm64, source4d2b3fdb1bplus this patch: - cargo build -p codewhale-cli --locked: exit 0 (16m55s cold, 6s incremental; one pre-existing __eh_frame linker warning). - scripts/dev-test.sh cli: 501 passed, 0 failed. - scripts/dev-test.sh release: 57 passed, 0 failed. - web node scripts/derive-install.mjs && node scripts/check-docs.mjs: PASS. - Website installer matrix against a local file:// fixture: fresh install, identical re-run, differing-without-force (original preserved), force (backup holds the original bytes), stale codewhale-tui present (untouched), symlink destination with force (still a symlink), managed directory (refused with zero downloads). All seven behave as described. - Interrupted forced run (second destination present as a directory): exits 1 and restores the first command instead of leaving it in .bak. Same case for the archive installer. - Real release: installed into a scratch directory from GitHub, reports 0.10.2 (ff466b2ed0), sha256 da022961...590d matches the manifest, for both codewhale and codew. - Real self-update: a fresh `0.10.2 (dev)` build ran `update --check` then `update`, downloaded and verified the release, and ended at 0.10.2 (ff466b2ed0); a second `--check` reports Already up to date. Not verified: clippy, Windows/NSIS installer, Linux/binary install paths, concurrent forced runs in the same directory, and the deployed codewhale.net copy of install.sh (needs a deploy, which stays a separate authorized action). The packaged-source (`cargo install`) branch of the new predicate is reasoned from format_build_version, not executed.
This commit is contained in:
1 parent
4d2b3fdb1b
commit
e9c9de775f
5 files changed
+173
-75
No files matched your search
@@ -61,6 +61,12 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
let plan = update_plan_for_exe(¤t_exe);
|
||||
let channel = ReleaseChannel::from_beta_flag(beta);
|
||||
let current_version = env!("CARGO_PKG_VERSION");
|
||||
let build_version = env!("CODEWHALE_BUILD_VERSION");
|
||||
// The build script marks an unstamped checkout with `(dev)` and nothing
|
||||
// else: a release artifact carries its short SHA and a Cargo source
|
||||
// package carries the plain version. Only the checkout marker means the
|
||||
// installed file has no published artifact behind it.
|
||||
let unreleased_checkout_build = build_version.ends_with(" (dev)");
|
||||
let proxy = proxy_arg
|
||||
.as_deref()
|
||||
.map(validate_and_build_proxy)
|
||||
@@ -68,7 +74,7 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
|
||||
println!("Checking for {} updates...", channel.label());
|
||||
println!("Current binary: {}", current_exe.display());
|
||||
println!("Current version: v{current_version}");
|
||||
println!("Current version: v{build_version}");
|
||||
if legacy_binary {
|
||||
println!();
|
||||
println!("{}", legacy_binary_message(¤t_exe));
|
||||
@@ -94,7 +100,14 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
.with_context(update_network_fallback_hint)?;
|
||||
let latest_tag = &fetched.release.tag_name;
|
||||
println!("Latest {} release: {latest_tag}", channel.label());
|
||||
if update_is_needed(channel, current_version, latest_tag)? {
|
||||
let replace_same_version = unreleased_checkout_build
|
||||
&& compare_release_versions(current_version, latest_tag)? == Ordering::Equal;
|
||||
if update_is_needed(channel, current_version, latest_tag)? || replace_same_version {
|
||||
if replace_same_version {
|
||||
println!(
|
||||
"This build is not a published release ({build_version}); the {latest_tag} release artifact replaces it."
|
||||
);
|
||||
}
|
||||
if install_method.supports_self_update() && !protected_location {
|
||||
println!(
|
||||
"Update available. Run `{} update` to install {latest_tag}.",
|
||||
@@ -138,7 +151,9 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
fetched.source.describe()
|
||||
);
|
||||
}
|
||||
if !update_is_needed(channel, current_version, latest_tag)? {
|
||||
let replace_same_version = unreleased_checkout_build
|
||||
&& compare_release_versions(current_version, latest_tag)? == Ordering::Equal;
|
||||
if !update_is_needed(channel, current_version, latest_tag)? && !replace_same_version {
|
||||
if compare_release_versions(current_version, latest_tag)? == Ordering::Greater {
|
||||
println!(
|
||||
"Current build is newer than the latest published release; keeping v{current_version}. No downgrade or download performed."
|
||||
@@ -148,6 +163,11 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if replace_same_version {
|
||||
println!(
|
||||
"This build is not a published release ({build_version}); installing the {latest_tag} release artifact over it."
|
||||
);
|
||||
}
|
||||
|
||||
// Reject unrelated command paths before downloads or any sibling changes.
|
||||
for target in &plan.target_paths {
|
||||
|
||||
+17
-2
@@ -170,7 +170,9 @@ What it does (verified):
|
||||
* It **never uses sudo and never edits your shell profile.** It refuses to
|
||||
install into system or package-manager directories (`/usr/bin`,
|
||||
`~/.cargo/bin`, Homebrew, `node_modules`, `/nix/store`…) and refuses to
|
||||
overwrite a *different* existing `codewhale`.
|
||||
overwrite a *different* existing `codewhale` unless you ask for it with
|
||||
`CODEWHALE_FORCE_REINSTALL=1` (see [re-running the
|
||||
installer](#re-running-the-installer)).
|
||||
|
||||
Expected output:
|
||||
|
||||
@@ -258,8 +260,15 @@ codewhale doctor # diagnostics; see the note in §8 about what it does NO
|
||||
`Already installed: …` and exits 0.
|
||||
* Different version already installed: it **refuses**
|
||||
(`codewhale install: refusing to replace existing …/codewhale`), exits 1 and
|
||||
changes nothing. It downloads ~160 MB before refusing. Use
|
||||
changes nothing. It downloads the release before refusing. Use
|
||||
[`codewhale update`](#7-updating-and-rolling-back) instead.
|
||||
* Not a release build, at the same version (a local `cargo build` reports
|
||||
`0.10.2 (dev)`): `codewhale update` still reinstalls the release artifact
|
||||
over it, because that build has no release provenance to preserve.
|
||||
* Deliberate replacement: `CODEWHALE_FORCE_REINSTALL=1` replaces a differing
|
||||
`codewhale`/`codew` in the install directory and keeps each previous file
|
||||
beside it as `<name>.bak-<utc-timestamp>`. It never replaces a symlink or a
|
||||
non-regular file, and never replaces a file in a managed directory.
|
||||
|
||||
### Upgrade / uninstall
|
||||
|
||||
@@ -587,6 +596,12 @@ It updates `codewhale` **and** `codew` together. It took 10 s here. Run it
|
||||
again and you get `Already up to date; no download needed.` Other options:
|
||||
`--beta` and `--proxy <URL>`.
|
||||
|
||||
A build that is not a published release reports its provenance in
|
||||
`codewhale --version` (`0.10.2 (dev)` for a local build, a commit hash for a
|
||||
release). `codewhale update` compares the release, not just the version, so a
|
||||
same-version `(dev)` build is replaced by the release artifact instead of
|
||||
reporting "Already up to date" forever.
|
||||
|
||||
<a id="roll-back-to-a-previous-release"></a>
|
||||
|
||||
### Rolling back (e.g. to v0.9.13)
|
||||
|
||||
@@ -198,7 +198,9 @@ codewhale doctor # diagnostics; see the note in §8 about what it does NO
|
||||
### 重新运行安装器
|
||||
|
||||
* 已安装的就是同一版本:无害。它会打印 `Already installed: …` 并以 0 退出。
|
||||
* 已安装的是不同版本:它会**拒绝**(`codewhale install: refusing to replace existing …/codewhale`),以 1 退出,并且不改动任何东西。拒绝之前它已经下载了约 160 MB。请改用 [`codewhale update`](#7-更新与回滚)。
|
||||
* 已安装的是不同版本:它会**拒绝**(`codewhale install: refusing to replace existing …/codewhale`),以 1 退出,并且不改动任何东西。拒绝之前它已经下载了发布包。请改用 [`codewhale update`](#7-更新与回滚)。
|
||||
* 是同一版本但不是发布构建(本地 `cargo build` 会报告 `0.10.2 (dev)`):`codewhale update` 仍会用发布产物覆盖它,因为这种构建没有需要保留的发布溯源信息。
|
||||
* 主动替换:`CODEWHALE_FORCE_REINSTALL=1` 会替换安装目录中不同的 `codewhale`/`codew`,并把每个旧文件保留在同目录的 `<名称>.bak-<UTC 时间戳>`。它绝不替换符号链接或非普通文件,也绝不替换受管目录中的文件。
|
||||
|
||||
### 升级 / 卸载
|
||||
|
||||
@@ -447,6 +449,8 @@ codewhale update
|
||||
|
||||
它会把 `codewhale` **和** `codew` 一起更新,在测试机上用了 10 秒。再运行一次会得到 `Already up to date; no download needed.`。其他选项:`--beta` 和 `--proxy <URL>`。
|
||||
|
||||
不是发布构建的构建会在 `codewhale --version` 中显示自己的来源(本地构建是 `0.10.2 (dev)`,发布构建是提交哈希)。`codewhale update` 比较的是发布产物而不只是版本号,因此同版本的 `(dev)` 构建会被发布产物替换,而不是永远报告 "Already up to date"。
|
||||
|
||||
<a id="roll-back-to-a-previous-release"></a>
|
||||
|
||||
### 回滚(例如回到 v0.9.13)
|
||||
|
||||
+45
-22
@@ -95,7 +95,8 @@ preflight_glibc() {
|
||||
}
|
||||
|
||||
# This script installs an already checksum-verified archive. Existing different
|
||||
# binaries go through `codewhale update`, the sole version-aware updater.
|
||||
# binaries go through `codewhale update`, or through CODEWHALE_FORCE_REINSTALL=1
|
||||
# here when the installed file is not a published release artifact.
|
||||
case "$BIN_DIR" in
|
||||
/*) ;;
|
||||
*) echo "ERROR: PREFIX must be an absolute user path" >&2; exit 1 ;;
|
||||
@@ -119,8 +120,21 @@ check_destination() {
|
||||
destination_exists=1
|
||||
return 0
|
||||
fi
|
||||
if [[ "${CODEWHALE_FORCE_REINSTALL:-}" == 1 && ! -L "$dst" && -f "$dst" ]]; then
|
||||
local backup="$dst.bak-$(date -u +%Y%m%dT%H%M%SZ)" suffix=1
|
||||
while [[ -e "$backup" || -L "$backup" ]]; do
|
||||
backup="$dst.bak-$(date -u +%Y%m%dT%H%M%SZ)-$suffix"
|
||||
suffix=$((suffix + 1))
|
||||
done
|
||||
mv "$dst" "$backup"
|
||||
forced_backups+=("$backup")
|
||||
echo "Replacing $dst (previous file kept at $backup)"
|
||||
return 0
|
||||
fi
|
||||
echo "ERROR: refusing to replace existing $dst; no existing file was changed." >&2
|
||||
echo "For an existing direct Codewhale install, run its full path with 'update'." >&2
|
||||
echo "To replace it with this archive and keep the previous file beside it:" >&2
|
||||
echo ' CODEWHALE_FORCE_REINSTALL=1 ./install.sh' >&2
|
||||
echo "To migrate, install this verified archive into a fresh user prefix:" >&2
|
||||
echo ' mkdir -p "$HOME/.local"' >&2
|
||||
echo ' codewhale_prefix="$(mktemp -d "$HOME/.local/codewhale-release.XXXXXX")"' >&2
|
||||
@@ -133,26 +147,10 @@ check_destination() {
|
||||
|
||||
# The image is an explicit installer choice; presence never changes Core's
|
||||
# Node default. Archives carry notices/source beside any qualified image.
|
||||
payloads=(codewhale codew)
|
||||
if [[ "${CODEWHALE_INSTALL_COMPILED_HOST:-}" == 1 ]]; then
|
||||
for file in codewhale-extension-host codewhale-extension-host.LICENSES.txt codewhale-extension-host.relink-source.tar.gz codewhale-extension-host.release.json; do
|
||||
[[ -f "$SCRIPT_DIR/$file" && ! -L "$SCRIPT_DIR/$file" ]] || { echo "ERROR: compiled host requested but archive has no complete qualified payload ($file); use Node" >&2; exit 1; }
|
||||
payloads+=("$file")
|
||||
done
|
||||
fi
|
||||
# Validate every source and destination before the first write.
|
||||
for bin in "${payloads[@]}"; do
|
||||
src="$SCRIPT_DIR/$bin"
|
||||
[[ -f "$src" ]] || { echo "ERROR: $src not found in archive" >&2; exit 1; }
|
||||
mode=0644
|
||||
case "$bin" in codewhale|codew|codewhale-extension-host) mode=0755; preflight_glibc "$src" "${bin/codewhale-extension-host/compiled host}" ;; esac
|
||||
check_destination "$src" "$BIN_DIR/$bin" "$mode"
|
||||
done
|
||||
legacy_tui="$BIN_DIR/codewhale-tui"
|
||||
if [[ -e "$legacy_tui" || -L "$legacy_tui" ]]; then
|
||||
check_destination "$SCRIPT_DIR/codewhale" "$legacy_tui"
|
||||
fi
|
||||
|
||||
# Backups this run made under CODEWHALE_FORCE_REINSTALL. A run that fails after
|
||||
# moving a command aside puts it back, so a forced install that does not
|
||||
# complete never leaves a command missing from the prefix.
|
||||
forced_backups=()
|
||||
stage=""
|
||||
stage_dir=""
|
||||
# Commands this run published, as "source<TAB>destination" lines. If a later
|
||||
@@ -171,12 +169,37 @@ rollback_published() {
|
||||
done <<< "$published"
|
||||
}
|
||||
on_exit() {
|
||||
local status=$?
|
||||
local status=$? dst backup
|
||||
if [[ -n "$stage" ]]; then rm -f "$stage"; fi
|
||||
if [[ -n "$stage_dir" ]]; then rmdir "$stage_dir"; fi
|
||||
if [[ "$status" -ne 0 && -n "$published" ]]; then rollback_published; fi
|
||||
if [[ "$status" -ne 0 ]]; then
|
||||
for backup in "${forced_backups[@]}"; do
|
||||
[[ -e "$backup" ]] || continue
|
||||
dst="$(printf '%s' "$backup" | sed 's/\.bak-[0-9TZ]*\(-[0-9][0-9]*\)\{0,1\}$//')"
|
||||
if [[ ! -e "$dst" && ! -L "$dst" ]]; then
|
||||
mv "$backup" "$dst" && echo "Restored $dst: this install did not complete." >&2
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
trap on_exit EXIT
|
||||
payloads=(codewhale codew)
|
||||
if [[ "${CODEWHALE_INSTALL_COMPILED_HOST:-}" == 1 ]]; then
|
||||
for file in codewhale-extension-host codewhale-extension-host.LICENSES.txt codewhale-extension-host.relink-source.tar.gz codewhale-extension-host.release.json; do
|
||||
[[ -f "$SCRIPT_DIR/$file" && ! -L "$SCRIPT_DIR/$file" ]] || { echo "ERROR: compiled host requested but archive has no complete qualified payload ($file); use Node" >&2; exit 1; }
|
||||
payloads+=("$file")
|
||||
done
|
||||
fi
|
||||
# Validate every source and destination before the first write.
|
||||
for bin in "${payloads[@]}"; do
|
||||
src="$SCRIPT_DIR/$bin"
|
||||
[[ -f "$src" ]] || { echo "ERROR: $src not found in archive" >&2; exit 1; }
|
||||
mode=0644
|
||||
case "$bin" in codewhale|codew|codewhale-extension-host) mode=0755; preflight_glibc "$src" "${bin/codewhale-extension-host/compiled host}" ;; esac
|
||||
check_destination "$src" "$BIN_DIR/$bin" "$mode"
|
||||
done
|
||||
|
||||
install_binary() {
|
||||
local src="$1" dst="$2" mode="${3:-0755}"
|
||||
check_destination "$src" "$dst" "$mode"
|
||||
|
||||
+83
-47
@@ -16,6 +16,9 @@ Usage:
|
||||
Environment:
|
||||
CODEWHALE_INSTALL_DIR Install directory. Default: $HOME/.local/bin
|
||||
CODEWHALE_VERSION Release tag for a fresh directory. Default: latest
|
||||
CODEWHALE_FORCE_REINSTALL=1
|
||||
Replace differing codewhale/codew already in the install
|
||||
directory, keeping each old file beside it as .bak-<time>
|
||||
CODEWHALE_RELEASE_BASE_URL
|
||||
Custom release asset base URL ending in /download
|
||||
CODEWHALE_INSTALL_COMPILED_HOST=1
|
||||
@@ -223,9 +226,75 @@ cli_asset="codewhale-$target"
|
||||
shim_asset="codew-$target"
|
||||
manifest_asset="codewhale-artifacts-sha256.txt"
|
||||
|
||||
# Resolve the real directory before applying managed-prefix checks. Never use
|
||||
# sudo or allow an install directory symlink to obscure which files will change.
|
||||
case "$install_dir" in
|
||||
/*) ;;
|
||||
*) fail "CODEWHALE_INSTALL_DIR must be an absolute path" ;;
|
||||
esac
|
||||
[ ! -L "$install_dir" ] || fail "install directory is a symlink: $install_dir; choose a fresh user directory"
|
||||
mkdir -p "$install_dir" || fail "cannot create $install_dir; choose a writable user directory (no sudo is used)"
|
||||
install_dir="$(cd -P "$install_dir" && pwd)"
|
||||
case "$install_dir/" in
|
||||
/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*)
|
||||
fail "refusing managed/system directory $install_dir; use a fresh user directory"
|
||||
;;
|
||||
esac
|
||||
[ -w "$install_dir" ] || fail "$install_dir is not writable; choose a user directory (no sudo is used)"
|
||||
|
||||
tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t codewhale-install)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT INT TERM
|
||||
|
||||
# Backups this run made under CODEWHALE_FORCE_REINSTALL, newest last. A run that
|
||||
# fails after moving a command aside puts it back, so a forced install that does
|
||||
# not complete never leaves the command missing from the install directory.
|
||||
forced_backups="$tmpdir/.forced-backups"
|
||||
: > "$forced_backups"
|
||||
|
||||
stage=""
|
||||
stage_dir=""
|
||||
# Commands this run published. If a later publication fails they are removed
|
||||
# again, but only while each is still the exact file this run wrote, so a
|
||||
# failed install leaves no half-installed pair and never touches a file that
|
||||
# was already installed.
|
||||
published="$tmpdir/.published"
|
||||
: > "$published"
|
||||
rollback_published() {
|
||||
while IFS= read -r name; do
|
||||
destination="$install_dir/$name"
|
||||
if [ ! -L "$destination" ] && [ -f "$destination" ] && cmp -s "$tmpdir/$name" "$destination"; then
|
||||
rm -f "$destination"
|
||||
say "Removed $destination: this install did not complete." >&2
|
||||
fi
|
||||
done < "$published"
|
||||
}
|
||||
restore_forced_backups() {
|
||||
[ -f "$forced_backups" ] || return 0
|
||||
while IFS= read -r backup; do
|
||||
[ -n "$backup" ] || continue
|
||||
[ -e "$backup" ] || continue
|
||||
# The name this run generated ends in .bak-<utc-stamp>[-<n>]; the
|
||||
# destination is everything before it.
|
||||
destination="$(printf '%s' "$backup" | sed 's/\.bak-[0-9TZ]*\(-[0-9][0-9]*\)\{0,1\}$//')"
|
||||
if [ ! -e "$destination" ] && [ ! -L "$destination" ]; then
|
||||
mv "$backup" "$destination" && say "Restored $destination: this install did not complete." >&2
|
||||
fi
|
||||
done < "$forced_backups"
|
||||
}
|
||||
on_exit() {
|
||||
status=$?
|
||||
if [ -n "$stage" ]; then rm -f "$stage"; fi
|
||||
if [ -n "$stage_dir" ]; then rmdir "$stage_dir"; fi
|
||||
if [ "$status" -ne 0 ]; then
|
||||
rollback_published
|
||||
restore_forced_backups
|
||||
fi
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap on_exit EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
say "Installing Codewhale for $target"
|
||||
say "Release assets: $release_base"
|
||||
say "Install dir: $install_dir"
|
||||
@@ -243,22 +312,6 @@ if command -v xattr >/dev/null 2>&1; then
|
||||
xattr -d com.apple.quarantine "$tmpdir/codewhale" "$tmpdir/codew" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Resolve the real directory before applying managed-prefix checks. Never use
|
||||
# sudo or allow an install directory symlink to obscure which files will change.
|
||||
case "$install_dir" in
|
||||
/*) ;;
|
||||
*) fail "CODEWHALE_INSTALL_DIR must be an absolute path" ;;
|
||||
esac
|
||||
[ ! -L "$install_dir" ] || fail "install directory is a symlink: $install_dir; choose a fresh user directory"
|
||||
mkdir -p "$install_dir" || fail "cannot create $install_dir; choose a writable user directory (no sudo is used)"
|
||||
install_dir="$(cd -P "$install_dir" && pwd)"
|
||||
case "$install_dir/" in
|
||||
/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*)
|
||||
fail "refusing managed/system directory $install_dir; use a fresh user directory"
|
||||
;;
|
||||
esac
|
||||
[ -w "$install_dir" ] || fail "$install_dir is not writable; choose a user directory (no sudo is used)"
|
||||
|
||||
check_destination() {
|
||||
destination="$1"
|
||||
source="$2"
|
||||
@@ -269,10 +322,24 @@ check_destination() {
|
||||
destination_exists=1
|
||||
return
|
||||
fi
|
||||
if [ "${CODEWHALE_FORCE_REINSTALL:-}" = 1 ] && [ ! -L "$destination" ] && [ -f "$destination" ]; then
|
||||
backup="$destination.bak-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
suffix=1
|
||||
while [ -e "$backup" ] || [ -L "$backup" ]; do
|
||||
backup="$destination.bak-$(date -u +%Y%m%dT%H%M%SZ)-$suffix"
|
||||
suffix=$((suffix + 1))
|
||||
done
|
||||
mv "$destination" "$backup" || fail "cannot move $destination aside; it was not replaced"
|
||||
printf '%s\n' "$backup" >> "$forced_backups"
|
||||
say "Replacing $destination (previous file kept at $backup)"
|
||||
return
|
||||
fi
|
||||
cat >&2 <<EOF
|
||||
codewhale install: refusing to replace existing $destination.
|
||||
It may be a newer build, another installation, or a symlink. No existing file was changed.
|
||||
For an existing direct Codewhale install, run its full path with 'update'.
|
||||
To replace it with this release and keep the previous file beside it:
|
||||
curl -fsSL https://codewhale.net/install.sh | CODEWHALE_FORCE_REINSTALL=1 sh
|
||||
To migrate safely from a package manager or mixed installation, create a fresh directory:
|
||||
mkdir -p "\$HOME/.local"
|
||||
codewhale_install_dir="\$(mktemp -d "\$HOME/.local/codewhale-release.XXXXXX")"
|
||||
@@ -316,38 +383,7 @@ fi
|
||||
|
||||
check_destination "$install_dir/codewhale" "$tmpdir/codewhale"
|
||||
check_destination "$install_dir/codew" "$tmpdir/codew"
|
||||
legacy_tui="$install_dir/codewhale-tui"
|
||||
if [ -e "$legacy_tui" ] || [ -L "$legacy_tui" ]; then
|
||||
check_destination "$legacy_tui" "$tmpdir/codewhale"
|
||||
fi
|
||||
|
||||
stage=""
|
||||
stage_dir=""
|
||||
# Commands this run published. If a later publication fails they are removed
|
||||
# again, but only while each is still the exact file this run wrote, so a
|
||||
# failed install leaves no half-installed pair and never touches a file that
|
||||
# was already installed.
|
||||
published="$tmpdir/.published"
|
||||
: > "$published"
|
||||
rollback_published() {
|
||||
while IFS= read -r name; do
|
||||
destination="$install_dir/$name"
|
||||
if [ ! -L "$destination" ] && [ -f "$destination" ] && cmp -s "$tmpdir/$name" "$destination"; then
|
||||
rm -f "$destination"
|
||||
say "Removed $destination: this install did not complete." >&2
|
||||
fi
|
||||
done < "$published"
|
||||
}
|
||||
on_exit() {
|
||||
status=$?
|
||||
if [ -n "$stage" ]; then rm -f "$stage"; fi
|
||||
if [ -n "$stage_dir" ]; then rmdir "$stage_dir"; fi
|
||||
if [ "$status" -ne 0 ]; then rollback_published; fi
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap on_exit EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
install_binary() {
|
||||
source="$1"
|
||||
destination="$2"
|
||||
|
||||
Reference in new issue
Block a user