TinyGo + Vim で gopls するための設定


2021/06/14 追記

sago35/tinygo.vim という Vim script を作成しました。
TinyGo + Vim (+ vim-lsp) の環境を作るなら、 sago35/tinygo.vim が良いはず。

インストール後は、 :TinygoTarget wioterminal のようにターゲットを指定することで vim-lsp が立ち上げ直します。

インストール方法は以下のような形でインストールしてください。

Plug 'sago35/tinygo.vim'

2020/09/23 追記

TinyGo 0.15 以降は以下のやり方を使うのが簡単です。


2020/08/10 追記

以下に記載している go.mod で replace を書く方法でも動かすことはできますが、ページ下部に記載のコメント 1 および 2 の方法/情報で実施するのが簡単で良いです。


以下の GIF のように、 PyPortal (atsamd51j20a) の設定で gopls を動かせるようになりました。
正しく board_pyportal.go 等に飛べているのが確認できます。
もちろん、メソッド等の補完も動きます。

はじめに

TinyGo は、以下の理由により gopls がほとんど動きませんでした。

  • 独自のビルトインパッケージを持っている
    • 例: machine や device など
  • buildtag による分岐

buildtag による分岐については、 gopls でも issue として上がっている内容になりますが、現時点でも gopls 全体で ある単一の buildtag を持つこと は可能なので何とかなりました。

ということで、 LSP を動かすために以下の 3 つを実施していきます。

  1. 開発しているフォルダの go.mod に replace を書く
  2. tinygo ディレクトリの各パッケージフォルダに go.mod (空で良い) を置く
  3. 環境変数 GOOS + GOARCH + GOFLAGS を設定する

環境

この記事を作るにあたり、以下の環境で確認しました。

  • Windows 10
  • Go version go1.14.1 windows/amd64
  • tinygo version 0.13.1 windows/amd64 (using go version go1.14.1 and LLVM version 10.0.1)
  • gopls ead0a569305d87def8dc4ad3899a7d78432b12c6
  • Vim 8.2.147
  • vim-go 13af5df6a1b3bc4bdfd03e3c05fa600d1dd16de6

多分、 Windows じゃなくても Vim じゃなくても同じように設定可能だと思います。

やり方

1. 開発しているフォルダの go.mod に replace を書く

背景

