【Golang】在 Alpine Docker 中运行时出现错误:找不到 stdlib.h 文件。报错信息指出缺少 “stdlib.h”
在使用 golang:alpine 的 Docker 镜像上执行 go run 或 go test 时会遇到 stdlib.h: No such file or directory 的致命错误。
有时候无法导入 C,这也会导致 cgo 出错。另外,还可能会受到 musl 或 gcc 的提示和错误信息。
因为在谷歌搜索“golang”“alpine”“fatal error: stdlib.h: No such file or directory”时找不到详细的日本语信息,因此要提高自己的谷歌搜索技能。
$ go test ./...
Testing main package
go: downloading ...
...
# runtime/cgo
exec: "gcc": executable file not found in $PATH
FAIL github.com/KEINOS/Sample [build failed]
FAIL github.com/KEINOS/Sample/hoge [build failed]
exec: "gcc": executable file not found in $PATH
could not import C (cgo preprocessing failed) (typecheck)
import "C"
cannot find crti.o: No such file or directory
running gcc failed: exec: "gcc": executable file not found in $PATH
cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in $PATH
-
- IMAGE: golang:alpine
Go: v1.15.3 linux/amd64
OS: Alpine Linux v3.12,x86_64 (Kernel: 4.19.76-linuxkit)
简述:(今北产业)
# Minimum, at-least-to-install packages to run/build
apk add --no-cache gcc musl-dev
# Base meta-package to run/build (This includes gcc and musl-dev as well)
apk add --no-cache build-base
# Better to be installed packages for dev
apk add --no-cache alpine-sdk build-base
除了gcc、musl-dev之外,还安装了在使用cgo(C语言模块)时经常使用的工具,例如git和make等。因此,我建议在Alpine上进行开发和编译时应该安装这些工具。
当前的 Go(v1.19)默认使用模块模式。因此,在使用 Go 1.15 及更高版本进行开发时,需要注意三个目录。
与 Go 本身相关的目录(go env GOROOT)
用户指定的包或模块安装目录(GOPATH, go env GOPATH)
各个项目的目录(除上述目录之外的任意工作目录)
需要注意的是第二点,即 GOPATH。
GOPATH 路径是用于存放通过 go get 或 go install 下载的包或模块的位置。
对于 go install @ 命令,除了下载,还会构建二进制文件并将其放置在 $GOPATH/bin 目录下。虽然 go install 是以前的 go get -u 命令的替代品,但是用于安装目的的 go get -u 已被弃用。从 Go 1.19 开始,go get -u 主要用于更新项目 go.mod 文件内的包。
第一点的 GOROOT 路径是存放 Go 本体二进制文件及标准库等的位置。
除了 GOROOT 和 GOPATH,工作目录就是存在 go.mod 文件的目录。
TS; DR (Go初学者经过了如下的学习过程)
新版本的信息对于那些从旧版本逐步积累经验的人来说,可以理解其中的差异。但对于像我这样接近“初学者”的笔者来说,反而会引起困惑。因此,我希望将自己的整理记录包括在日志中。
有一台工作机器需要从Go语言(以下简称Golang)的源代码中提取并编译。但是并没有安装Golang。而且不允许另外安装。。。
幸运的是,由于安装了Docker,我决定在golang:alpine镜像上进行尝试。只是简单地认为使用庞大的Ubuntu镜像进行编译太慢了,没想到会累成这样。唉。
请勿将代码放在$GOPATH(/go)中,这是个大问题。
在不理解 Go 的新旧版本差异的情况下,首先将本地的当前目录挂载到容器的 “/go” 目录中进行启动。
$ # Docker でマウントを取ってみる
$ cd /path/to/the/repo
$ docker run --rm -it -v $(pwd):/go golang:alpine /bin/sh
...
/go #
那么现在开始进行测试… 唉,它根本就不动。
/go # go test main.go ./...
$GOPATH/go.mod exists but should not
/go # go mod download
$GOPATH/go.mod exists but should not
/go # go mod verify
$GOPATH/go.mod exists but should not
/go # go env
$GOPATH/go.mod exists but should not
$GOPATH/go.mod存在,但不应存在 エラー
起初我以为 go.mod 可能没有被识别,但是由于存在 exists 关键字,看起来情况并不一样。
存在着 go.mod,但不应该存在,也就是说“虽然有 go.mod,但不可以存在”。
看起来上述的错误与GOPATH模式(Go:Dep)和模块模式有关。
顺便提一句,我听说自 Go 1.11 引入的模块模式从 Go 1.16 开始成为默认选项了。(我的教材上并没有提到,Go 进步了呢)
听说 Go 1.17 将不能再使用 GOPATH。
-
- 参考文献
Go: DepからGo Modulesへの移行 @ Qiita
Modules on by default | New module changes in Go 1.16 @ blog.golang.org
确实,环境变量中没有 GO111MODULE。噢,Go 的版本可能是1.15或更高。
/go # go version
go version go1.15.3 linux/amd64
/go # env
HOSTNAME=xxxxxxxxxxxx
SHLVL=1
HOME=/root
TERM=xterm
PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GOPATH=/go
PWD=/go
GOLANG_VERSION=1.15.3
我曾经尝试将 GO111MODULE=on 设置为”始终以模块兼容模式运行”,以便于确定是否因为 GOPATH 模式而无法正常工作,但遗憾的是这种方法行不通。
$ docker run --rm -it -v $(pwd):/go -e GO111MODULE=on golang:alpine /bin/sh
/go #
/go # echo $GO111MODULE
on
/go # go env
$GOPATH/go.mod exists but should not
根据Go v1.15的设定,据说GO111MODULE默认为auto。
如果在项目的根目录或当前目录下找到 go.mod,那么它将自动按照模块模式运行。我已经放置了 go.mod,所以应该已经启用了”模块模式”。
-
- 参考文献
GOPATH モードからモジュール対応モードへ移行せよ @ Qiita
[Golang]$GOPATH/go.mod exists but should notを回避する @ Selfnote
Getting GOPATH error “go: cannot use path@version syntax in GOPATH mode” in Ubuntu 16.04 @ StackOverflow
根据上述链接提供的信息,有两种回避方案。
-
- 在非 /go 目录之外的另一个目录中挂载源代码并执行。
将 export GOPATH= 及环境变量的设定值设为 “空”,并将其缓存至用户目录下。
原因是存在 go.mod 但不应该存在。
出现问题的原因是源代码的装载点。也就是说,将源代码挂载到容器上的位置 (-v $(pwd):/go),/go 是有问题的装载点。
因为参考了旧版的 Go 教科书,所以我将源代码挂载在 GOPATH 的同一层级上。
在模块模式下,将模块缓存到由GOPATH指定的目录中。因此,缓存目录中出现了不必要的文件(如go.mod),导致了错误的原因。
确实,它在说「$GOPATH/go.mod存在但不应该存在」和「$GOPATH/go.mod存在但是违规的」。就是这个意思吗?
暫時決定將它掛載到 /app 而不是 /go。只要是與 Go 沒有直接關聯的目錄,可以選擇 /workspaces 或 /tmp/myapp,都可以。
$ cd /path/to/the/repo
$ docker run --rm -it -v $(pwd):/app golang:alpine /bin/sh
/go #
/go # # マウントしたディレクトリに移動
/go # cd /app
/app #
/app # # 今度は go env が表示された。GO111MODULEは空なので auto ∴ モジュールモード
/app # go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOENV="/root/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build030913204=/tmp/go-build -gno-record-gcc-switches"
“gcc”:在$PATH中找不到可执行文件错误。
然后重新调整心态,进行再次测试。虽然已经继续前进但还是没有动作。
/app # go test ./...
go: downloading github.com/...
go: downloading github.com/...
...
# runtime/cgo
exec: "gcc": executable file not found in $PATH
FAIL github.com/KEINOS/Hello-Cobra [build failed]
FAIL github.com/KEINOS/Hello-Cobra/hoge [build failed]
听说cgo生气了,说没有gcc。唉,我可没有用cgo啊。也许是依赖的软件包在用吧。
cgoを使ったCとGoのリンクの裏側 (1) @ Qiita
暂时安装GCC并重新进行测试。
/app # # gcc 入ってない
/app # gcc --version
/bin/sh: gcc: not found
/app #
/app # # gcc 入れる
/app # apk add --no-cache gcc
...
/app # # gcc 入った
/app # gcc --version
gcc (Alpine 9.3.0) 9.3.0
/app # go test ./...
# runtime/cgo
_cgo_export.c:3:10: fatal error: stdlib.h: No such file or directory
3 | #include <stdlib.h>
| ^~~~~~~~~~
compilation terminated.
FAIL github.com/KEINOS/Hello-Cobra [build failed]
FAIL github.com/KEINOS/Hello-Cobra/hoge [build failed]
果然,似乎在某个地方使用了cgo,并受到了批评。这次是因为缺少stdlib.h的C代码。看起来依赖包似乎在使用cgo。
在 GitHub 上的 Golang Docker 存储库中出现了一个问题提出。
尝试添加musl-dev
不确定为什么gcc不依赖它。
(在GitHub的golang:1.6-alpine #86上看到CGO似乎无法工作)
据说Alpine的cgo需要musl-dev。顺便提一下,在Alpine系列的问题中经常看到”装musl-dev”这句话。
那么,我将再次进行测试… 它成功了。
/app # # musl-dev 入れる
/app # apk add --no-cache musl-dev
...
/app # # テストする
/app # go test ./...
ok github.com/KEINOS/Sample 0.003s
ok github.com/KEINOS/Sample/hoge 0.006s
/app # # ?
是的,阿尔卑斯镇形象是为了简约设计的。
(在 GitHub 上,CGO似乎无法在golang:1.6-alpine #86上工作。)
“是的。Alpine 镜像的设计是为了尽可能地精简,让它成为文档的基础。”
需要注意的主要限制是它使用musl libc而不是glibc及其相关库,这可能导致意外行为。
…
为了最小化镜像大小,基于Alpine的镜像不包含额外的相关工具(例如git、gcc或bash)。使用此镜像作为基础,在您自己的Dockerfile中添加您所需的内容(如果您不熟悉,请参阅alpine镜像描述中有关如何安装软件包的示例)。
(来源:golang:-alpine | 快速参考 | Golang | docker-library @ GitHub)
“总之,我要提醒你,Alpine使用的是无法预测其行为的musl libc,而不是glibc及其可爱的伙伴们。而且为了尽可能减小体积,它甚至没有安装git、gcc和bash等软件。”
順便提一句,是的,沒錯。
由于Alpine Linux 镜像专为容器而设计,旨在运行静态链接的单一二进制文件,因此没有太多不必要的东西,甚至连 Chage & Aska 也会对此感到惊讶。
因此,我忘记了当涉及到动态链接应用程序或在容器内进行编译时,需要在Dockerfile中逐个添加必需的内容。
“嗯,找到那个什么 apk 包并安装起来也挺麻烦的。我在 Ubuntu 上搜索有没有像是 ‘先装上备用包’ 这样的 build-essential,看到有一个 ‘集合了常用构建工具的包’ 和一个 ‘集合了常用开发工具的包’。”
apk add build-base
apk add alpine-sdk
What is the alpine equivalent to build-essential? #24 | docker-alpine | gliderlabs @ GitHub
确实,即使不是单独安装 gcc 和 musl-dev ,只需使用 build-base 就可以完成。
Alpine Linux は、他のディストロに慣れた人からは「親の仇」とでも言うくらい(特に musl-dev の件で)ケチョンケチョンに言われるので、何が違うのか調べてみました。同じ軽量 Linux で有名な Arch Linux はディスられていないのが不思議だったのです。
結論から言うと「サポートユーザー対応が大変だから」だと感じました。「Alpine のためだけ」に対応しないといけない事が多いのです。
Alpine Linux は「独特なポリシー設計思想」による俺様実装が多いため、普段使いの OS としては使いにくいったらありゃしないシロモノです。Linux カーネルを自分でビルドするような、基本的な仕組みを理解した人でも、その思想に賛同できなければ、堅牢性を引き換えに面倒が増えただけと感じやすいディストロだと思います。
逆に言えば、Docker のコンテナのベース OS として使うなど、普段使いの OS 用途でなければ、後述する独自仕様により堅牢で軽量なコンテナを作れます。その場合は、前述した「とりま入れておけパック」でビルドして、別のステージ(マルチステージビルド)でビルドしたバイナリを使うのがベターでしょう。
以下は、書き出さないと整理できない自分の備忘録です。それでも「もうちょっと理解したい」と感じた方は、ゴロ寝でもしながら読むと、良く寝れると思いますので、参考情報の 1 つとして引き続きお読みください。
Alpine Linux は登山家的な人向け。素人にはオススメできない。
Alpine Linux は、Arch Linux と並ぶ軽量 Linux として有名です。
とは言え、両者の根本思想が異なり、中身も別物と言っていいくらい違います。従来の Linux に慣れており、軽量を求めるなら Alpine より Arch を求めた方が良いと思います。
そのため、良い悪いでなく、対比として比べるべきは Debian 系の Linux だと思います。
Debian ベースの Linux が、ユーザーが選びやすいように全部用意しておき「機能を引き算して使う」のに対し、Arch Linux は最低限のものを提供して、ユーザー自身が使いやすいように「機能を足し算して使う」違いがあると思います。
そのため、Debian ベースの Linux が好まれるのは「全部入っているため、とりあえず、どこでも動くから」だと思います。
カスタム性をブラックリスト(引き算)形式か、ホワイトリスト(足し算)形式にしたような違いの印象を受けました。つまり、慣れてきて「引き算」が面倒になった人が、「足し算」式に移行するのが自然な流れではないでしょうか。
対して、Alpine Linux はネットワーク利用を前提とした、セキュリティの堅牢性を重視したディストロです。その副作用として軽量化されたとディストロと言えます。
とは言え、他のディストロが脆弱というわけではありません。ポリシー(思想)の違いから、手段が違うのです。そのため、Debian や Arch で出来たことが、Alpine だと別の独特な方法が必要になることが多く、自分好みにカスタムするにもトラブルが多いです。
例えば、その独特な思想により、従来より使われてきた標準ライブラリを作り直したり、標準コマンドも独自の実装の仕方をしています。「俺様実装」感満載であるため、従来の Linux の使い勝手とは別の勝手を求められます。そのため、ケチョンケチョンに言われるのです。
Alpine は「アルパイン」と発音しますが、日本語で「アルペン」(「アルプスの」「高い山脈の」と言った意味)のことです。
話がそれますが、山登りを面白いと感じるのは「苦労した先に美しい光景があるからだ」と思われがちです。しかし、いわゆる登山家や、本格的な登山が趣味の人が「面白い」と感じるのは「攻略」なのです。
つまり、可能な限りイレギュラー(想定外)や無駄な荷物を排除しつつも、安全かつ堅牢に登れるかを念頭に、綿密な計画や準備が「いかに実際にオンスケで進むか」を楽しみ、攻略することを醍醐味とするのです。つまり、十分な準備はするものの、イレギュラーを楽しむ冒険や探検とは目的が異なります。
さて、Alpine Linux の Alpine は “A Linux Powered Integrated Network Engine” の略です。「Linux に統合されたネットワークエンジン」と言うように、ネットワークでの利用を視野に入れているため、「いかに軽量で堅牢なエンジンを作るか」が重要になります。この「ネットワーク」と「セキュリティ」という高い山を軽量かつ堅牢に登れるかの計画が、登山計画のソレにも通じるところから、言い得て妙なネーミングだなと思いました。
カーネルさん出番だす
先述したように、Alpine Linux は、軽量ではあるものの、純粋なカーネル(OS の根幹となるプログラム群)に近くなるまで削ぎ落としたという類たぐいではありません。
ここで少し「カーネル」について復習してみたいと思います。
「カーネル」とは、「何かに覆われており、直接は触れないが、根幹・本質となるもの」を言います。
日本語で言えば「(牡蠣などの)身」や「種」「核」といったものです。触ろうとしても、殻(シェル)が間にあって、普通には触れない物といったイメージが近いと思います。
そして、OS におけるカーネルとは、「その OS たるものを形作るプログラム群」のことです。つまり、Debian Ubuntu Arch Android Alpine といった Linux OS の共通部分の小さなプログラム群です。
そして、ユーザーは直接触ることができず、シェル(殻)やプログラムを通してでしか触れないものが、カーネルです。Bash や zsh などがシェルと呼ばれるのは、そのためです(ちなみに、Windows でも、PowerShell などの CUI だけでなく「エクスプローラー」も GUI によるシェルです)。
Linux では「ドライバーはカーネルの一部」と聞いたことがあると思いますが、ユーザーに直接ハードウェアを触らせないという意味です。
ゆうてドライバは「入力データをハードウェアの仕様(プロトコル)に変換する」というバイナリ変換的な役割でしかありません。
Linux はハードウェアの入力や出力は「すべてファイルである」と聞いたことがあると思います。これは、コマンドの出力はパイプ渡し(|)やリダイレクション(> や >>)で次のコマンドに渡せるのと思想は同じです。
つまり、ユーザーやアプリが、とあるハードウェアにデータを送りたい場合は、そのハードの入力として割り当てられたファイルにデータを書き込みます。例えばプリンタのポートが /dev/lp0 とした場合、イメージとして cat /path/to/data >> /dev/lp0 するようなものと同じなのです。この時、/dev/lp0 を監視して、受け取ったデータをゴニョゴニョして、ハードウェアにバイナリデータを転送する関数やプログラムがカーネルに含まれています。そして、ゴニョゴニョする処理に差し込むのがドライバです。
つまり、このカーネルのファイルに「入力をファイルにも書き込む処理」を差し込んでコンパイルすれば、ロガーやプロトコルアナライザーにもなるのです。この原理は、Alpine であっても Linux であれば変わりはありません。
カーネルをコンパイルしなくても、パッケージマネージャーでドライバを入れたことがあるかもしれません。これは「特定のディレクトリにバイナリ(ドライバ)があったら、ゴニョゴニョ中にパイプ渡しする」という処理が、先のカーネルファイルに記載されているためです。このオーバーヘッドが気になる場合に、カーネルに書き込んでコンパイルするのです。
また、Alpine に限らず、Linux カーネルのプログラム群のうち、ユーザーや開発者にとって重要なのが「標準コマンド」と「標準ライブラリ」です。一般的にユーザーが使うのが「コマンド」で、アプリが使うのが「ライブラリ」です。
「標準」というのは「スタンダード」とも言われ、POSIX や ISO といった規格に準拠しており、Linux がデフォルトで同梱すると定められたものです。これに、ディストロ独自で必ず入れるコマンドやライブラリも「標準」と付いたりします。しかし「そのディストロのユーザーから見ればスタンダード」というだけの違いです。
一般的な Linux の場合、これら標準コマンドやライブラリは個別のファイルとなっています。
そして、ビルドする際に必要なもの(同梱するコマンド、ドライバやライブラリなど)を選別したり、追加してビルドしたものをディストロ(もしくはフレーバー)と呼びます。
Alpine の 1 番の特徴は、これらの標準コマンドと標準ライブラリを、各々 1 つのバイナリにまとめていることです。
標準コマンドが busybox(ビジーボックス)、そして標準ライブラリが musl(マッスル)という「俺様実装」された単体バイナリに集約されています。
この差異が、Alpine が嫌われる一番の原因なのです。
標準コマンド群 busybox
標準コマンドは、例えば、ls echo や cat といったデフォルトで使えるコマンドです。
先述したように「標準」といっても、ここでは POSIX といった「規格に準拠した動作をするコマンド」を「標準コマンド」と呼びます。昨今、Linux 界隈で Rust 言語が話題になっているのも、この標準に準拠したコマンドを Rust で再実装していくという方向に向いているからです。
ユーザーから見れば、中身(実装言語)が違うだけで、動作は変わらないので気にすることもないのですが。
そして、これらの「標準コマンド」もカーネルの一部であるため、直接は触れず、ターミナルなどから Bash や zsh といったシェル(殻)を通して実行する必要があります。
例えば、以下のように ls や echo コマンドは、Alpine Linux では busybox のエイリアスであることが確認できます。
lsやechoコマンドがbusyboxのエイリアスであることを確認する
$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME=”Alpine Linux v3.17″
$ which ls echo
/bin/ls
/bin/echo
$ ls -la /bin/ls /bin/echo
lrwxrwxrwx 1 root root 12 Nov 22 13:06 /bin/echo -> /bin/busybox
lrwxrwxrwx 1 root root 12 Nov 22 13:06 /bin/ls -> /bin/busybox
$ ls -lah /bin/busybox
-rwxr-xr-x 1 root root 821.7K Nov 19 10:13 /bin/busybox
このように 200 近い標準コマンドを 1 つにまとめたのが busybox です。
複数コマンドを 1 つにまとめるメリットは、実行ファイルに付くオーバーヘッドが 1 つで済むことです。塵も積もればで、トータルサイズは小さくなります(800KB 程度)。また、1 回ロードしておけば、ディスクアクセスも減らせます。
標準ライブラリ群 musl
そして、標準コマンド同様に、標準ライブラリを俺様実装して 1 つにしたものが musl(マッスル)です。他のディストロでは glibc と呼ばれるライブラリの代替となるものです。
musl を俺様アプリから使いたい場合に必要なのが musl-dev です。これをインストールしないと、musl のインターフェースの仕様がわからなくなるため、コンパイルできません。
そして、アプリがハードウェアにアクセスしたりできるようなライブラリ(例えばドライバ)や、枯れた(良く使われ、叩き上げられた)関数など、共通した動作をアプリに提供したいものをまとめたライブラリが標準ライブラリです。
これらライブラリは、C 言語のインターフェース(入出力の仕様)に対して互換を実装していれば、他のプログラム言語からでも使えるようになります。
プログラムが、これら標準ライブラリを利用する場合は、つなぎ・・・となる「ヘッダファイル」(互換の仕様書みたいなもの)が必要です。これがアプリをビルド(コンパイル)する際に glibc-dev musl-dev や sqlite3-dev といった「なんとか-dev」のパッケージを別途インストールする必要がある理由です。
問題は、musl は他のディスロの標準ライブラリを単純
… ばてました。「❤︎」が付いたら、またやる気だします。
请引用参考文献。
GOPATH モードからモジュール対応モードへ移行せよ @ Qiita
cgoを使ったCとGoのリンクの裏側 (1) @ Qiita
[Golang]$GOPATH/go.mod exists but should notを回避する @ Selfnote
When trying to build docker image, I get “gcc“: executable file not found in $PATH” @ StackOverflow
CGO does not seem to work on golang:1.6-alpine #86 @ GitHub
What is the alpine equivalent to build-essential? #24 | docker-alpine | gliderlabs @ GitHub
Getting GOPATH error “go: cannot use path@version syntax in GOPATH mode” in Ubuntu 16.04 @ StackOverflow