SmartCross 项目的介绍见这里。其中的控制器组件用 Rust 写成,需要编译到 aarch64 平台。我尝试写了一个 Nix 表达式来管理该项目的环境。

Nix 是很多东西的总称,包括

  • 一个函数式编程语言 Nix 表达式
  • 一个用 Nix Expression 进行打包的包管理系统 Nix,同时支持各种 Linux 和 MacOS,并允许多个版本的相同软件在系统中并存
  • 一个世界上最大的软件包仓库 Nixpkgs
  • 一个操作系统 NixOS,使用 Nix 表达式来定义整个系统的软件和配置,并实现了原子更新和方便地系统回滚

同时 Nixpkgs 提供了一流的交叉编译支持。下面将编写描述构建环境的 Nix 表达式。

CMake 项目的打包

首先该项目依赖的 libubootenv 没有在 nixpkgs 中打包,所以我们需要手动打包。只需要写一个 libubootenv.nix 文件,放在 nix/ 目录下即可。Nixpkgs 的“genericBuild”机制可以处理 CMake 项目,基本只需要按模版简单填空即可。

{ lib, stdenv, fetchFromGitHub, cmake, zlib }:

stdenv.mkDerivation rec {
  pname = "libubootenv";
  version = "0.3.3";

  src = fetchFromGitHub {
    owner = "sbabic";
    repo = "libubootenv";
    rev = "v${version}";
    sha256 = "sha256-BQZp+/UbaEkXFioYPAoEA74kVN2sXfBY1+0vitKdfho=";
  };

  nativeBuildInputs = [ cmake ];

  buildInputs = [ zlib ];

  # 这个选项是为了修复 pkg-config 给出路径错误的问题
  cmakeFlags = [
    "-DCMAKE_INSTALL_INCLUDEDIR=include"
  ];

  meta = with lib; {
    description = "Generic library and tools to access and modify U-Boot environment from User Space";
    homepage    = "https://github.com/sbabic/libubootenv";
    license = licenses.mit;
  };
}

构建依赖

用 Nix 表达式写好一个包的定义之后,Nixpkgs 可以自动处理到不同平台的交叉编译。注意到如果包 A 依赖了包 BbuildPlatform 是编译时的平台,hostPlatform 是编译产物实际运行的平台,那么一般有以下两种情况

  1. hostPlatform B == hostPlatform A
  2. hostPlatform B == buildPlatform A

如果符合情况 1,例如 BA 的运行时依赖,则需要将 B 放到 A.buildInputs 中去,如果符合情况 2,例如 B 是构建工具,则需要放到 A.nativeBuildInputs 中去。

Rust 项目打包

使用 buildRustPackage

Rust 项目可以用 nixpkgs.rustPlatform.buildRustPackage 打包。同样是简单按模版填空即可。下面是 nix/smartcross_controller.nix 文件。

{ rustToolchain,
  makeRustPlatform, pkgconfig, protobuf, libubootenv, avahi, openssl, dbus, alsa-lib, zlib
}:

let rustPlatform = makeRustPlatform {
  rustc = rustToolchain;
  cargo = rustToolchain;
};

in rustPlatform.buildRustPackage rec {
  pname = "smartcross_controller";
  version = "0.1.0";

  src = ../.;
  cargoLock = {
    lockFile = ../Cargo.lock;
    outputHashes = {
      "camilladsp-1.0.1" = "sha256-XpQE+XVgQyVRg/NHCkPnpB/SGLChsUZucvL8x/ieKzI=";
      "libubootenv-rs-0.1.0" = "sha256-FRPnFjrlVS09W7MTjY0X8kwU04HZMcYxQAiVvEvKc08=";
      "rfkill-rs-0.1.0" = "sha256-uN58uzTeaQWLAEizNFZSldq2wkmlo8Si5xbQDyfYmYI=";
    };
  };

  nativeBuildInputs = [
    pkgconfig
    protobuf
    rustPlatform.bindgenHook
  ];

  buildInputs = [
    libubootenv
    avahi
    openssl
    dbus.dev
    alsa-lib.dev
    zlib
  ];
}

基于可复现性的考虑,Rust 项目的 git 依赖需要全部填写哈希值。一些第三方库使用更高级的方法解决了这个问题。下面使用 Crane 来进行同样的打包。

使用 Crane 为 Rust 项目打包

Crane 比起 buildRustPackage 的另一个优点是提供了更细粒度的缓存。使用 buildRustPackage,每次更改项目代码后,重新编译都是一个 clean build,而使用 Crane,只要不更改依赖项,则不用重新编译 cargo 依赖。

遗憾的是,Crane 对交叉编译的支持没有 buildRustPackage 那么好。为了成功编译,我们需要给构建环境手动添加两个环境变量。其中一个环境变量名字中带有架构名称,使得一旦目标架构变化,我们就需要手动更改这个表达式。所幸我们不会频繁更换构建目标平台。

