Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

GitHub ActionsでC言語のコードをクロスコンパイルする

GitHub ActionsではARM64ランナーも公開されつつありますが、ここでは gcc を使ったクロスコンパイルを説明します。この記事ではホスト*1アーキテクチャx86_64、ターゲット*2アーキテクチャarm64 としていますが、他のターゲットでも同様の手順となるでしょう。また、C言語を前提に書いていますが、他の言語でもライブラリをリンクする場合は参考になるんじゃないかなと思います。

aptリポジトリの準備

まずはターゲットとなるアーキテクチャをパッケージ管理システムに追加します。

sudo dpkg --add-architecture arm64

GitHub Actionsのubuntuランナーにはx86パッケージのリポジトリしか設定されていないので、ARMパッケージがあるaptリポジトリのURLを/etc/apt/sources.list.d/arm64.listに設定します。

deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted

また、GitHub Actionsランナーの /etc/apt/sources.listアーキテクチャを制限していないので、このままだと arm64 パッケージも探してしまって警告が出力されます。どのみちデフォルトのリポジトリarm64 パッケージは用意されていないので、探さないよう arch= オプションを設定しておきます。

sudo sed -i -E '/^deb(-src)? ([^[])/s/ / [arch=amd64,i386] /' /etc/apt/sources.list

sources.list の各項目がどんな意味なのかは以下の記事が分かりやすいと思います。

kujira16.hateblo.jp

コンパイラとライブラリのインストール

ここまで終われば、コンパイラと必要なライブラリをインストールしましょう。

sudo apt update
sudo apt install -y gcc-aarch64-linux-gnu
sudo apt install -y libsystemd-dev libsystemd-dev:arm64 # libsystemdをリンクしたい場合の例

このとき、 gcc では aarch64 の部分がターゲーットのアーキテクチャ名になります。また、リンクするライブラリはパッケージ名の後ろに :arm64 のようにアーキテクチャ名を追加します。

arm64とaarch64の関係

ここまでで、 arm64aarch64 といった名称を使いましたが、何が違うのでしょうか。

これらの名前は、aarch64 は命令セットの名前に、arm64 はARMプロセッサの64bitアーキテクチャに由来します。クロスコンパイルする状況においては結局どちらも64bit ARMを意味していますが、歴史的な事情によって使っている名称が異なります。

以下は各システムがどちらの表記を使っているかまとめた表です。せっかくなのでx86の表記も加えてみました。

システム x86 ARM
GCC x86_64 aarch64
Clang x86_64 aarch64 1
GNU x86_64 arm64
Debian/Ubuntu amd64 arm64
RHEL x86_64 aarch64
Plan 9 amd64 arm64
Go amd64 arm64
Windows x64 2 arm64

なので、gccパッケージのターゲット名は aarch64 となっているし、ライブラリのアーキテクチャ名はDebian/Ubuntu命名に沿うので libsystemd:arm64 表記が使われているわけですね*3

ソースコードをビルドする

ビルドするときはターゲット用の gcc を使えばいいだけです。 make を使っている場合は CC 変数にセットします。

make CC=aarch64-linux-gnu-gcc

ライブラリのリンク等は、ターゲット用の gcc がターゲット用のライブラリを探してくれるので、開発者が意識することはありません。

ワークフロー

ここまでのワークフローをまとめます。

steps:
  - name: Add an architecture to install packages
    run: |
      sudo dpkg --add-architecture arm64
      sudo sed -i -E '/^deb(-src)? ([^[])/s/ / [arch=amd64,i386] /' /etc/apt/sources.list

      source /etc/lsb-release
      o="$(mktemp)"
      url='http://ports.ubuntu.com/ubuntu-ports'
      echo "deb [arch=arm64] $url $DISTRIB_CODENAME main restricted" >>"$o"
      echo "deb [arch=arm64] $url $DISTRIB_CODENAME-security main restricted" >>"$o"
      echo "deb [arch=arm64] $url $DISTRIB_CODENAME-updates main restricted" >>"$o"
      sudo install -m 644 "$o" /etc/apt/sources.list.d/arm64.list
  - name: Build sources
    run: |
      sudo apt update
      sudo apt install -y gcc-aarch64-linux-gnu
      sudo apt install -y libsystemd-dev libsystemd-dev:arm64
      make CC=aarch64-linux-gnu-gcc

