xshoji's blog

Goのバイナリにソースコードをそのまま同梱するという提案

目次

はじめに

業務でGoのスクリプトを書いて、バイナリをチームに配ることがよくあります。 Goでコンパイル済みのバイナリを配るメリットとしては

  • 実行環境に依存せずどこでも実行できる

という点かなーと思います。 ただ、デメリットもあって、それは実行するバイナリがどのソースコードから生成されたものか分かりづらいという点です。

php, python, rubyなどのスクリプト言語だとソースコードをそのまま実行するので、 中身を読むことができますし、最悪修正することも可能です。

一方、Goでビルド済みのバイナリを配布する場合、「このバイナリはこのソースで作ったよ!」という対応関係を示さない限り、 バイナリを使う利用者はソースコードの内容を把握する術がありません。

もちろん、golereaserなどを使い、コミット番号をバイナリに埋め込んだり、ちゃんとしたリリースのワークフローを介して配布すれば、 どのソースから生成されたかわからない、ということは防げるには防げるんですが ほんとに使い捨てのスクリプトなどの場合そこまでしっかり環境を整えるのは面倒な場合が多いです。

Goのバイナリにソースコードを同梱する

そこで、最近「だったらもうバイナリにソースコードを含めちゃえば良いのでは?」を思い始めました。 具体的には、go 1.16から追加された

embed package - embed - Go Packages
https://pkg.go.dev/embed

を利用して、ビルド時に自身のバイナリへ元のソースコードを埋め込んでしまえば良い。

(ただし、スクリプトがシンプルで main.go だけで構成されるような、本当にちょっとしたツールを前提にしてます。)

具体的なコードの例を示します。

package main

import (
	_ "embed"
	"flag"
	"fmt"
	"os"
)

var (
	//go:embed main.go
	srcBytes       []byte
	paramsPrintSrc = flag.Bool("print-src", false, "[optional] Print main.go")
	paramsHelp     = flag.Bool("h", false, "\nhelp")
)

func main() {
	flag.Parse()
	if *paramsHelp {
		flag.Usage()
		os.Exit(0)
	}
	if *paramsPrintSrc {
		fmt.Printf("%s", srcBytes)
	}
}

上のような実装にします。で、ビルドします。

$ go build main.go

こうすることで、以下のように実行時のオプション指定でバイナリの元となったソースコードを表示できます。

$ ./main -h
Usage of ./main:
  -h
    	help
  -print-src
    	[optional] Print main.go
$ ./main -print-src
package main

import (
	_ "embed"
	"flag"
	"fmt"
	"os"
)

...

これで、ビルド済みのバイナリの配布のみでもととなったソースも同時に配布できることになるので捗るなと思ってます。

複数ファイルの場合

複数ファイルの場合は、 embed.FS を使ってディレクトリ全体を埋め込むか、あるいはもっとお手軽にやるのであれば

package main

import (
	"encoding/base64"
	"flag"
	"fmt"
	"os"
)

var (
	base64src      string
	paramsPrintSrc = flag.Bool("print-src", false, "[optional] Print main.go")
	paramsHelp     = flag.Bool("h", false, "\nhelp")
)

// Build: 
// $ go build -ldflags="-s -w -X 'main.base64src=$(cat main.go |base64)'" -trimpath main.go
func main() {
	flag.Parse()
	if *paramsHelp {
		flag.Usage()
		os.Exit(0)
	}
	if *paramsPrintSrc {
		b, _ := base64.StdEncoding.DecodeString(base64src)
		fmt.Printf("%s", b)
	}
}

という実装にし、

$ go build -ldflags="-X 'main.base64src=$(find . -type f |xargs -I{} bash -c "echo -e \"====\n{}\"; cat {}" |base64)'" .

という感じで、ファイルをリストアップしつつ、各ファイルの内容をcatで出力し、その文字列を埋め込む形でビルドすれば、一応複数ファイルをバイナリに埋め込めます。

ソースコードを同梱するメリット

以下の点でメリットがあると思いました。

  • バイナリの配布先の利用者がバイナリの処理内容を確認できる
  • バイナリの配布先の利用者が処理を修正できる
  • ソースコード内に書いた細かい処理のコメントやスクリプトのREADME的な説明を読んで貰える
    • (コマンドのdescriptionに書かなくても良い)

おわりに

ソースコードの量が膨大じゃない限りはバイナリの容量が大きくなってつらい、みたいなことはそんなにないかなーと思います。