TinyGo では、以下のパッケージ以外のものは GOROOT にあるものを使っています。
以下のパッケージは tinygo をインストールしたディレクトリ直下のものが使われます。

  • machine
  • os
  • reflect
  • runtime
  • runtime/interrupt
  • runtime/volatile
  • sync
  • testing
  • internal/reflectlite
  • internal/task
  • device/*
  • examples/*

例えば fmt パッケージは GOROOT にあるものが使われるので、インストールしている Go の fmt パッケージが使われます。
fmt.Printf() はほとんどのマイコンボードにおいて、 USBCDC (USB の SerialPort) への書き込みに使われますが、 fmt パッケージ自体は Go (not TinyGo) の物が使われます。
Go の fmt パッケージを使っているのにどうやって USBCDC に出力するかというと、 TinyGo 内で os.Stdout を USBCDC への書き込みにマッピングして実現しています。

この 特別扱い は以下のコードにより実現しています。
そして、この 特別扱い が gopls がうまく動かない状態を作っています。

// tinygo/compiler/compiler.go
func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Config) (llvm.Module, []string, []error) {
    // ...
    lprogram := &loader.Program{
        // ...
        OverlayPath: func(path string) string {
            // Return the (overlay) import path when it should be overlaid, and
            // "" if it should not.
            if strings.HasPrefix(path, tinygoPath+"/src/") {
                // Avoid issues with packages that are imported twice, one from
                // GOPATH and one from TINYGOPATH.
                path = path[len(tinygoPath+"/src/"):]
            }
            switch path {
            case "machine", "os", "reflect", "runtime", "runtime/interrupt", "runtime/volatile", "sync", "testing", "internal/reflectlite", "internal/task":
                return path
            default:
                if strings.HasPrefix(path, "device/") || strings.HasPrefix(path, "examples/") {
                    return path
                } else if path == "syscall" {
                    for _, tag := range c.BuildTags() {
                        if tag == "baremetal" || tag == "darwin" {
                            return path
                        }
                    }
                }
            }
            return ""
        },
        // ...
    }
    // ...
}

対策

ということで、上記の特別扱いされたフォルダに対して go.mod で replace ディレクティブを書くことにより解決できます。

Windows での標準的なインストール場所に合わせた設定は以下になります。

module tinygo.org/x/drivers

go 1.14

replace (
    device/sam => C:\tinygo\src\device/sam
    internal/reflectlite => C:\tinygo\src\internal/reflectlite
    internal/task => C:\tinygo\src\internal/task
    machine => C:\tinygo\src\machine
    os => C:\tinygo\src\os
    reflect => C:\tinygo\src\reflect
    runtime => C:\tinygo\src\runtime
    runtime/interrupt => C:\tinygo\src\runtime/interrupt
    runtime/volatile => C:\tinygo\src\runtime/volatile
    sync => C:\tinygo\src\sync
    testing => C:\tinygo\src\testing
)

なお、 replace ディレクティブで指し示す先は、空でも良いので go.mod ファイルが必要になります。
後述の 各パッケージフォルダに go.mod を置く を実行しないと、この時点ではうまく動作しません。

2. tinygo ディレクトリの各パッケージフォルダに go.mod (空で良い) を置く

背景

以下に記載の通り、 replace される側については go.mod ファイルが必要となります。
空でも良いので作成しておく必要があります。

Note: if the right-hand side of a replace directive is a filesystem path, then the target must have a go.mod file at that location. If the go.mod file is not present, you can create one with go mod init.
https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive

対策

touch 等を使ってからファイルを置いてください。
git-bash 等が使える場合は以下を実行することで go.mod を簡単に作成することができます。

touch C:/tinygo/src/device/sam/go.mod
touch C:/tinygo/src/internal/reflectlite/go.mod
touch C:/tinygo/src/internal/task/go.mod
touch C:/tinygo/src/machine/go.mod
touch C:/tinygo/src/os/go.mod
touch C:/tinygo/src/reflect/go.mod
touch C:/tinygo/src/runtime/go.mod
touch C:/tinygo/src/runtime/interrupt/go.mod
touch C:/tinygo/src/runtime/volatile/go.mod
touch C:/tinygo/src/sync/go.mod
touch C:/tinygo/src/testing/go.mod

3. 環境変数 GOOS + GOARCH + GOFLAGS を設定する

背景

TinyGo では以下を使用しています。
これらを gopls に伝える必要があります。

  • GOOS
  • GOARCH
  • buildtag (マイコンやボードなどの分岐)

対策

buildtag については、環境変数 GOFLAGS で設定できます。
それぞれの値をどう設定すべきかは tinygo info で調べることができます。
例えば、 PyPortal というターゲットに対しては tinygo flash -target pyportal . というようなコマンドでビルドしますが、その際の -target pyportal の部分を tinygo info に設定します。

$ C:\tinygo\bin\tinygo.exe info -target pyportal
LLVM triple:       armv7em-none-eabi
GOOS:              linux
GOARCH:            arm
build tags:        cortexm baremetal linux arm sam atsamd51 atsamd51j20 atsamd51j20a pyportal tinygo gc.conservative scheduler.tasks
garbage collector: conservative
scheduler:         tasks

上記を調べることができたので、後は環境変数を設定します。
GOFLAGS はカンマ区切りなので注意が必要です。

set GOOS=linux
set GOARCH=arm
set GOFLAGS=-tags=cortexm,baremetal,linux,arm,sam,atsamd51,atsamd51j20,atsamd51j20a,pyportal,tinygo,gc.conservative,scheduler.tasks

bash 等では以下のように設定します。

export GOOS=linux
export GOARCH=arm
export GOFLAGS=-tags=cortexm,baremetal,linux,arm,sam,atsamd51,atsamd51j20,atsamd51j20a,pyportal,tinygo,gc.conservative,scheduler.tasks

おまけ

3. 環境変数 GOOS + GOARCH + GOFLAGS を設定する の部分だけをヘルプする小さな自分用 CLI ツールとして tinygo-edit を作りました。
GOOS + GOARCH + GOFLAGS を設定しつつ --editor で指定した editor で開きます。

以下でインストールできます。

$ go get github.com/sago35/tinygo-edit

以下のように使用できます。

$ cd ./examples/blinky1

# feather-m4 の設定で gvim を立ち上げ
$ tinygo-edit --editor gvim --target feather-m4

# pyportal の設定で vim を立ち上げ
$ tinygo-edit --editor vim --target pyportal

まとめ

以下を実施することで、快適な TinyGo 環境になりました。
今後、 (主に gopls 側の version up により) 以下の設定は不要になるかと思いますが、しばらくはこの方法を使っていくことになりそうです。

  • 開発しているフォルダの go.mod に replace を書く
  • tinygo ディレクトリの各パッケージフォルダに go.mod (空で良い) を置く
  • 環境変数 GOOS + GOARCH + GOFLAGS を設定する

リンク