summaryrefslogtreecommitdiff
path: root/tramp-rpc/lisp/tramp-rpc-deploy.el
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-01-21 05:34:19 +0100
committercashmere <cashmere@autistici.org>2026-01-21 05:34:19 +0100
commit21ca8efb5fe1bf4577e34f735e5aea0f767343b4 (patch)
treed0743ff4651b72841c083e35fb76ddb15d019186 /tramp-rpc/lisp/tramp-rpc-deploy.el
parent1d255ed82ceaac7561869c46f2183cea9eae2768 (diff)
downloademacs-21ca8efb5fe1bf4577e34f735e5aea0f767343b4.tar.gz
sync
Diffstat (limited to 'tramp-rpc/lisp/tramp-rpc-deploy.el')
-rw-r--r--tramp-rpc/lisp/tramp-rpc-deploy.el774
1 files changed, 774 insertions, 0 deletions
diff --git a/tramp-rpc/lisp/tramp-rpc-deploy.el b/tramp-rpc/lisp/tramp-rpc-deploy.el
new file mode 100644
index 0000000..3cfcb2d
--- /dev/null
+++ b/tramp-rpc/lisp/tramp-rpc-deploy.el
@@ -0,0 +1,774 @@
+;;; tramp-rpc-deploy.el --- Binary deployment for TRAMP-RPC -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+
+;; Author: Arthur Heymans <arthur@aheymans.xyz>
+;; Keywords: comm, processes
+;; Package-Requires: ((emacs "30.1"))
+
+;; This file is part of tramp-rpc.
+
+;;; Commentary:
+
+;; This file handles deployment of the tramp-rpc-server binary to
+;; remote hosts. It supports:
+;; - Automatic detection of remote architecture
+;; - Downloading pre-compiled binaries from GitHub releases
+;; - Building from source as fallback (requires Rust)
+;; - Local caching of binaries
+;; - Transfer to remote hosts with checksum verification
+
+;;; Code:
+
+(require 'tramp)
+(require 'url)
+
+;; Silence byte-compiler warnings for functions defined in tramp-sh
+(declare-function tramp-send-command "tramp-sh")
+(declare-function tramp-send-command-and-check "tramp-sh")
+(declare-function tramp-send-command-and-read "tramp-sh")
+
+;;; ============================================================================
+;;; Customization
+;;; ============================================================================
+
+(defgroup tramp-rpc-deploy nil
+ "Deployment settings for TRAMP-RPC."
+ :group 'tramp)
+
+(defconst tramp-rpc-deploy-version "0.3.0"
+ "Current version of tramp-rpc-server.")
+
+(defconst tramp-rpc-deploy-binary-name "tramp-rpc-server"
+ "Name of the server binary.")
+
+(defcustom tramp-rpc-deploy-github-repo "ArthurHeymans/emacs-tramp-rpc"
+ "GitHub repository for downloading pre-compiled binaries.
+Format: \"owner/repo\"."
+ :type 'string
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-release-url-format
+ "https://github.com/%s/releases/download/v%s/%s"
+ "URL format for downloading release assets.
+Arguments: repo, version, filename."
+ :type 'string
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-local-cache-directory
+ (expand-file-name "tramp-rpc" user-emacs-directory)
+ "Local directory for caching downloaded/built binaries.
+Binaries are stored as CACHE-DIR/VERSION/ARCH/tramp-rpc-server."
+ :type 'directory
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-source-directory
+ (when load-file-name
+ (expand-file-name ".." (file-name-directory load-file-name)))
+ "Directory containing the tramp-rpc source code.
+Used for building from source. Set to nil to disable source builds."
+ :type '(choice directory (const nil))
+ :group 'tramp-rpc-deploy)
+
+(defconst tramp-rpc-deploy-bundled-binary-directory
+ (when load-file-name
+ (expand-file-name "binaries" (file-name-directory load-file-name)))
+ "Directory containing pre-built binaries bundled with the package.
+This is useful for development - binaries built by scripts/build-all.sh
+are placed here and used directly without needing to download or cache.")
+
+(defcustom tramp-rpc-deploy-remote-directory "~/.cache/tramp-rpc"
+ "Remote directory where the server binary will be installed."
+ :type 'string
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-auto-deploy t
+ "If non-nil, automatically deploy the server binary when needed."
+ :type 'boolean
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-prefer-build nil
+ "If non-nil, prefer building from source over downloading.
+By default, downloading is attempted first as it's faster."
+ :type 'boolean
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-bootstrap-method "sshx"
+ "TRAMP method to use for bootstrapping (deploying the binary).
+Use \"sshx\" to avoid PTY allocation issues, or \"ssh\" for standard SSH."
+ :type 'string
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-max-retries 3
+ "Maximum number of retries for binary transfer."
+ :type 'integer
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-download-timeout 120
+ "Timeout in seconds for downloading binaries."
+ :type 'integer
+ :group 'tramp-rpc-deploy)
+
+(defcustom tramp-rpc-deploy-debug nil
+ "When non-nil, log verbose debug messages during deployment.
+Messages are logged to *tramp-rpc-deploy* buffer."
+ :type 'boolean
+ :group 'tramp-rpc-deploy)
+
+(defun tramp-rpc-deploy--log (format-string &rest args)
+ "Log a debug message if `tramp-rpc-deploy-debug' is non-nil.
+FORMAT-STRING and ARGS are passed to `format'."
+ (when tramp-rpc-deploy-debug
+ (with-current-buffer (get-buffer-create "*tramp-rpc-deploy*")
+ (goto-char (point-max))
+ (insert (format-time-string "[%Y-%m-%d %H:%M:%S] ")
+ (apply #'format format-string args)
+ "\n"))))
+
+;;; ============================================================================
+;;; Architecture detection and path helpers
+;;; ============================================================================
+
+(defun tramp-rpc-deploy--bootstrap-vec (vec)
+ "Convert VEC to use the bootstrap method for deployment operations.
+This allows us to use sshx for deployment even when the main method is rpc."
+ (let ((method (tramp-file-name-method vec)))
+ (if (member method '("ssh" "sshx" "scpx"))
+ vec ; Already a shell-based method
+ ;; Convert to bootstrap method - create a new tramp-file-name struct
+ (make-tramp-file-name
+ :method tramp-rpc-deploy-bootstrap-method
+ :user (tramp-file-name-user vec)
+ :domain (tramp-file-name-domain vec)
+ :host (tramp-file-name-host vec)
+ :port (tramp-file-name-port vec)
+ :localname (tramp-file-name-localname vec)
+ :hop (tramp-file-name-hop vec)))))
+
+(defun tramp-rpc-deploy--detect-remote-arch (vec)
+ "Detect the architecture of remote host specified by VEC.
+Returns a string like \"x86_64-linux\" or \"aarch64-darwin\"."
+ (let* ((uname-m (string-trim
+ (tramp-send-command-and-read
+ vec "echo \\\"`uname -m`\\\"")))
+ (uname-s (string-trim
+ (tramp-send-command-and-read
+ vec "echo \\\"`uname -s`\\\"")))
+ (arch (pcase uname-m
+ ("x86_64" "x86_64")
+ ("amd64" "x86_64")
+ ("aarch64" "aarch64")
+ ("arm64" "aarch64")
+ (_ uname-m)))
+ (os (pcase (downcase uname-s)
+ ("linux" "linux")
+ ("darwin" "darwin")
+ (_ (downcase uname-s)))))
+ (format "%s-%s" arch os)))
+
+(defun tramp-rpc-deploy--detect-local-arch ()
+ "Detect the architecture of the local system.
+Returns a string like \"x86_64-linux\" or \"aarch64-darwin\"."
+ (let* ((arch (pcase system-type
+ ('gnu/linux "linux")
+ ('darwin "darwin")
+ (_ (symbol-name system-type))))
+ (machine (car (split-string system-configuration "-")))
+ (normalized-machine (pcase machine
+ ("x86_64" "x86_64")
+ ("aarch64" "aarch64")
+ ("arm64" "aarch64")
+ (_ machine))))
+ (format "%s-%s" normalized-machine arch)))
+
+(defun tramp-rpc-deploy--arch-to-rust-target (arch)
+ "Convert ARCH string to Rust target triple.
+E.g., \"x86_64-linux\" -> \"x86_64-unknown-linux-musl\".
+Linux targets use musl for fully static binaries."
+ (pcase arch
+ ("x86_64-linux" "x86_64-unknown-linux-musl")
+ ("aarch64-linux" "aarch64-unknown-linux-musl")
+ ("x86_64-darwin" "x86_64-apple-darwin")
+ ("aarch64-darwin" "aarch64-apple-darwin")
+ (_ (error "Unknown architecture: %s" arch))))
+
+(defun tramp-rpc-deploy--local-cache-path (arch)
+ "Return the local cache path for binary of ARCH."
+ (expand-file-name
+ tramp-rpc-deploy-binary-name
+ (expand-file-name
+ arch
+ (expand-file-name
+ tramp-rpc-deploy-version
+ tramp-rpc-deploy-local-cache-directory))))
+
+(defun tramp-rpc-deploy--bundled-binary-path (arch)
+ "Return the path to a bundled binary for ARCH, or nil if not available.
+Bundled binaries are in lisp/binaries/<arch>/tramp-rpc-server.
+This is useful for development - run scripts/build-all.sh to populate."
+ (when tramp-rpc-deploy-bundled-binary-directory
+ (let ((path (expand-file-name
+ tramp-rpc-deploy-binary-name
+ (expand-file-name arch tramp-rpc-deploy-bundled-binary-directory))))
+ (when (and (file-exists-p path) (file-executable-p path))
+ path))))
+
+(defun tramp-rpc-deploy--remote-binary-path (vec)
+ "Return the remote path where the binary should be installed for VEC."
+ (tramp-make-tramp-file-name
+ vec
+ ;; Use concat instead of expand-file-name to preserve ~ for remote expansion.
+ ;; expand-file-name would expand ~ to the LOCAL user's home directory,
+ ;; causing failures when local and remote usernames differ.
+ (concat (file-name-as-directory tramp-rpc-deploy-remote-directory)
+ (format "%s-%s" tramp-rpc-deploy-binary-name tramp-rpc-deploy-version))))
+
+;;; ============================================================================
+;;; Download from GitHub Releases
+;;; ============================================================================
+
+(defun tramp-rpc-deploy--release-asset-name (arch)
+ "Return the release asset filename for ARCH."
+ (format "tramp-rpc-server-%s-%s.tar.gz"
+ (tramp-rpc-deploy--arch-to-rust-target arch)
+ tramp-rpc-deploy-version))
+
+(defun tramp-rpc-deploy--download-url (arch)
+ "Return the download URL for binary of ARCH."
+ (format tramp-rpc-deploy-release-url-format
+ tramp-rpc-deploy-github-repo
+ tramp-rpc-deploy-version
+ (tramp-rpc-deploy--release-asset-name arch)))
+
+(defun tramp-rpc-deploy--checksum-url (arch)
+ "Return the checksum file URL for binary of ARCH."
+ (format tramp-rpc-deploy-release-url-format
+ tramp-rpc-deploy-github-repo
+ tramp-rpc-deploy-version
+ (format "tramp-rpc-server-%s-%s.tar.gz.sha256"
+ (tramp-rpc-deploy--arch-to-rust-target arch)
+ tramp-rpc-deploy-version)))
+
+(defun tramp-rpc-deploy--download-file (url dest)
+ "Download URL to DEST synchronously.
+Returns t on success, nil on failure."
+ (condition-case err
+ (let ((url-request-method "GET")
+ (url-show-status nil))
+ (message "Downloading %s..." url)
+ (with-timeout (tramp-rpc-deploy-download-timeout
+ (error "Download timed out after %d seconds"
+ tramp-rpc-deploy-download-timeout))
+ (with-current-buffer (url-retrieve-synchronously url t t)
+ (goto-char (point-min))
+ ;; Check for HTTP errors
+ (unless (re-search-forward "^HTTP/[0-9.]+ 200" nil t)
+ (if (re-search-forward "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t)
+ (error "HTTP error %s" (match-string 1))
+ (error "Invalid HTTP response")))
+ ;; Find body (after blank line)
+ (re-search-forward "^\r?\n" nil t)
+ ;; Write body to file
+ (let ((coding-system-for-write 'binary))
+ (write-region (point) (point-max) dest nil 'silent))
+ (kill-buffer)
+ t)))
+ (error
+ (message "Download failed: %s" (error-message-string err))
+ nil)))
+
+(defun tramp-rpc-deploy--verify-checksum (file expected-checksum)
+ "Verify that FILE has EXPECTED-CHECKSUM.
+Returns t if checksum matches, nil otherwise."
+ (when (and file (file-exists-p file) expected-checksum)
+ (let ((actual (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally file)
+ (secure-hash 'sha256 (current-buffer)))))
+ (string= actual (car (split-string expected-checksum))))))
+
+(defun tramp-rpc-deploy--extract-tarball (tarball dest-dir)
+ "Extract TARBALL to DEST-DIR.
+Returns the path to the extracted binary, or nil on failure."
+ (let ((default-directory dest-dir))
+ (make-directory dest-dir t)
+ (if (zerop (call-process "tar" nil nil nil "-xzf" tarball "-C" dest-dir))
+ (let ((binary (expand-file-name tramp-rpc-deploy-binary-name dest-dir)))
+ (when (file-exists-p binary)
+ (set-file-modes binary #o755)
+ binary))
+ nil)))
+
+(defun tramp-rpc-deploy--download-binary (arch)
+ "Download pre-compiled binary for ARCH from GitHub releases.
+Returns the path to the binary on success, signals error on failure."
+ (let* ((cache-path (tramp-rpc-deploy--local-cache-path arch))
+ (cache-dir (file-name-directory cache-path))
+ (tarball-url (tramp-rpc-deploy--download-url arch))
+ (checksum-url (tramp-rpc-deploy--checksum-url arch))
+ (temp-dir (make-temp-file "tramp-rpc-" t))
+ (tarball-path (expand-file-name "server.tar.gz" temp-dir))
+ (checksum-path (expand-file-name "server.tar.gz.sha256" temp-dir)))
+ (unwind-protect
+ (progn
+ ;; Download checksum first
+ (message "Fetching checksum for %s..." arch)
+ (let ((checksum-ok (tramp-rpc-deploy--download-file checksum-url checksum-path)))
+ ;; Download tarball
+ (message "Downloading tramp-rpc-server for %s..." arch)
+ (unless (tramp-rpc-deploy--download-file tarball-url tarball-path)
+ (error "Download failed from %s (release may not exist)" tarball-url))
+ ;; Verify checksum if we got one
+ (when checksum-ok
+ (let ((expected (with-temp-buffer
+ (insert-file-contents checksum-path)
+ (buffer-string))))
+ (unless (tramp-rpc-deploy--verify-checksum tarball-path expected)
+ (error "Checksum verification failed"))))
+ ;; Extract
+ (message "Extracting binary...")
+ (make-directory cache-dir t)
+ (unless (tramp-rpc-deploy--extract-tarball tarball-path cache-dir)
+ (error "Failed to extract tarball"))
+ (message "Downloaded tramp-rpc-server for %s" arch)
+ cache-path))
+ ;; Cleanup temp dir
+ (delete-directory temp-dir t))))
+
+;;; ============================================================================
+;;; Build from source
+;;; ============================================================================
+
+(defun tramp-rpc-deploy--cargo-available-p ()
+ "Check if cargo (Rust) is available."
+ (executable-find "cargo"))
+
+(defun tramp-rpc-deploy--can-build-for-arch-p (arch)
+ "Check if we can build for ARCH on this system.
+Cross-compilation requires additional setup, so we only build natively."
+ (string= arch (tramp-rpc-deploy--detect-local-arch)))
+
+(defun tramp-rpc-deploy--build-binary (arch)
+ "Build the binary for ARCH from source.
+Returns the path to the binary on success, nil on failure."
+ (unless tramp-rpc-deploy-source-directory
+ (error "Source directory not configured"))
+ (unless (tramp-rpc-deploy--cargo-available-p)
+ (error "Rust toolchain (cargo) not found"))
+ (unless (tramp-rpc-deploy--can-build-for-arch-p arch)
+ (error "Cannot cross-compile for %s on %s"
+ arch (tramp-rpc-deploy--detect-local-arch)))
+
+ (let* ((default-directory tramp-rpc-deploy-source-directory)
+ (target (tramp-rpc-deploy--arch-to-rust-target arch))
+ (cache-path (tramp-rpc-deploy--local-cache-path arch))
+ (cache-dir (file-name-directory cache-path))
+ (build-output (expand-file-name
+ (format "target/%s/release/%s"
+ target tramp-rpc-deploy-binary-name)
+ tramp-rpc-deploy-source-directory))
+ (build-buffer (get-buffer-create "*tramp-rpc-build*")))
+
+ (message "Building tramp-rpc-server for %s (this may take a minute)..." arch)
+
+ (with-current-buffer build-buffer
+ (erase-buffer))
+
+ (let ((exit-code
+ (call-process "cargo" nil build-buffer nil
+ "build" "--release"
+ "--target" target
+ "--manifest-path"
+ (expand-file-name "Cargo.toml" tramp-rpc-deploy-source-directory))))
+ (if (zerop exit-code)
+ (progn
+ ;; Copy to cache
+ (make-directory cache-dir t)
+ (copy-file build-output cache-path t)
+ (set-file-modes cache-path #o755)
+ (message "Built tramp-rpc-server for %s" arch)
+ cache-path)
+ (with-current-buffer build-buffer
+ (error "Build failed (exit %d):\n%s" exit-code (buffer-string)))))))
+
+;;; ============================================================================
+;;; Main logic: ensure local binary exists
+;;; ============================================================================
+
+(defun tramp-rpc-deploy--ensure-local-binary (arch)
+ "Ensure a local binary exists for ARCH.
+Tries in order:
+1. Check bundled binaries (useful for development)
+2. Check local cache
+3. Download from GitHub releases
+4. Build from source (if on same architecture)
+
+Returns the path to the local binary."
+ (let ((bundled-path (tramp-rpc-deploy--bundled-binary-path arch))
+ (cache-path (tramp-rpc-deploy--local-cache-path arch)))
+ (cond
+ ;; Check bundled binaries first (useful for development - run
+ ;; scripts/build-all.sh to populate lisp/binaries/)
+ (bundled-path
+ (message "Using bundled binary for %s" arch)
+ bundled-path)
+
+ ;; Check cache
+ ((and (file-exists-p cache-path)
+ (file-executable-p cache-path))
+ (message "Using cached binary for %s" arch)
+ cache-path)
+
+ ;; Need to obtain binary
+ (t
+ (let ((methods (if tramp-rpc-deploy-prefer-build
+ '(build download)
+ '(download build)))
+ (result nil)
+ (errors nil))
+
+ (dolist (method methods)
+ (unless result
+ (condition-case err
+ (setq result
+ (pcase method
+ ('download
+ (tramp-rpc-deploy--download-binary arch))
+ ('build
+ (when (and tramp-rpc-deploy-source-directory
+ (tramp-rpc-deploy--cargo-available-p)
+ (tramp-rpc-deploy--can-build-for-arch-p arch))
+ (tramp-rpc-deploy--build-binary arch)))))
+ (error
+ (push (cons method (error-message-string err)) errors)))))
+
+ (or result
+ (error "Failed to obtain tramp-rpc-server for %s.\n\nErrors:\n%s\n\n%s"
+ arch
+ (mapconcat (lambda (e)
+ (format " %s: %s" (car e) (cdr e)))
+ (reverse errors)
+ "\n")
+ (tramp-rpc-deploy--help-message arch))))))))
+
+(defun tramp-rpc-deploy--help-message (arch)
+ "Return a help message for obtaining binary for ARCH."
+ (let ((local-arch (tramp-rpc-deploy--detect-local-arch)))
+ (concat
+ "To resolve this, you can:\n\n"
+ (format "1. Download manually from:\n %s\n\n"
+ (tramp-rpc-deploy--download-url arch))
+ (if (string= arch local-arch)
+ (concat
+ "2. Install Rust and build from source:\n"
+ " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n"
+ " Then restart Emacs and try again.\n\n")
+ (format
+ "2. Build on a %s machine and copy to:\n %s\n\n"
+ arch
+ (tramp-rpc-deploy--local-cache-path arch)))
+ (format "Binary should be placed at:\n %s"
+ (tramp-rpc-deploy--local-cache-path arch)))))
+
+;;; ============================================================================
+;;; Remote deployment
+;;; ============================================================================
+
+(defun tramp-rpc-deploy--remote-binary-exists-p (vec)
+ "Check if the correct version of the binary exists on remote VEC."
+ (let ((remote-path (tramp-rpc-deploy--remote-binary-path vec)))
+ ;; Use tramp-sh operations for checking since we're bootstrapping
+ (tramp-send-command-and-check
+ vec
+ (format "test -x %s"
+ (tramp-shell-quote-argument
+ (tramp-file-local-name remote-path))))))
+
+(defun tramp-rpc-deploy--ensure-remote-directory (vec)
+ "Ensure the remote deployment directory exists on VEC."
+ (let ((dir (tramp-file-local-name
+ (tramp-make-tramp-file-name vec tramp-rpc-deploy-remote-directory))))
+ (tramp-send-command vec (format "mkdir -p %s" (tramp-shell-quote-argument dir)))))
+
+(defun tramp-rpc-deploy--compute-checksum (file)
+ "Compute SHA256 checksum of local FILE."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally file)
+ (secure-hash 'sha256 (current-buffer))))
+
+(defun tramp-rpc-deploy--remote-checksum (vec path)
+ "Get SHA256 checksum of remote PATH on VEC.
+Tries sha256sum first, then shasum -a 256 for macOS compatibility."
+ ;; Try sha256sum first (Linux), then shasum -a 256 (macOS)
+ (tramp-send-command vec
+ (format "{ sha256sum %s 2>/dev/null || shasum -a 256 %s 2>/dev/null; } | cut -d' ' -f1"
+ (tramp-shell-quote-argument path)
+ (tramp-shell-quote-argument path)))
+ (with-current-buffer (tramp-get-connection-buffer vec)
+ (goto-char (point-min))
+ ;; Match exactly 64 hex chars to avoid false positives from error messages
+ (when (looking-at "\\([a-f0-9]\\{64\\}\\)")
+ (match-string 1))))
+
+(defun tramp-rpc-deploy--transfer-binary (vec local-path)
+ "Transfer the binary at LOCAL-PATH to the remote host VEC.
+Uses TRAMP's copy-file for reliable binary transfer with checksum verification."
+ (let* ((remote-path (tramp-rpc-deploy--remote-binary-path vec))
+ (remote-local (tramp-file-local-name remote-path))
+ (remote-tmp-name (format "%s.tmp.%d"
+ (file-name-nondirectory remote-local)
+ (random 100000)))
+ (remote-tmp-path (tramp-make-tramp-file-name
+ vec
+ ;; Use concat to preserve ~ for remote expansion
+ (concat (file-name-as-directory tramp-rpc-deploy-remote-directory)
+ remote-tmp-name)))
+ (remote-tmp-local (tramp-file-local-name remote-tmp-path))
+ (local-checksum (tramp-rpc-deploy--compute-checksum local-path))
+ (retries 0)
+ (success nil)
+ (errors nil))
+
+ (tramp-rpc-deploy--log "Transfer starting: local=%s remote=%s" local-path remote-local)
+ (tramp-rpc-deploy--log "Local binary size: %d bytes, checksum: %s..."
+ (file-attribute-size (file-attributes local-path))
+ (substring local-checksum 0 16))
+
+ ;; Ensure remote directory exists
+ (tramp-rpc-deploy--ensure-remote-directory vec)
+
+ (message "Transferring binary to %s:%s..." (tramp-file-name-host vec) remote-local)
+
+ ;; Retry loop for reliability
+ (while (and (not success) (< retries tramp-rpc-deploy-max-retries))
+ (let ((attempt (1+ retries)))
+ (message "Transfer attempt %d/%d..." attempt tramp-rpc-deploy-max-retries)
+ (condition-case err
+ (progn
+ ;; Use TRAMP's copy-file for binary transfer (via sshx bootstrap method)
+ ;; This is much more reliable than base64 encoding through heredocs
+ (copy-file local-path remote-tmp-path t)
+
+ ;; Verify the file was created and has content
+ (unless (tramp-send-command-and-check
+ vec
+ (format "test -s %s" (tramp-shell-quote-argument remote-tmp-local)))
+ (error "Temp file not created or is empty after copy"))
+
+ ;; Verify checksum
+ (let ((remote-checksum (tramp-rpc-deploy--remote-checksum vec remote-tmp-local)))
+ (unless remote-checksum
+ (error "Could not compute remote checksum (sha256sum/shasum not available?)"))
+ (if (string= local-checksum remote-checksum)
+ (progn
+ ;; Checksum matches - make executable and atomically move
+ (tramp-send-command
+ vec
+ (format "chmod +x %s && mv -f %s %s"
+ (tramp-shell-quote-argument remote-tmp-local)
+ (tramp-shell-quote-argument remote-tmp-local)
+ (tramp-shell-quote-argument remote-local)))
+ (setq success t)
+ (message "Transfer completed successfully"))
+ ;; Checksum mismatch - clean up and retry
+ (let ((err-msg (format "Attempt %d: Checksum mismatch (local: %s, remote: %s)"
+ attempt
+ (substring local-checksum 0 12)
+ (substring remote-checksum 0 12))))
+ (push err-msg errors)
+ (message "%s" err-msg))
+ (ignore-errors (delete-file remote-tmp-path))
+ (setq retries (1+ retries)))))
+ (error
+ ;; Clean up on error and retry
+ (let ((err-msg (format "Attempt %d: %s" attempt (error-message-string err))))
+ (push err-msg errors)
+ (message "Transfer error: %s" err-msg))
+ (ignore-errors (delete-file remote-tmp-path))
+ (setq retries (1+ retries))))))
+
+ (unless success
+ (error "Failed to transfer binary after %d attempts.\n\nErrors:\n%s\n\nTroubleshooting:\n- Verify SSH access: ssh %s@%s echo success\n- Check write permissions to %s on remote host\n- Ensure sha256sum or shasum command is available on remote host"
+ tramp-rpc-deploy-max-retries
+ (mapconcat #'identity (nreverse errors) "\n")
+ (or (tramp-file-name-user vec) "USER")
+ (tramp-file-name-host vec)
+ tramp-rpc-deploy-remote-directory))
+
+ remote-path))
+
+;;; ============================================================================
+;;; Public API
+;;; ============================================================================
+
+(defun tramp-rpc-deploy-ensure-binary (vec)
+ "Ensure the tramp-rpc-server binary is available on remote VEC.
+Returns the remote path to the binary.
+If `tramp-rpc-deploy-auto-deploy' is nil and the binary is missing,
+signals an error."
+ (let ((bootstrap-vec (tramp-rpc-deploy--bootstrap-vec vec)))
+ (if (tramp-rpc-deploy--remote-binary-exists-p bootstrap-vec)
+ ;; Binary already exists
+ (tramp-file-local-name (tramp-rpc-deploy--remote-binary-path bootstrap-vec))
+ ;; Need to deploy
+ (if tramp-rpc-deploy-auto-deploy
+ (let* ((arch (tramp-rpc-deploy--detect-remote-arch bootstrap-vec))
+ (local-binary (tramp-rpc-deploy--ensure-local-binary arch)))
+ (message "Deploying tramp-rpc-server (%s) to %s..."
+ arch (tramp-file-name-host vec))
+ (tramp-file-local-name
+ (tramp-rpc-deploy--transfer-binary bootstrap-vec local-binary)))
+ (error "tramp-rpc-server not found on %s and auto-deploy is disabled"
+ (tramp-file-name-host vec))))))
+
+(defun tramp-rpc-deploy-remove-binary (vec)
+ "Remove the tramp-rpc-server binary from remote VEC."
+ (interactive
+ (list (tramp-dissect-file-name
+ (read-file-name "Remote host: " "/ssh:"))))
+ (let ((bootstrap-vec (tramp-rpc-deploy--bootstrap-vec vec)))
+ (when (tramp-rpc-deploy--remote-binary-exists-p bootstrap-vec)
+ (tramp-send-command
+ bootstrap-vec
+ (format "rm -f %s"
+ (tramp-shell-quote-argument
+ (tramp-file-local-name
+ (tramp-rpc-deploy--remote-binary-path bootstrap-vec)))))
+ (message "Removed %s from %s"
+ tramp-rpc-deploy-binary-name
+ (tramp-file-name-host vec)))))
+
+(defun tramp-rpc-deploy-clear-cache ()
+ "Clear the local binary cache."
+ (interactive)
+ (when (file-exists-p tramp-rpc-deploy-local-cache-directory)
+ (delete-directory tramp-rpc-deploy-local-cache-directory t)
+ (message "Cleared tramp-rpc binary cache")))
+
+(defun tramp-rpc-deploy-status ()
+ "Show the status of tramp-rpc-server binaries."
+ (interactive)
+ (let ((buf (get-buffer-create "*tramp-rpc-deploy-status*")))
+ (with-current-buffer buf
+ (erase-buffer)
+ (insert "TRAMP-RPC Server Deployment Status\n")
+ (insert "===================================\n\n")
+ (insert (format "Version: %s\n" tramp-rpc-deploy-version))
+ (insert (format "Local arch: %s\n" (tramp-rpc-deploy--detect-local-arch)))
+ (insert (format "Cargo available: %s\n"
+ (if (tramp-rpc-deploy--cargo-available-p) "yes" "no")))
+ (insert (format "Source directory: %s\n"
+ (or tramp-rpc-deploy-source-directory "not set")))
+ (insert (format "Cache directory: %s\n\n" tramp-rpc-deploy-local-cache-directory))
+
+ (insert "Cached Binaries:\n")
+ (insert "----------------\n")
+ (dolist (arch '("x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"))
+ (let ((path (tramp-rpc-deploy--local-cache-path arch)))
+ (insert (format " %s: %s\n"
+ arch
+ (if (file-exists-p path)
+ (format "cached (%s)"
+ (file-size-human-readable
+ (file-attribute-size (file-attributes path))))
+ "not cached")))))
+ (insert "\n")
+ (insert "Download URLs:\n")
+ (insert "--------------\n")
+ (dolist (arch '("x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"))
+ (insert (format " %s:\n %s\n" arch (tramp-rpc-deploy--download-url arch)))))
+ (display-buffer buf)))
+
+(defun tramp-rpc-deploy-diagnose (host &optional user)
+ "Run diagnostics for deploying to HOST.
+Optional USER specifies the SSH user.
+This helps troubleshoot deployment issues."
+ (interactive "sHost: \nsUser (leave empty for default): ")
+ (when (string-empty-p user)
+ (setq user nil))
+ (let ((buf (get-buffer-create "*tramp-rpc-diagnose*")))
+ (with-current-buffer buf
+ (erase-buffer)
+ (insert (format "TRAMP-RPC Deployment Diagnostics for %s%s\n"
+ (if user (concat user "@") "") host))
+ (insert "=" (make-string 50 ?=) "\n\n")
+
+ ;; Test 1: SSH connectivity
+ (insert "1. Testing SSH connectivity...\n")
+ (let* ((ssh-cmd (append
+ (list "ssh" "-o" "BatchMode=yes" "-o" "ConnectTimeout=10")
+ (when user (list "-l" user))
+ (list host "echo 'SSH_OK'")))
+ (output (with-temp-buffer
+ (apply #'call-process (car ssh-cmd) nil t nil (cdr ssh-cmd))
+ (buffer-string))))
+ (if (string-match-p "SSH_OK" output)
+ (insert " [OK] SSH connection successful\n")
+ (insert " [FAIL] SSH connection failed\n")
+ (insert (format " Output: %s\n" (string-trim output)))))
+
+ ;; Test 2: Remote architecture
+ (insert "\n2. Detecting remote architecture...\n")
+ (let* ((ssh-cmd (append
+ (list "ssh" "-o" "BatchMode=yes")
+ (when user (list "-l" user))
+ (list host "uname -m && uname -s")))
+ (output (with-temp-buffer
+ (if (zerop (apply #'call-process (car ssh-cmd) nil t nil (cdr ssh-cmd)))
+ (buffer-string)
+ "FAILED"))))
+ (if (string-match-p "FAILED" output)
+ (insert " [FAIL] Could not detect architecture\n")
+ (insert (format " [OK] Architecture: %s\n" (string-trim output)))))
+
+ ;; Test 3: Remote directory writable
+ (insert "\n3. Testing remote directory access...\n")
+ (let* ((dir tramp-rpc-deploy-remote-directory)
+ (ssh-cmd (append
+ (list "ssh" "-o" "BatchMode=yes")
+ (when user (list "-l" user))
+ (list host (format "mkdir -p %s && test -w %s && echo 'WRITABLE'"
+ (shell-quote-argument dir)
+ (shell-quote-argument dir)))))
+ (output (with-temp-buffer
+ (apply #'call-process (car ssh-cmd) nil t nil (cdr ssh-cmd))
+ (buffer-string))))
+ (if (string-match-p "WRITABLE" output)
+ (insert (format " [OK] Directory %s is writable\n" dir))
+ (insert (format " [FAIL] Directory %s not writable\n" dir))))
+
+ ;; Test 4: Checksum command
+ (insert "\n4. Testing checksum command availability...\n")
+ (let* ((ssh-cmd (append
+ (list "ssh" "-o" "BatchMode=yes")
+ (when user (list "-l" user))
+ (list host "which sha256sum || which shasum || echo 'NONE'")))
+ (output (with-temp-buffer
+ (apply #'call-process (car ssh-cmd) nil t nil (cdr ssh-cmd))
+ (string-trim (buffer-string)))))
+ (if (string-match-p "NONE" output)
+ (insert " [FAIL] No checksum command found (need sha256sum or shasum)\n")
+ (insert (format " [OK] Found: %s\n" output))))
+
+ ;; Test 5: Local binary availability
+ (insert "\n5. Checking local binary cache...\n")
+ (dolist (arch '("x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"))
+ (let ((path (tramp-rpc-deploy--local-cache-path arch))
+ (bundled (tramp-rpc-deploy--bundled-binary-path arch)))
+ (cond
+ ((and bundled (file-exists-p bundled))
+ (insert (format " [OK] %s: bundled binary available\n" arch)))
+ ((file-exists-p path)
+ (insert (format " [OK] %s: cached at %s\n" arch path)))
+ (t
+ (insert (format " [ ] %s: not available locally\n" arch))))))
+
+ (insert "\n\nIf deployment fails, try:\n")
+ (insert " 1. Enable debug logging: (setq tramp-rpc-deploy-debug t)\n")
+ (insert " 2. Retry the connection and check *tramp-rpc-deploy* buffer\n")
+ (insert " 3. Manually test: ssh " (if user (concat user "@") "") host " echo success\n"))
+ (display-buffer buf)))
+
+(provide 'tramp-rpc-deploy)
+;;; tramp-rpc-deploy.el ends here