リポジトリの設定部分はlufia/workflows/.github.actions/setup-multiarchとして複合ワークフローにしておいたので、よければ使ってください。

steps:
    - uses: lufia/workflows/.github/actions/setup-multiarch@v0.5.0
      with:
        arch: arm64

  1. AppleバックエンドのことをARM64と呼んでいたがAArch64に統合された
  2. x86-64 表記もあるが、x64 の方が多いと思う

*1:ソースコードコンパイルする側

*2:ビルドされたバイナリを実行する側

*3:理屈は分かるけど紛らわしいので統一してほしい

Goで関数呼び出しを繋げてパイプライン演算子を再現する

最近、Goで関数呼び出しを無限に繋げる書き方を気に入っています。文字で書いても伝わらないと思うので実例を挙げると、例えばこういう書き方。

repeat(yield)("しか", 1)("のこ", 3)("こし", 1)("たん", 2)

どうやって実現しているのかというと、自身を参照する型を作ればいいだけです。

type Emitter func(s string, n int) Emitter

func repeat(yield func(string) bool) Emitter

完全なコード例は以下のGo Playgroundを見てください。

このような、関数呼び出しを繋げる方法でパイプライン演算子を再現するとどうなるか?と思って試してみた記事です。

パイプラインを作る

パイプライン演算子を使うと、 c(b(a(10))) という呼び出しを 10 |> a |> b |> c のように書けます。左から右に読めるので、処理の流れを追いやすくなりますね。

話は変わって、いま所属している企業では関数型ドメインモデリングの読書会が行われています。この書籍ではパイプライン演算子を多用していますが、Goにはパイプライン演算子がありません。無くてもそれほど困らないものの、パイプライン自体は関数を無限に繋げていくものなので、最初に紹介した方法を使ってパイプラインを実現できないかなと考えました。

// 空文字列ならエラー
func require(s string) (string, error)

// printしてsを返す
func tee(s string) string

// 以下のように書けると嬉しいが、このまま実現はできない
result := pipe("hello world")(require)(tee)(strings.ToUpper)

なんだけど、実際は色々な課題があって上記のようには実現できません。

  • パイプラインの初期値や、計算途中の状態を保存する場所がない
  • 関数の戻り値が関数なので、最後に結果を返す手段がない
  • requirestringerror を返すので型が異なる

試行錯誤の結果、dmmf-go/internal/pipeでは少し不恰好だけど近しいものを実現できました。

result, err := pipe.Value("hello world").Catch(require)(tee)(strings.ToUpper).ValueErr()

以下で、実現のためにやったことの一部を紹介します。

計算の状態を保存する

まず、ここで実装するパイプラインでは関数呼び出しを繋げたいので、パイプライン型の基底型は関数です。

type pipe[T any] func(f func(v T) T) pipe[T]

このように定義すると pipe(f1)(f2) のように連続して呼び出せるのですが、 pipe[T] は関数なので任意の値を持たせることができません。具体的には、pipe[T] 型に パイプラインを識別する情報 が追加できません。そういった制約があるため、計算の状態を残すには「実行時に取れる情報」から決める必要があります。例えば実行時のコールスタックやゴルーチンIDなどが考えられますが、今回は関数ポインタをパイプライン識別に利用しました。

どういうことかというと、一般的に関数ポインタは関数ごとに1つですが、無名関数の場合は記述する毎に作られます。例えば以下の場合、

package main
func main() {
    f1 := func() { ... }
    f2 := func() { ... }
}

このとき、f1f2 は異なる関数ポインタを持ちます。内部的には、無名関数は main.main.func1main.main.func2 として作られるようですね。そしてGoは関数のインライン展開を行うので、以下の例でいえば pipe.Value の呼び出しをインライン展開できれば、関数ポインタをパイプラインの特定に使えます。

