Skip to content

Commit 44622e5

Browse files
committed
Fix remote cider-jack-in-clj breaking with cider-enrich-classpath #3567
Fixes `cider-jack-in-clj` in tramp buffers throwing: ``` Could not start nREPL server: %s (%S) bash: /remote/path/to/cider/clojure.sh: No such file or directory\n" "exited abnormally with code 127") ``` in cases where cider is not installed in the same directory on the remote. To fix this, we create temporary copy of the enrich-classpath script named `.cider__<clojure.sh|lein.sh>__<random>` on the remote before starting the server. The possible locations of the script are, in this order: - tramp-tempdir (usually "/tmp") - clojure-project-dirj - default-directory If the script can't be created for any reason, the server is started with `cider-enrich-classpath` set to nil. Note: the temporary script will remove itself after use, but stick around when something goes wrong before the remote process is started.
1 parent ef87c71 commit 44622e5

File tree

3 files changed

+149
-35
lines changed

3 files changed

+149
-35
lines changed

cider-util.el

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,64 @@ Any other value is just returned."
515515
(mapcar #'cider--deep-vector-to-list x)
516516
x))
517517

518+
519+
;;; Files & Directories
520+
(defun cider--ensure-executable (file)
521+
"Try to make FILE executable if it isn't already.
522+
Returns FILE on success, nil on failure."
523+
(with-demoted-errors "Error while trying to make file executable:\n %s"
524+
(when (or (file-executable-p file)
525+
(and (set-file-modes file "u+x")
526+
(file-executable-p file)))
527+
file)))
528+
529+
(defconst cider--temp-name-prefix ".cider__"
530+
"Prefix for marking temporary files created by cider.")
531+
532+
(defun cider--make-temp-name (file)
533+
"Generate a randomized name from FILEs basename.
534+
Tag it with `cider--temp-name-prefix'"
535+
(make-temp-name
536+
(concat cider--temp-name-prefix (file-name-nondirectory file) "__")))
537+
538+
(defun cider--make-nearby-temp-copy (file)
539+
"Create a copy of FILE in the default local or remote tempdir.
540+
Falls back to `clojure-project-dir' or `default-directory'.
541+
The copy is marked with `cider--temp-name-prefix'."
542+
(let* ((default-directory (or (clojure-project-dir) default-directory))
543+
(new-file (file-name-concat (temporary-file-directory)
544+
(cider--make-temp-name file))))
545+
(copy-file file new-file :exists-ok nil nil :keep-permissions)
546+
new-file))
547+
548+
(defun cider--inject-self-delete (bash-file)
549+
"Make BASH-FILE delete itself on exit.
550+
Injects the self-delete script after the first line, assuming it is a
551+
shebang."
552+
(let (;; Don't create any temporary files.
553+
(remote-file-name-inhibit-locks t)
554+
(remote-file-name-inhibit-auto-save-visited t)
555+
(make-backup-files nil)
556+
(auto-save-default nil)
557+
;; Disable version-control check
558+
(vc-handled-backends nil))
559+
(with-temp-buffer
560+
(insert-file-contents bash-file)
561+
;; inject after the first line, assuming it is the shebang
562+
(goto-char (point-min))
563+
(skip-chars-forward "^\n")
564+
(insert "\n")
565+
(insert (format
566+
"trap 'ARG=$?
567+
rm -v %s
568+
echo \"cider: Cleaned up temporary script after use.\"
569+
exit $ARG
570+
' EXIT"
571+
(file-local-name bash-file)))
572+
(write-file bash-file))
573+
bash-file))
518574

575+
519576
;;; Help mode
520577

521578
;; Same as https://github.com/emacs-mirror/emacs/blob/86d083438dba60dc00e9e96414bf7e832720c05a/lisp/help-mode.el#L355

cider.el

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -411,40 +411,64 @@ without interfering with classloaders."
411411
:package-version '(cider . "1.2.0")
412412
:safe #'booleanp)
413413

414-
(defun cider--get-enrich-classpath-lein-script ()
415-
"Returns the location of enrich-classpath's lein.sh wrapper script."
416-
(when-let ((cider-location (locate-library "cider.el" t)))
417-
(concat (file-name-directory cider-location)
418-
"lein.sh")))
419-
420-
(defun cider--get-enrich-classpath-clojure-cli-script ()
421-
"Returns the location of enrich-classpath's clojure.sh wrapper script."
422-
(when-let ((cider-location (locate-library "cider.el" t)))
423-
(concat (file-name-directory cider-location)
424-
"clojure.sh")))
414+
(defvar cider--enrich-classpath-script-names
415+
'((lein . "lein.sh")
416+
(clojure-cli . "clojure.sh")))
417+
418+
(defun cider--enriched-cmd-p (cmd)
419+
"Test if the shell-quoted CMD contains the name of an enrich-classpath script.
420+
Returns the local path to the script or nil."
421+
(let* ((script-names (map-values cider--enrich-classpath-script-names))
422+
(temp-prefix cider--temp-name-prefix)
423+
(any-name (rx-to-string
424+
`(or (: (or bos "/") (or ,@script-names) (or eos space))
425+
(: ,temp-prefix (or ,@script-names)))))
426+
(script (thread-last
427+
(split-string-shell-command cmd)
428+
(seq-filter (lambda (part) (string-match any-name part)))
429+
(seq-first))))
430+
(when script
431+
(shell-quote-argument script))))
432+
433+
(defun cider--get-enrich-classpath-script (project-type)
434+
"Get or create an executable enrich-classpath script for PROJECT-TYPE.
435+
If `default-directory' is remote, create a copy at
436+
'<remote-tempdir>/.cider__<script-name>__<random>' that deletes itself after
437+
use. The search for '<remote-tempdir>' is handled by tramp and falls back to
438+
`clojure-project-dir' or `default-directory'. Returns nil if anything goes wrong."
439+
(when-let* ((cider-dir (file-name-directory (locate-library "cider.el" t)))
440+
(name (map-elt cider--enrich-classpath-script-names project-type))
441+
(location (concat cider-dir name))
442+
(script (cider--ensure-executable location)))
443+
(if (file-remote-p default-directory)
444+
(with-demoted-errors
445+
"cider: Failed to initialize enrich-classpath on remote."
446+
(thread-first
447+
(cider--make-nearby-temp-copy script)
448+
(cider--ensure-executable)
449+
(cider--inject-self-delete)))
450+
script)))
451+
452+
(defun cider--jack-in-resolve-command-enrich (project-type)
453+
"Conditionally wrap the command for PROJECT-TYPE with an enrich-classpath script.
454+
Resolves to the non-wrapped `cider-jack-in-command' if `cider-enrich-classpath' is nil or the
455+
wrapper-script can't be initialized."
456+
(when-let ((command (cider--resolve-command (cider-jack-in-command project-type))))
457+
(if-let ((wrapper-script (and cider-enrich-classpath
458+
(not (eq system-type 'windows-nt))
459+
(cider--get-enrich-classpath-script project-type))))
460+
(concat "bash "
461+
(shell-quote-argument (file-local-name wrapper-script)) " "
462+
command)
463+
command)))
425464

426465
(defun cider-jack-in-resolve-command (project-type)
427466
"Determine the resolved file path to `cider-jack-in-command'.
428467
Throws an error if PROJECT-TYPE is unknown."
429468
(pcase project-type
430-
('lein (let ((r (cider--resolve-command cider-lein-command)))
431-
(if (and cider-enrich-classpath
432-
(not (eq system-type 'windows-nt))
433-
(executable-find (cider--get-enrich-classpath-lein-script)))
434-
(concat "bash " ;; don't assume lein.sh is executable - MELPA might change that
435-
(cider--get-enrich-classpath-lein-script)
436-
" "
437-
r)
438-
r)))
469+
('lein (cider--jack-in-resolve-command-enrich 'lein))
439470
('boot (cider--resolve-command cider-boot-command))
440-
('clojure-cli (if (and cider-enrich-classpath
441-
(not (eq system-type 'windows-nt))
442-
(executable-find (cider--get-enrich-classpath-clojure-cli-script)))
443-
(concat "bash " ;; don't assume clojure.sh is executable - MELPA might change that
444-
(cider--get-enrich-classpath-clojure-cli-script)
445-
" "
446-
(cider--resolve-command cider-clojure-cli-command))
447-
(cider--resolve-command cider-clojure-cli-command)))
471+
('clojure-cli (cider--jack-in-resolve-command-enrich 'clojure-cli))
448472
('babashka (cider--resolve-command cider-babashka-command))
449473
;; here we have to account for the possibility that the command is either
450474
;; "npx shadow-cljs" or just "shadow-cljs"
@@ -1661,7 +1685,11 @@ PARAMS is a plist with the following keys (non-exhaustive list)
16611685
(command-resolved (cider-jack-in-resolve-command project-type))
16621686
;; TODO: global-options are deprecated and should be removed in CIDER 2.0
16631687
(command-global-opts (cider-jack-in-global-options project-type))
1664-
(command-params (cider-jack-in-params project-type)))
1688+
(command-params (cider-jack-in-params project-type))
1689+
;; ignore `cider-enrich-classpath' if the jack-in-command does not include
1690+
;; the necessary wrapper script at this point
1691+
(cider-enrich-classpath (and cider-enrich-classpath
1692+
(cider--enriched-cmd-p command-resolved))))
16651693
(if command-resolved
16661694
(with-current-buffer (or (plist-get params :--context-buffer)
16671695
(current-buffer))
@@ -2114,13 +2142,11 @@ M-2 \\[cider-jack-in-universal]."
21142142
(cider-jack-in-clj arg))))
21152143

21162144

2117-
;; TODO: Implement a check for command presence over tramp
21182145
(defun cider--resolve-command (command)
2119-
"Find COMMAND in exec path (see variable `exec-path').
2120-
Return nil if not found. In case `default-directory' is non-local we
2121-
assume the command is available."
2122-
(when-let* ((command (or (and (file-remote-p default-directory) command)
2123-
(executable-find command)
2146+
"Test if COMMAND exists, is executable and shell-quote it.
2147+
Return nil otherwise. When `default-directory' is remote, the check is
2148+
performed by tramp."
2149+
(when-let* ((command (or (executable-find command :remote)
21242150
(executable-find (concat command ".bat")))))
21252151
(shell-quote-argument command)))
21262152

test/cider-tests.el

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,37 @@
106106
:and-return-value '())
107107
(expect (cider-project-type) :to-equal cider-jack-in-default))))
108108

