Go で Build Tool Mage を使う

自分の参加しているプロジェクトで、Mage というビルドツールを使っている。私の環境では、Windowsを使っている人も多くて、私は、Go を書くときはLinux の shell 派なんだけど。普通のGoのリポジトリは Makefile のケースがが多いから、Windows派の人が困る、、、的な時に、結構よさげ。Go言語自体を使ってビルドツールが書けます。

* [Mage](https://github.com/magefile/mage)
* [Mage Go Doc](https://pkg.go.dev/github.com/magefile/mage/mage)
* [Mage.org](https://magefile.org/)

通常は、Mageをとってきて、ブートストラップするのだけど、面倒なので、main.go を書いて、その中で、

_main.go_

```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_

```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
}
```

```bash
$ go run main.go hello:world
Running target: Hello:World
Hello World:
```

いい感じです。ちなみに複数のターゲットをまとめて書けるようです。ちなみにターゲットはケースインセンシティブなようです。

```bash
$ go run main.go hello:world some:other
```

パラメータ付きって出来るでしょうか？ これは残念ながらエラーになります。

```go 
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
}

```

これなら動きます。

```go 
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
}
```

```bash
$ go run main.go hello:worldwithname ushio
Running target: Hello:WorldWithName
Hello World:ushio
```


# マルチターゲット

先ほど説明したマルチターゲットですが、やってみると動きません。

```go
func (Publish) A() {
	fmt.Println("hello")
}

func (Publish) B() {
	fmt.Println("world")
}
```

```bash
$ 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
```

あれ、出来るはず？と思っていましたが、マニュアルを読むと、タイムアウトを指定していると２つ目のターゲットを呼んだときにどうやらコンテキストがキャンセルされてしまうようです。最初の `main.go`  から `-t` の箇所をコメントアウトすると動作します。

```bash
$ go run main.go publish:a publish:b
Running target: Publish:A
hello
Running target: Publish:B
world
```

# 依存関係

ビルドツールではおなじみの依存関係ですが、簡単に書けます。

```go:publish.go
type Publish mg.Namespace

func (Publish) All() error {
	mg.Deps(Hello.World)
	fmt.Println("publish them all")
	return nil
}

```

```go:hello.go
type Hello mg.Namespace

func (Hello) World() error {
	fmt.Println("Hello World:")
	return nil
}
```

```bash:実行結果
$ go run main.go publish:all
Running target: Publish:All
Running dependency: main.Hello.World
Hello World:
publish them all
```

# コンテキスト

Mageのターゲットにはコンテキストがあります。どう使うのでしょう？ちなみに、`-t` 付きでMageを使うと、タイムアウトとかさせるのに使えます。こんなコードを書いてみました。

```go:context.go
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` 付きでタイムアウトを１０秒にして、実行します。私としては、`Reached Deadline!` が来ることを予想していたのですが、そうではなく、フレームワークの方でタイムアウトがかかるようになっているみたいですね。

```bash:実行結果
$ go run main.go publish:exec
Running target: Publish:Exec
Now working....
ctx err: context deadline exceeded
Error: context deadline exceeded
exit status 1
```

# コマンド実行

コマンド実行の関数も用意されています。

```bash
func (Publish) Ps() {
	sh.RunV("ps", "-ef")
}
```

```bash:実行結果
$ 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
```

エラーをトラックするようにしてみます。

```go
func (Publish) PsErr() error {
	env := map[string]string{
		"NAME": "ushio",
	}
	return sh.RunWith(env, "p")
}
```

```bash:実行結果
$ 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のコマンド実行で大抵問題になるのが、実行されているのが、シェルではないということです。だからパイプは動きません。

```go:pipe.go
func (Publish) Pipe() {
	env := map[string]string{
		"NAME": "ushio",
	}
	sh.RunWithV(env, "grep", "docker", textFile)
}
```

```bash:実行結果
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).
```

強引にパイプを書いてみました。

```go
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)
```

```bash:実行結果
$ 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

```go
func (Publish) Pipe() {
	sh.RunWithV(env, "bash", "-c", "ps -ef | grep docker")
}
```

# リソース

* [shコマンドの関数](https://github.com/magefile/mage/blob/master/sh/cmd.go)