背景
很多人的 package 安装脚本都是简单的做一个 shell 数组 packages=(...)
然后循环安装。但是这样做在跨平台的时候有以下问题:
- 软件包的名字在不同的 package manager 下不一定相同
- 有时候仅仅按照
<installer-prefix> <pkg-name>
组装的命令完全不够用(e.g. 需要配置 AUR、Brew tap 等) - 有的软件是仅在某个系统中使用的,不希望在别的系统安装
导致最后抽象程度很低,往往要维护多个文件(如 install_xxx.sh),心智负担有点重。(我个人还有个文本文件用来记录每个软件具体安装的目的和一些附加的信息。)
思路
最近刚入坑 Emacs,翻看配置的时候发现 use-package
抽象的很漂亮,于是提醒我了可以用 macro 做一个简单的抽象,然后根据需要 expand 成不同的安装脚本。
同时,我希望这个过程可以和 org babel tangle 有机结合,这样我的 package 描述和安装都可以写在同一个 org 文件。这点受到 Doom Emacs Configuration 的启发。
具体做法
为啥不用 Elisp
琢磨了半天没找到怎么在 eval 代码块的时候不污染 global namspace,而且我并不希望因为这个去给宏起个很长的 prefix 名。
好在利用 noweb 可以直接把 racket 输出的 list 直接给 elisp 代码用。
在 Packages
section 中定义和介绍包,然后用 :noweb-ref
来把它们组合起来。
* Packages
:PROPERTIES:
:header-args:racket: :lang racket :eval no :noweb-ref package-definitions
:END:
** CLI
*** Neovim
#+begin_src racket
(package "Neovim"
(brew "neovim" --head))
#+end_src
** GUI
*** Discord
#+begin_src racket
(package "Discord"
(brew "discord" --cask))
#+end_src
*** Emacs
#+begin_src racket
(package "Emacs Plus"
(brew "emacs-plus"
#:tap "d12frosted/emacs-plus"))
#+end_src
* Generate script
(这里是生成脚本的地方)
然后在 Generate script
当中放入下面定义宏的代码块:
#+name: get-commands
#+begin_src racket :noweb eval :noweb-prefix no :results output
(require (for-syntax syntax/parse))
(define-syntax (brew stx)
(define-splicing-syntax-class option
(pattern (~literal --cask)
#:with target #'"--cask")
(pattern (~literal --head)
#:with target #'"--head"))
(syntax-parse stx
[(_ pkg-name:string
(~alt opt:option
(~optional (~seq #:tap tap:string))) ...)
#'(list
(~? (list "brew" "tap" tap))
(list "brew" "install" pkg-name (~? opt.target) ...))]))
(define-syntax (package stx)
(syntax-case stx ()
;; No matching arms.
[(package name) #'(void)]
;; (package (<mgr0> <rest0>*) (<mgr> <rest>*)*)
[(package name (mgr0 rest0 ...) (mgr rest ...) ...)
(if (equal? (syntax-e #'mgr0) 'brew)
(syntax-case #'(mgr0 rest0 ...) (::)
[(mgr :: raw0 raw ...) #'(:: raw0 raw ...)]
[_ #'(mgr0 rest0 ...)])
#'(package name (mgr rest ...) ...))]))
(append <<package-definitions>>)
#+end_src
最后用以下两个简单的代码块分别控制使用的 package manager 和生成最终的 tangle 代码块:
#+name: pkgmgr
#+begin_src racket :results output :eval no
'brew
#+end_src
#+name: generate
#+begin_src elisp :noweb eval :noweb-prefix no :results raw
(format-spec
"#+begin_src shell :tangle ~/Desktop/test.sh :eval no
%p
,#+end_src"
`((?p . ,(string-join (-map (lambda (line) (string-join line " "))
<<get-commands()>>)
"\n"))))
#+end_src
最终结果:
#+call: generate()
#+RESULTS:
#+begin_src shell :tangle ~/Desktop/test.sh :eval no
brew install neovim --head
brew install discord --cask
brew tap d12frosted/emacs-plus
brew install emacs-plus
#+end_src
最初设想是跨平台,甚至跨语言(不局限于 Bash,所以宏展开的不是直接的字符串)的生成,然后还可以在最终生成的脚本上下加入一些 prologue 和 epilogue。
做完这些回想了一下感觉有点蠢,因为:
其实我只有 mac:var xxx=xxx
不能在宏展开生效,导致必须多做个代码块控制目标 package manager,而不能直接类似#+call: generate("brew")