{ src, craneLib, target,
  stdenv, pkgconfig, protobuf, rustPlatform, libubootenv, avahi, openssl, dbus, alsa-lib, zlib
}:

craneLib.buildPackage {
  inherit src;

  nativeBuildInputs = [
    # ... 和前面相同
  ];

  buildInputs = [
    # ... 和前面相同
  ];

  # 任何不含有特殊意义的字段都将会直接变成构建环境中的环境变量
  CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = "${stdenv.cc.targetPrefix}cc";
  CARGO_BUILD_TARGET = target;
}

这里我们没有指定 src,而是将其作为参数,稍后我们会具体给 src 赋值。

编写 Flake 表达式

接下来我们在项目根目录编写 flake.nix。这里面包含了关于构建环境所有的配置信息。

{
  # inputs 声明所有用到的库
  inputs = {
    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs = {
        nixpkgs.follows = "nixpkgs";
        flake-utils.follows = "flake-utils";
      };
    };
    crane = {
      url = "github:ipetkov/crane";
      inputs = {
        nixpkgs.follows = "nixpkgs";
        flake-utils.follows = "flake-utils";
      };
    };
    flake-utils.url = "github:numtide/flake-utils";
    nixpkgs.url = "nixpkgs/nixos-unstable";
  };

  outputs = { self, rust-overlay, crane, flake-utils, nixpkgs }:
    flake-utils.lib.eachDefaultSystem (system:
      let # 下面将声明一系列变量(值)
        target = "aarch64-unknown-linux-gnu"; # 目标平台
        # pkgs 将成为 nixpkgs 特定目标平台的实例
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ (import rust-overlay) ]; # 为了自由选择 Rust 版本和组件,我们使用了 rust-overlay
          crossSystem = {
            config = target;
          };
        };
        # 选择最新的 stable Rust,带有 minimal 配置的组件
        rustToolchain = pkgs.pkgsBuildHost.rust-bin.stable.latest.minimal.override {
          targets = [ target ]; # 同时能够交叉编译到目标平台
        };
        # 将我们选择的 Rust 工具链应用到 Crane
        craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
        # 辅助函数
        protoFilter = path: _type: builtins.match ".*proto$" path != null;
        # 一个函数,用来判断 path 是否是需要带入构建环境的文件
        protoOrCargo = path: type:
          (protoFilter path type) || (craneLib.filterCargoSources path type);
      in {
        # 这个 flake 可以生成的包
        packages = rec {
          default = smartcross_controller;

          # 构建 libubootenv
          libubootenv = pkgs.callPackage ./nix/libubootenv.nix {}; # 没有参数需要传递

          # 构建 smartcross_controller
          smartcross_controller = pkgs.callPackage ./nix/smartcross_controller.nix {
            # src 参数。构建 Rust 项目所需要的文件。无关的文件如 README 将不会被带入编译环境,也就不会因为修改而引发重新构建
            src = pkgs.lib.cleanSourceWith {
              src = ./.;
              filter = protoOrCargo;
            };

            # 语法糖,用于传递同名的参数
            inherit craneLib target libubootenv;
          };

          # 并不真正产生二进制产物,只是用于提供构建环境
          env = pkgs.callPackage (
            { mkShell, llvm, clang }:
            with smartcross_controller;
            mkShell {
              nativeBuildInputs = nativeBuildInputs ++ [ llvm clang ];
              inherit buildInputs CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER;
            }
          ) {};
        };
      });
}

接下来运行 nix build '.#' 即可开始 smartcross_controller 的构建。构建完成后,将留下 result 目录。

$ ls -ld result
lrwxrwxrwx 1 pgw pgw 97 Dec 11 17:17 result -> /nix/store/7zsmhixg5kf03ni0q0cc8i75c47kw8vr-smartcross_controller-aarch64-unknown-linux-gnu-0.1.0/
$ ls result/bin/
smartcross_controller*  updater*
$ file result/bin/smartcross_controller
result/bin/smartcross_controller: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /nix/store/0kbwlxnaxl0jly5szmgxqljrj3dxzyw8-glibc-aarch64-unknown-linux-gnu-2.35-163/lib/ld-linux-aarch64.so.1, for GNU/Linux 2.6.32, not stripped

使用 nix build '.#libubootenv' 可以单独构建 libubootenv,使用 nix develop '.#env' 可以进入一个 shell,这里面有全部定义好的构建环境。

$ nix develop '.#env'
[user@host SmartCrossCtrl]$ rustc --version
rustc 1.65.0 (897e37553 2022-11-02)
[user@host SmartCrossCtrl]$ cargo build
...(build success)