这个帖子记录了一些我在 Windows 上尝试让 Emacs 与 MSYS2 工具链配合使用踩过的一些坑,如果你也碰到了,希望有些帮助。如果你不使用 Windows 系统或者不需要 MSYS2 工具链这个帖子可能没什么用。
本帖子使用的 Emacs 来自 Index of /gnu/emacs/windows/emacs-29,版本是 29.2。
我们可以从 MSYS2 官网 获取安装包,完成安装后能够在任务栏的搜索功能中找到几个相似的图标,它们的区别可以参考 Environments - MSYS2。官方比较推荐的是 UCRT 环境,它使用的 C runtime 是 UCRT 而不是比较老的 MSVCRT。关于 UCRT 和 MSVCRT 的区别可以参考 crt - Differences between msvcrt, ucrt and vcruntime libraries - Stack Overflow。通过点击如下图标可进入 MSYS2 环境:
MSYS2 使用 pacman 作为包管理工具,MSYS2 为不同的环境提供了不同的包,比如 mingw-w64-x86_64-cmake 和 mingw-w64-ucrt-x86_64-cmake,这些包的前缀都很长,建议通过 MSYS2 Packages 直接搜索而不是手敲。
如果你想在类似 Linux 的环境里面编程,但是又懒得装 Linux 双系统或者虚拟机(或者是 WSL2),那么 MSYS2 应该是一个不错的选择。
(草,前置内容是不是长了点)
在 MSYS2 环境中使用 Emacs
如果我们想要让 Emacs 继承 MSYS2 的开发环境,我们可以在 MSYS2 Shell 中启动 Emacs,此时 Emacs 的 shell
命令将会启动 MSYS2 的 bash 而不是 cmd。我一般会在机器上下载和安装 Emacs 时,借用 UCRT 环境编译拉下来的 tree-sitter parser。但是在这个环境下,你启动 shell 后可以看到如下的效果:
首先,shell 的首行会显示无法设置 terminal process group,这是由于 emacs 的内置 shell 并不是一个完整的 Terminal,你也能够注意到 shell 每行输出的开头和末尾有奇怪的字符,这应该是 Emacs 无法识别的一些终端控制字符。关于这个问题,以下是我能够找到的一些问答:
- cygwin bash does not display correctly in emacs shell - Stack Overflow
- EmacsWiki: NTEmacs With Cygwin
在上面的第一个链接中,提问者在 Cgywin 环境中发现和我一样的问题,由于无法使用 job control 这个 shell 的用处比较有限。MSYS2 直接基于 Cygwin,因此也继承了这个问题。在上面的 Emacs wiki 页面中给出了一个叫做 d5884/fakecygpty 的工具,可以用来模拟在 Emacs 中模拟 pty(Windows 上没有 pty 这样的东西),我尝试在 UCRT 下编译它,编译器提示我缺少 termios.h 这个头文件。参考这个 issue,我们需要在 MSYS2 环境中编译(可以通过 pacman -S gcc
安装 MSYS2 环境中的基础工具)。
如果读者按照项目 README 完成了相关设置,那么在调用 shell
命令时会弹出一个黑框,且不会再出现 ioctl 错误:
(我只是尝试编译了一下,没有长时间使用过)
在 Emacs 中使用 MSYS2 shell
很多情况下我们可能并不想让 Emacs 处于 MSYS2 环境下,而只是想使用它的 SHELL 或一些命令行工具,此时我们可以尝试设置一些变量来达到目的,比如以下这些链接:
- microsoft windows - mingw shell in emacs - Emacs Stack Exchange
- MSYS2 inferior shell in Emacs on Windows - Stack Overflow
- windows - How can I run Cygwin Bash Shell from within Emacs? - Stack Overflow
以上帖子都涉及到了对 explicit-shell-file-name
这个变量的使用,这里参考 (info "(emacs) Interactive Shell)
做一点知识补充:
To specify the shell file name used by ‘M-x shell’, customize the variable ‘explicit-shell-file-name’. If this is ‘nil’ (the default), Emacs uses the environment variable ‘ESHELL’ if it exists. Otherwise, it usually uses the variable ‘shell-file-name’ (*note Single Shell::); but if the default directory is remote (*note Remote Files::), it prompts you for the shell file name. *Note Minibuffer File::, for hints how to type remote file names effectively.
也就是说,在没有设置 explicit-shell-file-name
的情况下,Emacs 会使用 shell-file-name
中的路径来启动 shell(在 Windows 中就是 Emacs 自带的 cmdproxy.exe)。我们可以设置 explicit-{name}-args
来设定启动 shell 时向其传递的命令行参数,比如对 bash 就是 explicit-bash.exe-args
。一个比较简单的设定是这样的:
;; https://emacs.stackexchange.com/questions/71487/mingw-shell-in-emacs
(setq explicit-shell-file-name "C:/tools/msys64/usr/bin/bash.exe")
(setq explicit-bash.exe-args '("--login" "-i"))
这样的设定会出现上一节一样的问题,无法设定 terminal process group,如果我们去掉传给 bash.exe 的 -i
参数则会得到如下效果(非交互式的 shell):
如果我们对 SHELL 也采用上一节提到的辅助程序也许能规避这个问题。对于 Emacs 内部调用 MSYS2 shell,如果我们指定 -i
会出现 warning,不指定 -i
就没有 PROMPT。
另一个问题是,通过这种方法指定的 SHELL 只是默认的 MSYS2 环境,而不是 Mingw64 或 ucrt 64 环境。但是 msys2 提供的打开各环境终端的 EXE 文件并不方便被 Emacs 所使用,因为它们打开了额外的窗口而不是 Emacs 的 buffer。所幸 msys2 提供了一个叫做 msys2_shell.cmd 的启动脚本,它位于 msys2 根目录下,可以通过它启动不同的环境:
使用以下代码,可以让 Emacs 打开的 shell 为 ucrt64 环境(参考 Terminals - MSYS2):
;; https://stackoverflow.com/a/235356
(defun my-ucrt64-shell ()
(interactive)
(dlet ((explicit-shell-file-name "d:/_D/msys2/msys2_shell.cmd")
(explicit-msys2_shell.cmd-args '("-defterm" "-here" "-no-start" "-ucrt64")))
(call-interactively 'shell)))
(在 “-ucrt64” 后面加上 “-i” 即可启动交互式 shell,不过还是有上面的缺陷)
除了定义命令,我们也可以在 shell buffer 中直接使用 msys2_shell.cmd 激活对应的环境:
在 Emacs 中使用 MSYS2 中的一些工具
如果我们只是想在 MSYS2 环境中调用一些特定的命令,我们只需要使用 -c 即可,比如查看 gcc 版本:
(defun my-ucrt64-gcc-version ()
(interactive)
(dlet ((explicit-shell-file-name "d:/_D/msys2/msys2_shell.cmd")
(explicit-msys2_shell.cmd-args '("-defterm" "-here" "-no-start"
"-ucrt64" "-c" "gcc --version")))
(call-interactively 'shell)))
(call-process-shell-command "d:/_D/msys2/msys2_shell.cmd -defterm -here \
-no-start -ucrt64 -c \"gcc --version\"" nil t)
附上我在 Emacs 中使用来自 UCRT 的 clangd 的配置:
(defcustom yy/use-msys2-ucrt-clangd nil
"是否使用 msys2 的 ucrt 中的 clangd LSP server
若要使用,该变量值应为 msys2 的根目录路径")
(if yy/use-msys2-ucrt-clangd
(progn (add-to-list 'eglot-server-programs
`((c++-ts-mode c-ts-mode c-or-c++-mode)
;; https://stackoverflow.com/a/76077729
,(file-name-concat yy/use-msys2-ucrt-clangd
"msys2_shell.cmd")
"-defterm" "-no-start" "-ucrt64" "-here"
"-c" "\"clangd\""))
(add-hook 'c-ts-mode-hook 'eglot-ensure)
(add-hook 'c++-ts-mode-hook 'eglot-ensure))
(when (executable-find "clangd")
(add-to-list 'eglot-server-programs
'((c++-ts-mode c-ts-mode c-or-c++-mode) "clangd"))
(add-hook 'c-ts-mode-hook 'eglot-ensure)
(add-hook 'c++-ts-mode-hook 'eglot-ensure)))