// pipe.Valueをインライン展開できれば、p1とp2の関数ポインタは異なるので識別できる
p1 := pipe.Value(10)
p2 := pipe.Value(20)

インライン展開されるためには、複雑な関数ではないことが条件です。

Go 1.22.5では、次のような複雑度ならインライン展開されます。

var states map[uintprt]*state

func Value[T any](v T) pipe[T] {
    s := &state{}
    var f pipe[T]
    f = func(g func(T) T) pipe[T] {
        s.current = g(s.current)
        return f
    }
    addr :=  **(**uintptr)(unsafe.Pointer(&f))
    states[addr] = s
    return f
}

ここで、本当は reflect.Value.Pointer を使いたいけれど、使ってしまうとインライン展開されなかったので、関数ポインタの取得を unsafe.Pointer で行っています。

エラーを扱う

Goでは型にメソッドを実装できるので、関数にメソッドを追加しました。計算の状態を保存できるようになったので、これはすぐに実装できます。

func (p pipe[T]) Catch(f func(T) (T, error)) pipe[T] {
    addr :=  **(**uintptr)(unsafe.Pointer(&p))
    s := states[addr]
    s.current, s.err = f(s.current)
    return p
}

pipe[T]func(f func(T) (T, error)) pipe[T] としてもいいのですが、エラーを常に求められるのも使いづらいなと感じたので、そのようにはしませんでした。

結果を返す

上記と同様に、こちらもメソッドを実装して対応しました。

func (p pipe[T]) ValueErr() (T, error) {
    addr :=  **(**uintptr)(unsafe.Pointer(&p))
    s := states[addr]
    delete(states, addr)
    return s.current, s.err
}

作ってみた感想など

上記の他にも、関数呼び出しを繋げるために色々と工夫をしています。

  • パイプラインの途中でエラーが発生した場合は後続の関数を呼ばない
  • パイプラインをコピーさせないように pipe[T] 型を公開しない
    • インライン展開された場所に依存するので、例えば再帰呼び出しされると関数ポインタが競合する
    • 他の関数引数や戻り値に pipe[T] を使えないのでコピーされるリスクを減らせる
  • 型の変換をするために別の関数を使って行う
    • Go 1.22.5時点ではメソッドに型パラメータを持たせられないので仕方なく

今回、パイプラインを作ってみてどうかでいえば、エラー処理を一箇所にまとめられるのは便利かなと思いました。次のコードは書籍の例ですが、エラーを最後に判定するだけになっていて若干すっきり記述できています。

func PlaceOrder(order *UnvalidatedOrder) {
    var (
        validateOrderConfig    ValidateOrderConfig
        priceOrderConfig       PriceOrderConfig
        acknowledgeOrderConfig AcknowledgeOrderConfig
    )
    p1 := pipe.Value(order)
    p2 := pipe.From(p1, validateOrderConfig.ValidateOrder)
    p3 := pipe.From(p2, priceOrderConfig.PriceOrder)
    p4 := pipe.From(p3, acknowledgeOrderConfig.AcknowledgeOrder)
    v, err := p4.ValueErr()
}

ただし、関数を繋げられる必要はあまりないかもしれません。関数呼び出しを繋げるために不要な制限を持ち込んでしまっているので、普通に構造体を返した方が扱いやすいと思います。まあ試してみた記事の結論としては、これで十分でしょう。

Plan 9とInfernoにおけるtar(1)の変化

小ネタです。以下の記事を読んでいて、

なぜ不要なのかは元記事を読んでもらうといいのだけど、ここではPlan 9ではどうなのか気になったのでtar(1)を調べてみた。ベル研UNIXの子孫なので当然だろうけど、Plan 9のマニュアルでは key の存在がそのまま残っている。

tar key [ file ... ]

The key is a string that contains at most one function letter plus optional modifiers.

