自分の参加しているプロジェクトで、Mage というビルドツールを使っている。私の環境では、Windowsを使っている人も多くて、私は、Go を書くときはLinux の shell 派なんだけど。普通のGoのリポジトリは Makefile のケースがが多いから、Windows派の人が困る、、、的な時に、結構よさげ。Go言語自体を使ってビルドツールが書けます。
通常は、Mageをとってきて、ブートストラップするのだけど、面倒なので、main.go を書いて、その中で、
main.go
func main() {
os.Exit(mage.ParseAndRun(os.Stdout, os.Stderr, os.Stdin, args()))
}
func args() []string {
args := make([]string, 0, len(os.Args)+2)
args = append(args, "-v")
args = append(args, "-d")
args = append(args, "scripts")
args = append(args, "-w")
args = append(args, ".")
args = append(args, "-t")
args = append(args, "10s")
source := os.Args[1:]
for i := 0; i < len(source); i++ {
if source[i] == "-d" || source[i] == "-w" {
fmt.Printf("warn: ignoring %s flag\n", source[i])
i++
continue
} else if source[i] == "-v" {
continue
}
args = append(args, source[i])
}
return args
}
こんな感じで、書いてあげると、Mage
をインストールした感じで -d
の箇所で、指定した Magefile と呼ばれる go のクラスを呼んでくれます。わざと間違えるとこんな感じでヘルプを返してくれます。
$ go run main.go publish:exec
invalid value "10" for flag -t: parse error
mage [options] [target]
Mage is a make-like command runner. See https://magefile.org for full docs.
Commands:
-clean clean out old generated binaries from CACHE_DIR
-compile <string>
output a static binary to the given path
-h show this help
-init create a starting template if no mage files exist
-l list mage targets in this directory
-version show version info for the mage binary
Options:
-d <string>
directory to read magefiles from (default ".")
-debug turn on debug messages
-f force recreation of compiled magefile
-goarch sets the GOARCH for the binary created by -compile (default: current arch)
-gocmd <string>
use the given go binary to compile the output (default: "go")
-goos sets the GOOS for the binary created by -compile (default: current OS)
-ldflags sets the ldflags for the binary created by -compile (default: "")
-h show description of a target
-keep keep intermediate mage files around after running
-t <string>
timeout in duration parsable format (e.g. 5m30s)
-v show verbose output when running mage targets
-w <string>
working directory where magefiles will run (default -d value)
Hello World
-d
で指定したスクリプトファイルの箇所に、次のファイルを作ります。mg.Namespace
によって指定したら、サブコマンド風にMageを呼び出してこのメソッドを呼ぶことが出来ます。
scripts/docker.go
//+build mage
package main
import (
"fmt"
"github.com/magefile/mage/mg"
)
type Hello mg.Namespace
func (Hello) World() error {
fmt.Println("Hello World:")
return nil
}
$ go run main.go hello:world
Running target: Hello:World
Hello World:
いい感じです。ちなみに複数のターゲットをまとめて書けるようです。ちなみにターゲットはケースインセンシティブなようです。
$ go run main.go hello:world some:other
パラメータ付きって出来るでしょうか? これは残念ながらエラーになります。
type Hello mg.Namespace
func (Hello) World() error {
fmt.Println("Hello World:")
return nil
}
func (Hello) World(name string) error {
fmt.Println("Hello World:" + name)
return nil
}
これなら動きます。
type Hello mg.Namespace
func (Hello) World() error {
fmt.Println("Hello World:")
return nil
}
func (Hello) WorldWithName(name string) error {
fmt.Println("Hello World:" + name)
return nil
}
$ go run main.go hello:worldwithname ushio
Running target: Hello:WorldWithName
Hello World:ushio
マルチターゲット
先ほど説明したマルチターゲットですが、やってみると動きません。
func (Publish) A() {
fmt.Println("hello")
}
func (Publish) B() {
fmt.Println("world")
}
$ go run main.go publish:a
# command-line-arguments
./test.go:39:18: undefined: ctx
./test.go:43:18: undefined: ctx
Error: error compiling magefiles
exit status 1
あれ、出来るはず?と思っていましたが、マニュアルを読むと、タイムアウトを指定していると2つ目のターゲットを呼んだときにどうやらコンテキストがキャンセルされてしまうようです。最初の main.go
から -t
の箇所をコメントアウトすると動作します。
$ go run main.go publish:a publish:b
Running target: Publish:A
hello
Running target: Publish:B
world
依存関係
ビルドツールではおなじみの依存関係ですが、簡単に書けます。
type Publish mg.Namespace
func (Publish) All() error {
mg.Deps(Hello.World)
fmt.Println("publish them all")
return nil
}
type Hello mg.Namespace
func (Hello) World() error {
fmt.Println("Hello World:")
return nil
}
$ go run main.go publish:all
Running target: Publish:All
Running dependency: main.Hello.World
Hello World:
publish them all
コンテキスト
Mageのターゲットにはコンテキストがあります。どう使うのでしょう?ちなみに、-t
付きでMageを使うと、タイムアウトとかさせるのに使えます。こんなコードを書いてみました。
func (Publish) Exec(ctx context.Context) error {
finished := make(chan bool)
go func() {
fmt.Println("Now working....")
time.Sleep(5 * time.Second)
finished <- true
}()
select {
case <-ctx.Done(): // Not reachable.
t, ok := ctx.Deadline()
fmt.Println("Reached Deadline! %v: %v\n", t, ok)
return nil
case <-finished:
fmt.Println("Finished Successfully")
return nil
}
}
これを、-t
付きでタイムアウトを10秒にして、実行します。私としては、Reached Deadline!
が来ることを予想していたのですが、そうではなく、フレームワークの方でタイムアウトがかかるようになっているみたいですね。
$ go run main.go publish:exec
Running target: Publish:Exec
Now working....
ctx err: context deadline exceeded
Error: context deadline exceeded
exit status 1
コマンド実行
コマンド実行の関数も用意されています。
func (Publish) Ps() {
sh.RunV("ps", "-ef")
}
$ go run main.go publish:ps
Running target: Publish:Ps
exec: ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:41 ? 00:00:00 /init
root 3504 1 0 14:30 ? 00:00:00 /init
root 3505 3504 0 14:30 ? 00:00:00 /init
ushio 3506 3505 0 14:30 pts/0 00:00:00 sh -c "$VSCODE_WSL_EXT_LOCATION/scripts/wslServer.sh" 2b9aebd5354a3629c3aba0a5f5df49f4
ushio 3507 3506 0 14:30 pts/0 00:00:00 sh /mnt/c/Users/tsushi/.vscode/extensions/ms-vscode-remote.remote-wsl-0.54.6/scripts/w
ushio 3513 3507 0 14:30 pts/0 00:00:00 sh /home/ushio/.vscode-server/bin/2b9aebd5354a3629c3aba0a5f5df49f43d6689f8/server.sh
ushio 3515 3513 0 14:30 pts/0 00:00:53 /home/ushio/.vscode-server/bin/2b9aebd5354a3629c3aba0a5f5df49f43d6689f8/node /home/ush
ushio 3541 3515 0 14:30 pts/0 00:00:19 /home/ushio/.vscode-server/bin/2b9aebd5354a3629c3aba0a5f5df49f43d6689f8/node /home/ush
ushio 3552 3515 0 14:30 pts/1 00:00:00 /bin/bash
エラーをトラックするようにしてみます。
func (Publish) PsErr() error {
env := map[string]string{
"NAME": "ushio",
}
return sh.RunWith(env, "p")
}
$ go run main.go publish:pserr
Running target: Publish:PsErr
exec: p
Error: failed to run "p : exec: "p": executable file not found in $PATH"
exit status 1
パイプ
Goのコマンド実行で大抵問題になるのが、実行されているのが、シェルではないということです。だからパイプは動きません。
func (Publish) Pipe() {
env := map[string]string{
"NAME": "ushio",
}
sh.RunWithV(env, "grep", "docker", textFile)
}
exec: ps -ef | gerp docker
error: garbage option
Usage:
ps [options]
Try 'ps --help <simple|list|output|threads|misc|all>'
or 'ps --help <s|l|o|t|m|a>'
for additional help text.
For more details see ps(1).
強引にパイプを書いてみました。
func (Publish) Pipe() {
env := map[string]string{
"NAME": "ushio",
}
var stdio bytes.Buffer
var stderr bytes.Buffer
sh.Exec(env, &stdio, &stderr, "ps", "-ef")
textFile := "tmp.txt"
defer os.Remove(textFile)
ioutil.WriteFile(textFile, stdio.Bytes(), 0644)
sh.RunWithV(env, "echo", "$NAME")
sh.RunWithV(env, "grep", "docker", textFile)
$ go run main.go publish:pipe
Running target: Publish:Pipe
exec: ps -ef
exec: echo ushio
ushio
exec: grep docker tmp.txt
ushio 4760 3541 0 14:33 pts/0 00:00:00 /home/ushio/.vscode-server/bin/2b9aebd5354a3629c3aba0a5f5df49f43d6689f8/node /home/ushio/.vscode-server/extensions/ms-azuretools.vscode-docker-1.11.0/dist/dockerfile-language-server-nodejs/lib/server.js --node-ipc --node-ipc --clientProcessId=3541
ushio 5430 3714 0 14:38 pts/0 00:00:00 /home/ushio/.vscode-server/bin/2b9aebd5354a3629c3aba0a5f5df49f43d6689f8/node /home/ushio/.vscode-server/extensions/ms-azuretools.vscode-docker-1.11.0/dist/dockerfile-language-server-nodejs/lib/server.js --node-ipc --node-ipc --clientProcessId=3714
root 17753 3505 0 14:57 ? 00:01:23 /usr/bin/dockerd -p /var/run/docker.pid
root 17773 17753 0 14:57 ? 00:01:32 containerd --config /var/run/docker/containerd/containerd.toml --log-level info
ちなみにほんとはこれでイケますけどねw
func (Publish) Pipe() {
sh.RunWithV(env, "bash", "-c", "ps -ef | grep docker")
}