diff --git a/lua/remote-nvim/providers/devpod/devpod_provider.lua b/lua/remote-nvim/providers/devpod/devpod_provider.lua index 08b04d68..30bb5337 100644 --- a/lua/remote-nvim/providers/devpod/devpod_provider.lua +++ b/lua/remote-nvim/providers/devpod/devpod_provider.lua @@ -158,7 +158,11 @@ function DevpodProvider:_handle_provider_setup() if self._devpod_provider then self.local_provider:run_command( ("%s provider list --output json"):format(remote_nvim.config.devpod.binary), - ("Checking if the %s provider is present"):format(self._devpod_provider) + ("Checking if the %s provider is present"):format(self._devpod_provider), + nil, + nil, + false, + false ) local stdout = self.local_provider.executor:job_stdout() local provider_list_output = vim.json.decode(vim.tbl_isempty(stdout) and "{}" or table.concat(stdout, "\n")) diff --git a/lua/remote-nvim/providers/executor.lua b/lua/remote-nvim/providers/executor.lua index d73336eb..61a867dd 100644 --- a/lua/remote-nvim/providers/executor.lua +++ b/lua/remote-nvim/providers/executor.lua @@ -11,6 +11,7 @@ local Executor = require("remote-nvim.middleclass")("Executor") ---@field additional_opts? string[] Additional options to pass to the `tar` command. See `man tar` for possible options ---@class remote-nvim.provider.Executor.JobOpts +---@field pty boolean? Whether to set pty option ---@field additional_conn_opts string? Connection options ---@field stdout_cb function? Standard output callback ---@field exit_cb function? On exit callback @@ -76,7 +77,7 @@ function Executor:run_executor_job(command, job_opts) self:reset() -- Reset job internal state variables self._job_id = vim.fn.jobstart(command, { - pty = true, + pty = job_opts.pty == nil or job_opts.pty, on_stdout = function(_, data_chunk) self:process_stdout(data_chunk, job_opts.stdout_cb) end, diff --git a/lua/remote-nvim/providers/provider.lua b/lua/remote-nvim/providers/provider.lua index d55dc026..2486b97c 100644 --- a/lua/remote-nvim/providers/provider.lua +++ b/lua/remote-nvim/providers/provider.lua @@ -1,7 +1,7 @@ ---@alias provider_type "ssh"|"devpod"|"local" ---@alias os_type "macOS"|"Windows"|"Linux" ---@alias arch_type "x86_64"|"arm64" ----@alias neovim_install_method "binary"|"source"|"system" +---@alias neovim_install_method "binary"|"tarball"|"source"|"system" ---@class remote-nvim.providers.WorkspaceConfig ---@field provider provider_type? Which provider is responsible for managing this workspace @@ -17,6 +17,7 @@ ---@field client_auto_start boolean? Flag indicating if the client should be auto started or not ---@field offline_mode boolean? Should we operate in offline mode ---@field devpod_source_opts remote-nvim.providers.DevpodSourceOpts? Devpod related source options +---@field is_docker_container boolean? Flag indicating if the remote is running in a Docker container ---@class remote-nvim.providers.Provider: remote-nvim.Object ---@field host string Host name @@ -175,6 +176,18 @@ function Provider:_setup_workspace_variables() self._host_config.neovim_install_method = "source" prompt_title = "Binary release not available. Choose Neovim version to install" end + + -- Check if remote is running in Docker container and override install method + local is_docker = self:_is_remote_docker_container() + if is_docker and self._host_config.neovim_install_method == "binary" then + self._host_config.neovim_install_method = "tarball" + prompt_title = "Docker container detected. Choose Neovim version to install (using tarball)" + self.logger.info("Overriding install method from 'binary' to 'tarball' due to Docker container detection") + end + + -- Store Docker detection result for future reference + self._host_config.is_docker_container = is_docker + self._remote_neovim_install_method = self._host_config.neovim_install_method self._host_config.neovim_version = self:_get_remote_neovim_version_preference(prompt_title) @@ -186,6 +199,7 @@ function Provider:_setup_workspace_variables() self._config_provider:update_workspace_config(self.unique_host_id, { neovim_install_method = self._host_config.neovim_install_method, neovim_version = self._host_config.neovim_version, + is_docker_container = self._host_config.is_docker_container, }) end self._remote_neovim_version = self._host_config.neovim_version @@ -358,6 +372,34 @@ function Provider:_get_remote_os_and_arch() return self._remote_os, self._remote_arch end +---@private +---Detect if the remote host is running in a Docker container +---@return boolean is_docker True if running in Docker container +function Provider:_is_remote_docker_container() + -- Check multiple indicators of container environment + local docker_checks = { + "test -f /.dockerenv", -- Standard Docker indicator file + "test -f /run/.containerenv", -- Standard Podman indicator file + 'test -n "${container}"', -- Container environment variable + "grep -q 'docker\\|lxc' /proc/1/cgroup 2>/dev/null", -- Process cgroup check + "test -f /proc/self/mountinfo && grep -q 'docker' /proc/self/mountinfo 2>/dev/null", -- Mount info check + } + + local check_cmd = table.concat(docker_checks, " || ") + local full_cmd = ("(%s) && echo 'DOCKER_DETECTED' || echo 'NOT_DOCKER'"):format(check_cmd) + + self:run_command(full_cmd, "Checking if remote environment is a Docker container") + local cmd_output = self.executor:job_stdout() + local result = cmd_output[#cmd_output] or "" + + local is_docker = result:find("DOCKER_DETECTED") ~= nil + if is_docker then + self.logger.info("Docker container detected on remote host - will use tarball installation instead of AppImage") + end + + return is_docker +end + ---@private ---Get user's home directory on the remote host ---@return string home_path User's home directory path @@ -655,7 +697,7 @@ function Provider:_setup_remote() ) local local_upload_paths = { local_release_path } - if self._remote_neovim_install_method == "binary" then + if self._remote_neovim_install_method == "binary" or self._remote_neovim_install_method == "tarball" then table.insert(local_upload_paths, ("%s.sha256sum"):format(local_release_path)) end self:upload( @@ -1016,7 +1058,8 @@ end ---@param extra_opts string? Extra options to pass to the underlying command ---@param exit_cb function? Exit callback to execute ---@param on_local_executor boolean? Should run this command on the local executor -function Provider:run_command(command, desc, extra_opts, exit_cb, on_local_executor) +---@param pty boolean? Whether to use pty or not +function Provider:run_command(command, desc, extra_opts, exit_cb, on_local_executor, pty) self.logger.fmt_debug("[%s][%s] Running %s", self.provider_type, self.unique_host_id, command) on_local_executor = on_local_executor or false local executor = on_local_executor and self.local_executor or self.executor @@ -1033,6 +1076,7 @@ function Provider:run_command(command, desc, extra_opts, exit_cb, on_local_execu exit_cb = exit_cb(section_node) end executor:run_command(command, { + pty = pty == nil or pty, additional_conn_opts = extra_opts, exit_cb = exit_cb, stdout_cb = self:_get_stdout_fn_for_node(section_node), diff --git a/scripts/neovim_download.sh b/scripts/neovim_download.sh index e12e154f..6707e15b 100755 --- a/scripts/neovim_download.sh +++ b/scripts/neovim_download.sh @@ -19,7 +19,7 @@ Options: -v Specify the desired Neovim version to download. -d Specify directory inside which Neovim release should be downloaded. -o OS whose binary is to be downloaded. - -t What to download: 'binary' or 'source' + -t What to download: 'binary', 'tarball' or 'source' -a Specify architecture that should be downloaded. -h Display this help message and exit. EOM @@ -67,6 +67,39 @@ function download_neovim_source() { info "Downloaded Neovim source version ${version} to ${download_path}" } +# Download Neovim tarball +function download_neovim_tarball() { + local os="$1" version="$2" download_dir="$3" arch_type="$4" + + local download_url + local download_path + local checksum_path + local expected_checksum + local actual_checksum + download_url=$(safe_subshell build_tarball_github_uri "$version" "$os" "$arch_type") + download_path="$download_dir/$(basename "$download_url")" + + checksum_path="$download_path".sha256sum + actual_checksum="$expected_checksum-actual" # This ensures that they do not match + expected_checksum=$(safe_subshell get_tarball_sha256 "$version" "$os" "$arch_type") + + if [ -e "$download_path" ] && [ -e "$checksum_path" ]; then + expected_checksum=$(<"$checksum_path") + actual_checksum=$(sha256sum "$download_path" | cut -d ' ' -f 1) + fi + + if [ "$actual_checksum" == "$expected_checksum" ]; then + info "Existing installation with matching checksum found. Skipping downloading..." + return 0 + fi + + download_file "$download_url" "$download_path" + info "Downloaded Neovim tarball ${version} for ${os} (${arch_type}) to ${download_path}" + + # Save checksum for future verification + echo "$expected_checksum" >"$checksum_path" +} + # Parse command-line options while getopts "v:d:o:t:a:h" opt; do case $opt in @@ -129,8 +162,10 @@ fi if [[ $download_type == "source" ]]; then download_neovim_source "$nvim_version" "$download_dir" +elif [[ $download_type == "tarball" ]]; then + download_neovim_tarball "$os_name" "$nvim_version" "$download_dir" "$arch_type" elif [[ $download_type == "system" ]]; then - error "Cannot download a system-type Neovim release. Choose from either 'source' or 'binary'." + error "Cannot download a system-type Neovim release. Choose from 'source', 'binary', or 'tarball'." exit 1 else download_neovim "$os_name" "$nvim_version" "$download_dir" "$arch_type" diff --git a/scripts/neovim_install.sh b/scripts/neovim_install.sh index 9644a4d9..5c14dda5 100755 --- a/scripts/neovim_install.sh +++ b/scripts/neovim_install.sh @@ -42,7 +42,7 @@ Options: -d Specify directory for storing Neovim binaries. NOTE: Installation would happen in 'nvim-downloads' subdirectory. -f Force installation. Would overwrite any existing installation. - -m Installation method: binary, source, system + -m Installation method: binary, tarball, source, system -a Architecture type of the machine -o Offline mode. Assume release is already downloaded. -h Display this help message and exit. @@ -126,6 +126,51 @@ function setup_neovim_linux_appimage() { ln -sf "$nvim_version_dir"/usr/bin/nvim "$nvim_binary" } +# Install on Linux using tarball +function setup_neovim_linux_tarball() { + local version="$1" arch_type="$2" + + local nvim_release_name + local download_url + download_url=$(safe_subshell build_tarball_github_uri "$version" "Linux" "$arch_type") + nvim_release_name=$(basename "$download_url") + local nvim_tarball_temp_path="$temp_dir/$nvim_release_name" + + if [ ! -e "$nvim_version_dir/$nvim_release_name" ]; then + error "Expected release to be present at $nvim_version_dir/$nvim_release_name. Aborting..." + exit 1 + fi + + cp "$nvim_version_dir/$nvim_release_name" "$nvim_tarball_temp_path" + + info "Extracting Neovim tarball..." + tar -xzf "$nvim_tarball_temp_path" -C "$temp_dir" + + info "Finishing up installing Neovim..." + # Determine the extracted directory name based on version + local extracted_dir + local is_lesser + is_lesser_version "$version" v0.10.4 + is_lesser=$? + + if [[ $is_lesser -eq 0 ]]; then + # Older versions extract to nvim-linux64 + extracted_dir="nvim-linux64" + else + # Newer versions extract to nvim-linux- + extracted_dir="nvim-linux-${arch_type}" + fi + + if [ ! -d "$temp_dir/$extracted_dir" ]; then + error "Expected extracted directory $temp_dir/$extracted_dir not found. Aborting..." + exit 1 + fi + + # Move extracted contents to version directory + mkdir -p "$nvim_version_dir" + mv -f "$temp_dir/$extracted_dir"/* "$nvim_version_dir" +} + # Function to download and decompress Neovim binary for macOS function setup_neovim_macos() { local version="$1" arch_type="$2" @@ -196,6 +241,22 @@ function install_neovim() { echo "Unsupported operating system: $(uname)" exit 1 fi + elif [[ $install_method == "tarball" ]]; then + if [ "$offline_mode" == true ]; then + info "Operating in offline mode. Will not download Neovim tarball" + else + "$download_neovim_script" -o "$os" -v "$nvim_version" -d "$nvim_version_dir" -t "tarball" -a "$arch_type" + fi + + # Install Neovim tarball based on the detected OS + if [[ $os == "Linux" ]]; then + safe_subshell setup_neovim_linux_tarball "$nvim_version" "$arch_type" + elif [[ $os == "Darwin" ]]; then + safe_subshell setup_neovim_macos "$nvim_version" "$arch_type" + else + echo "Unsupported operating system: $(uname)" + exit 1 + fi elif [[ $install_method == "source" ]]; then if [ "$offline_mode" == true ]; then info "Operating in offline mode. Will not download Neovim source" diff --git a/scripts/utils/neovim.sh b/scripts/utils/neovim.sh index bb325381..668098fc 100755 --- a/scripts/utils/neovim.sh +++ b/scripts/utils/neovim.sh @@ -57,6 +57,23 @@ function _linux_asset_name { echo "$asset_name" } +function _linux_tarball_asset_name { + # If version is less than 0.10.4, tarball name is nvim-linux64.tar.gz + # else it includes architecture: nvim-linux-x86_64.tar.gz or nvim-linux-arm64.tar.gz + local version="$1" arch_type="$2" + local asset_name="nvim-linux-${arch_type}.tar.gz" + + info "Determining tarball asset name for Neovim version $version on Linux with architecture $arch_type" + local is_lesser + is_lesser_version "$version" v0.10.4 + is_lesser=$? + + if [[ $is_lesser -eq 0 ]]; then + asset_name="nvim-linux64.tar.gz" + fi + echo "$asset_name" +} + function _macos_asset_name { # If version is less than 0.10.0, there is no architecture in the download URL # else it has to be added into the URL suffix. @@ -89,6 +106,22 @@ function _get_asset_name { echo "$asset_name" } +function _get_tarball_asset_name { + local version="$1" os="$2" arch_type="$3" + + local asset_name + if [[ $os == "Linux" ]]; then + asset_name=$(safe_subshell _linux_tarball_asset_name "$version" "$arch_type") + elif [[ $os == "Darwin" ]]; then + asset_name=$(safe_subshell _macos_asset_name "$version" "$arch_type") + else + fatal --status=3 "Unsupported OS: $os" + fi + + debug "Tarball asset name for ${os} ${arch_type} version ${version}: ${asset_name}" + echo "$asset_name" +} + # Get release download URL based on OS, Arch type and version function build_github_uri { local VERSION=$1 OS=$2 ARCH_TYPE=$3 @@ -101,6 +134,18 @@ function build_github_uri { echo "${BASE_GITHUB_URI_PATH}/${ASSET_NAME}" } +# Get tarball download URL based on OS, Arch type and version +function build_tarball_github_uri { + local VERSION=$1 OS=$2 ARCH_TYPE=$3 + + local BASE_GITHUB_URI_PATH="https://github.com/neovim/neovim/releases/download/${VERSION}" + + local ASSET_NAME + ASSET_NAME=$(safe_subshell _get_tarball_asset_name "$VERSION" "$OS" "$ARCH_TYPE") + + echo "${BASE_GITHUB_URI_PATH}/${ASSET_NAME}" +} + function _find_sha256_for_version { local url="$1" local release_json @@ -168,3 +213,43 @@ function get_sha256 { debug "SHA256 sum: $SHA256_SUM for release item $DOWNLOAD_URI" echo "$SHA256_SUM" } + +function get_tarball_sha256 { + local VERSION=$1 OS=$2 ARCH_TYPE=$3 + + local DOWNLOAD_URI + DOWNLOAD_URI=$(safe_subshell build_tarball_github_uri "$VERSION" "$OS" "$ARCH_TYPE") + + local is_lesser + is_lesser_version "$VERSION" v0.11.3 + is_lesser=$? + + local SHA256_URI + local SHA256_SUM + if [[ $is_lesser -eq 0 ]]; then + info "Neovim version $VERSION is less than 0.11.3, using legacy checksum file" + + SHA256_URI="${DOWNLOAD_URI}.sha256sum" + SHA256_SUM=$(safe_subshell run_api_call "$SHA256_URI") + + SHA256_SUM=$(safe_subshell printf '%s\n' "$SHA256_SUM" | awk '{print $1}') + else + info "Neovim version $VERSION is greater than or equal to 0.11.3, using GitHub's checksum API" + + SHA256_URI="https://api.github.com/repos/neovim/neovim/releases/tags/${VERSION}" + debug "Downloading SHA256 from $SHA256_URI" + + local response + response=$(safe_subshell run_api_call "$SHA256_URI") + debug "Response from GitHub API: $response" + + SHA256_SUM=$(safe_subshell printf '%s\n' "$response" | _find_sha256_for_version "$DOWNLOAD_URI") + fi + + if [[ -z $SHA256_SUM ]]; then + fatal --status=3 "Failed to retrieve SHA256 sum for release item $DOWNLOAD_URI" + fi + + debug "SHA256 sum: $SHA256_SUM for release item $DOWNLOAD_URI" + echo "$SHA256_SUM" +} diff --git a/tests/remote-nvim/providers/provider_spec.lua b/tests/remote-nvim/providers/provider_spec.lua index 055e1141..6c69140a 100644 --- a/tests/remote-nvim/providers/provider_spec.lua +++ b/tests/remote-nvim/providers/provider_spec.lua @@ -114,6 +114,7 @@ describe("Provider", function() assert.are.same({ provider = "local", host = provider.host, + is_docker_container = false, arch = "x86_64", neovim_install_method = "binary", connection_options = provider.conn_opts,