なんだけど、そこで終わりではなく、Plan 9から派生したInfernoでは tar(1) コマンドが無くなっていて、代わりにgettar(1)で置き換えられている。他にも puttar(1)lstar(1) があって、それぞれ tar x, tar c, tar t に相当する。もともと、tar(1)crtx はサブコマンドのようなものだと言われていたけど、Infernoで再定義する際にサブコマンドではなく別のコマンドとして整理したのは「一つのことをうまくやる」の現れなのかなと思った。

余談だけど、1つの文字に固有の意味があって、それらを並べて一連の文字列で表現するものはPlan 9にいくつか残っている。例えばパーミッションlarwx もそうだし、以前使われていたファイルサーバ専用カーネルではディスクの構成も1つの文字列で表現していた。具体的には h は(S)ATAディスクを、 wSCSIディスクを意味して、それに続く数字で「どのディスクなのか」を識別する。これを組み合わせて (w1w2w3) なら3つのディスクを単純に連結する意味になるし、[w1w2w3]{w1w2w3}RAID 0RAID 1相当の意味となっていた。初見だとむちゃくちゃ混乱するけど、慣れるとこれはこれで使い易いと思うのですよね。

Goでモンキーパッチするライブラリを作った

Goで単体テストを実装する場合、動的な言語のように「テスト実行中に外部への依存を置き換える」といったことはできません。代わりに、

のように、テスト対象をテスト可能な実装に変更しておき、テストの時は外部への依存をモック等に置き換えて実行する場合が多いのではないかと思います。

個人的な体験でいえば、テスト可能な実装に置き換えていく過程で設計が洗練されていく*1ことは度々あるので、面倒を強制されているというよりは設計を整理するための道具といった捉え方をしているのですが、そうは言っても動的な言語に比べると面倒だなと感じるときは少なからずあります。既存の実装がテスト可能になっておらず、変更するコストが高い場合は特にそうですね。

そんなとき、気軽にモンキーパッチできると嬉しいんじゃないかと思って、テストの時だけ関数を置き換えられるようなライブラリを作りました。

github.com

このライブラリはtenntenn/testtimeにとても影響を受けています。

使い方

試しに標準ライブラリの time.Now を置き換えます。具体的なコードは次のようになります。

import (
    "testing"
    "time"

    "github.lufia/plug"
)

func isLeap() bool {
    now := time.Now()
    return (now.Year() % 4) == 0 // 主題ではないのでうるう年の実装は省略
}

func TestIsLeap(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("time.Now", time.Now)
    plug.Set(scope, key, func() time.Time {
        return time.Date(2024, 5, 10, 11, 0, 0, 0, time.UTC)
    })
    if !isLeap() {
        t.Errorf("2024 is a leap year")
    }
}

plug.Func の第1引数は関数の名前を指定します。理由は後述しますが、これは必ず以下の書式で記述してください。

  • (package-path).(function-name)
  • (package-path).(type-name).(method-name)

例を挙げると math/rand/v2.Nnet/http.Client.Do などです。標準の go doc が受け取る引数と似せていますが、パッケージ名の省略はできません*2

これで、TestIsLeap の中で実行した time.Now は固定で2024年5月10日の時刻を返すようになります。スタックを抜けない限り影響は続くので、isLeap 関数が呼び出す time.Time も固定の値を返します。

テストの実行

テストを実行するときは以下のように実行してください。-overlayオプションが必要です。

go test -overlay <(go run github.com/lufia/plug/cmd/plug@latest)

# 分けて書いてもいい
go run github.com/lufia/plug/cmd/plug@latest >overlay.json
go test -overlay overlay.json

-overlay オプションの詳細は、上で挙げたtenntennさんの記事を読んでもらうと良いのですが、ここでは以下のようなことを実行しています。

  • カレントディレクトリのソースコードから plug.Func を探す
  • plug.Func の第2引数を動的に置き換えできるように書き換える
  • 実行スタックに関連づいたスコープを抜けるまで、plug.Set の第3引数に渡す関数で time.Now を置き換える
  • time.Now を呼び出したとき、実行スタックを遡って直近の time.Now を呼び出し、結果を返す
  • 該当する関数が実行スタック上で置き換えられてなければ本物の結果を返す

