Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions scripts/install_ruby.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
. "$DIR/lib/common.sh"

ACTION="${1:-reconcile}"
# Remember whether the caller pinned a version — uninstall treats an explicit
# RUBY_VERSION as "remove only this cycle" (like NODE_VERSION/UV_PYTHON_SPEC)
RUBY_VERSION_EXPLICIT="${RUBY_VERSION:+1}"
# Target Ruby version (default: latest stable)
RUBY_VERSION="${RUBY_VERSION:-3.3.6}"

Expand Down Expand Up @@ -79,8 +82,10 @@
fi

# Check version before install
local before="$(get_specific_ruby_version "$RUBY_VERSION")"
local version_cycle="$(get_version_cycle "$RUBY_VERSION")"
local before
before="$(get_specific_ruby_version "$RUBY_VERSION")"
local version_cycle
version_cycle="$(get_version_cycle "$RUBY_VERSION")"
local display_name="ruby"
[ -n "$version_cycle" ] && display_name="ruby@${version_cycle}"

Expand All @@ -99,7 +104,8 @@
fi

# Only set global if no global version is set yet
local current_global="$(rbenv global 2>/dev/null || true)"
local current_global
current_global="$(rbenv global 2>/dev/null || true)"
if [ -z "$current_global" ] || [ "$current_global" = "system" ]; then
rbenv global "$RUBY_VERSION" || true
fi
Expand All @@ -113,7 +119,8 @@
rbenv rehash || true

# Report version for this specific install
local after="$(get_specific_ruby_version "$RUBY_VERSION")"
local after
after="$(get_specific_ruby_version "$RUBY_VERSION")"
local path="$HOME/.rbenv/versions/$RUBY_VERSION/bin/ruby"
printf "[%s] before: %s\n" "$display_name" "${before:-<none>}"
printf "[%s] after: %s\n" "$display_name" "${after:-<none>}"
Expand All @@ -133,7 +140,42 @@
}

