Go
golang
Cygwin
IntelliJ

Go言語の依存管理ツールを作って、開発環境構築を覚えた

More than 1 year has passed since last update.

@eaglesakura です。

基本的にAndroid世界の片隅でコードを書いている私ですが、テスト用のサーバーをGAE/Goで構築しようかという目論見のもと、Go言語の学習もはじめました。Go言語は半年くらい前にHello World!を一度書いて以来、まったく触れてないです。

「そろそろ本格的にGoやろうかなー」みたいに考えたとき、色々障害となりそうなことを最初に排除しようとして苦労した話です。

あと、CygwinメインのWindowsユーザー以外はたぶん普通に構築できます。


IDEの選定

記憶力が低い私にとって、IDE的な機能(せめてIntelliSense、ブレークポイント、変数ウォッチ)がない環境は非常につらいです。

Android StudioのようにGoの開発環境決定版のようなものは2017Q1時点でなさそうですが、Atom, Visual Studio Code, Intellij+Goあたりが多いようです。

慣れの問題から考えて、Intellij+Goプラグインを選択しました。IntelliSense、デバッガ、ブレークポイントを始めとして、基本的な機能はかなり揃っています。


Go言語のCygwin/コマンドプロンプト問題

Go言語世界のみで、お一人様前提に構築を行なうのであれば、さほど問題はありません。

ですが、Go言語を使ったプロジェクトはGAE/Goを始めとしたサーバーサイド系が多いため、「プロジェクト」という点を考えると幾つかの問題が起こります。

例として、セットアップ等やビルド確認のためのスクリプトがあります。

私はAndroidアプリを中心にお仕事をしているので、基本的にはMac/Windows共にbashを前提にシェルスクリプトを書いておけば大きな問題は起こりませんでした。Go言語も基本的にCygwinから go get 等のコマンドが使えますが、cgo(C言語と組み合わせた)場合に問題が起こります。

cgoが含まれているとコンパイルが飛躍的に長くなり、 go install 等で事前ビルドを行なうことでビルド時間短縮が可能になります。

ですが、cygwinに含まれているgccコンパイラは一部のビルドオプションが使えず、代わりにMingwを使う場合があります。

Cygwin+Mingwを使い、 go install して生成されたstatic libraryは、Intellij/GOプラグインからリンクすることができません。Intellijの環境が、通常のWindows環境(つまりコマンドプロンプト)を前提としているためです。

コマンドプロンプトから go install しておけばこの問題は発生せず、Intellijでもビルドが正常に行なえます。ですが、逆にcygwinから go builid した際に問題が発生するようになりました。

GOはライブラリ等のインストール先を GOPATH に記述されたパスで解決します。微妙に差異がある2環境(Cygwin/プロンプト)を同居させようという点で問題が起きているようです。

そこで、Cygwinとコマンドプロンプト&IDE環境で GOPATH を切り替えることにしました。


GOPATH の設置場所問題

Go言語の仕様上、 GOPATH は複数ディレクトリを指すことができます。ですが、 go get で取得できるソースコードの配置先は「最初に記述されたパス」と定められています。

通常は問題になりにくいかもしれませんが、ライブラリがバージョンアップした場合に厄介です。

あるプロジェクトは古いバージョンに依存している、新規のプロジェクトは新しいバージョンに依存している、等の状況が発生することが予想されます。

そこで、プロジェクトごとに GOPATH を完全に切り替えるという方針を取ることにしました。

場所はプロジェクトごとに root/.gopath/cygwin root/.gopath/windows をそれぞれ作成して使い分けることで cgo 依存問題を回避しています。

また、リポジトリにそれらが含まれて他人に迷惑がかからないよう、 .gitignore にそれらのパスを追加しています。

# .gitignore

/.gopath

問題点として、cygwinでフルパスを単純設定すると /cygdrive/c/path/to/project のようなUnixライクなパス設定になってしまう(Go言語はC:\のWindows形式でパスを与えなければならない)ので、 cygpath コマンドでの変換も必要になります。

Cygwinではこのようにすることで GOPATH を固有に指定できます。

# Cygwinでは工夫が必要になる

# プロジェクトのrootに移動する
cd path/to/project

# 環境変数を書き換える
# GOPATHはWindows形式フルパスで存在させなければならない
# cgoを使うなら必要に応じてgccも書き換えなければならない
export GOPATH=`cygpath -w $PWD`\\.gopath\\cygwin
export CGO_ENABLED=1
export CC=x86_64-w64-mingw32-gcc
export CXX=x86_64-w64-mingw32-g++

Windowsのコマンドプロンプトではこうなります。

REM 環境変数のGOPATHをプロジェクト固有に入れ替える

SET GOPATH=%CD%\.gopath\windows
SET PATH=%MINGW64_PATH%\bin;%PATH%


GOPATH のデフォルト問題

GOPATHをプロジェクトごとに完全に切り替えてしまう

Go言語で作られたツール類は go get で手軽に使えるという利点があります。ですが、GOPATHを完全にプロジェクト依存にしてしまうと、そのツール類をどこにインストールするか?という問題が発生してしまいました。

結局、ツール類をインストールするために環境変数 GOPATH GOBIN GOROOT を設定した上で、開発プロジェクトごとに別途 GOPATH を指定するという方向に落ち着いています。


GOPATH切替を行ってIntellijを使用する

Intellij/Goプラグインはプロジェクト固有の環境変数(GOPATH)を指定できないようです。

IDEが起動したプロセスに設定されているGOPATHを読み込み、セットアップされます。

