背景

有时, 我们需要安装同一个软件的不同版本. 如 gcc-4.0, gcc-5.0 等等, 如 qemu 2.5 和 qemu 3.1 等. 就算是同一个版本, 有时 (尤其是源码编译的时候) 我们需要使用不同的配置, 如 DEBUG 和 RELEASE, 以及我在 riscv 开发中需要频繁切换 gcc/qemu 的软硬浮点.

这种情况对于编译器尤其常见 – 不同项目经常要求使用不同版本的编译器. 有些语言对这样的情况有很好的支持, 如 Rust 有 rustup, Haskell 有 ghcup. 但这些方法通常是比较 case specific 的.

我希望达到的目的是, 能有一个命令类似 allup, 然后可以

$ allup use   gcc-rv32imac-ilp32   llvm-8.0-release   qemu-riscv
Ok, using the above toolchains!
$ allup use   gcc-rv64imacfd-lp64   llvm-7.0-debug   qemu-riscv
Ok, using the above toolchains!

已有方法

我之前的方法是配置 $HOME/.local. 把它变成一个符号链接. 我需要使用什么工具链, 就让它指向那个工具链的安装目录 – 我多数情况下是源码编译安装, 然后设置自己的 PREFIX 而不是默认的.

举个例子, 我现在的安装目录是在 $HOME/locals, 其中有

$ ls -1 $HOME/locals
jdk-11.0.1
llvm-7.0.1-x86-release
qemu-3.1.0
linux-rv64
local-old
pip3
sifive-rv64
riscv32imac-ilp32-gnu-toolchain
riscv-pk-build
riscv64gc-lp64d-linux-gnu-toolchain
riscv-qemu
riscv64imac-lp64-gnu-toolchain
riscv-qemu-2.12.92
riscv64imac-lp64-linux-gnu-toolchain

这样的好处是避免污染所有用户 (虽然只有我一个用户), 我只是用了自己的 $HOME/.local. 并且修改符号链接是一个很快很容易的事情.

但是如果我需要多个工具链合到一起, 这个方法就比较尴尬了. 符号链接只能指向一个目标, 而多个工具链的安装目录是不同的. 比如同时使用 riscv-qemu-2.12.92sifive-rv64 怎么办?

暴力的方法当然就是复制到一起, 不过可以使用 overlayfs 达到更好更快的效果.

overlayfs 概述

overlayfs 这种文件系统被 docker 使用, 其结构是一层一层的. 每层都对应一系列文件, 在我的例子中每层就对应主机上某个目录.

然后在 overlayfs 里面查找某个文件, 就如

if 顶层有这个文件
  返回顶层的文件
elseif 第二层有这个文件
  返回第二层的文件
elseif
  ...
else
  哪一层都没有这个文件, 报告文件没有找到

如一个两层的 overlayfs, 顶层对应的主机目录中内容如

top:
.
├── a
│   ├── a
│   └── b
├── b
├── c
└── d

底层对应主机目录中有

bot:
.
├── a
│   └── c
├── b
└── e

假设 overlayfs 的挂载点是 mntpt. 那么 mntpt 目录中的结构如 (top:b 表示顶层中的 b 文件, 而非底层的 b 文件)

.
├── a
│   ├── top:a
│   ├── top:b
│   └── bot:c
├── top:b
├── top:c
├── top:d
└── bot:e

可以看到, overlayfs 实际上就是把多个目录按照某个优先顺序给 merge 了一下. 简单的 cp 也有类似的效果, 但是它耗时占地麻烦.

创建上面这样一个 overlayfs 的命令很简单, 类似

sudo mount -t overlay overlay -o lowerdir=$HOME/locals/sifive-rv64:$HOME/locals/llvm-7.0.1-x86-release $HOME/.local

这样创建的 $HOME/.local 是不可写的 – 符合我们的希望. 当然 overlay 也支持可写, 参考 upperdir 和 workingdir 选项.

实际应用

为了方便, 实际应用我通过一个脚本完成. 每次要修改工具链的时候, 直接修改 newlocals 数组即可. 注意为了这个工具链生效, 你需要配置 ~/.locals 为一个工具目录. 其中 ~/.localsPATH, 并且 LD_LIBRARY_PATH 加上 ~/.locals/lib.

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
46
47
48
49
50
51
52
53
54
55
56
57
#!/bin/bash

### CONFIGURATIONS

# target: where you did all the LD_LIBRARY_PATH, PATH etc
target="$HOME/.local"

# base: where you put your toolchain installations
base="$HOME/locals"

# newlocals: which toolchains to use
declare -a newlocals
newlocals=(
    sifive-rv64
    llvm-7.0.1-x86-release
)


### ACTUAL CODE
# Check for valid entries in newlocals
for X in ${newlocals[@]}
do
    if ! [[ -d ${base}/${X} ]]
    then
        echo Entry ${X} is invalid: no such directory in base.
        exit 1
    fi
done


# Check for remounting
if mountpoint -q ${target}
then
    echo Current locals:
    grep -F ${target} /proc/self/mountinfo |#
        sed "s/.*lowerdir=\\(\\S*\\).*/\\1/" |#
        tr ':' '\n'
    echo ""
    echo Unmount? [y/N]
    read confirm
    if [[ ${confirm} != Y && ${confirm} != y ]]
    then
        echo "Cancelled"
        exit 0
    fi
    if ! sudo umount ${target}
    then
        echo "Failed to umount"
        exit 0
    fi
fi


# Finally remount with overlayfs
lowerdirs=$(printf ":$base/%s" "${newlocals[@]}")
lowerdirs=${lowerdirs:1}
echo sudo mount -t overlay overlay -o lowerdir=${lowerdirs} ${target}