From 0e760b8729c91d5b1b5cfe971368e55f0087e019 Mon Sep 17 00:00:00 2001 From: proxy-turkey Date: Thu, 29 Oct 2026 07:47:03 +0300 Subject: [PATCH] feat: support RPM-based Linux distributions in install.sh Fedora, RHEL, and openSUSE users currently get a hard stop because the installer only knows apt-get. The Linux release is a .deb, so the installer now detects the package-manager family and, on rpm-based systems: - prefers a native CommandCode--x86_64.rpm as soon as a release publishes one (no further changes needed when that happens), - otherwise downloads the .deb and verifies its SHA-256 digest exactly as on Debian/Ubuntu, then repacks the verified payload into a local RPM with rpmbuild and installs it through dnf or zypper, keeping the package tracked and removable by the package manager, - falls back to installing the payload directly with the same file layout, launcher, and desktop entry when rpmbuild is not available, printing the exact paths to remove on uninstall, - sets the SUID bit on chrome-sandbox (same as the deb postinst fallback), refreshes desktop/icon caches, and relabels files on SELinux systems. Payload sanity checks mirror the macOS installer (executable, desktop entry, compiled engine) and everything runs only after SHA-256 verification. The apt and macOS paths are unchanged. --- INSTALL.md | 21 +++- README.md | 7 +- install.sh | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 290 insertions(+), 14 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index c0f1f72..11ba0e8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -18,7 +18,8 @@ irm https://raw.githubusercontent.com/CommandCodeAI/desktop/main/install.ps1 | i 1. Detect the operating system and supported CPU architecture. 2. Read the newest release from `CommandCodeAI/desktop`. -3. Select the matching `.dmg`, `.deb`, or `.exe`. +3. Select the matching `.dmg`, `.deb`, or `.exe` (or a native `.rpm` when + one is published). 4. Verify the SHA-256 digest published by GitHub. 5. Install and open Command Code. @@ -37,6 +38,24 @@ The source is public: - [install.sh](install.sh) - [install.ps1](install.ps1) +## RPM-based Linux (Fedora, RHEL, openSUSE) + +Releases currently publish a Linux `.deb`. On rpm-based distributions the +installer: + +1. Downloads the `.deb` and verifies its SHA-256 digest first, exactly as on + Debian and Ubuntu. +2. Prefers a native `.rpm` automatically as soon as a release publishes one. +3. Unpacks the verified `.deb` and repacks the payload into a local RPM with + `rpmbuild`, then installs it with `dnf` or `zypper` so the package is + tracked and removable through the package manager. +4. Falls back to installing the payload directly (with the same file layout, + launcher, and desktop entry) when `rpmbuild` is not available; that + variant prints the exact paths to remove when uninstalling. + +Uninstall a local-RPM installation with `sudo dnf remove command-code` (or +`sudo zypper remove command-code`). + ## Install a specific version macOS or Linux: diff --git a/README.md b/README.md index 9f41483..8fcc0ec 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Running the same command again installs the newest available version. ### macOS and Linux One command for both. The script detects the system - Apple silicon or Intel -macOS, or Ubuntu/Debian x64 Linux - and installs the matching build: +macOS, or x64 Linux (Debian, Ubuntu, Fedora, RHEL, openSUSE) - and installs +the matching build: ```bash curl -fsSL https://raw.githubusercontent.com/CommandCodeAI/desktop/main/install.sh | bash @@ -72,6 +73,10 @@ automatically. Manual packages are available on the | Linux | `.deb` | x64 | | Windows | `.exe` | x64 | +On Fedora, RHEL, and openSUSE, the installer prefers a native `.rpm` when a +release publishes one and otherwise installs from the verified `.deb`, +repacking it into a local RPM on the machine. + ## What you can do - Organize projects and session history diff --git a/install.sh b/install.sh index 3f441cb..5390962 100755 --- a/install.sh +++ b/install.sh @@ -6,6 +6,8 @@ REPOSITORY="CommandCodeAI/desktop" API_ROOT="https://api.github.com/repos/$REPOSITORY" TEMPORARY_DIRECTORY="$(mktemp -d "${TMPDIR:-/tmp}/command-code-install.XXXXXX")" MOUNT_POINT="" +linux_flavor="" +linux_installer="" cleanup() { if [ -n "$MOUNT_POINT" ]; then @@ -98,6 +100,37 @@ verify_sha256() { fail "Downloaded file failed SHA-256 verification." } +detect_linux_package_manager() { + if command -v apt-get >/dev/null 2>&1; then + linux_flavor="apt" + linux_installer="apt-get" + elif command -v dnf >/dev/null 2>&1; then + linux_flavor="rpm" + linux_installer="dnf" + elif command -v zypper >/dev/null 2>&1; then + linux_flavor="rpm" + linux_installer="zypper" + elif command -v yum >/dev/null 2>&1; then + linux_flavor="rpm" + linux_installer="yum" + fi +} + +# On rpm-based systems prefer a native .rpm when the release publishes one; +# otherwise fall back to the .deb, which this installer repacks locally. +artifact_candidates() { + version_for_artifacts="$1" + + if [ "${linux_flavor:-}" = "rpm" ]; then + printf '%s\n' \ + "CommandCode-$version_for_artifacts-x86_64.rpm" \ + "CommandCode-$version_for_artifacts-$artifact_architecture.$artifact_extension" + else + printf '%s\n' \ + "CommandCode-$version_for_artifacts-$artifact_architecture.$artifact_extension" + fi +} + replace_macos_app() { staged_app="$1" destination="/Applications/Command Code.app" @@ -187,13 +220,228 @@ install_macos() { open "$destination" } +extract_deb_payload() { + deb_path="$1" + payload_root="$2" + payload_archive="" + + if command -v ar >/dev/null 2>&1; then + extract_directory="$TEMPORARY_DIRECTORY/deb" + mkdir -p "$extract_directory" + (cd "$extract_directory" && ar x "$deb_path") || + fail "Could not unpack the downloaded .deb package." + for archive_member in data.tar.xz data.tar.gz; do + if [ -f "$extract_directory/$archive_member" ]; then + payload_archive="$extract_directory/$archive_member" + break + fi + done + elif command -v bsdtar >/dev/null 2>&1; then + for archive_member in data.tar.xz data.tar.gz; do + if bsdtar -x -f "$deb_path" -C "$TEMPORARY_DIRECTORY" "$archive_member" >/dev/null 2>&1; then + payload_archive="$TEMPORARY_DIRECTORY/$archive_member" + break + fi + done + else + fail "ar (binutils) or bsdtar is required to unpack the package on this system." + fi + + [ -n "$payload_archive" ] || + fail "Could not find the payload inside the downloaded .deb package." + mkdir -p "$payload_root" + tar -xf "$payload_archive" -C "$payload_root" || + fail "Could not extract the payload from the downloaded .deb package." + + [ -x "$payload_root/opt/Command Code/command-code" ] || + fail "The package payload is missing the Command Code executable." + [ -s "$payload_root/usr/share/applications/command-code.desktop" ] || + fail "The package payload is missing the desktop entry." + [ -s "$payload_root/opt/Command Code/resources/app/node_modules/command-code/dist/cli.mjs" ] || + fail "The package payload is missing the compiled engine." +} + +install_rpm_package() { + rpm_path="$1" + + printf 'Administrator permission is required to install the package.\n' + case "$linux_installer" in + dnf | yum) + if ! sudo "$linux_installer" install -y "$rpm_path" >/dev/null; then + sudo "$linux_installer" reinstall -y "$rpm_path" || + fail "Could not install the RPM package." + fi + ;; + zypper) + sudo zypper --non-interactive install "$rpm_path" || + fail "Could not install the RPM package." + ;; + *) + fail "Unsupported package manager: $linux_installer." + ;; + esac +} + +install_linux_rpm_from_deb() { + deb_path="$1" + payload_root="$TEMPORARY_DIRECTORY/payload" + + printf 'This release has no native .rpm; repacking the verified .deb.\n' + extract_deb_payload "$deb_path" "$payload_root" + + if command -v rpmbuild >/dev/null 2>&1; then + install_linux_rpm_via_rpmbuild "$payload_root" + else + install_linux_rpm_direct "$payload_root" + fi +} + +install_linux_rpm_via_rpmbuild() { + payload_root="$1" + rpm_version="${version//-/.}" + build_root="$TEMPORARY_DIRECTORY/rpmbuild" + spec_path="$build_root/SPECS/command-code.spec" + doc_install="" + doc_files="" + + mkdir -p "$build_root/BUILD" "$build_root/BUILDROOT" "$build_root/RPMS" \ + "$build_root/SOURCES" "$build_root/SPECS" "$build_root/SRPMS" + if [ -d "$payload_root/usr/share/doc/command-code" ]; then + doc_install="mkdir -p %{buildroot}%{_docdir}/%{name} +cp -a '$payload_root/usr/share/doc/command-code/.' %{buildroot}%{_docdir}/%{name}/" + doc_files="%{_docdir}/%{name}/*" + fi + + cat >"$spec_path" </dev/null 2>&1 || : +if [ -x /usr/bin/update-desktop-database ]; then + /usr/bin/update-desktop-database /usr/share/applications >/dev/null 2>&1 || : +fi +if [ -x /usr/bin/gtk-update-icon-cache ]; then + /usr/bin/gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor >/dev/null 2>&1 || : +fi +if [ -x /usr/sbin/restorecon ]; then + /usr/sbin/restorecon -R '/opt/Command Code' /usr/bin/command-code >/dev/null 2>&1 || : +fi + +%postun +if [ -x /usr/bin/gtk-update-icon-cache ]; then + /usr/bin/gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor >/dev/null 2>&1 || : +fi +SPEC + + if ! rpmbuild -bb --define "_topdir $build_root" "$spec_path" \ + >"$TEMPORARY_DIRECTORY/rpmbuild.log" 2>&1; then + build_error="$(tr '\n' ' ' <"$TEMPORARY_DIRECTORY/rpmbuild.log" | cut -c1-300)" + fail "Could not build a local RPM from the verified download. $build_error" + fi + + built_rpm="$(find "$build_root/RPMS" -type f -name '*.rpm' -print -quit)" + [ -n "$built_rpm" ] || + fail "The local RPM build produced no package." + + install_rpm_package "$built_rpm" +} + +install_linux_rpm_direct() { + payload_root="$1" + + printf 'rpmbuild is not installed; installing the payload directly.\n' + printf 'Administrator permission is required to install the package.\n' + sudo cp -a "$payload_root/opt/." /opt/ || + fail "Could not copy Command Code into /opt." + sudo mkdir -p /usr/share/applications /usr/share/icons + sudo cp -a "$payload_root/usr/share/applications/command-code.desktop" /usr/share/applications/ || + fail "Could not install the desktop entry." + sudo cp -a "$payload_root/usr/share/icons/." /usr/share/icons/ || + fail "Could not install the icons." + if [ -d "$payload_root/usr/share/doc/command-code" ]; then + sudo mkdir -p /usr/share/doc/command-code + sudo cp -a "$payload_root/usr/share/doc/command-code/." /usr/share/doc/command-code/ || true + fi + sudo ln -sf '/opt/Command Code/command-code' /usr/bin/command-code || + fail "Could not create the command-code launcher." + sudo chmod 4755 '/opt/Command Code/chrome-sandbox' + sudo update-desktop-database /usr/share/applications >/dev/null 2>&1 || true + sudo gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor >/dev/null 2>&1 || true + sudo restorecon -R '/opt/Command Code' /usr/bin/command-code >/dev/null 2>&1 || true + + printf 'To remove this direct installation, run:\n' + printf " sudo rm -rf '/opt/Command Code' /usr/bin/command-code /usr/share/doc/command-code\n" + printf ' sudo rm -f /usr/share/applications/command-code.desktop\n' + printf ' sudo rm -f /usr/share/icons/hicolor/*/apps/command-code.png\n' +} + install_linux() { archive_path="$1" - command -v apt-get >/dev/null 2>&1 || - fail "This preview currently supports Debian and Ubuntu through a .deb package." - printf 'Administrator permission is required to install the package.\n' - sudo apt-get install -y "$archive_path" + case "$linux_flavor" in + apt) + printf 'Administrator permission is required to install the package.\n' + sudo apt-get install -y "$archive_path" + ;; + rpm) + case "$artifact_extension" in + rpm) install_rpm_package "$archive_path" ;; + deb) install_linux_rpm_from_deb "$archive_path" ;; + esac + ;; + *) + fail "This preview supports Debian and Ubuntu through a .deb package and Fedora, RHEL, and openSUSE through the installer." + ;; + esac } require_command curl @@ -229,6 +477,9 @@ case "$operating_system:$machine_architecture" in platform="Linux" artifact_architecture="amd64" artifact_extension="deb" + detect_linux_package_manager + [ -n "$linux_flavor" ] || + fail "No supported package manager found. This preview supports Debian and Ubuntu through a .deb package and Fedora, RHEL, and openSUSE through the installer." ;; Darwin:*) fail "The current macOS preview supports Apple silicon only." @@ -274,14 +525,15 @@ asset_metadata="" while IFS= read -r candidate_version; do [ -n "$candidate_version" ] || continue is_valid_version "$candidate_version" || continue - candidate_artifact="CommandCode-$candidate_version-$artifact_architecture.$artifact_extension" - candidate_metadata="$(read_asset_metadata "$candidate_artifact" "$release_file")" - if [ -n "$candidate_metadata" ]; then - version="$candidate_version" - artifact_name="$candidate_artifact" - asset_metadata="$candidate_metadata" - break - fi + for candidate_artifact in $(artifact_candidates "$candidate_version"); do + candidate_metadata="$(read_asset_metadata "$candidate_artifact" "$release_file")" + if [ -n "$candidate_metadata" ]; then + version="$candidate_version" + artifact_name="$candidate_artifact" + asset_metadata="$candidate_metadata" + break 2 + fi + done done <