交叉编译的一点心得——把fish移植到OpenWrt上

前言

其实很早就有把 fish 交叉编译到 OpenWrt 上的想法了,但是尝试过一次踩了坑就放弃了。前段时间折腾 golang 交叉编译的时候进一步学习了一波 GNU 工具链的用法,感觉对 GNU 的扭曲哲学又了解一点,于是又尝试着填这个坑,总算是成功把 fish 比较完整的移植到 OpenWrt 了,这里算是记录下踩的坑和一些心得吧。

为什么不用 SDK

众所周知,为了方便让现有的包能快速移植,OpenWrt 提供了专用的 SDK 可以用于交叉编译,但是写 Makefile 和 patches 实际上也蛮麻烦的,想了想还是决定交叉编译。

不过由于解释器一般是 musl 的实现而不是 gnu 的,所以下面交叉编译后都是全静态链接。

交叉编译器选择

由于目标是 arm 架构,交叉编译器有两种 arm-linux-gnueabi-gccarm-linux-gnueabihf-gcc,两者其实只有在浮点的处理上有所不同,前者的兼容性好一些,想了想 fish 也没什么浮点需求所以选择了前者。

类似的,mips 也有好几种交叉编译器,反正编译前 cat /proc/cpuinfo 看一看总是没错的。

分离编译系统

现在我们系统中应该有两套编译系统了,一套适用于本机,一套适用于 arm,那么编译的时候怎么让 make 知道我们的目标架构呢,这时候 configure 的作用就体现出来了,比如 ./configure --help 可以看到很多有用的信息。

环境变量

首先可以观察到一些关键性的环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Some influential environment variables:
CC C compiler command
CFLAGS C compiler flags
LDFLAGS linker flags, e.g. -L<lib dir> if you have libraries in a
nonstandard directory <lib dir>
CPPFLAGS C/C++ preprocessor flags, e.g. -I<include dir> if you have
headers in a nonstandard directory <include dir>
CPP C preprocessor
CXX C++ compiler command
CXXFLAGS C++ compiler flags
CXXCPP C++ preprocessor