plug@latest はカレントディレクトリに plug/ というディレクトリを作成しますが、これは実行するたびに生成するので、不要になったら消しても問題ありません。

サブテストで部分的に置き換える

一部のサブテスト実行中だけ、別の値に置き換えたい場合は、サブテストで同じように書くと実現できます。

func TestIsLeap(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("time.Now", time.Now)
    plug.Set(scope, key, func() time.Time {
        return time.Date(2024, 5, 10, 11, 0, 0, 0, time.UTC)
    })
    t.Run("サブテスト", func(t *testing.T) {
        scope := plug.CurrentScopeFor(t)
        plug.Set(scope, key, func() time.Time { ... })
        // これ以降、サブテストの中では別の値を返す
    })
    // サブテストの外では2024年5月の時刻を返す
}

メソッドを置き換える

メソッドも置き換えできます。以下の例では、net/http.ClientDo メソッドを置き換えているので、http.Get にも影響しています。

func TestHTTPClientGet(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("net/http.Client.Do", (*http.Client)(nil).Do)
    plug.Set(scope, key, func(req *http.Request) (*http.Response, error) {
        return &http.Response{StatusCode: 200}, nil
    })
    resp, _ := http.Get("https://example.com")
}

ジェネリック関数を置き換える

型パラメータのある関数は、型ごとに関数を渡します。

func TestMathRand(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("math/rand/v2.N", rand.N[int])
    plug.Set(scope, key, func(n int) int {
        return 3
    })
    fmt.Println(rand.N[int](10))
}

このとき、 rand.N[int]plug.Set で差し替わった関数が使われますが、 rand.N[int64] は登録していないので本物の実装が使われます。

関数の引数や呼び出し回数を検査する

内部的に呼ばれた回数を持っているので、それを使って期待した通りに呼ばれているかを検査できます。plug.FuncRecorder[T] に渡す構造体のフィールドは、関数引数の名前に対応したものが使われます。このとき、関数引数の名前をブランク指定子(_)にしていると無視します。

func TestRecorder(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("os.Getenv", func(string) string {
        return "dummy"
    })
    var r plug.FuncRecorder[struct {
        Key string `plug:"key"`
    }]
    plug.Set(scope, key, fake).SetRecorder(&r)

    os.Getenv("PATH")
    if r.Count() != 1 {
        t.Errorf("Count = %d; want 1", r.Count())
    }
    if r.At(0).Key != "PATH" {
        t.Errorf("At(0).Key = %s; want PATH", r.At(0).Key)
    }
}

今後の予定

実行のたびに静的解析をして必要なファイルを生成しているので、パッケージが多くなってくると有意に遅くなります。Goツールチェーンとパッケージのバージョンが変わらなければ基本的には生成するファイルも同じものになるので、うまく最適化ができるといいですね。

他にも、ジェネリック型のメソッドに対応したりとか、go build でも使えるようにしたりなど、色々とやりたいことはあります。

捕捉: なぜ文字列のキーを必要としているか

Goでは関数が同一かどうかを比較することができません。ジェネリックでない関数の場合は reflect.ValueOf(os.Getenv).Pointer() を経由することで比較できますし、Linux/AMD64の場合はだいたい期待通りに動きますが reflect.Value.Pointer のドキュメントには以下のように書かれています。

If v's Kind is Func, the returned pointer is an underlying code pointer, but not necessarily enough to identify a single function uniquely. The only guarantee is that the result is zero if and only if v is a nil func Value.

さらにジェネリック関数では、型パラメータごとに異なる関数ポインタが割り当てられるようで、 reflect.Value.Pointer での比較にも失敗します。

func N[T any](n T) {}

func N1[T any](n T) func(T) {
    return N[T]
}

func N2[T any](n T) func(T) {
    return N[T]
}

