diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e83c9bba..6a33b16ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -176,6 +176,7 @@ ### Changes +- [#3567](https://github.com/clojure-emacs/cider/pull/3567): Fix remote `cider-jack-in-clj` breaking with `cider-enrich-classpath` when `clojure.sh|lein.sh` are not available on the remote. - CIDER [Inspector](https://docs.cider.mx/cider/debugging/inspector.html): display Java class/method/field info when available. - This info is available when [enrich-classpath](https://docs.cider.mx/cider/config/basic_config.html#use-enrich-classpath) is active. - [#3495](https://github.com/clojure-emacs/cider/issues/3495): possibly display error overlays on [`cider-load-buffer`](https://docs.cider.mx/cider/usage/code_evaluation.html#basic-evaluation). diff --git a/cider-util.el b/cider-util.el index 4cd424439..6b47086f1 100644 --- a/cider-util.el +++ b/cider-util.el @@ -529,7 +529,63 @@ Any other value is just returned." (mapcar #'cider--deep-vector-to-list x) x)) + +;;; Files & Directories +(defun cider--ensure-executable (file) + "Try to make FILE executable if it isn't already. +Returns FILE on success, nil on failure." + (with-demoted-errors "Error while trying to make file executable:\n %s" + (when (or (file-executable-p file) + (and (set-file-modes file "u+x") + (file-executable-p file))) + file))) + +(defconst cider--temp-name-prefix ".cider__" + "Prefix for marking temporary files created by cider.") + +(defun cider--make-temp-name (file) + "Generate a randomized name from FILEs basename. +Tag it with `cider--temp-name-prefix'" + (make-temp-name + (concat cider--temp-name-prefix (file-name-nondirectory file) "__"))) + +(defun cider--make-nearby-temp-copy (file) + "Create a copy of FILE in the default local or remote tempdir. +Falls back to `clojure-project-dir' or `default-directory'. +The copy is marked with `cider--temp-name-prefix'." + (let* ((default-directory (or (clojure-project-dir) default-directory)) + (new-file (file-name-concat (temporary-file-directory) + (cider--make-temp-name file)))) + (copy-file file new-file :exists-ok nil nil :keep-permissions) + new-file)) + +(defun cider--inject-self-delete (bash-file) + "Make BASH-FILE delete itself on exit. +Injects the self-delete script after the first line, assuming it is a +shebang." + (let (;; Don't create any temporary files. + (remote-file-name-inhibit-locks t) + (make-backup-files nil) + (auto-save-default nil) + ;; Disable version-control check + (vc-handled-backends nil)) + (with-temp-buffer + (insert-file-contents bash-file) + ;; inject after the first line, assuming it is the shebang + (goto-char (point-min)) + (skip-chars-forward "^\n") + (insert "\n") + (insert (format + "trap 'ARG=$? + rm -v %s + echo \"cider: Cleaned up temporary script after use.\" + exit $ARG + ' EXIT" + (file-local-name bash-file))) + (write-file bash-file)) + bash-file)) + ;;; Help mode ;; Same as https://github.com/emacs-mirror/emacs/blob/86d083438dba60dc00e9e96414bf7e832720c05a/lisp/help-mode.el#L355 diff --git a/cider.el b/cider.el index 80cdb402b..e4621ca3c 100644 --- a/cider.el +++ b/cider.el @@ -452,40 +452,58 @@ without interfering with classloaders." :package-version '(cider . "1.2.0") :safe #'booleanp) -(defun cider--get-enrich-classpath-lein-script () - "Returns the location of enrich-classpath's lein.sh wrapper script." - (when-let ((cider-location (locate-library "cider.el" t))) - (concat (file-name-directory cider-location) - "lein.sh"))) - -(defun cider--get-enrich-classpath-clojure-cli-script () - "Returns the location of enrich-classpath's clojure.sh wrapper script." - (when-let ((cider-location (locate-library "cider.el" t))) - (concat (file-name-directory cider-location) - "clojure.sh"))) +(defvar cider--enrich-classpath-script-names + '((lein . "lein.sh") + (clojure-cli . "clojure.sh"))) + +(defun cider--enriched-cmd-p (cmd) + "Test if the shell-quoted CMD contains the name of an enrich-classpath script." + (let* ((script-names (map-values cider--enrich-classpath-script-names)) + (temp-prefix cider--temp-name-prefix) + (any-name (rx-to-string + `(or (: (or ,@script-names) (or eos space)) + (: ,temp-prefix (or ,@script-names)))))) + (string-match any-name cmd))) + +(defun cider--get-enrich-classpath-script (project-type) + "Get or create an executable enrich-classpath script for PROJECT-TYPE. +If `default-directory' is remote, create a copy at +'/.cider____' that deletes itself after +use. The search for '' is handled by tramp and falls back to +`clojure-project-dir' or `default-directory'. Returns nil if anything goes wrong." + (when-let* ((cider-dir (file-name-directory (locate-library "cider.el" t))) + (name (map-elt cider--enrich-classpath-script-names project-type)) + (location (concat cider-dir name)) + (script (cider--ensure-executable location))) + (if (file-remote-p default-directory) + (with-demoted-errors + "cider: Failed to initialize enrich-classpath on remote:\n %s" + (thread-first + (cider--make-nearby-temp-copy script) + (cider--ensure-executable) + (cider--inject-self-delete))) + script))) + +(defun cider--jack-in-resolve-command-enrich (project-type) + "Conditionally wrap the command for PROJECT-TYPE with an enrich-classpath script. +Resolves to the non-wrapped `cider-jack-in-command' if `cider-enrich-classpath' is nil or the + wrapper-script can't be initialized." + (when-let ((command (cider--resolve-command (cider-jack-in-command project-type)))) + (if-let ((wrapper-script (and cider-enrich-classpath + (not (eq system-type 'windows-nt)) + (cider--get-enrich-classpath-script project-type)))) + (concat "bash " + (shell-quote-argument (file-local-name wrapper-script)) " " + command) + command))) (defun cider-jack-in-resolve-command (project-type) "Determine the resolved file path to `cider-jack-in-command'. Throws an error if PROJECT-TYPE is unknown." (pcase project-type - ('lein (let ((r (cider--resolve-command cider-lein-command))) - (if (and cider-enrich-classpath - (not (eq system-type 'windows-nt)) - (executable-find (cider--get-enrich-classpath-lein-script))) - (concat "bash " ;; don't assume lein.sh is executable - MELPA might change that - (cider--get-enrich-classpath-lein-script) - " " - r) - r))) + ('lein (cider--jack-in-resolve-command-enrich 'lein)) ('boot (cider--resolve-command cider-boot-command)) - ('clojure-cli (if (and cider-enrich-classpath - (not (eq system-type 'windows-nt)) - (executable-find (cider--get-enrich-classpath-clojure-cli-script))) - (concat "bash " ;; don't assume clojure.sh is executable - MELPA might change that - (cider--get-enrich-classpath-clojure-cli-script) - " " - (cider--resolve-command cider-clojure-cli-command)) - (cider--resolve-command cider-clojure-cli-command))) + ('clojure-cli (cider--jack-in-resolve-command-enrich 'clojure-cli)) ('babashka (cider--resolve-command cider-babashka-command)) ;; here we have to account for the possibility that the command is either ;; "npx shadow-cljs" or just "shadow-cljs" @@ -1713,7 +1731,11 @@ PARAMS is a plist with the following keys (non-exhaustive list) (command-resolved (cider-jack-in-resolve-command project-type)) ;; TODO: global-options are deprecated and should be removed in CIDER 2.0 (command-global-opts (cider-jack-in-global-options project-type)) - (command-params (cider-jack-in-params project-type))) + (command-params (cider-jack-in-params project-type)) + ;; ignore `cider-enrich-classpath' if the jack-in-command does not include + ;; the necessary wrapper script at this point + (cider-enrich-classpath (and cider-enrich-classpath + (cider--enriched-cmd-p command-resolved)))) (if command-resolved (with-current-buffer (or (plist-get params :--context-buffer) (current-buffer)) @@ -2165,13 +2187,11 @@ M-2 \\[cider-jack-in-universal]." (cider-jack-in-clj arg)))) -;; TODO: Implement a check for command presence over tramp (defun cider--resolve-command (command) - "Find COMMAND in exec path (see variable `exec-path'). -Return nil if not found. In case `default-directory' is non-local we -assume the command is available." - (when-let* ((command (or (and (file-remote-p default-directory) command) - (executable-find command) + "Test if COMMAND exists, is executable and shell-quote it. +Return nil otherwise. When `default-directory' is remote, the check is +performed by tramp." + (when-let* ((command (or (executable-find command :remote) (executable-find (concat command ".bat"))))) (shell-quote-argument command))) diff --git a/test/cider-tests.el b/test/cider-tests.el index b7298e36b..95fd9c0ed 100644 --- a/test/cider-tests.el +++ b/test/cider-tests.el @@ -106,6 +106,37 @@ :and-return-value '()) (expect (cider-project-type) :to-equal cider-jack-in-default)))) +(describe "cider--enriched-cmd-p" + (describe "when cmd does not contain the path to an enrich-classpath script" + (it "returns nil" + (expect (cider--enriched-cmd-p "/usr/bin/lein") + :to-equal nil) + (expect (cider--enriched-cmd-p "bash /usr/bin/lein") + :to-equal nil))) + (describe "for different path + script-name combinations" + :var* ((paths `("/simple/path/" + ,"/tmp/path/ with spaces/" + ,"/ssh:!slightly@cra --zy!path #enrich me/")) + (simple-names (map-values cider--enrich-classpath-script-names)) + (tmp-names (mapcar (lambda (s) (cider--make-temp-name s)) simple-names)) + (all-names (seq-concatenate 'list simple-names tmp-names))) + (cl-loop + for path in paths do + (cl-loop + for name in all-names do + (describe (format "cider--enriched-cmd-p with script: %s" (shell-quote-argument (concat path name))) + :var ((script (shell-quote-argument (concat path name)))) + (it "is true in basic cases " + (expect (cider--enriched-cmd-p (concat "bash " script " /usr/bin/lein")) + :to-be-truthy) + (expect (cider--enriched-cmd-p (concat "TEST=1 bash " script " /usr/bin/env lein")) + :to-be-truthy)) + (it "handles a fully constructed jack-in-cmd." + ;; TODO is it worth generating this? + (let ((cmd (concat "bash " script " /usr/local/bin/lein update-in :dependencies conj \[nrepl/nrepl\ \"1.0.0\"\] -- update-in :plugins conj \[cider/cider-nrepl\ \"0.43.0\"\] -- update-in :plugins conj \[mx.cider/lein-enrich-classpath\ \"1.18.2\"\] -- update-in :middleware conj cider.enrich-classpath.plugin-v2/middleware -- repl :headless :host localhost"))) + (expect (cider--enriched-cmd-p cmd) + :to-be-truthy)))))))) + ;;; cider-jack-in tests (describe "cider--gradle-dependency-notation" (it "returns a GAV when given a two-element list"