-
Notifications
You must be signed in to change notification settings - Fork 4
/
opam-switch-mode.el
404 lines (357 loc) · 15.4 KB
/
opam-switch-mode.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
;;; opam-switch-mode.el --- Select OCaml opam switches via a menu -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2022 Hendrik Tews
;; Copyright (C) 2022 Erik Martin-Dorel
;;
;; Authors: Hendrik Tews
;; Maintainer: proof-general-maintainers@groupes.renater.fr
;; URL: https://github.com/ProofGeneral/opam-switch-mode
;; Package-Requires: ((emacs "25.1"))
;; Version: 1.8-git
;;
;; SPDX-License-Identifier: GPL-3.0-or-later
;;
;; This file is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;;
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License in file COPYING in this or one of the parent
;; directories for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with "prooftree". If not, see <http://www.gnu.org/licenses/>.
;;
;;
;;; Commentary:
;;
;; Provide command `opam-switch-set-switch' to change the opam switch
;; of the running Emacs session and minor mode `opam-switch-mode' to
;; select the opam switch via a menu-bar menu.
;;
;; `opam-switch-set-switch' reads the name of the switch in the
;; minibuffer, providing completion with all available switches. With
;; no input (i.e., leaving the minibuffer empty) the environment is
;; reset to the state before the first call of
;; `opam-switch-set-switch'.
;;
;; The menu is generated each time the minor mode is enabled and
;; contains the switches that are known at that time. If you create a
;; new switch, re-enable the minor mode to get it added to the menu.
;; The menu contains an additional entry "reset" to reset the
;; environment to the state when Emacs was started.
;;
;; For obvious reasons, `opam-switch-set-switch' does not change the
;; switch of any other shell.
;;
;; See https://opam.ocaml.org for comprehensive documentation on opam.
;;
(require 'seq)
;;; User options and variables
(defgroup opam-switch ()
"Customization for opam switch support in Emacs."
:group 'external)
(defcustom opam-switch-program-name "opam"
"Name or path of the opam binary."
:type 'string)
(defcustom opam-switch-common-options ()
"Options to be supplied to every opam invocation.
This must be a list of strings, each member string an option
accepted by opam."
:type '(repeat string))
(defcustom opam-switch-common-environment
'("OPAMUTF8=never"
"OPAMCOLOR=never"
"LC_ALL=C")
"Process environment to be set for every opam invocation.
List of strings compatible with `process-environment', i.e., each
element should have the form of ENVVARNAME=VALUE.
The process environment must ensure that output is plain ascii
without color, non-ascii arrow symbols and that it is in English.
Otherwise parsing the output of opam commands won't work."
:type '(repeat string))
(defcustom opam-switch-before-change-opam-switch-hook nil
"Hook run before the opam switch changes.
This is used, for instance, to let merlin kill the ocamlmerlin
background process before the opam switch changes."
:type 'hook)
(defcustom opam-switch-change-opam-switch-hook nil
"Hook run after the opam switch changes."
:type 'hook)
;;; Code:
(defvar opam-switch--mode-lighter nil)
(defun opam-switch--run-command-without-stderr (sub-cmd
&optional switch sexp
&rest args)
"Run opam SUB-CMD, without capturing error output.
Run opam SUB-CMD with additional arguments and insert the output
in the current buffer at point. Error output (stderr) is discarded.
If SWITCH is not nil, options \"--switch=SWITCH --set-switch\" are
added. If SEXP is t, option --sexp is added. All remaining
arguments ARGS are added as arguments.
Return exit status of the opam invocation.
Internally this function uses `process-file' internally and will
therefore respect file-name handlers specified via
`default-directory'."
(let ((process-environment
(append opam-switch-common-environment process-environment))
(options (append args opam-switch-common-options)))
(when switch
(push (format "--switch=%s" switch) options)
(push "--set-switch" options))
(when sexp
(push "--sexp" options))
;; (message "run %s %s %s" opam-switch-program-name sub-cmd options)
(apply #'process-file opam-switch-program-name
nil '(t nil) nil sub-cmd options)))
(defun opam-switch--command-as-string (sub-cmd &optional switch sexp &rest args)
"Run opam SUB-CMD, with additional arguments, without capturing stderr.
Return nil if the opam command fails.
Return all output as string otherwise.
If SWITCH is not nil, an option \"--switch=SWITCH\" is added.
If SEXP is t, option --sexp is added.
All remaining arguments ARGS are added as arguments.
This function `opam-switch--run-command-without-stderr'."
(with-temp-buffer
(let ((status
(apply #'opam-switch--run-command-without-stderr sub-cmd switch sexp args)))
(if (eq status 0)
(buffer-string)
nil))))
(defun opam-switch--get-root ()
"Get the opam root directory.
This function gets the opam variable `root'.
This function should not be called directly; see `opam-switch--root'."
(let ((root (opam-switch--command-as-string "var" nil nil "root")))
(unless root
(error "Command 'opam var root' failed"))
(if (eq (aref root (1- (length root))) ?\n)
(substring root 0 -1)
root)))
(defvar opam-switch--root nil
"The opam root directory.")
(defun opam-switch--root ()
"Set variable `opam-switch--root' once, if possible, and return it."
(or opam-switch--root
(let ((result
(condition-case err
(opam-switch--get-root)
(file-missing (error "Cannot run opam: %S" err) nil))))
(when result
(setq opam-switch--root result)))))
;; Example output of opam switch. The warning is output on stderr.
;;
;; OPAMUTF8=never OPAMCOLOR=never LC_ALL=C opam switch
;; # switch compiler description
;; -> 4112-coq-812 ocaml-variants.4.11.2+flambda 4112-coq-812
;; 44 ocaml-base-compiler.4.04.0
;; 450-coq-8.9 ocaml-base-compiler.4.05.0 450-coq-8.9
;; 471-no-coq ocaml-base-compiler.4.07.1 471-no-coq
;; system ocaml-system.4.01.0
;;
;; # switch compiler description
;; -> default ocaml.4.13.1 default
;;
;; [WARNING] The environment is not in sync with the current switch.
;; You should run: eval $(opam env)
(defun opam-switch--get-switches ()
"Return all opam switches as list of strings."
(with-temp-buffer
(unless
(eq (opam-switch--run-command-without-stderr "switch" nil nil "-s") 0)
;; option -s means --short
;; opam exit status different from 0 implies some error occurred
(error "Command 'opam switch' failed"))
(split-string (buffer-string) "\n" t)))
(defvar opam-switch--switch-history nil
"Minibuffer history list for `opam-switch-set-switch'.")
(defvar opam-switch--saved-env nil
"Saved environment variables, overwritten by an opam switch.
This is a list of saved environment variables. Each saved
variable is a list of two strings, the variable and the value.
Set when the first chosen opam switch overwrites the
environment.")
(defvar opam-switch--saved-exec-path nil
"Saved value of variable `exec-path'.
Set when the first chosen opam switch overwrites variable `exec-path'.")
(defun opam-switch--save-current-env (opam-env)
"Save the current environment values relevant to opam.
Argument OPAM-ENV, coming from calling `opam env', is only used
to find the environment variables to save.
The variable `exec-path' is saved in addition to environment variables."
(setq opam-switch--saved-env
(mapcar (lambda (x) (list (car x) (getenv (car x)))) opam-env))
(setq opam-switch--saved-exec-path exec-path))
(defun opam-switch--set-env (opam-env previous-prefix)
"Set a new opam environment.
Environment variables in OPAM-ENV are put into the environment of
the current Emacs session. The variable `exec-path' is changed to
match the environment PATH.
It's not that clear which value in variable `exec-path' corresponds to
a previously set opam switch and also which entry in the PATH
environment variable in OPAM-ENV corresponds to the new switch.
Therefore this function uses the following heuristic.
First, all entries in variable `exec-path' that match
PREVIOUS-PREFIX or `(opam-switch--root)' are deleted.
Then, the first entry for PATH that matches the new switch prefix
is added at the front of variable `exec-path'."
(let ((new-bin-dir
(seq-find
(lambda (dir) (string-prefix-p (opam-switch--get-current-switch-prefix-from opam-env) dir))
(parse-colon-path (cadr (assoc "PATH" opam-env))))))
(unless new-bin-dir
(error "No OPAM_SWITCH_PREFIX directory in PATH"))
(mapc (lambda (x) (setenv (car x) (cadr x))) opam-env)
(setq exec-path
(seq-remove
(lambda (dir) (or (string-prefix-p (opam-switch--root) dir)
(and previous-prefix (string-prefix-p previous-prefix dir))))
exec-path))
(push new-bin-dir exec-path)))
(defun opam-switch--reset-env ()
"Reset process environment to the state before setting the first opam switch.
Reset variable `exec-path' and all environment variables to the values
they had in this Emacs session before the first chosen opam
switch overwrote them."
(mapc (lambda (x) (setenv (car x) (cadr x))) opam-switch--saved-env)
(when opam-switch--saved-exec-path
;; it's nil if one enables `opam-switch-mode'
;; but does not call `opam-switch-set-switch'
;; cf. https://github.com/ProofGeneral/opam-switch-mode/issues/13
(setq exec-path opam-switch--saved-exec-path))
(setq opam-switch--saved-env nil)
(setq opam-switch--saved-exec-path nil))
(defun opam-switch--get-current-switch-prefix ()
"Return prefix of current switch or nil."
(getenv "OPAM_SWITCH_PREFIX"))
(defun opam-switch--get-current-switch-prefix-from (opam-env)
"Return prefix of next switch from `opam-env'."
(cadr (assoc "OPAM_SWITCH_PREFIX" opam-env)))
(defun opam-switch--get-current-switch ()
"Return name of current switch or \"<none>\"."
(let ((current-switch (opam-switch--get-current-switch-prefix)))
(if current-switch
(if (string-prefix-p (opam-switch--root) current-switch)
(file-name-nondirectory current-switch)
;; else current-switch is local
(directory-file-name (file-name-directory current-switch)))
"<none>")))
;;;###autoload
(defun opam-switch-set-switch (switch-name)
"Choose and set an opam switch.
Set opam switch SWITCH-NAME, which must be a valid opam switch
name. When called interactively, the switch name must be entered
in the minibuffer, which forces completion to a valid switch name
or the empty string.
Setting the opam switch for the first time inside Emacs will save
the current environment. Using the empty string for SWITCH-NAME
will reset the environment to the saved values.
The switch is set such that all process invocations from
Emacs respect the newly set opam switch. In addition to setting
environment variables such as PATH and CAML_LD_LIBRARY_PATH, this
also sets variable `exec-path', which controls Emacs'
subprocesses (`call-process', `make-process' and similar
functions).
When the switch is changed, `opam-switch-change-opam-switch-hook'
runs. This a can be used to inform other modes that may run
background processes that depend on the currently active opam
switch.
For obvious reasons, `opam-switch-set-switch' will only affect Emacs and
not any other shells outside Emacs."
(interactive
(let* ((switches (opam-switch--get-switches))
(current-switch (opam-switch--get-current-switch)))
(list
(completing-read
(format "current switch %s; switch to (empty to reset): " current-switch)
switches nil t "" 'opam-switch--switch-history nil))))
(when (and (equal switch-name "") (not opam-switch--saved-env))
(error "No saved opam environment, cannot reset"))
(run-hooks 'opam-switch-before-change-opam-switch-hook)
(if (equal switch-name "")
(opam-switch--reset-env)
(let ((output-string (opam-switch--command-as-string "env" switch-name t))
(prefix (opam-switch--get-current-switch-prefix))
opam-env)
(unless output-string
(error
"Command 'opam env --switch=%s --set-switch' failed - probably because of invalid opam switch \"%s\""
switch-name switch-name))
(setq opam-env (car (read-from-string output-string)))
(unless opam-switch--saved-env
(opam-switch--save-current-env opam-env))
(opam-switch--set-env opam-env prefix)))
(setq opam-switch--mode-lighter nil)
(force-mode-line-update t)
(run-hooks 'opam-switch-change-opam-switch-hook))
(define-obsolete-function-alias 'opam-switch--set-switch
#'opam-switch-set-switch "opam-switch-mode 1.1")
;;; minor mode, keymap and menu
(defvar opam-switch-mode-map (make-sparse-keymap)
"Keymap for `opam-switch-mode'.")
(defun opam-switch--menu-items ()
"Create list of opam switches as menu items for `easy-menu'."
(append
;; first the current switch as info with a separator
'(["current switch: " nil
:active nil
:suffix (opam-switch--get-current-switch)
:help "Shows the currently selected opam switch"]
"-------")
;; then the list with all the real opam switches
(mapcar
(lambda (switch)
`[,switch
(opam-switch-set-switch ,switch)
:active t
:help ,(concat "Select opam switch \"" switch "\"")])
(opam-switch--get-switches))
;; now reset as last element
'(
["reset"
(opam-switch-set-switch "")
:active opam-switch--saved-env
:help "Reset to state when Emacs was started"])))
(defun opam-switch--setup-opam-switch-mode ()
"Re-define menu for `opam-switch-mode'.
This function runs when `opam-switch-mode' is enabled to setup
`opam-switch-mode'. Currently it only redefines the menu.
Note that the code for setting up the keymap and running the hook
is automatically created by `define-minor-mode'."
(easy-menu-define
opam-switch--mode-menu
opam-switch-mode-map
"opam mode menu"
;; FIXME: Use `:filter'?
(append
(list "OPSW - Opam Switch Mode"
:label "OPSW")
(opam-switch--menu-items))))
(defun opam-switch-mode-lighter ()
"Return the lighter for opam-switch-mode which indicates the current switch."
(or opam-switch--mode-lighter
(let* ((current-switch (opam-switch--get-current-switch))
;; handle the case of local switches for better UX
(shortened
(replace-regexp-in-string ".*/" "…/" current-switch
t t)))
(setq opam-switch--mode-lighter
(format " OPSW-%s" shortened)))))
;;;###autoload
(define-minor-mode opam-switch-mode
"Toggle opam-switch mode.
The mode can be enabled only if opam is found and \"opam var root\" succeeds."
:lighter (:eval (opam-switch-mode-lighter))
(if (not opam-switch-mode)
(opam-switch--reset-env)
(condition-case sig
(progn
(opam-switch--root)
(opam-switch--setup-opam-switch-mode))
(t (setq opam-switch-mode nil)
(message "Opam-Switch mode disabled: %s" (pp-to-string sig))))))
(provide 'opam-switch-mode)
;;; opam-switch-mode.el ends here