func main() {
    fmt.Println(reflect.ValueOf(N[int]).Pointer() == reflect.ValueOf(N[int]).Pointer())   // true
    fmt.Println(reflect.ValueOf(N1[int]).Pointer() == reflect.ValueOf(N[int]).Pointer())  // false
    fmt.Println(reflect.ValueOf(N2[int]).Pointer() == reflect.ValueOf(N2[int]).Pointer()) // true
}

runtime.FuncForPC なども含めて色々と試してみたけれど、Go 1.22時点では良い方法がなかったので、今の形に落ち着きました。

*1:テストコードと同様にドキュメントを書いているときにもよく起きる

*2:go docは http.Client と記述すると推測してくれる

Go製バイナリを配布するためのGitHubワークフロー

前置き

以前、BuildInfoからバージョンを取得する方法を紹介しました。

blog.lufia.org

go installで正規の公開されたバージョンをインストールした場合は、以下の出力においてmodの行が示すように、sum.golang.orgチェックサム等が検証されてバイナリのメタデータに埋め込まれます。

$ go version -m dotsync
dotsync: go1.22.2
    path    github.com/lufia/dotsync
    mod github.com/lufia/dotsync    v0.0.2  h1:JWm92Aw8pSKJ4eHiQZIsE/4rgwk3h5CjEbJ/S30wiOU=
    build   -buildmode=exe
    build   -compiler=gc
    build   -trimpath=true
    build   DefaultGODEBUG=httplaxcontentlength=1,httpmuxgo121=1,panicnil=1,tls10server=1,tlsrsakex=1,tlsunsafeekm=1
    build   CGO_ENABLED=0
    build   GOARCH=amd64
    build   GOOS=linux
    build   GOAMD64=v1

上記の出力から「dotsyncのバージョン0.0.2をgo1.22.2でビルドした」ことを読み取れますね。チェックサムhttps://sum.golang.org/lookup/<module-path>@<version> のようなURLにアクセスすると、正規のものかどうかの確認を行えます。完全なURLの仕様はGo Modules Reference/Checksum databaseをみてください。

このチェックサムが一度でも登録されてしまった後は、消したり変更したりできません。消せないのは困ると思うかもしれませんが、proxy.golang.org

I removed a bad release from my repository but it still appears in the mirror, what should I do?

への回答があるので、不備などではなく意図してデザインされていることが読み取れます。削除はできないものの、Goで非推奨(Deprecated)や撤回(Retracted)を明示する方法のようにすれば意思を表明することは可能です。

正しくソースコードからビルドされたことを検証する

主に以下の条件を満たす*1場合、Go 1.21以降では生成するバイナリが完全に一致するので、同じパラメータを与えて手元でビルドしてみると検証できます。

  • Goコンパイラのバージョンが同じ
  • GOOS, GOARCH, GOAMD64 などターゲットが同じ
  • cgoを使わない
  • os/user, netなどで動的リンクをしない
  • ビルドするディレクトリ名が同じ、または-trimpathオプションを与える

以下の例はlegoコマンドをLinuxPlan 9でビルドしたものですが、同じハッシュ値になっている様子が分かると思います。

Linuxでビルド

$ go version
go version go1.22.2 linux/amd64

$ export GOTOOLCHAIN=go1.22.1
$ export GOOS=plan9
$ export GOARCH=amd64
$ export GOAMD64=v1
$ export CGO_ENABLED=0
$ go install -trimpath github.com/go-acme/lego/v4/cmd/lego@v4.16.1

$ sha1sum lego
ee2e9c121604c1f52cb53c0d0824288d772de1e7

Plan 9でビルド

% go version
go version go1.22.1 plan9/386

% GOTOOLCHAIN=go1.22.1
% GOOS=plan9
% GOARCH=amd64
% GOAMD64=v1
% CGO_ENABLED=0
% go install -trimpath github.com/go-acme/lego/v4/cmd/lego@v4.16.1

% sha1sum lego
ee2e9c121604c1f52cb53c0d0824288d772de1e7

再現可能なビルド