それを利用し、次のようなバッチファイルを /root/script/windows-intellij.bat というパスに用意することで、「現在のプロジェクトにGOPATHを指定した状態でIntellijを起動する」ということを行えるようにしています。

バッチファイルの中身はこうなります。

バッチファイルを実行するとDOS窓が居座りますが、Intellij起動後はウィンドウを閉じても問題ありません。

@echo off

REM カレントディレクトリをプロジェクトのrootにする(正確には、バッチファイルが配置されているフォルダの1階層上にする)
CD /d %~dp0\..\

REM GOPATHをプロジェクト固有に書き換える
SET GOPATH=%CD%\.gopath\windows
SET PATH=%MINGW64_PATH%\bin;%PATH%

echo GOROOT=%GOROOT%
echo GOPATH=%GOPATH%

REM Intellijを起動する
%GO_IDEA_PATH%\bin\idea64.exe


Goの依存管理ツール prjdep を作ることにした

GOPATHをCygwin/コマンドプロンプトを切り替える(1マシン・1プロジェクトに対して複数GOPATHが存在する)という都合上、既存の dep godep が正常に機能しませんでした。

そこで、Go言語開発の勉強を兼ねて依存解決ツール prjdep を作ることにしました。

目標は次の点です。


  1. Pure Golangであること

  2. 上記の特殊な GOPATH に対応可能であること(今見ているGOPATHに作用する)

  3. 1ファイルのみで管理できること


  4. go getのみでインストール可能であること

  5. CIを構築すること


prjdep をインストールする

go get github.com/eaglesakura/prjdep でインストールできます。

私の環境だと次のようになります。

$GOPATH/binにパスを通すのを忘れないようにしましょう。

$ which prjdep

/cygdrive/c/dev-home/tools/gopath/bin/prjdep


prjdep でGOPATHの状態を保存する

prjdep init を実行すると、「コマンドが実行された時点のGOPATHの状態を保存」します。

具体的には次のような内容の dependencies.jsonがカレントディレクトリに書き出されます。

{

"Repositories": [
{
"ImportPath": "github.com/stretchr/testify",
"Rev": "4d4bfba8f1d1027c4fdbe371823030df51419987",
"Lang": "golang"
},
{
"ImportPath": "github.com/urfave/cli",
"Rev": "347a9884a87374d000eec7e6445a34487c1f4a2b",
"Lang": "golang"
}
]
}


prjdep でGOPATHの状態を復元する

prjdep restore を実行すると、カレントディレクトリにある dependencies.json をロードし、次のコマンドを実行します。

# 全てのライブラリに対して書きを実行する

# ライブラリをgetする
go get -d ${ImportPath}
# 保存されたリビジョンを復元する
git checkout -f ${Rev}
# ライブラリをビルドする
go install ${ImportPath}

prjdep プロジェクト自体の依存管理も prjdep restore で行っています。

# circle.yml抜粋

dependencies:
override:
- go version
- go get github.com/eaglesakura/prjdep
- prjdep restore


配布する上で引っかかったこと

go get で配布するためには、ローカルpackageが使えないというルールに引っかかりました。つまり、ツール内で次のような書き方は認められていないようです。

import "./hoge/package"

また、ディレクトリ名がそのままツール名になるという go get のルールに従うため、リポジトリ名をそのままツール名に変更しています。


【おまけ】 cgoのデバッグ実行が行えない問題の無理矢理な回避

cgo を使ったライブラリをリンクした場合、Intellij付属のデバッガ delve が正常に起動できないという問題がありました( issue )。

フォーラム等の情報を元にすると、まずはデバッグモードで起動したい場合に -ldflags="-linkmode internal" のビルドフラグを指定する必要があるようです。ただし、それを行った場合に上記の delve がバイナリ解析に失敗して正常起動できなくなる問題が発生します。

そこで、Delve自体を次のように改変してビルドし、Intellij組み込みのdelveと入れ替えることでブレークポイントは働くようになります(変数のウォッチは行えなかったので、諦めてPrintfデバッグしたほうが良さそうです)。

delveはPure Golangなソリューションなので、簡単に自前ビルドを行えます。

// https://github.com/derekparker/delve/blob/master/proc/proc.go

func (dbp *Process) getGoInformation() (ver GoVersion, isextld bool, err error) {
// Debug版の場合このバージョンチェックに失敗するが、そもそもPrint以外の用途に使っていないので多分大丈夫
- vv, err := dbp.EvalPackageVariable("runtime.buildVersion", LoadConfig{true, 0, 64, 0, 0})
- if err != nil {
- err = fmt.Errorf("Could not determine version number: %v\n", err)
- return
- }
- if vv.Unreadable != nil {
- err = fmt.Errorf("Unreadable version number: %v\n", vv.Unreadable)
- return
- }
-
- ver, ok := ParseVersionString(constant.StringVal(vv.Value))
- if !ok {
- err = fmt.Errorf("Could not parse version number: %v\n", vv.Value)
- return
- }

rdr := dbp.DwarfReader()
rdr.Seek(0)
for entry, err := rdr.NextCompileUnit(); entry != nil; entry, err = rdr.NextCompileUnit() {
if err != nil {
return ver, isextld, err
}
if prod, ok := entry.Val(dwarf.AttrProducer).(string); ok && (strings.HasPrefix(prod, "GNU AS")) {
isextld = true
break
}
}
return
}


まとめ

Go言語とWindowsとCygwinとコマンドプロンプトとIntellijを悪魔合体させようとすると、開発環境構築が大変だね。

諦め is 肝心。

ドザーに幸あれ。