Use these variables to override the choices made by `configure' or to help
it to find libraries and programs with nonstandard names/locations.

这里可以看到很关键的两个参数 CC 和 CXX 表示用到的编译器,同时为了全静态链接,我还设置了 LDFLAGS=-static,所以我在交叉编译的过程中都是以 env CC=arm-linux-gnueabi-gcc CXX=arm-linux-gnueabi-g++ LDFLAGS='-static' 开头的。

不过看起来 --with-build-cc --with-build-cxx 等几个参数也应该能起到同样的效果。

host, host, target

除了通过 env 指定交叉编译器,还可以通过 host 参数指定目标架构

1
2
3
--build=BUILD           configure for building on BUILD [guessed]
--host=HOST build programs to run on HOST [BUILD]
--target=TARGET configure for building compilers for TARGET [HOST]

这三个参数非常关键,其中 build 表示 toolchain 的架构,host 表示编译出来目标的架构,target 表示编译出来目标产生的目标的架构(一般用于编译 toolchain,比如现在使用的交叉编译器)。

其中如果使用了 host 默认会开启交叉编译模式,所以我们只需要指定 host 的值为 arm-linux-gnueabi 即可,其中 build 默认会采用 config.guess 输出的结果,不过 GNU 文档推荐我们手动设置 build 的值。

不过有个问题是,我们怎么知道这三个参数的可选值?

实际上这些参数的可选值也是从另一个脚本 config.sub 来的,比如

1
2
~/f/ncurses-6.1 # ./config.sub arm-gnueabi-linux
arm-gnueabi-linux-gnu

也就是说对于 arm-linux-gnueabi 实际上是 arm-gnueabi-linux-gnu 的缩写。

prefix

另外我们当然不希望编译出来的包被直接安装到适用本机的编译系统上,也就是说需要一个类似 chroot 的设置,正好 configure 提供了类似的参数

1
2
3
4
5
6
By default, `make install' will install all the files in
`/usr/local/bin', `/usr/local/lib' etc. You can specify
an installation prefix other than `/usr/local' using `--prefix',
for instance `--prefix=$HOME'.

--prefix=PREFIX install architecture-independent files in PREFIX

之前一次踩坑是因为 fish 依赖了 libncurses,虽然可以成功交叉编译后者,但是因为不知道可以调 prefix 导致编译 fish 的时候一直找不到编译好的库。

编译依赖

首先正如上面提到的,fish 依赖了 libncurses,虽然 libncurses 基本是 linux 必备的一个库,但是它并没有用于交叉编译的预编译版本,所以需要我们自己动手编译一个出来。

了解了上面交叉编译过程后其实就很简单了,指令如下

1
2
3
env CC=/usr/bin/arm-linux-gnueabi-gcc CXX=/usr/bin/arm-linux-gnueabi-g++ LDFLAGS='-static' ./configure --build=x86_64-pc-linux-gnu --host=arm-gnueabi-linux --prefix=/usr/arm-linux-gnueabi
make -j4
make install

但是这里有一个坑,在 make install 的时候,会提示 strip 出错,看了下输出是因为使用了 strip 而不是 arm-gnueabi-strip,wtf?

所以只能在 configure 的时候再加上一个 --disable-stripping 了,没找到哪里能设置 strip,感觉是 configure 哪里推导出了问题,所以第一步的 configure 应该是

1
env CC=/usr/bin/arm-linux-gnueabi-gcc CXX=/usr/bin/arm-linux-gnueabi-g++ LDFLAGS='-static' ./configure --build=x86_64-pc-linux-gnu --host=arm-gnueabi-linux --prefix=/usr/arm-linux-gnueabi

反正嫌大到时候可以手动 strip。

编译fish

在第一步 configure 的时候会报错。

1
checking for /proc/self/stat... configure: error: cannot check for file existence when cross compiling

这是一个已知 issue,见 #1067,所以最好的办法就是 vim 打开 configure 然后删除报错的那三行。

直接删除 else 和后面两行即可。

然后就是正常编译了,但是要注意的是 prefix 要改一下,因为稍后要放到 OpenWrt 上。

1
2
3
env CC=/usr/bin/arm-linux-gnueabi-gcc CXX=/usr/bin/arm-linux-gnueabi-g++ LDFLAGS='-static' ./configure --build=x86_64-pc-linux-gnu --host=arm-gnueabi-linux --prefix=/path/to/targets --disable-stripping
make -j4
make install

之后在 targets 下就可以看到 fish 所有生成的文件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
~/f/f/targets # tree -d
.
├── bin
├── etc
│   └── fish
│   └── conf.d
└── share
├── doc
│   └── fish
├── fish
│   ├── completions
│   ├── functions
│   ├── groff
│   ├── man
│   │   └── man1
│   ├── tools
│   │   └── web_config
│   │   ├── js
│   │   ├── partials
│   │   └── sample_prompts
│   ├── vendor_completions.d
│   ├── vendor_conf.d
│   └── vendor_functions.d
├── locale
│   ├── de
│   │   └── LC_MESSAGES
│   ├── en
│   │   └── LC_MESSAGES
│   ├── fr
│   │   └── LC_MESSAGES
│   ├── nb
│   │   └── LC_MESSAGES
│   ├── nn
│   │   └── LC_MESSAGES
│   ├── pl
│   │   └── LC_MESSAGES
│   ├── pt_BR
│   │   └── LC_MESSAGES
│   ├── sv
│   │   └── LC_MESSAGES
│   └── zh_CN
│   └── LC_MESSAGES
├── man
│   └── man1
└── pkgconfig

这时候由于之前没有 strip 而且还是静态链接,因此 binary 可能相当大,所以我们选择手动 strip 一下

1
2
cd bin
arm-gnueabi-linux-strip *

strip 前大概是 20+M,strip 后就只有 2M 左右了。

最后传到 OpenWrt 的相应目录即可,其中 share 需要手动建立。

OpenWrt 的 terminfo

但是到这里还没有结束,如果此时在 OpenWrt 中直接运行 fish 会出现下面的错误

1
2
3
4
5
6
7
8
9
root@LEDE:~# fish
<W> fish: Could not set up terminal.
<W> fish: TERM environment variable set to 'xterm-256color'.
<W> fish: Check that this terminal type is supported on this system.
<W> fish: Using fallback terminal type 'ansi'.
<W> fish: Could not set up terminal using the fallback terminal type 'ansi'.
<W> fish: Using fallback terminal type 'dumb'.
<W> fish: Could not set up terminal using the fallback terminal type 'dumb'.
Welcome to fish, the friendly interactive shell

意思是没有找到可用的 term,但是 ls /usr/share/terminfo 显然是有的,这时候有两种解决办法

设置 TERMINFO

直接 env TERMINFO=/usr/share/terminfo fish 就可以正常使用 fish 了。

链接一个 terminfo

也可以选择 ln -s /usr/share/terminfo ~/.terminfo,这样 fish 启动的时候就可以找到 terminfo 了。

其实这两个解决方法殊途同归,相同点就是都坑了我很久。

缺失的依赖

hostname

解决 terminfo 的问题后,启动 fish 还是会出错,因为没有 hostname 这个命令。

hostname 这个指令在一般 linux 发行版都是自带的,但是 OpenWrt 中没有,所以直接写个脚本替代。

1
2
3
~ # cat /bin/hostname
#!/bin/sh
echo `uname -n`

apropos

由于 OpenWrt 删掉了 man pages 因此没有 man 和 apropos 指令,导致 fish 的 tab 功能可能出问题,所以直接写个脚本替代。

1
2
3
~ # cat /bin/apropos
#!/bin/sh
echo "$1: nothing appropriate"

whoami

同 hostname,脚本替代

1
2
3
~ # cat /bin/whoami
#!/bin/sh
echo `id -un`

到这里 fish 基本可以正常运行在 OpenWrt 上了,如果还有什么缺失的指令都可以用类似的方法解决。

小结

在学习交叉编译的时候骂傻逼 GNU,但是在用脚本代替缺失指令的时候又不得不说 GNU 面向字符串编程的哲学真香,反正终于能抛弃辣鸡a壳用上鱼壳咯,我爱贝壳(逃

总之 Google 和 Man pages 是真的很重要。

最后预编译版本放在了 Github 上,欢迎提 issue 和 pr。

参考资料

fish-README
curs_terminfo
fish cannot be crosscompiled
Specifying target triplets
Could set up terminal
put curses/terminfo vars into the environment
SSH to machine with fish gives: `fish: Could not set up terminal`
What’s the difference of “./configure” option “–build”, “–host” and “–target”?