このように、第三者が特定のソースコードから生成されたものであると検証できるような概念は「再現可能なビルド」とか「再現性のあるビルド」と呼ばれるようです。

また、Go 1.21以降は、Goのコンパイラやライブラリも再現可能になっているようです。

コード署名とは違うのか

コード署名は、誰がビルドしたものなのかを検証できますが、特定のソースコードから生成されたものかどうかは保証しません。例えばビルドプロセスの途中で改ざんが行われた場合、コードの署名は正しく検証を通ってしまいます。

go buildの場合は正規のバージョンが入らない

ようやく本題です。この記事の冒頭で挙げたエントリでも書いたように、手元にソースコードを置いてgo buildした場合などでは、メインモジュールのバージョンやチェックサムが埋め込まれずに (devel) という文字列になります。

$ go version -m dotsync
dotsync: go1.22.2
    path    github.com/lufia/dotsync
    mod github.com/lufia/dotsync    (devel) 
    ...

(devel) の代わりに(おそらくv1.0.1-0.20240418xxxのような)疑似バージョンを埋め込む提案がcmd/go: stamp the pseudo-version in builds generated by go buildで承認されていますが、それでも正規のバージョンとは区別されていますし、少なくともGo 1.22の時点ではまだ実装されていません。

GoReleaserでビルドしたバイナリはメインモジュールのチェックサムを持たない

上記と同様に、2024年5月時点では、GoReleaserGoReleaser Actionでビルドしたバイナリは公開された正規なバージョンを持ちません。

実用上は致命的に困るものではないけれど、どうせなら検証可能になっていたほうが嬉しいですね。

GitHub Releasesでリリースしたらビルドして成果物に追加するワークフロー

というわけで、正規のバージョンが埋め込まれたバイナリをリリースするためのワークフローを作ってみました。以下のワークフローは、GitHub Releasesで新しいバージョンをPublishすると開始して、最終的にバイナリをリリースに添付します。

name: Release

on:
  release:
    types:
    - published