uninstall_ruby() {
# Remove rbenv-managed Ruby
if [ -n "$RUBY_VERSION_EXPLICIT" ]; then

Check failure on line 143 in scripts/install_ruby.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_coding_agent_cli_toolset&issues=AZ9tCKA7N1fS3h9xCqsU&open=AZ9tCKA7N1fS3h9xCqsU&pullRequest=109
# Multi-version: only remove the requested cycle, keep rbenv and other versions
if ! have rbenv && [ -x "$HOME/.rbenv/bin/rbenv" ]; then

Check failure on line 145 in scripts/install_ruby.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_coding_agent_cli_toolset&issues=AZ9tCKA8N1fS3h9xCqsV&open=AZ9tCKA8N1fS3h9xCqsV&pullRequest=109
export PATH="$HOME/.rbenv/bin:$PATH"
fi
if ! have rbenv; then
echo "[ruby] rbenv not found; nothing to remove for cycle $RUBY_VERSION" >&2
return 0
fi

local cycle cycle_re resolved
cycle="$(get_version_cycle "$RUBY_VERSION")"
cycle_re="^${cycle//./\\.}(\\.|$)"
resolved="$(rbenv versions --bare 2>/dev/null | grep -E "$cycle_re" || true)"
if [ -z "$resolved" ]; then

Check failure on line 157 in scripts/install_ruby.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_coding_agent_cli_toolset&issues=AZ9tCKA8N1fS3h9xCqsW&open=AZ9tCKA8N1fS3h9xCqsW&pullRequest=109
echo "[ruby] Ruby $RUBY_VERSION not found in rbenv" >&2
return 0
fi

local current_global other_ver v
current_global="$(rbenv global 2>/dev/null || true)"
for v in $resolved; do
if [ "$v" = "$current_global" ]; then

Check failure on line 165 in scripts/install_ruby.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_coding_agent_cli_toolset&issues=AZ9tCKA8N1fS3h9xCqsX&open=AZ9tCKA8N1fS3h9xCqsX&pullRequest=109
other_ver="$(rbenv versions --bare 2>/dev/null | grep -vE "$cycle_re" | tail -1 || true)"
if [ -n "$other_ver" ]; then

Check failure on line 167 in scripts/install_ruby.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_coding_agent_cli_toolset&issues=AZ9tCKA8N1fS3h9xCqsY&open=AZ9tCKA8N1fS3h9xCqsY&pullRequest=109
echo "[ruby] Warning: ruby $v is the current global, switching to $other_ver first" >&2
rbenv global "$other_ver" || true
fi
fi
rbenv uninstall -f "$v" || true
echo "[ruby] Removed ruby $v (cycle $cycle)" >&2
done
return 0
fi

# Full uninstall: remove rbenv-managed Ruby entirely
if [ -d "$HOME/.rbenv" ]; then
rm -rf "$HOME/.rbenv"
fi
Expand Down
136 changes: 136 additions & 0 deletions tests/test_ruby_uninstall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Tests for per-cycle ruby uninstall (RUBY_VERSION=3.3 install_ruby.sh uninstall).

node (NODE_VERSION) and python (UV_PYTHON_SPEC) support removing a single
version cycle; ruby's uninstall previously always nuked all of ~/.rbenv.
"""

from __future__ import annotations

import os
import subprocess
import sys
import tempfile
from pathlib import Path

import pytest

skip_on_windows = pytest.mark.skipif(
sys.platform == "win32", reason="Shell script tests require POSIX shell"
)

SCRIPT = Path(__file__).parent.parent / "scripts" / "install_ruby.sh"

RBENV_STUB = """#!/usr/bin/env bash
echo "rbenv $*" >> "$RBENV_STUB_LOG"
case "$1" in
versions)
printf '%s\\n' $RBENV_STUB_VERSIONS
;;
global)
if [ $# -eq 1 ]; then echo "$RBENV_STUB_GLOBAL"; fi
;;
esac
exit 0
"""


@skip_on_windows
class TestRubyPerCycleUninstall:
def _run(self, tmpdir: str, env_extra: dict, versions: str, global_ver: str,
args: list[str]) -> tuple[subprocess.CompletedProcess, str, Path]:
stub_dir = Path(tmpdir) / "stubs"
stub_dir.mkdir(exist_ok=True)
log = Path(tmpdir) / "rbenv.log"
log.touch()
for name, body in (
("rbenv", RBENV_STUB),
# keep the full-uninstall branch harmless if ever reached
("sudo", "#!/usr/bin/env bash\nexit 0\n"),
("dpkg", "#!/usr/bin/env bash\nexit 1\n"),
("apt-get", "#!/usr/bin/env bash\nexit 0\n"),
):
stub = stub_dir / name
stub.write_text(body)
stub.chmod(0o755)

home = Path(tmpdir) / "home"
(home / ".rbenv" / "versions").mkdir(parents=True, exist_ok=True)

env = {
**os.environ,
"HOME": str(home),
"PATH": f"{stub_dir}:{os.environ['PATH']}",
"RBENV_STUB_LOG": str(log),
"RBENV_STUB_VERSIONS": versions,
"RBENV_STUB_GLOBAL": global_ver,
**env_extra,
}
env.pop("RUBY_VERSION", None)
env.update({k: v for k, v in env_extra.items()})

Check warning on line 69 in tests/test_ruby_uninstall.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this comprehension with passing the iterable to the dict constructor call

See more on https://sonarcloud.io/project/issues?id=netresearch_coding_agent_cli_toolset&issues=AZ9tCJ-9N1fS3h9xCqsT&open=AZ9tCJ-9N1fS3h9xCqsT&pullRequest=109
result = subprocess.run(
["bash", str(SCRIPT), *args],
capture_output=True, text=True, timeout=30, env=env,
)
return result, log.read_text(), home

def test_removes_only_requested_cycle(self):
with tempfile.TemporaryDirectory() as tmpdir:
result, log, home = self._run(
tmpdir, {"RUBY_VERSION": "3.3"},
versions="3.3.6 4.0.1 4.0.5", global_ver="4.0.5",
args=["uninstall"],
)
assert result.returncode == 0, result.stderr
assert "rbenv uninstall -f 3.3.6" in log
assert "uninstall -f 4.0.1" not in log
assert "uninstall -f 4.0.5" not in log
# rbenv itself must survive a per-cycle removal
assert (home / ".rbenv").exists()

def test_full_version_spec_removes_its_cycle(self):
with tempfile.TemporaryDirectory() as tmpdir:
result, log, _ = self._run(
tmpdir, {"RUBY_VERSION": "3.3.6"},
versions="3.3.6 4.0.5", global_ver="4.0.5",
args=["uninstall"],
)
assert result.returncode == 0, result.stderr
assert "rbenv uninstall -f 3.3.6" in log

def test_switches_global_before_removing_it(self):
with tempfile.TemporaryDirectory() as tmpdir:
result, log, _ = self._run(
tmpdir, {"RUBY_VERSION": "3.3"},
versions="3.3.6 4.0.5", global_ver="3.3.6",
args=["uninstall"],
)
assert result.returncode == 0, result.stderr
lines = [line for line in log.splitlines() if line]
assert "rbenv global 4.0.5" in lines
assert lines.index("rbenv global 4.0.5") < lines.index(
"rbenv uninstall -f 3.3.6"
)

def test_missing_cycle_is_a_noop(self):
with tempfile.TemporaryDirectory() as tmpdir:
result, log, home = self._run(
tmpdir, {"RUBY_VERSION": "3.1"},
versions="3.3.6 4.0.5", global_ver="4.0.5",
args=["uninstall"],
)
assert result.returncode == 0, result.stderr
assert "uninstall -f" not in log
assert "not found" in result.stderr + result.stdout
assert (home / ".rbenv").exists()

def test_full_uninstall_without_ruby_version_still_removes_rbenv(self):
with tempfile.TemporaryDirectory() as tmpdir:
result, _, home = self._run(
tmpdir, {},
versions="3.3.6 4.0.5", global_ver="4.0.5",
args=["uninstall"],
)
assert result.returncode == 0, result.stderr
assert not (home / ".rbenv").exists(), (
"full uninstall must keep removing ~/.rbenv entirely"
)
Loading