From 64c6f9e60b9975cb35dc42ce874ae6a6c8dc71c6 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Tue, 17 Feb 2026 10:27:41 -0800 Subject: [PATCH 1/6] Improve dependency checking on install script. --- install.sh | 291 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) diff --git a/install.sh b/install.sh index 880b94f0f6..7c15ff8f71 100755 --- a/install.sh +++ b/install.sh @@ -8,6 +8,277 @@ set -e REPO="stellar/stellar-cli" BINARY_NAME="stellar" +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +detect_linux_distro() { + if [ -r /etc/os-release ]; then + # shellcheck disable=SC1091 + . /etc/os-release + if [ -n "$ID" ]; then + echo "$ID" + return + fi + fi + echo "unknown" +} + +detect_linux_libc() { + if command_exists getconf; then + if getconf GNU_LIBC_VERSION >/dev/null 2>&1; then + echo "glibc" + return + fi + fi + + if command_exists ldd; then + if ldd --version 2>&1 | grep -qi musl; then + echo "musl" + return + fi + if ldd --version 2>&1 | grep -qi "gnu libc"; then + echo "glibc" + return + fi + fi + + if [ -e /lib/ld-musl-x86_64.so.1 ] || [ -e /lib/ld-musl-aarch64.so.1 ]; then + echo "musl" + return + fi + + echo "unknown" +} + +suggest_dependency_install() { + missing="$1" + if [ "$OS" = "macos" ]; then + cat <&1)"; then + return 0 + fi + + missing_lib="$(printf '%s\n' "$version_output" | sed -n 's/.*error while loading shared libraries: \([^:]*\):.*/\1/p')" + + if [ -n "$missing_lib" ]; then + echo "" + echo "Warning: $BINARY_NAME was installed, but a runtime shared library is missing:" + echo " $missing_lib" + echo "" + suggest_runtime_library_install "$missing_lib" + echo "" + echo "After installing the runtime dependency, run:" + echo " $installed_binary --version" + return 0 + fi + + echo "" + echo "Warning: post-install check failed:" + echo "$version_output" + + if [ "$OS" = "linux" ] && [ "${LIBC:-unknown}" = "musl" ]; then + if [ -f "$installed_binary" ] && printf '%s\n' "$version_output" | grep -qi "not found"; then + cat </dev/null 2>&1; then @@ -96,12 +367,30 @@ esac # Construct target triple if [ "$OS" = "linux" ]; then + LIBC="$(detect_linux_libc)" + if [ "$LIBC" = "musl" ]; then + DISTRO="$(detect_linux_distro)" + echo "Error: Detected Linux distro '$DISTRO' using musl libc." + echo "The prebuilt Stellar CLI release in this script targets glibc (GNU/Linux) and is not supported on musl systems (e.g. Alpine)." + if [ -f "${INSTALL_DIR}/${BINARY_NAME}" ]; then + echo "" + echo "Note: Found an existing install at ${INSTALL_DIR}/${BINARY_NAME}." + echo "It may be from a previous run and will continue to fail on musl." + fi + echo "" + echo "Recommended next steps:" + echo " 1) Use a glibc-based image (Debian/Ubuntu/Fedora)." + echo " Example: docker run --platform=linux/arm64 -it --rm debian:bookworm-slim" + echo " 2) If you must stay on Alpine, build stellar-cli from source on Alpine." + exit 1 + fi TARGET="${ARCH}-unknown-linux-gnu" elif [ "$OS" = "macos" ]; then TARGET="${ARCH}-apple-darwin" fi echo "Detected platform: $OS ($TARGET)" +check_dependencies # Get latest release version echo "Fetching latest release..." @@ -151,6 +440,8 @@ else sudo chmod +x "$INSTALL_DIR/$BINARY_NAME" fi +post_install_check "$INSTALL_DIR/$BINARY_NAME" + echo "" echo "✓ Stellar CLI installed successfully!" echo "" From 3db5f07f729d65da657b2fe4dbbb356f59aa5fd8 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Tue, 17 Feb 2026 11:22:19 -0800 Subject: [PATCH 2/6] Add support for installing dependencies. --- README.md | 9 ++ install.sh | 437 +++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 397 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 7537169da5..1406d8a123 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,15 @@ For installation options, run the script with `--help`: curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh -s -- --help ``` +Install with dependency setup (`--install-deps`) to attempt installing: + +- Linux development dependencies (`libdbus`/`libudev` development packages) +- Rust toolchain and `wasm32v1-none` target (for contract build workflows) + +```sh +curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh -s -- --install-deps +``` + Install with Homebrew (macOS, Linux): ```sh diff --git a/install.sh b/install.sh index 7c15ff8f71..c44b98c127 100755 --- a/install.sh +++ b/install.sh @@ -12,6 +12,18 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } +run_as_root() { + if [ "$(id -u)" = "0" ]; then + "$@" + return + fi + if command_exists sudo; then + sudo "$@" + return + fi + return 1 +} + detect_linux_distro() { if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 @@ -56,47 +68,41 @@ suggest_dependency_install() { if [ "$OS" = "macos" ]; then cat </dev/null; then + return 0 + fi + if [ -f /usr/include/dbus-1.0/dbus/dbus.h ]; then + return 0 + fi + ;; + udev) + if command_exists pkg-config && pkg-config --exists libudev 2>/dev/null; then + return 0 + fi + if [ -f /usr/include/libudev.h ]; then + return 0 + fi + ;; + esac + return 1 +} + +attempt_runtime_deps_setup() { + if [ "$OS" != "linux" ]; then + return + fi + + pkg_dbus_dev="" + pkg_udev_dev="" + manager="" + missing_pkgs="" + + if command_exists apt-get; then + manager="apt-get" + pkg_dbus_dev="libdbus-1-dev" + pkg_udev_dev="libudev-dev" + elif command_exists dnf; then + manager="dnf" + pkg_dbus_dev="dbus-devel" + pkg_udev_dev="systemd-devel" + elif command_exists yum; then + manager="yum" + pkg_dbus_dev="dbus-devel" + pkg_udev_dev="systemd-devel" + elif command_exists pacman; then + manager="pacman" + pkg_dbus_dev="dbus" + pkg_udev_dev="systemd" + elif command_exists zypper; then + manager="zypper" + pkg_dbus_dev="dbus-1-devel" + pkg_udev_dev="systemd-devel" + else + echo "Warning: Could not detect a supported package manager to install development dependencies." + return + fi + + if [ -n "$pkg_dbus_dev" ] && ! has_dev_dependency dbus; then + missing_pkgs="$missing_pkgs $pkg_dbus_dev" + fi + if [ -n "$pkg_udev_dev" ] && ! has_dev_dependency udev; then + missing_pkgs="$missing_pkgs $pkg_udev_dev" + fi + + missing_pkgs="${missing_pkgs# }" + if [ -z "$missing_pkgs" ]; then + return + fi + + echo "" + echo "Attempting development dependency setup (--install-deps): $missing_pkgs" + + case "$manager" in + apt-get) + if ! run_as_root apt-get update; then + echo "Warning: apt-get update failed." + fi + # shellcheck disable=SC2086 + if ! run_as_root apt-get install -y $missing_pkgs; then + echo "Warning: Failed to install development dependencies via apt-get." + fi + ;; + dnf) + # shellcheck disable=SC2086 + if ! run_as_root dnf install -y $missing_pkgs; then + echo "Warning: Failed to install development dependencies via dnf." + fi + ;; + yum) + # shellcheck disable=SC2086 + if ! run_as_root yum install -y $missing_pkgs; then + echo "Warning: Failed to install development dependencies via yum." + fi + ;; + pacman) + # shellcheck disable=SC2086 + if ! run_as_root pacman -S --needed --noconfirm $missing_pkgs; then + echo "Warning: Failed to install development dependencies via pacman." + fi + ;; + zypper) + # shellcheck disable=SC2086 + if ! run_as_root zypper install -y $missing_pkgs; then + echo "Warning: Failed to install development dependencies via zypper." + fi + ;; + esac +} + +suggest_rust_install() { + if [ "$OS" = "macos" ]; then + cat </dev/null | grep -q '^wasm32v1-none$'; then + attempt_rust_setup + fi + fi + fi + + echo "" + echo "Smart contract Rust environment summary:" + + has_rustup="false" + has_cargo="false" + has_rustc="false" + has_wasm_target="false" + + if command_exists rustup; then + has_rustup="true" + echo " [OK] rustup" + else + echo " [MISSING] rustup" + fi + + if command_exists cargo; then + has_cargo="true" + echo " [OK] cargo" + else + echo " [MISSING] cargo" + fi + + if command_exists rustc; then + has_rustc="true" + echo " [OK] rustc" + else + echo " [MISSING] rustc" + fi + + if [ "$has_rustup" = "true" ]; then + if rustup target list --installed 2>/dev/null | grep -q '^wasm32v1-none$'; then + has_wasm_target="true" + echo " [OK] wasm32v1-none target" + else + echo " [MISSING] wasm32v1-none target" + fi + else + echo " [MISSING] wasm32v1-none target (requires rustup)" + fi + + if [ "$has_rustup" = "false" ] || [ "$has_cargo" = "false" ] || [ "$has_rustc" = "false" ]; then + echo "" + suggest_rust_install + echo "" + echo "Then run:" + echo " rustup default stable" + echo " rustup target add wasm32v1-none" + return + fi + + if [ "$has_wasm_target" = "false" ]; then + echo "" + echo "Install missing target with:" + echo " rustup target add wasm32v1-none" + return + fi + + echo " Ready for smart contract builds." +} + post_install_check() { installed_binary="$1" version_output="" @@ -257,7 +556,7 @@ post_install_check() { echo "" echo "After installing the runtime dependency, run:" echo " $installed_binary --version" - return 0 + return 1 fi echo "" @@ -276,21 +575,16 @@ EOF fi fi - return 0 + return 1 } # Check if user has sudo privileges has_sudo() { if command -v sudo >/dev/null 2>&1; then - # Try sudo -n true to check if we can run sudo without password prompt - # or if user is in sudoers + # Non-interactive check only; avoid prompting during auto-detection. if sudo -n true 2>/dev/null; then return 0 fi - # If sudo requires password, prompt for it once to check - if sudo -v 2>/dev/null; then - return 0 - fi fi return 1 } @@ -298,17 +592,19 @@ has_sudo() { # Parse arguments and set defaults INSTALL_DIR="" USER_INSTALL=false +INSTALL_DEPS=false for arg in "$@"; do case $arg in --user) USER_INSTALL=true INSTALL_DIR="${HOME}/.local/bin" - shift ;; --dir=*) INSTALL_DIR="${arg#*=}" - shift + ;; + --install-deps) + INSTALL_DEPS=true ;; --help) echo "Stellar CLI installer" @@ -318,15 +614,21 @@ for arg in "$@"; do echo "Options:" echo " --user Install to ~/.local/bin (no sudo required)" echo " --dir=PATH Install to custom directory" + echo " --install-deps Attempt to install missing libdbus/libudev dev dependencies and Rust toolchain" echo " --help Show this help message" exit 0 ;; + *) + echo "Error: Unknown option: $arg" + echo "Run with --help to see available options." + exit 1 + ;; esac done # Set default install directory if not specified if [ -z "$INSTALL_DIR" ]; then - if has_sudo; then + if [ "$(id -u)" = "0" ] || has_sudo; then INSTALL_DIR="/usr/local/bin" else INSTALL_DIR="${HOME}/.local/bin" @@ -350,6 +652,9 @@ case "$OS" in ;; esac +# Validate basic installer dependencies before other checks use them +check_dependencies + # Detect architecture ARCH="$(uname -m)" case "$ARCH" in @@ -369,8 +674,7 @@ esac if [ "$OS" = "linux" ]; then LIBC="$(detect_linux_libc)" if [ "$LIBC" = "musl" ]; then - DISTRO="$(detect_linux_distro)" - echo "Error: Detected Linux distro '$DISTRO' using musl libc." + echo "Error: Detected Linux distro '$(detect_linux_distro)' using musl libc." echo "The prebuilt Stellar CLI release in this script targets glibc (GNU/Linux) and is not supported on musl systems (e.g. Alpine)." if [ -f "${INSTALL_DIR}/${BINARY_NAME}" ]; then echo "" @@ -380,7 +684,7 @@ if [ "$OS" = "linux" ]; then echo "" echo "Recommended next steps:" echo " 1) Use a glibc-based image (Debian/Ubuntu/Fedora)." - echo " Example: docker run --platform=linux/arm64 -it --rm debian:bookworm-slim" + echo " Example: docker run -it --rm debian:bookworm-slim" echo " 2) If you must stay on Alpine, build stellar-cli from source on Alpine." exit 1 fi @@ -390,11 +694,29 @@ elif [ "$OS" = "macos" ]; then fi echo "Detected platform: $OS ($TARGET)" -check_dependencies # Get latest release version echo "Fetching latest release..." -LATEST_RELEASE=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') +RELEASE_RESPONSE_FILE="$(mktemp)" +if ! RELEASE_HTTP_STATUS="$(curl -sSL -o "$RELEASE_RESPONSE_FILE" -w "%{http_code}" "https://api.github.com/repos/${REPO}/releases/latest")"; then + echo "Error: Could not reach GitHub API to fetch latest release" + rm -f "$RELEASE_RESPONSE_FILE" + exit 1 +fi + +if [ "$RELEASE_HTTP_STATUS" != "200" ]; then + if [ "$RELEASE_HTTP_STATUS" = "403" ] && grep -qi "rate limit" "$RELEASE_RESPONSE_FILE"; then + echo "Error: GitHub API rate limit exceeded while fetching latest release." + echo "Try again later or set GITHUB_TOKEN for authenticated requests." + else + echo "Error: Could not fetch latest release (GitHub API HTTP $RELEASE_HTTP_STATUS)" + fi + rm -f "$RELEASE_RESPONSE_FILE" + exit 1 +fi + +LATEST_RELEASE="$(grep '"tag_name":' "$RELEASE_RESPONSE_FILE" | sed -E 's/.*"([^"]+)".*/\1/')" +rm -f "$RELEASE_RESPONSE_FILE" if [ -z "$LATEST_RELEASE" ]; then echo "Error: Could not fetch latest release" @@ -440,10 +762,21 @@ else sudo chmod +x "$INSTALL_DIR/$BINARY_NAME" fi -post_install_check "$INSTALL_DIR/$BINARY_NAME" +if [ "$INSTALL_DEPS" = true ]; then + attempt_runtime_deps_setup +fi + +POST_INSTALL_OK=true +if ! post_install_check "$INSTALL_DIR/$BINARY_NAME"; then + POST_INSTALL_OK=false +fi echo "" -echo "✓ Stellar CLI installed successfully!" +if [ "$POST_INSTALL_OK" = true ]; then + echo "[OK] Stellar CLI installed successfully!" +else + echo "Warning: Stellar CLI binary was installed, but failed post-install verification." +fi echo "" echo "Location: $INSTALL_DIR/$BINARY_NAME" echo "Version: $VERSION" @@ -462,3 +795,9 @@ case ":$PATH:" in echo "" ;; esac + +print_contract_tooling_summary + +if [ "$POST_INSTALL_OK" != true ]; then + exit 1 +fi From 550eece0db767dab0d7b0989932d8369d51808af Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Tue, 17 Feb 2026 12:04:56 -0800 Subject: [PATCH 3/6] Handle copilot feedback. --- README.md | 2 +- install.sh | 63 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 1406d8a123..5516c0f731 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh -s -- Install with dependency setup (`--install-deps`) to attempt installing: -- Linux development dependencies (`libdbus`/`libudev` development packages) +- Linux development dependencies (`libdbus`/`libudev` development packages; Linux only, not required on macOS) - Rust toolchain and `wasm32v1-none` target (for contract build workflows) ```sh diff --git a/install.sh b/install.sh index c44b98c127..1ca3b54ccc 100755 --- a/install.sh +++ b/install.sh @@ -21,6 +21,7 @@ run_as_root() { sudo "$@" return fi + echo "Warning: '$*' requires root privileges, but neither root access nor sudo is available." >&2 return 1 } @@ -64,12 +65,23 @@ detect_linux_libc() { } suggest_dependency_install() { - missing="$1" + missing_cmds="$1" + # Map missing command names to their actual package names + pkgs="" + for cmd in $missing_cmds; do + case "$cmd" in + sed) [ "$OS" = "macos" ] && pkgs="$pkgs gnu-sed" || pkgs="$pkgs sed" ;; + tar) [ "$OS" = "macos" ] && pkgs="$pkgs gnu-tar" || pkgs="$pkgs tar" ;; + mktemp) pkgs="$pkgs coreutils" ;; + *) pkgs="$pkgs $cmd" ;; + esac + done + pkgs="${pkgs# }" + if [ "$OS" = "macos" ]; then cat < Date: Thu, 19 Feb 2026 10:03:00 -0800 Subject: [PATCH 4/6] Only prepend sudo if not running as root already. --- install.sh | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/install.sh b/install.sh index 1ca3b54ccc..6e43b3572d 100755 --- a/install.sh +++ b/install.sh @@ -89,27 +89,27 @@ EOF if command_exists apt-get; then cat < Date: Thu, 19 Feb 2026 11:06:08 -0800 Subject: [PATCH 5/6] Reminiscent from #2396. --- Cargo.lock | 15 --------------- FULL_HELP_DOCS.md | 11 ----------- 2 files changed, 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0cfd55bdc..bb90369ad0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5021,7 +5021,6 @@ dependencies = [ "soroban-ledger-snapshot", "soroban-sdk", "soroban-spec", - "soroban-spec-json", "soroban-spec-rust", "soroban-spec-tools", "soroban-spec-typescript", @@ -5210,20 +5209,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "soroban-spec-json" -version = "25.1.0" -dependencies = [ - "pretty_assertions", - "serde", - "serde_derive", - "serde_json", - "sha2 0.9.9", - "soroban-spec", - "stellar-xdr", - "thiserror 1.0.69", -] - [[package]] name = "soroban-spec-rust" version = "25.0.2" diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index d5a23df285..56df30e1f6 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -274,7 +274,6 @@ Generate code client bindings for a contract ###### **Subcommands:** -- `json` — Generate Json Bindings - `rust` — Generate Rust bindings - `typescript` — Generate a TypeScript / JavaScript package - `python` — Generate Python bindings @@ -283,16 +282,6 @@ Generate code client bindings for a contract - `swift` — Generate Swift bindings - `php` — Generate PHP bindings -## `stellar contract bindings json` - -Generate Json Bindings - -**Usage:** `stellar contract bindings json --wasm ` - -###### **Options:** - -- `--wasm ` — Path to wasm binary - ## `stellar contract bindings rust` Generate Rust bindings From 61339b7d6808875150cb79e317ced86eaa2e0fa9 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Thu, 19 Feb 2026 14:53:23 -0800 Subject: [PATCH 6/6] Make check less brittle and add test. --- .github/workflows/test-install.yml | 33 +++++ install.sh | 96 +++++++----- test_install.rb | 228 +++++++++++++++++++++++++++++ 3 files changed, 317 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/test-install.yml create mode 100644 test_install.rb diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml new file mode 100644 index 0000000000..5d74c92f70 --- /dev/null +++ b/.github/workflows/test-install.yml @@ -0,0 +1,33 @@ +name: Test Install + +on: + push: + branches: [main, release/**] + paths: + - install.sh + - test_install.rb + pull_request: + paths: + - install.sh + - test_install.rb + +concurrency: + group: + ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || + github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + test-install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0 + with: + ruby-version: ruby + - name: Run install tests + run: ruby test_install.rb diff --git a/install.sh b/install.sh index 6e43b3572d..17b09a4495 100755 --- a/install.sh +++ b/install.sh @@ -149,9 +149,9 @@ suggest_runtime_library_install() { package="" if command_exists apt-get; then case "$missing_lib" in - libdbus-1.so.3) package="libdbus-1-3" ;; - libudev.so.1) package="libudev1" ;; - *) package="" ;; + libdbus-1*) package="libdbus-1-3" ;; + libudev*) package="libudev1" ;; + *) package="" ;; esac if [ -n "$package" ]; then cat <&1)"; then + + if [ "$OS" != "linux" ]; then return 0 fi - missing_lib="$(printf '%s\n' "$version_output" | sed -n 's/.*error while loading shared libraries: \([^:]*\):.*/\1/p')" + if ! command_exists ldconfig; then + return 0 + fi - if [ -n "$missing_lib" ]; then - echo "" - echo "Warning: $BINARY_NAME was installed, but a runtime shared library is missing:" - printf ' %s\n' "$missing_lib" - echo "" - suggest_runtime_library_install "$missing_lib" - echo "" - echo "After installing the runtime dependency, run:" - printf ' %s --version\n' "$installed_binary" - return 1 + ldconfig_output="$(ldconfig -p 2>/dev/null)" + missing_libs="" + + for lib in libdbus-1 libudev; do + if ! printf '%s\n' "$ldconfig_output" | grep -q "$lib"; then + missing_libs="$missing_libs $lib" + fi + done + + missing_libs="${missing_libs# }" + if [ -z "$missing_libs" ]; then + return 0 fi echo "" - echo "Warning: post-install check failed:" - printf '%s\n' "$version_output" + echo "Warning: $BINARY_NAME was installed, but runtime shared libraries are missing:" + for lib in $missing_libs; do + printf ' %s\n' "$lib" + echo "" + suggest_runtime_library_install "$lib" + done + echo "" + echo "After installing the runtime dependencies, run:" + printf ' %s --version\n' "$installed_binary" + return 1 +} - if [ "$OS" = "linux" ] && [ "${LIBC:-unknown}" = "musl" ]; then - if [ -f "$installed_binary" ] && printf '%s\n' "$version_output" | grep -qi "not found"; then - cat <&1)"; then + return "$runtime_ok" fi + echo "" + echo "Warning: post-install check failed:" + printf '%s\n' "$version_output" + return 1 } diff --git a/test_install.rb b/test_install.rb new file mode 100644 index 0000000000..4ca9713269 --- /dev/null +++ b/test_install.rb @@ -0,0 +1,228 @@ +require "open3" +require "shellwords" + +class Matcher + def initialize(string, expected) + @string = string + @expected = expected + end + + def match?(out) + out.include?(@string) == @expected + end + + def inspect + @expected ? "match #{@string.inspect}" : "not match #{@string.inspect}" + end +end + +$EXPECTED_EXIT = 0 + +DISTROS = { + debian: "debian:bookworm-slim", + ubuntu: "ubuntu:latest", + fedora: "fedora:latest" +} + +def run_cmd(*cmd, exit: 0) + formatted_cmd = cmd.map { Shellwords.escape(it) }.join(" ") + out, err, status = Open3.capture3(*cmd) + + if status.exitstatus != exit + fail "Expected status to be #{exit}; " \ + "got #{status.exitstatus}\n\n" \ + "== command ==\n#{formatted_cmd}\n\n" \ + "== stdout ==\n#{format_output(out)}\n\n" \ + "== stderr ==\n#{format_output(err)}" + end + + [out, err] +end + +def run_docker(distro, cmd, exit: 0) + run_cmd( + "docker", + "run", + "--rm", + "-v", + "#{__dir__}:/source:ro", + DISTROS.fetch(distro), + "sh", + "-c", + cmd, + exit: + ) +end + +def match(matcher) + Matcher.new(matcher, true) +end + +def doesnt_match(matcher) + Matcher.new(matcher, false) +end + +def format_output(output) + output = "\\n" if output == "\n" + output = "" if output.empty? + output +end + +def run_test(title, distro:, cmd:, exit: 0, out: nil, err: nil) + title = "\e[34m#{title}\e[0m" + print "\e[33m[ACTING]\e[0m #{title}" + + stdout, stderr = run_docker(distro, cmd, exit:) + outputs = "\n\n== stdout ==\n#{format_output(stdout)}\n\n" + outputs = "#{outputs}== stderr ==\n#{format_output(stderr)}" + + Array(out).each do |item| + if item + unless item.match?(stdout) + fail "Expected output to #{item.inspect}#{outputs}" + end + end + end + + Array(err).each do |item| + if item + unless item.match?(stderr) + fail "Expected output to #{item.inspect}#{outputs}" + end + end + end + + puts "\r\e[32m[PASSED] #{title}\e[0m" +rescue RuntimeError => error + puts "\r\e[31m[FAILED]\e[0m #{title}\nError: #{error.message}\n\n" + $EXPECTED_EXIT = 1 +end + +run_test "sanity: output capture works", + distro: :debian, + cmd: "echo hello_from_container", + out: match("hello_from_container") + +run_test "debian: exits when curl is missing", + distro: :debian, + cmd: "sh /source/install.sh", + exit: 1, + out: match("Error: Missing required command(s): curl") + +run_test "debian: no sudo in suggestions when running as root", + distro: :debian, + cmd: "apt-get update && apt-get -y install curl; sh /source/install.sh", + exit: 1, + out: [doesnt_match("sudo apt-get"), doesnt_match("sudo dnf")] + +run_test "fedora: no sudo in suggestions when running as root", + distro: :fedora, + cmd: "dnf -y install curl; sh /source/install.sh", + exit: 1, + out: [doesnt_match("sudo apt-get"), doesnt_match("sudo dnf")] + +run_test "debian: sudo in suggestions when running as user", + distro: :debian, + cmd: <<~SH, + apt-get update && apt-get -y install curl sudo + adduser --disabled-password --gecos "" test + su -c "sh /source/install.sh" test + SH + exit: 1, + out: match("sudo apt-get") + +run_test "fedora: sudo in suggestions when running as user", + distro: :fedora, + cmd: <<~SH, + dnf -y install curl util-linux + adduser test + runuser -u test -- sh /source/install.sh + SH + exit: 1, + out: match("sudo dnf") + +run_test "debian: warns about missing runtime libraries", + distro: :debian, + cmd: <<~SH, + apt-get update && apt-get -y install curl + apt-get -y --allow-remove-essential remove util-linux libudev1 libapt-pkg6.0 apt + sh /source/install.sh + SH + exit: 1, + out: [ + match("runtime shared libraries are missing"), + match("libdbus-1"), + match("libudev") + ] + +run_test "fedora: warns about missing runtime libraries", + distro: :fedora, + cmd: "dnf -y install curl; rpm -e --nodeps systemd-libs; sh /source/install.sh", + exit: 1, + out: [ + match("runtime shared libraries are missing"), + match("libdbus-1"), + match("libudev") + ] + +run_test "debian: successful install", + distro: :debian, + cmd: "apt-get update && apt-get -y install curl libdbus-1-3 libudev1; sh /source/install.sh", + out: [ + match("Stellar CLI installed successfully"), + doesnt_match("runtime shared libraries are missing") + ] + +run_test "ubuntu: successful install", + distro: :ubuntu, + cmd: "apt-get update && apt-get -y install curl libdbus-1-3 libudev1; sh /source/install.sh", + out: [ + match("Stellar CLI installed successfully"), + doesnt_match("runtime shared libraries are missing") + ] + +run_test "fedora: successful install", + distro: :fedora, + cmd: "dnf -y install curl dbus-libs systemd-libs; sh /source/install.sh", + out: [ + match("Stellar CLI installed successfully"), + doesnt_match("runtime shared libraries are missing") + ] + +run_test "debian: install missing dependencies", + distro: :debian, + cmd: "apt-get update && apt-get -y install curl; sh /source/install.sh --install-deps", + out: [ + match("Stellar CLI installed successfully"), + match("[OK] rustup"), + match("[OK] cargo"), + match("[OK] rustc"), + match("[OK] wasm32v1-none target"), + doesnt_match("runtime shared libraries are missing") + ] + +run_test "ubuntu: install missing dependencies", + distro: :ubuntu, + cmd: "apt-get update && apt-get -y install curl; sh /source/install.sh --install-deps", + out: [ + match("Stellar CLI installed successfully"), + match("[OK] rustup"), + match("[OK] cargo"), + match("[OK] rustc"), + match("[OK] wasm32v1-none target"), + doesnt_match("runtime shared libraries are missing") + ] + +run_test "fedora: install missing dependencies", + distro: :fedora, + cmd: "dnf -y install curl; sh /source/install.sh --install-deps", + out: [ + match("Stellar CLI installed successfully"), + match("[OK] rustup"), + match("[OK] cargo"), + match("[OK] rustc"), + match("[OK] wasm32v1-none target"), + doesnt_match("runtime shared libraries are missing") + ] + +exit $EXPECTED_EXIT