在 Windows 上的 Emacs 中使用 MSYS2 shell

这个帖子记录了一些我在 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-cmakemingw-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 无法识别的一些终端控制字符。关于这个问题,以下是我能够找到的一些问答:

在上面的第一个链接中,提问者在 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 或一些命令行工具,此时我们可以尝试设置一些变量来达到目的,比如以下这些链接:

以上帖子都涉及到了对 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)))
4 个赞

msys 非常慢,感觉比 wsl2 慢了十倍不止。

就 shell 执行效率来说,慢十倍都说少了…

但是用 gcc 编译出来的 exe 效率还行

1 个赞

写得很详细,不过还是感觉用wsl省事

1 个赞

WSL 确实,不过我只尝试过 WSL1,现在的 2 和 gWSL WSLg 应该是完全的虚拟机了吧,打开 Windows 的 WSL 支持后似乎整个系统都被虚拟化了。我的朋友用过一次,打游戏的帧数好像有些影响。现在比较复杂的环境我就直接用虚拟机了,简单的玩具代码就用 MSYS2

WSL2是基于虚拟化技术,但不是传统的VM。我使用体验还是挺好的,比cygwin/msys 快多了。如果只用命令行,装个ubuntu或者arch足够了,不用装GUI,跟开一个shell没什么差别,git使用比Windows原生的快太多了。

2 个赞

我的体会是如果一味的强调命令行其实也不是可持续发展的思路。毕竟还会是由很多图形化需求在命令行中体验不好。我个人的感受,使用org或者完全emacs作为主力编辑器的情况下,特别是输入文档或者coding场景heavy duty的时候,还是需要较为合理的gui环境。wsl 需要使用arch,但是不是ms的亲儿子,所以wslg设置不甚完美,及时修改了wayland配置,图像化使用没有问题,但是总是会闪退。个人觉得要是能有完美的windows原生其实还是会更好。但是,windows毕竟需要依赖,msys是必然解决依赖的好办法。所以,这里其实是两难,windows确实很折腾,wsl虽然便捷但是gui支持确实不够完美,最好的办法还是有linux的本子。

不过这里分享一个windows build的git repo,这里有直接的脚本,有需求的可以参考。 Scripts to build a distribution of Emacs from sources, using MSYS2 and Mingw64(32)

WSL2下GUI也没啥问题,ubuntu和arch我都试过了。除了用多屏会有些小问题,其他使用和原生没啥区别,使用git速度还快不少。不能叫完美,但是很接近了。

WSL2下ARCH的GUI,有没有试过合上笔记本,也就是挂起之后再打开,会不会GUI挂掉,或者黑屏之后GUI挂掉。另外,有没有使用PYIM的输入,这个输入解决掉了输入法的问题,但是有了图的org会出现延时。你有没有这个问题?

wsl 不主动释放 内存…

多显示器的情况下遇到过,因为最新的wslg用的remote desktop方式,好像对多屏支持有问题。其他情况下还算正常。

wsl2总崩溃,需要 winsock reset,好奇怪。

我是直接用的git-bash,msfork的git快很多,也不太用得着msys。

(when (file-exists-p "C:/Program Files/Git/usr/bin")
  (setenv "PATH"
		  (concat
		   "C:\\Program Files\\Git\\usr\\bin" ";"
		   (getenv "PATH")))
  (setq explicit-shell-file-name
		"C:/Program Files/Git/bin/bash.exe")
  (setq shell-file-name explicit-shell-file-name)
  (setq explicit-bash.exe-args '("--login" "-i"))
  nil)
(when (file-exists-p "~/.local/bin")
  (setenv "PATH"
          (concat
           "~/.local/bin" ";"
           (getenv "PATH")))
  nil)
1 个赞