Skip to content

Commit 7456966

Browse files
committed
Implement upgrading of nREPL connections
Piece together the final pieces to make dynamic loading + sideloading of middleware work. - Add cider-jar mini-library : download jars, list their contents and extract files - Install unzip on CI, needed by arc-mode which we use for the jars
1 parent 76dea32 commit 7456966

File tree

6 files changed

+316
-13
lines changed

6 files changed

+316
-13
lines changed

.circleci/config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ commands:
1111
- run:
1212
name: Install Eldev
1313
command: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/circle-eldev > x.sh && source ./x.sh
14+
- run:
15+
name: Install unzip
16+
command: apt-get update && apt-get install unzip
17+
1418
setup-windows:
1519
steps:
1620
- checkout

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* [#3031](https://github.com/clojure-emacs/cider/pull/3031): Fix `cider-eval-defun-up-to-point` failing to match delimiters correctly in some cases, resulting in reader exceptions.
1616
* [#3039](https://github.com/clojure-emacs/cider/pull/3039): Allow starting the sideloader for the tooling session.
1717
* [#3041](https://github.com/clojure-emacs/cider/pull/3041): Sideloader: handle binary files, support multiple directories
18+
* [#3044](https://github.com/clojure-emacs/cider/pull/3044): Dynamically upgrade nREPL connection
1819

1920
## 1.1.1 (2021-05-24)
2021

cider-eval.el

Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
;; Artur Malabarba <[email protected]>
1010
;; Hugo Duncan <[email protected]>
1111
;; Steve Purcell <[email protected]>
12+
;; Arne Brasseur <[email protected]>
1213

1314
;; This program is free software: you can redistribute it and/or modify
1415
;; it under the terms of the GNU General Public License as published by
@@ -52,6 +53,7 @@
5253
(require 'cider-client)
5354
(require 'cider-common)
5455
(require 'cider-compat)
56+
(require 'cider-jar)
5557
(require 'cider-overlays)
5658
(require 'cider-popup)
5759
(require 'cider-repl)
@@ -189,29 +191,50 @@ When invoked with a prefix ARG the command doesn't prompt for confirmation."
189191

190192
;;; Sideloader
191193

192-
(defvar cider-sideloader-dirs
193-
(list (file-name-directory load-file-name))
194-
"Directories where we look for resources requested by the sideloader.")
194+
(defcustom cider-sideloader-path nil
195+
"List of directories and jar files to scan for sideloader resources.
196+
When not set the cider-nrepl jar will be added automatically when upgrading
197+
an nREPL connection."
198+
:type 'list
199+
:group 'cider
200+
:package-version '(cider . "0.27.0"))
201+
202+
(defcustom cider-dynload-cider-nrepl-version nil
203+
"Version of the cider-nrepl jar used for dynamically upgrading a connection.
204+
Defaults to `cider-required-middleware-version'"
205+
:type 'string
206+
:group 'cider
207+
:package-version '(cider . "0.27.0"))
195208

196-
;; based on f-read-bytes
197209
(defun cider-read-bytes (path)
198210
"Read binary data from PATH.
199211
Return the binary data as unibyte string."
212+
;; based on f-read-bytes
200213
(with-temp-buffer
201214
(set-buffer-multibyte nil)
202215
(setq buffer-file-coding-system 'binary)
203216
(insert-file-contents-literally path nil)
204217
(buffer-substring-no-properties (point-min) (point-max))))
205218

219+
(defun cider-retrieve-resource (dirs name)
220+
"Find a resource NAME in a list DIRS of directories or jar files.
221+
Similar to a classpath lookup. Returns the file contents as a string."
222+
(seq-some
223+
(lambda (path)
224+
(cond
225+
((file-directory-p path)
226+
(let ((expanded (expand-file-name name path)))
227+
(when (file-exists-p expanded)
228+
(cider-read-bytes expanded))))
229+
((and (file-exists-p path) (string-suffix-p ".jar" path))
230+
(cider-jar-retrieve-resource path name))))
231+
dirs))
232+
206233
(defun cider-provide-file (file)
207234
"Provide FILE in a format suitable for sideloading."
208-
(let ((file (seq-find
209-
#'file-exists-p
210-
(seq-map (lambda (dir)
211-
(expand-file-name file dir))
212-
cider-sideloader-dirs))))
213-
(if file
214-
(base64-encode-string (cider-read-bytes file) 'no-line-breaks)
235+
(let ((contents (cider-retrieve-resource cider-sideloader-path file)))
236+
(if contents
237+
(base64-encode-string contents 'no-line-breaks)
215238
;; if we can't find the file we should return an empty string
216239
(base64-encode-string ""))))
217240

@@ -223,6 +246,20 @@ Return the binary data as unibyte string."
223246
(when (member "sideloader-lookup" status)
224247
(cider-request:sideloader-provide id type name))))))
225248

249+
(defun cider-add-middleware-handler (continue)
250+
"Make a add-middleware handler.
251+
CONTINUE is an optional continuation function."
252+
(lambda (response)
253+
(nrepl-dbind-response response (status unresolved-middleware) ;; id middleware
254+
(when unresolved-middleware
255+
(seq-do
256+
(lambda (mw)
257+
(cider-repl-emit-interactive-stderr
258+
(concat "WARNING: middleware " mw " was not found or failed to load.\n")))
259+
unresolved-middleware))
260+
(when (and status (member "done" status) continue)
261+
(funcall continue)))))
262+
226263
(defun cider-request:sideloader-start (&optional connection tooling)
227264
"Perform the nREPL \"sideloader-start\" op.
228265
If CONNECTION is nil, use `cider-current-repl'.
@@ -253,6 +290,76 @@ If CONNECTION is nil, use `cider-current-repl'."
253290
(cider-request:sideloader-start connection)
254291
(cider-request:sideloader-start connection 'tooling))
255292

293+
(defvar cider-nrepl-middlewares
294+
'("cider.nrepl/wrap-apropos"
295+
"cider.nrepl/wrap-classpath"
296+
"cider.nrepl/wrap-clojuredocs"
297+
"cider.nrepl/wrap-complete"
298+
"cider.nrepl/wrap-content-type"
299+
"cider.nrepl/wrap-debug"
300+
"cider.nrepl/wrap-enlighten"
301+
"cider.nrepl/wrap-format"
302+
"cider.nrepl/wrap-info"
303+
"cider.nrepl/wrap-inspect"
304+
"cider.nrepl/wrap-macroexpand"
305+
"cider.nrepl/wrap-ns"
306+
"cider.nrepl/wrap-out"
307+
"cider.nrepl/wrap-slurp"
308+
"cider.nrepl/wrap-profile"
309+
"cider.nrepl/wrap-refresh"
310+
"cider.nrepl/wrap-resource"
311+
"cider.nrepl/wrap-spec"
312+
"cider.nrepl/wrap-stacktrace"
313+
"cider.nrepl/wrap-test"
314+
"cider.nrepl/wrap-trace"
315+
"cider.nrepl/wrap-tracker"
316+
"cider.nrepl/wrap-undef"
317+
"cider.nrepl/wrap-version"
318+
"cider.nrepl/wrap-xref"))
319+
320+
(defun cider-request:add-middleware (middlewares
321+
&optional connection tooling continue)
322+
"Use the nREPL dynamic loader to add MIDDLEWARES to the nREPL session.
323+
324+
- If CONNECTION is nil, use `cider-current-repl'.
325+
- If TOOLING it truthy, use the tooling session instead of the main session.
326+
- CONTINUE is an optional continuation function, which will be called when the
327+
add-middleware op has finished succesfully."
328+
(cider-nrepl-send-request `("op" "add-middleware"
329+
"middleware" ,middlewares)
330+
(cider-add-middleware-handler continue)
331+
connection
332+
tooling))
333+
334+
(defun cider-add-cider-nrepl-middlewares (&optional connection)
335+
"Use dynamic loading to add the cider-nrepl middlewares to nREPL.
336+
If CONNECTION is nil, use `cider-current-repl'."
337+
(cider-request:add-middleware
338+
cider-nrepl-middlewares connection nil
339+
(lambda ()
340+
;; When the main session is done adding middleware, then do the tooling
341+
;; session. At this point all the namespaces have been sideloaded so this
342+
;; is faster, we don't want these to race to sideload resources.
343+
(cider-request:add-middleware
344+
cider-nrepl-middlewares connection 'tooling
345+
(lambda ()
346+
;; Ask nREPL again what its capabilities are, so we know which new
347+
;; operations are supported.
348+
(nrepl--init-capabilities (or connection (cider-current-repl))))))))
349+
350+
(defvar cider-required-middleware-version)
351+
(defun cider-upgrade-nrepl-connection (&optional connection)
352+
"Sideload cider-nrepl middleware.
353+
If CONNECTION is nil, use `cider-current-repl'."
354+
(interactive)
355+
(when (not cider-sideloader-path)
356+
(setq cider-sideloader-path (list (cider-jar-find-or-fetch
357+
"cider" "cider-nrepl"
358+
(or cider-dynload-cider-nrepl-version
359+
cider-required-middleware-version)))))
360+
(cider-sideloader-start connection)
361+
(cider-add-cider-nrepl-middlewares connection))
362+
256363

257364
;;; Dealing with compilation (evaluation) errors and warnings
258365
(defun cider-find-property (property &optional backward)

cider-jar.el

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
;;; cider-jar.el --- Jar functionality for Clojure -*- lexical-binding: t -*-
2+
3+
;; Copyright © 2021 Arne Brasseur
4+
;;
5+
;; Author: Arne Brasseur <[email protected]>
6+
7+
;; This program is free software: you can redistribute it and/or modify
8+
;; it under the terms of the GNU General Public License as published by
9+
;; the Free Software Foundation, either version 3 of the License, or
10+
;; (at your option) any later version.
11+
12+
;; This program is distributed in the hope that it will be useful,
13+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
;; GNU General Public License for more details.
16+
17+
;; You should have received a copy of the GNU General Public License
18+
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+
;; This file is not part of GNU Emacs.
21+
22+
;;; Commentary:
23+
24+
;; Dealing with JAR (Java archive) files, which are really just zip files in
25+
;; disguise. In particular downloading and retrieving the cider-nrepl jar.
26+
27+
;;; Code:
28+
29+
(require 'url)
30+
(require 'arc-mode)
31+
(require 'map)
32+
33+
34+
(defvar cider-jar-cache-dir (expand-file-name "cider-cache" user-emacs-directory)
35+
"Location where we store dowloaded files for later use.")
36+
37+
(defvar cider-jar-content-cache (make-hash-table :test #'equal)
38+
"Nested hash table of jar-path -> file-path -> bool.
39+
This provides an efficient check to see if a file exists in a jar or not.")
40+
41+
(defun cider-jar-clojars-url (group artifact version)
42+
"URL to download a specific jar from Clojars.
43+
GROUP, ARTIFACT, and VERSION are the components of the Maven coordinates."
44+
(concat "https://repo.clojars.org/" group "/" artifact "/"
45+
version
46+
"/cider-nrepl-"
47+
version
48+
".jar"))
49+
50+
(defun cider-jar-find-or-fetch (group artifact version)
51+
"Download the given jar off clojars and cache it.
52+
53+
GROUP, ARTIFACT, and VERSION are the components of the Maven coordinates.
54+
Returns the path to the jar."
55+
(let* ((m2-path (expand-file-name (concat "~/.m2/repository/" group "/" artifact "/" version "/" artifact "-" version ".jar")))
56+
(clojars-url (cider-jar-clojars-url group artifact version))
57+
(cache-path (expand-file-name (replace-regexp-in-string "https://" "" clojars-url) cider-jar-cache-dir)))
58+
(cond
59+
((file-exists-p m2-path) m2-path)
60+
((file-exists-p cache-path) cache-path)
61+
(t
62+
(make-directory (file-name-directory cache-path) t)
63+
(url-copy-file clojars-url cache-path)
64+
cache-path))))
65+
66+
(defun cider-jar-contents (jarfile)
67+
"Get the list of filenames in a jar (or zip) file.
68+
JARFILE is the location of the archive."
69+
(with-temp-buffer
70+
(set-buffer-multibyte nil)
71+
(setq buffer-file-coding-system 'binary)
72+
(insert-file-contents-literally jarfile nil)
73+
(seq-map
74+
(lambda (v)
75+
(if (vectorp v)
76+
;; Earlier emacsen, result is a vector, first slot is the name
77+
(elt v 0)
78+
;; Emacs 28, result is a recordp / cl-defstruct. First slot contains the
79+
;; name.
80+
;; This should really be a (funcall #'archive--file-desc-ext-file-name v)
81+
;; but because of linting we can't have nice things.
82+
(aref v 1)))
83+
(archive-zip-summarize))))
84+
85+
(defun cider-jar-contents-cached (jarfile)
86+
"Like cider-jar-contents, but cached.
87+
88+
Instead of returning a list of strings this returns a hash table of string
89+
keys and values `t`, for quick lookup. JARFILE is the location of the
90+
archive."
91+
(let ((m (map-elt cider-jar-content-cache jarfile)))
92+
(if m
93+
m
94+
(let ((m (make-hash-table :test #'equal)))
95+
(seq-do (lambda (path)
96+
(puthash path t m))
97+
(cider-jar-contents jarfile))
98+
(puthash jarfile m cider-jar-content-cache)
99+
m))))
100+
101+
(defun cider-jar-contains-p (jarfile name)
102+
"Does the JARFILE contain a file with the given NAME?"
103+
(map-elt (cider-jar-contents-cached jarfile) name))
104+
105+
(defun cider-jar-retrieve-resource (jarfile name)
106+
"Extract a file NAME from a JARFILE as a string."
107+
(make-directory archive-tmpdir :make-parents)
108+
(when (cider-jar-contains-p jarfile name)
109+
(let ((default-directory archive-tmpdir))
110+
(with-temp-buffer
111+
(set-buffer-multibyte nil)
112+
(setq buffer-file-coding-system 'binary)
113+
(archive-zip-extract jarfile name)
114+
(buffer-substring-no-properties (point-min) (point-max))))))
115+
116+
(provide 'cider-jar)
117+
;;; cider-jar.el ends here

test/cider-eval-test.el

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@
3232
(it "returns an empty string when the file is not found"
3333
(expect (cider-provide-file "abc.clj") :to-equal ""))
3434
(it "base64 encodes without newlines"
35-
(let ((cider-sideloader-dir "/tmp")
35+
(let ((cider-sideloader-path (list "/tmp"))
3636
(default-directory "/tmp")
3737
(filename (make-temp-file "abc.clj")))
3838
(with-temp-file filename
3939
(dotimes (_ 60) (insert "x")))
4040
(expect (cider-provide-file filename) :not :to-match "\n")))
4141
(it "can handle multibyte characters"
42-
(let ((cider-sideloader-dir "/tmp")
42+
(let ((cider-sideloader-path (list "/tmp"))
4343
(default-directory "/tmp")
4444
(filename (make-temp-file "abc.clj")))
4545
(with-temp-file filename

0 commit comments

Comments
 (0)