第三篇内容会继续上一篇的话题,仍然是有关编译,与前一篇稍有区别的是编译工具。
Contents
基于 pbuilder 的源码构建:
在上一篇出现的 dpkg-buildpackage / debuild
非常依赖与当前的系统,也就是说,一个系统发行版就只能编译它专用的安装包。同时由于并非净室(chroot)编译环境,所以有可能会出现编译完成后,有部分依赖没有写入 control 文件的问题。pbuilder 可以很方便的解决这些问题,他能确保软件包能够在不同发行版本中的自动编译器能够干净准确的编译出 deb 安装包。
首先安装 pbuilder 以及相关的软件包:
$ sudo apt-get install cowdancer cowbuilder pbuilder packaging-dev dh-make ubuntu-keyring debian-archive-keyring
pbuilder create
初始化本地 pbuilder chroot 系统,但是这不利于多个 chroot 系统的管理。于是我们可以通过为当前用户添加 ~/.pbuilderrc 的方式 (或者 /etc/pbuilderrc) 达到自定义 chroot 系统构建的目的,以下是一个示例:
#!/bin/sh ## define cowbuilder as pbuilder warpper PDEBUILD_PBUILDER=cowbuilder set -e if [ "$OS" == "debian" ]; then MIRRORSITE="http://ftp.de.debian.org/debian/" COMPONENTS="main contrib non-free" DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--keyring=/usr/share/keyrings/debian-archive-keyring.gpg") : ${DIST:="stretch"} : ${ARCH:="amd64"} elif [ "$OS" == "ubuntu" ]; then MIRRORSITE="http://de.archive.ubuntu.com/ubuntu/" COMPONENTS="main restricted universe multiverse" DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg") : ${DIST:="bionic"} : ${ARCH:="amd64"} else echo "Unknown OS: $OS" exit 1 fi if [ "$DIST" == "" ]; then echo "DIST is not set" exit 1 fi if [ "$ARCH" == "" ]; then echo "ARCH is not set" exit 1 fi ## define locations NAME="$OS-$DIST-$ARCH" USRDISTPATH=/var/cache/pbuilder DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--arch=$ARCH") BASETGZ="$USRDISTPATH/$NAME-base.tgz" DISTRIBUTION="$DIST" BUILDRESULT="$USRDISTPATH/$NAME/result" APTCACHE="$USRDISTPATH/$NAME/aptcache" BUILDPLACE="$USRDISTPATH/build" HOOKDIR="$USRDISTPATH/hook.d" # parallel make DEBBUILDOPTS=-j$(nproc) # create local repository if it doesn't already exist, # such as during initial 'pbuilder create' if [ ! -d $BUILDRESULT ] ; then mkdir -p $BUILDRESULT chmod g+rwx $BUILDRESULT fi if [ ! -e $BUILDRESULT/Packages ] ; then touch $BUILDRESULT/Packages fi if [ ! -e $BUILDRESULT/Release ] ; then cat << EOF > $BUILDRESULT/Release Archive: $DIST Component: main Origin: pbuilder Label: pbuilder Architecture: $ARCH EOF fi # create user script hook mkdir -p $HOOKDIR # local result package autoload if [ -d $BUILDRESULT ] ; then export DEPSPATH=$BUILDRESULT BINDMOUNTS=$DEPSPATH DEBS=`ls $DEPSPATH | grep deb | wc -l` if [ ${DEBS} != 0 ] ; then OTHERMIRROR="deb [trusted=yes] file://$DEPSPATH ./" echo "add $DEPSPATH to source" cat > $HOOKDIR/D05deps <<'EOF' #!/bin/sh apt-get install --assume-yes apt-utils ( cd "$DEPSPATH"; apt-ftparchive packages . > Packages ) apt-get update EOF chmod +x "${HOOKDIR}/D05deps" fi fi # failure redirect cat > $HOOKDIR/C10shell <<'EOF' #!/bin/sh # invoke shell if build fails. apt-get install -y --force-yes vim less bash cd /tmp/buildd/*/debian/.. /bin/bash < /dev/tty > /dev/tty 2> /dev/tty EOF chmod +x "${HOOKDIR}/C10shell"
简单解释以下上面的脚本:首先在脚本中定义了 debian 和 ubuntu 的 mirror server,没有指定的话,ubuntu 无法下载 debian 的 chroot,反之亦然。接下去是对诸如文件保存位置,文件名称,钩子/缓存位置的设定。通过这些设定,构建出来的 chroot 归档就有比较规整的名字了。再下去是两个构建钩子。第一个钩子能自动载入生成在 result 中的 deb 包,外部的预编译二进制包也可以同样的自动被载入;第二个钩子是在编译失败时将 chroot 中的 Terminal 自动转发到现在使用的 Terminal 中。
利用这样的一个配置文件,我们就可以很简单的使用如下指令批量构建 chroot 环境了:
sudo OS=ubuntu DIST=bionic ARCH=amd64 pbuilder --create; \ sudo OS=ubuntu DIST=xenial ARCH=amd64 pbuilder --create; \ sudo OS=debian DIST=buster ARCH=amd64 pbuilder --create; \ sudo OS=debian DIST=stretch ARCH=amd64 pbuilder --create; \ sudo OS=debian DIST=jessie ARCH=amd64 pbuilder --create
Tips: 这里的 ARCH 只要 CPU 指令集支持即可进行编译,也就是说在一台运行 amd64 的系统上完全可以编译 i386 的二进制包,arm 平台也是如此。另一点值得注意的是比如 Scaleway 使用的 Cavium ThunderX SoCs
构建简单的二进制包:
pbuilder
需要的是由 dpkg-buildpackage
生成的 .orig.tar.gz、 .dsc 和 .debian.tar.gz。当然,还有一种形式,是仅有 .orig.tar.gz 和 .dsc:这种打包方式会将 debian 文件夹也存入压缩包,通常是由一些拥有版本管理能力的自动化打包系统生成的,常见于一些 ppa 的 nightly build。
实际的构建指令非常简单:
sudo OS=ubuntu DIST=bionic ARCH=amd64 pbuilder build libtorrent_0.13.6-1build1.dsc
/var/cache/pbuilder/$OS-$DIST-$ARCH/result/
。
需要给生成的 .dsc 和 .changes 文件签名的时候,只需要运行以下指令:
cd /var/cache/pbuilder/$OS-$DIST-$ARCH/result/ debsign libtorrent_0.13.6-1build1.dsc
构建具有本地依赖的二进制包:
-
方法一:
如果说使用上述的 .pbuilderrc 那么,内含的钩子函数就会在编译的时候生成 Package 和 Release 信息。这些信息能够指导 apt 包管理器通过
deb [trusted=yes] file://$DEPSPATH ./
的形式找到本地的 deb 二进制包并根据 control 文件中描述的依赖关系选择性安装。你可以通过运行以下指令,在编译前更新源列表:
$ sudo OS=ubuntu DIST=bionic ARCH=amd64 pbuilder update --override-config
如果依赖仍然没有被载入,那么反复尝试编译和更新源列表即可,通常在 1-2 次更新后就会解决。
-
手动安装依赖。通常情况下在执行完
build/login
等指令后 pbuilder 的系统会被重置,但是我们可以通过使用以下代码登录到 chroot 环境中进行配置并且保存:$ sudo OS=ubuntu DIST=bionic ARCH=amd64 pbuilder --login --save-after-login
^D
(Control-D)离开这个 shell 时环境会被保存。
Tips: 在编译过程中通常需要注意 control 文件与 chroot 环境的匹配,并非所有的编译规则都可以同时用在多个 chroot 环境中。另外在出现错误进入 chroot 环境使用的 Terminal 以后也可以再次尝试使用 dpkg-buildpackage
命令构建,pbuilder 默认的规则比起 dpkg-buildpackage
更加严格,因此有时候会因为一些小问题导致无法完成编译,使用 dpkg-buildpackage
则可以绕过这些检查。
基于 qemu 的 pbuilder 源码构建:
上文提到的编译都是针对同构架硬件的,也就是说 x86/amd64 平台只能编译 x86/amd64 使用的二进制;arm 平台也只能编译它的二进制。在日常使用中,不难知道,arm 平台(特别是现有的 armel/armhf) 性能是非常孱弱的,如果直接使用它们进行 native 编译,需要的时间是很长的,举例来说,Allwinner 的 A83T 在使用多线程编译 transmission 的时候,使用的时间大约是一小时,如果需要针对不同的系统编译,时间也是成倍增加。解决方法实际上也很简单,使用交叉编译工具进行编译,并且使用 qemu 解决指令集翻译问题。在使用一台 4 线程 VPS (实验对象为 Hetzner Cloud VPS) 的情况下,编译时间则只需要20分钟。
注意:这里需要满足 pbuilder 版本 > 0.23 也就是说 Xenial 版本是没办法用来做 qemu 的跨构架编译的。
初始化环境:
qemu 环境的构建非常简单,通常来说不需要自己编译,可以直接通过以下指令就可以安装:
$ sudo apt-get install qemu binfmt-support qemu-user-static
.pbuilderrc:
#!/bin/sh ## define cowbuilder as pbuilder warpper PDEBUILD_PBUILDER=cowbuilder ## must use the apt "satisfydepends" resolver with pbuilder # https://wiki.debian.org/CrossCompiling#Building_with_pbuilder PBUILDERSATISFYDEPENDSCMD="/usr/lib/pbuilder/pbuilder-satisfydepends-apt" ## if pbuilder-satisfydepends-apt not work, try following resolver #PBUILDERSATISFYDEPENDSCMD="/usr/lib/pbuilder/pbuilder-satisfydepends-experimental" set -e if [ "$OS" == "debian" ]; then MIRRORSITE="http://ftp.de.debian.org/debian/" COMPONENTS="main contrib non-free" DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--keyring=/usr/share/keyrings/debian-archive-keyring.gpg") : ${DIST:="stretch"} : ${ARCH:="armhf"} elif [ "$OS" == "ubuntu" ]; then MIRRORSITE="http://ports.ubuntu.com/" COMPONENTS="main restricted universe multiverse" DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg") : ${DIST:="bionic"} : ${ARCH:="armhf"} elif [ "$OS" == "raspbian" ]; then MIRRORSITE="http://mirror.de.leaseweb.net/raspbian/raspbian" COMPONENTS="main contrib non-free" DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--keyring=/usr/share/keyrings/raspbian-archive-keyring.gpg") : ${DIST:="stretch"} : ${ARCH:="armhf"} else echo "Unknown OS: $OS" exit 1 fi if [ "$DIST" == "" ]; then echo "DIST is not set" exit 1 fi if [ "$ARCH" == "" ]; then echo "ARCH is not set" exit 1 fi if [ "$ARCH" == "armel" ] && [ "$(dpkg --print-architecture)" != "armel" ]; then DEBOOTSTRAP="qemu-debootstrap" fi if [ "$ARCH" == "armhf" ] && [ "$(dpkg --print-architecture)" != "armhf" ]; then DEBOOTSTRAP="qemu-debootstrap" fi if [ "$ARCH" == "aarch64" ] && [ "$(dpkg --print-architecture)" != "aarch64" ]; then DEBOOTSTRAP="qemu-debootstrap" fi ## define locations NAME="$OS-$DIST-$ARCH" USRDISTPATH=/var/cache/pbuilder DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--arch=$ARCH") BASETGZ="$USRDISTPATH/$NAME-base.tgz" DISTRIBUTION="$DIST" BUILDRESULT="$USRDISTPATH/$NAME/result" APTCACHE="$USRDISTPATH/$NAME/aptcache" BUILDPLACE="$USRDISTPATH/build" HOOKDIR="$USRDISTPATH/hook.d" # parallel make DEBBUILDOPTS=-j$(nproc) # create local repository if it doesn't already exist, # such as during initial 'pbuilder create' if [ ! -d $BUILDRESULT ] ; then mkdir -p $BUILDRESULT chmod g+rwx $BUILDRESULT fi if [ ! -e $BUILDRESULT/Packages ] ; then touch $BUILDRESULT/Packages fi if [ ! -e $BUILDRESULT/Release ] ; then cat << EOF > $BUILDRESULT/Release Archive: $DIST Component: main Origin: pbuilder Label: pbuilder Architecture: $ARCH EOF fi # create user script hook mkdir -p $HOOKDIR # local result package autoload if [ -d $BUILDRESULT ] ; then export DEPSPATH=$BUILDRESULT BINDMOUNTS=$DEPSPATH DEBS=`ls $DEPSPATH | grep deb | wc -l` if [ ${DEBS} != 0 ] ; then OTHERMIRROR="deb [trusted=yes] file://$DEPSPATH ./" echo "add $DEPSPATH to source" cat > $HOOKDIR/D05deps <<'EOF' #!/bin/sh apt-get install --assume-yes apt-utils ( cd "$DEPSPATH"; apt-ftparchive packages . > Packages ) apt-get update EOF chmod +x "${HOOKDIR}/D05deps" fi fi # failure redirect cat > $HOOKDIR/C10shell <<'EOF' #!/bin/sh # invoke shell if build fails. apt-get install -y --force-yes vim less bash cd /tmp/buildd/*/debian/.. /bin/bash < /dev/tty > /dev/tty 2> /dev/tty EOF chmod +x "${HOOKDIR}/C10shell"
这个配置的主体内容与 native 编译相同,区别在于:
-
使用了pbuilder-satisfydepends-apt 解决依赖关系,有些时候这个包管理器也不能处理好依赖,那么还可以尝试一下 pbuilder-satisfydepends-experimental。
-
使用到的 repo 也与 native 编译不同
-
使用 qemu-debootstrap 部署 chroot 环境。
$ wget http://archive.raspbian.org/raspbian/pool/main/r/raspbian-archive-keyring/raspbian-archive-keyring_20120528.2_all.deb $ sudo dpkg -i raspbian-archive-keyring_20120528.2_all.deb
我们使用如下命令初始化 chroot:
sudo OS=ubuntu DIST=bionic ARCH=armhf pbuilder --create; \ sudo OS=ubuntu DIST=xenial ARCH=armhf pbuilder --create; \ sudo OS=debian DIST=stretch ARCH=armhf pbuilder --create; \ sudo OS=debian DIST=jessie ARCH=armhf pbuilder --create; \ sudo OS=raspbian DIST=stretch ARCH=armhf pbuilder --create; \ sudo OS=raspbian DIST=jessie ARCH=armhf pbuilder --create
构建二进制包:
为了区别一个跨构架的编译,我们需要将编译参数改为如下形式:
$ sudo OS=ubuntu DIST=bionic ARCH=armhf pbuilder build --host-arch armhf libtorrent_0.13.6-1build1.dsc
qemu 由于执行二进制代码的翻译,因此并不会像常见的 KVM 等虚拟机那样高效。但是由于常用的 arm 开发板通常在 CPU 计算能力和 IO 能力方面都与 x86/x64 平台有较大差距,因此这种方法能够确实的提高编译的效率。
参考文档:
https://www.debian.org/doc/manuals/maint-guide/
https://wiki.debian.org/CrossCompiling#Building_with_pbuilder
https://wiki.ubuntu.com/ARM/RootfsFromScratch/QemuDebootstrap
https://wiki.debian.org/PbuilderTricks#How_to_include_local_packages_in_the_build
https://pbuilder-docs.readthedocs.io/en/latest/faq.html
https://www.tolaris.com/2009/04/09/adding-a-local-respository-to-pbuilder/
https://jodal.no/2015/03/08/building-arm-debs-with-pbuilder/