109+
(describe "cider--enriched-cmd-p"
110+
(describe "when cmd does not contain the path to an enrich-classpath script"
111+
(it "returns nil"
112+
(expect (cider--enriched-cmd-p "/usr/bin/lein")
113+
:to-equal nil)
114+
(expect (cider--enriched-cmd-p "bash /usr/bin/lein")
115+
:to-equal nil)))
116+
(describe "for different path + script-name combinations"
117+
:var* ((paths `("/simple/path/"
118+
,(shell-quote-argument "/tmp/path/ with spaces/")
119+
,(shell-quote-argument "/ssh:!slightly@cra --zy!path #enrich me/")))
120+
(simple-names (map-values cider--enrich-classpath-script-names))
121+
(tmp-names (mapcar (lambda (s) (cider--make-temp-name s)) simple-names))
122+
(all-names (seq-concatenate 'list simple-names tmp-names)))
123+
(cl-loop
124+
for path in paths do
125+
(cl-loop
126+
for name in all-names do
127+
(describe (format "cider--enriched-cmd-p with script: %s" (concat path name))
128+
:var ((script (concat path name)))
129+
(it "returns the script path in basic cases "
130+
(expect (cider--enriched-cmd-p (concat "bash " script " /usr/bin/lein"))
131+
:to-equal script)
132+
(expect (cider--enriched-cmd-p (concat "TEST=1 bash " script " /usr/bin/env lein"))
133+
:to-equal script))
134+
(it "handles a fully constructed jack-in-cmd."
135+
;; TODO is it worth generating this?
136+
(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")))
137+
(expect (cider--enriched-cmd-p cmd)
138+
:to-equal script))))))))
139+
109140
;;; cider-jack-in tests
110141
(describe "cider--gradle-dependency-notation"
111142
(it "returns a GAV when given a two-element list"

0 commit comments

Comments
 (0)