標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

S式で Makefile を書く make を作ったよ!

zenn.dev に投稿しようかと思って書き始めたけど、結局、塩漬けのままになってしまったのを再利用シリーズ第二弾

Go言語で ISLisp のサブセットを作ってる

でも、単に規格書に載ってる標準関数を一つずつ実装してゆくたけというのもつまらんなぁ。何か実用的なもんを作ってみたいなぁ。もともと、自分が作る Goアプリのカスタマイズ言語として作ったもんだしなぁ。

そういえば、前にどこの環境(WindowsLinux) でも動くビルドスクリプトを書こうとしたが、結局、OSの違いは吸収できても、GNU Make 依存は脱しきれず、nmake 環境でも大丈夫とまではいかなかった。

なら、S式でビルドルールを書くMakeを作ればいいんじゃね?1

ということで作ってみた。

hymkor/smake: SMake (Make by S-expression)

  • Makefile.lsp に make のルールを記述する.
  • Makefile.lsp 自体は純粋な Lisp スクリプトだが、これを解釈する smake には (make) というビルドを行う関数が追加されている.
  • (make) 関数の仕様は、Lisp(cond)(case) に似た形で引数を与える.
    • 第一パラメータは、ビルドする最終ターゲット名
      • サンプルでは $1 (smakeコマンド自身の第一引数が入っている)
    • 第二パラメータ以降は、ルール定義のリスト
      • リストの第一要素は、(ターゲット ソース…) を表すリスト。評価されるので、動的にルールを作ることが可能.
      • リストの第二要素以降は、ソースがターゲットよりも新しい時に実行される Lisp コマンド郡
(let
  ((EXE (if (equal (getenv "OS") "Windows_NT") ".exe" "")))
  (make $1
    ((cons ($ "smake$(EXE)") (wildcard "*.go"))
     (sh "go fmt")
     (sh "go build")
     )
    ('("get")
     (sh "go get -u")
     (sh "go mod tidy")
     )
    ('("update")
     (apply #'touch (wildcard "*.go"))
     )
    ('("readme" "README.md" "Makefile.lsp")
     )
    ('("README.md" "_README.md" "Makefile.lsp")
       (sh ($ "gmnlpp$(EXE) $< > \"$@\""))
     )
    ('("clean")
     (rm ($ "smake$(EXE)~"))
     (rm ($ "smake$(EXE)"))
     )
  )
)

make 相当の機能を簡単に実現できるよう組み込み関数も追加した。

  • sh - シェルコマンドを実行する。sh や CMD.EXE で実行される。リダイレクトとかパイプラインも使える
  • x - 外部の実行ファイルを直接実行する。コマンド名とパラメータは別の文字列として分けて与える。C言語のexec/spawnに近いので、リダイレクト等は直接はサポートしていない
  • $ - 文字列リテラルの中の "$(..)" を Makefile のように展開する。ただ、展開のタイミングがあいまいなので、将来的に廃止を検討中
  • touch - ファイルのタイムスタンプを更新する。Windows には OS 標準になかったので実装
  • rm - ファイルを削除する。UNIX系とWindowsでは rm と del と名前が違っているので、共通化のため実装
  • wildcard - ワイルドカード展開をする。戻り値はリスト
  • getenv - 環境変数を取得する
  • *args* - smake コマンドに与えたパラメータが入ったリスト
  • $1,$2,$3... - *args* をばらしたもの

他の関数はだいたい ISLisp 準拠 (※ 規格を満たしているとは言っていない)

ルールを動的に作ることも可能だ!

ルールが S 式で表現されるということは、スクリプト内でルールを生成することもできる。 C言語向け Makefile

a.out: $(subst .c,.o,$(wildcard *.c))
    gcc -o $@ $^
.c.o:
    gcc -c $<

に相当するものを書いてみると:

(let*
  ((c-to-o (lambda (c) (string-append (basename c) ".o")))
   (c-files (wildcard "*.c"))
   (o-files (mapcar #'c-to-o c-files))
   (exe (if windows ".exe" ""))
   (a-out (string-append (notdir (getwd)) exe)))
  (apply #'make $1
    ((cons a-out o-files)
     (apply #'x "gcc" "-o" $@ o-files))
    ('("clean")
     (apply #'rm a-out o-files))
    (mapcar
      (lambda (c)
        (let ((o (c-to-o c)))
          `((list ,o ,c)
            (x "gcc" "-c" ,c))))
      c-files))) ; let* code

ISLisp や CommonLisp にある (apply) 関数が便利で、これを使うと第一引数の関数の末尾の引数(可変長のパラメータ部分)を、別途リストで与えることができる。

(F A B C D E F) == (apply #'F A B C (list D E F))

だが、なに書いてあるか、さっぱり分からん

ですよねー2


  1. 「それは結局お前のビルドツールに依存するだけじゃね?」「あー聞こえない聞こえない」
  2. みんな Lisp スキなら、GNU Make with Guile がもうちょっと普及しているはず(自分だって、仕事で autolisp に触れる機会がなかったら、Lisp やろうとは思わなかったしなー)