jobs:
  release:
    strategy:
      matrix:
        os:
        - linux
        - darwin
        - windows
        arch:
        - amd64
        - arm64
        include:
        - format: tgz
        - os: windows
          format: zip
        exclude:
        - os: darwin
          arch: amd64
        - os: windows
          arch: arm64
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-go@v5
      with:
        go-version: stable
    - name: Build the package
      uses: lufia/workflows/.github/actions/go-install@v0.4.0
      with:
        package-path: github.com/lufia/dotsync
        version: ${{ github.ref_name }}
      env:
        GOOS: ${{ matrix.os }}
        GOARCH: ${{ matrix.arch }}
        CGO_ENABLED: 0
      id: build
    - name: Create the asset consists of the build artifacts
      uses: lufia/workflows/.github/actions/upload-asset@v0.4.0
      with:
        tag: ${{ github.ref_name }}
        path: >
          ${{ steps.build.outputs.target }}
          LICENSE
          README.md
        name: dotsync-${{ github.ref_name }}.${{ matrix.os }}-${{ matrix.arch }}
        format: ${{ matrix.format }}

  upload:
    needs: release
    permissions:
      contents: write
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/download-artifact@v4
      with:
        path: assets
        merge-multiple: true
    - name: Upload the assets to the release
      run: gh release upload -R "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" assets/*
      env:
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

どうでしょうか。記述量は多いですが、1/3くらいはビルド用のマトリクスを作っているところなので、第一印象ほど複雑ではないかなと思います。

ワークフローの途中で読んでいる複合アクションは以下の2つなので、興味があれば眺めてみてください。

*1:他にもあるかもしれないけど、これだけ揃えればだいたい同じになるはず

Steamクライアントが起動しなくなっていた

2024年2月ごろにSteamクライアントを更新してから、以下のログで停止して起動しなくなっていました。

$ flatpak run com.valvesoftware.Steam
...
Steam Runtime Launch Service: starting steam-runtime-launcher-service
Steam Runtime Launch Service: steam-runtime-launcher-service is running pid 34081
bus_name=com.steampowered.PressureVessel.LaunchAlongsideSteam

Steam is killed with no error message when steam runtime launch service is ranによると、落ちている原因はSteamにbackgroundパーミッションを与えていないからのようなので、以下のコマンドで追加すると起動できるようになります。

$ flatpak permission-set background background com.valvesoftware.Steam yes

ここでbackgroundが2つ並んでいるのは正しくて、最初がパーミッションストアのテーブル名、2つ目がパーミッションストアのオブジェクトの名前です。

以下のような結果になっていれば動作します。

$ flatpak permission-show com.valvesoftware.Steam
Table      Object     App                     Permissions                  Data
background background com.valvesoftware.Steam yes                          0x00

Titan Security Keyの新しいバージョンがPasskeysに対応していた

Titan Security Keyがパスキーに対応していたようですが、Google Storeの製品ページを見ても、それがパスキー対応したバージョンのTitan Security Keyなのか分からなくて混乱しました*1。ストア上では全部同じ名前でバージョン表記も無いし、国内販売が遅れた事例も過去にあるので「国内版はまだ古いバージョンだったりしないか」が気になります。

そこで、過去の記事などから形状と対応規格を調べてみました。過去バージョンは公式のGoogle Titan セキュリティ キーの安全および保証ガイドを参照しています。

先に結論を述べると、2024年1月時点で販売されているTitan Security Keyはパスキー対応と謳われているバージョンです。

2018年

  • Model: K9T (USB-A/NFC)
  • Model: K13T (Micro-USB/NFC/BLE)

Titan Security Keys: Now available on the Google Storeで登場したバージョンです。購入したパッケージに2つ同梱されていました。もともと2018年に登場していましたが、国内販売が遅れて2019年販売開始でした。過去記事で買ったのはこれ。

blog.lufia.org

具体的な形状はGoogleの安全な2段階認証を構築し不正アクセスを防ぐ物理キー「Titan セキュリティ キー」が日本で登場 - GIGAZINEの写真を見てもらえれば分かりますが、K13Tは卵形をしていて中央に楕円のボタンがあります。

2019年

  • Model: YT1 (USB-C)

USB-C Titan Security Keys - available tomorrow in the US でUSB-C版が追加になりました。こちらも形状はGoogleの2段階認証を構築する「Titan セキュリティ キー」にUSB Type-Cタイプが登場 - GIGAZINEの写真を見てもらえれば分かりますが、円形の鍵穴とTITANの刻印があります。

ところで、Titan Security Key - Wikipediaには

USB-C/NFC

と書かれているけど、YT1の安全および保証ガイドを見るとNFCは含まれていないので間違いかなと思っています。

2021年

  • Model: K40T (USB-C/NFC)

この辺りはよく知らないのですが、おそらくSimplifying Titan Security Key options for our usersの辺りでアップデートされたバージョンです。接続インターフェイスとしてのBLEが廃止となる代わりにNFCが追加になったようです。

K40Tの画像はGoogle「Titan セキュリティキー」ってどう使うの?NFCによるスマホ利用にも対応を見てください。円形の鍵穴と丸いボタンがあります。

これも、Titan Security Key - Wikipediaによると

supporting U2F and FIDO2

とありますが、Google USB-C/NFC Titan Security Key ReviewではFIDO U2F対応(FIDO2ではない)とあるので間違いかなと思っています。

2023年

  • Model: K51T (USB-A/NFC)
  • Model: K52T (USB-C/NFC)

The latest Titan Security Key is in the Google Storeで更新されたバージョンで、現在販売されているモデルです。公式情報にFIDO2対応とあるので本物でしょう。

形状は、ストアの画像はおそらく次のバージョンが出たら変わってしまうので、その場合は最大250種類のパスキーの保存が可能なGoogle Titan セキュリティ キーを使ってパスキー認証してみた - GIGAZINEの写真をみてください。楕円形の鍵穴と四角のボタンがあります。

*1:本当に困るので更新日くらいは書いてほしい