Организуем окружение Rust и сборку Docker с применением Nix Flakes

image

Чем интересен Nix


В Rust новое окружение для разработки обычно настраивается без труда – просто воспользуйтесь rustup и в добрый путь. Но при использовании такого сборочного инструмента как язык Nix, вы можете приобрести гораздо больше, лишь немного потрудившись. Ведь Nix позволяет:


Стоит начать применять Nix при работе в репозитории – и «как раньше» уже не захочется. Никаких больше README со списком команд Homebrew, apt, pacman и др., которые было бы необходимо выполнять. Сборка тонких контейнеров Docker делается в два счёта, без необходимости вручную вручную обрабатывать множество слоёв, из которых требуется копировать сборочные артефакты.

Этот пост – спартанское руководство, которое поможет быстро вкатиться в работу с nix, поэтому я не буду вдаваться в подробности о том, как nix работает под капотом, и каков синтаксис nix. Если вам нужна дешёвая и сердитая справка по синтаксису nix – рекомендую пост learn X in Y's. Если у вас есть некоторый опыт функционального программирования, то большую часть базовой информации сразу подхватите.

Окружение для разработки


Воспользуемся nix flakes, чтобы настроить nix для нашего проекта. Flakes (далее — «снежинки») – это новаторский инструмент для улучшения воспроизводимости сборок; для этого в проект добавляется так называемый «файл блокировок» (lock file). Каждая снежинка может иметь вводы inputs, в качестве которых служат другие снежинки/файлы nix и много выводов. Здесь стоит отметить, что все файлы, на которые стоят ссылки из ваших снежинок (в том числе, сама снежинка) должны добавляться в git. Если вы столкнётесь с ошибками «file not found» (файл не найден), убедитесь, что добавили всё необходимое командой git add.
Чтобы приступить к работе в корне вашего проекта, выполните:

$ nix flake init

В результате у вас сформируется снежинка flake.nix – файл, который будет иметь вид:

flake.nix

{
  description = "A very basic flake";
  outputs = { self, nixpkgs }: {
	packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
	defaultPackage.x86_64-linux = self.packages.x86_64-linux.hello;
  };
}

Эта стартовая снежинка соберёт для вас двоичный файл hello world при помощи nix build .#hello, вызывающей первую строку, или просто при помощи nix build, вызывающей строку defaultPackage. Недостаток такой сборки в том, что получающийся пакет применим только в x86/64 Linux. Поэтому давайте расширим ввод, чтобы круг пригодных систем также увеличился.

flake.nix

{
  inputs = {
	nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
	flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, nixpkgs, flake-utils, ... }:
	flake-utils.lib.eachDefaultSystem (system:
  	let pkgs = import nixpkgs { inherit system; };
  	in {
    	packages.hello = pkgs.hello;
    	defaultPackage = pkgs.hello;
  	});
}

Мы добавили ещё два ввода. Первый — nixpkgs, позволяющий указать, какую версию nixpkgs следует использовать. В репозитории nixpkg насчитывается много тысяч пакетов, и обновляются они настолько часто, что здесь мы воспользуемся нестабильной веткой. Также добавим flake-utils, помогающий обеспечить поддержку снежинки сразу во многих системах, а не только в Linux.

Теперь под Linux и mac соберётся пакет hello. Запустив nix build, вы должны увидеть каталог result, в котором содержится пакет hello, и его можно выполнить при помощи ./result/bin/hello. Каталог result – это символьная ссылка на вывод сборки в хранилище nix (там nix хранит все файлы вывода). Это не обязательно будет каталог, результат просто зависит от сборки.

Rust в Nix


Чтобы продвинуться от «hello world» к rust, давайте добавим ещё один ввод:

flake.nix

inputs = {
	nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
	flake-utils.url = "github:numtide/flake-utils";
	rust-overlay.url = "github:oxalica/rust-overlay";
  };
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
  flake-utils.lib.eachDefaultSystem (system:
	let
  	overlays = [ (import rust-overlay) ];
  	pkgs = import nixpkgs { inherit system overlays; };
  	rustVersion = pkgs.rust-bin.stable.latest.default;
	in {
  	devShell = pkgs.mkShell {
    	buildInputs =
      	[ (rustVersion.override { extensions = [ "rust-src" ]; }) ];
  	};
	});

Мы добавили rust-overlay, что позволяет нам без труда указывать различные версии rust – и не полагаться на nixpkgs для справки о том, какая версия rust используется в конкретном случае.

Мы также переключили outputs на работу только c devShell, и этот вывод привязан к nix develop. Запустив его, вы получите в песочнице новую оболочку со стабильной версией rust.

Если вы хотите воспользоваться конкретной версией/ночной сборкой, то попробуйте rustVersion = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml); для считывания файла инструментария rust. Работайте с указанной там версией.

Также вы могли заметить, что мы добавили .override { extensions = [ "rust-src" ]; }). Этот код нужен анализатору rust, чтобы получить исходный код rust.

Автоматически загружаем окружение Nix


Теперь, когда мы обзавелись интересующей нас версией rust, давайте автоматизируем шаг nix develop.

Установите direnv и nix-direnv. Второй компонент опционален, но помогает с кэшированием, так что рекомендую взять и его.

Direnv добавит в вашу оболочку перехватчики, поэтому, когда вы зайдёте в ваш проект через cd, то проект автоматически загрузит окружение nix за вас, и вам не потребуется выполнять nix develop.

В корне вашего проекта выполните:

$ echo "use flake" >> .envrc
$ direnv allow


Файл .envrc будет загружен direnv и для обустройства вашего окружения будет использоваться полученный от снежинки вывод devShell. При внесении изменений в direnv вашей снежинки загружайте только то, что изменилось.

Если вы работаете с VS Code, пользуйтесь nix env selector, так, чтобы VS Code «знала» о снежинке. Без этого можно и обойтись, если вы открываете VS Code из командной строки, но настроить такую возможность не составляет труда.

Сборка проекта Rust


Теперь, когда мы добавили rust в наше рабочее окружение, и новое приложение на rust мы можем сделать с помощью:

$ cargo init

Затем можно запустить/собрать проект так, как это обычно делается при помощи cargo run/cargo build. В процессе разработки всё функционирует нормально, но давайте попытаемся собрать проект при помощи nix. Это пригодится нам позже, на этапе сборки образа docker.
Давайте и выводы обновим.

flake.nix

outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
	flake-utils.lib.eachDefaultSystem (system:
  	let
    	overlays = [ (import rust-overlay) ];
    	pkgs = import nixpkgs { inherit system overlays; };
    	rustVersion = pkgs.rust-bin.stable.latest.default;
    	rustPlatform = pkgs.makeRustPlatform {
      	cargo = rustVersion;
      	rustc = rustVersion;
    	};
    	myRustBuild = rustPlatform.buildRustPackage {
      	pname =
        	"rust_nix_blog"; # make this what ever your cargo.toml package.name is
      	version = "0.1.0";
      	src = ./.; # the folder with the cargo.toml
      	cargoLock.lockFile = ./Cargo.lock;
    	};
  	in {
    	defaultPackage = myRustBuild;
    	devShell = pkgs.mkShell {
      	buildInputs =
        	[ (rustVersion.override { extensions = [ "rust-src" ]; }) ];
    	};
  	});

Сначала нам нужно сделать rustPlatform с нашей версией rust. На этой платформе позволяется собрать пакет rust при помощи rustPlatform.buildRustPackage. В nix это равноценно cargo build. Нам понадобится cargoLock.lockFile, чтобы nix мог кэшировать все зависимости вашего проекта, основываясь на имеющемся у вас файле блокировок.

Теперь можно выполнить nix build, тогда ваш проект снова окажется в каталоге result. В моём случае я могу выполнить ./result/bin/rust_nix_blog.

Делаем образ Docker


Теперь, когда мы научили nix собирать проект rust, сделать контейнер docker не составляет труда.

flake.nix

outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
	flake-utils.lib.eachDefaultSystem (system:
  	let
    	overlays = [ (import rust-overlay) ];
    	pkgs = import nixpkgs { inherit system overlays; };
    	rustVersion = pkgs.rust-bin.stable.latest.default;
    	rustPlatform = pkgs.makeRustPlatform {
      	cargo = rustVersion;
      	rustc = rustVersion;
    	};
    	myRustBuild = rustPlatform.buildRustPackage {
      	pname =
        	"rust_nix_blog"; # make this what ever your cargo.toml package.name is
      	version = "0.1.0";
      	src = ./.; # the folder with the cargo.toml
      	cargoLock.lockFile = ./Cargo.lock;
    	};
    	dockerImage = pkgs.dockerTools.buildImage {
      	name = "rust-nix-blog";
      	config = { Cmd = [ "${myRustBuild}/bin/rust_nix_blog" ]; };
    	};
  	in {
    	packages = {
      	rustPackage = myRustBuild;
      	docker = dockerImage;
    	};
    	defaultPackage = dockerImage;
    	devShell = pkgs.mkShell {
      	buildInputs =
        	[ (rustVersion.override { extensions = [ "rust-src" ]; }) ];
    	};
  	});

Теперь nix build или nix build .#docker соберёт образ docker. После сборки result станет просто символьной ссылкой на tar-файл с образом; в архиве, как и раньше, будет лежать каталог. Поскольку nix декларативен, он не загружает этот файл в docker за вас. Вы можете загрузить его сами при помощи:

$ docker load < result

В качестве вывода вы должны получить rust-nix-blog:yyc9gd4nkydrikzpsvlp3gmwnpxhh1ik – это загруженный образ с тегом.

Теперь запустим образ с помощью:

$ docker run rust-nix-blog:yyc9gd4nkydrikzpsvlp3gmwnpxhh1ik

Этот процесс можно немного автоматизировать при помощи скрипта. (Слегка модифицированный пример предлагается здесь).

#!/usr/bin/env bash
set -e; set -o pipefail;
nix build '.#docker'
image=$((docker load < result) | sed -n '$s/^Loaded image: //p')
docker image tag "$image" rust-nix-blog:latest

Воспользуемся dive, чтобы просмотреть образ. Для временного использования подойдёт nix shell nixpkgs#dive.

33 MB  └── nix
33 MB  	└── store
374 kB      	├─⊕ h4isr1pyv1fjdrxj2g80vsa4hp2hp76s-rust_nix_blog-0.1.0
1.8 MB      	├─⊕ ik4qlj53grwmg7avzrfrn34bjf6a30ch-libunistring-1.0
246 kB      	├─⊕ w3zngkrag7vnm7v1q8vnqb71q6a1w8gn-libidn2-2.3.2
30 MB      	└─⊕ ybkkrhdwdj227kr20vk8qnzqnmj7a06x-glibc-2.34-115

Просмотрев вывод, видим, что это однослойный образ, в котором содержится только необходимый минимум для запуска двоичного файла (в нашем случае – только libc и код rust).

Распространённые проблемы при устранении неполадок


Сборочные зависимости, не относящиеся к Rust


Когда сборка при помощи nix настроена – это отличная штука, она уже никогда не откажет. Но доведение её до рабочего состояния – процесс порой болезненный. Если ваш код rust полагается на системные пакеты (например, на OpenSSL), не забудьте включить их, например, в buildInputs.

rustPlatform.buildRustPackage {
  pname =
	"rust_nix_blog"; # make this what ever your cargo.toml package.name is
  version = "0.1.0";
  src = ./.; # the folder with the cargo.toml
  nativeBuildInputs = [pkgs.pkg-config ]; # just for the host building the package
  buildInputs = [pkgs.openssl]; # packages needed by the consumer
  cargoLock.lockFile = ./Cargo.lock;
};

Конкретно для OpenSSL я рекомендовал бы по возможности использовать rustls. Сборка с ней протекает легче, и написана она на rust.

Документация Nix


Документация Nix не самая качественная. Да, некоторая информация отыскивается в вики и мануале, мне кажется, что наилучшие материалы есть в разнообразных блогах по nix. Вот несколько хороших ссылок:


Также исключительно рекомендую самостоятельно покопаться в nix, освоив Nixos в качестве рабочего дистрибутива Linux. Так вам будет проще освоиться с языком и быстро перейти к его использованию, когда у вас сложится полная картинка.
@Albert_Wesker
25.04.2023 18:02 UTC
Первоисточник

Комментарии

@kuraga97
28.04.2023 06:05 UTC
0
Создание проекта

Создадим новый проект Rust с помощью команды:

cargo new my_project --bin

  1. Использование Nix Flakes

Для использования Nix Flakes в проекте, необходимо создать файл flake.nix в корневой директории проекта со следующим содержимым:

{
description = "My Rust project";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
};

outputs = { self, nixpkgs }: {
defaultPackage.x86_64-linux = nixpkgs.pkgs.rustPlatform.buildRustPackage {
name = "my_project";
src = ./.;
cargoSha256 = "";
};
};
}

  1. Сборка Docker образа

Для сборки Docker образа с помощью Nix Flakes, необходимо создать файл docker.nix со следующим содержимым:

{
description = "Docker image for my Rust project";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
my_project.url = ".";
my_project.inputs.nixpkgs.follows = "nixpkgs";
};

outputs = { self, nixpkgs, my_project }: {
dockerImage = let
pkgs = import nixpkgs { inherit system; };
my_project_pkg = my_project.defaultPackage.x86_64-linux;
cargoSha256 = pkgs.fetchurl {
url = "https://github.com/rust-lang/cargo/blob/master/Cargo.lock?raw=true";
sha256 = "";
};
in
pkgs.dockerTools.buildLayeredImage {
name = "my_project";
tag = "latest";
contents = [
{ paths = [ my_project_pkg ]; destination = "/opt/my_project"; }
];
config = {
Cmd = [ "/opt/my_project/target/release/my_project" ];
ExposedPorts = { "8080/tcp" = {}; };
};
meta = {
description = "Docker image for my Rust project";
homepage = "https://github.com/my_username/my_project";
license = "MIT";
source = my_project.url;
};
};
};
}

  1. Сборка Docker образа

Для сборки Docker образа с помощью Nix Flakes, необходимо выполнить следующую команду:

nix build .#dockerImage

После успешной сборки, можно запустить Docker контейнер с помощью команды:

docker run -p 8080:8080 my_project

  1. Заключение

Таким образом, мы организовали окружение Rust и сборку Docker с применением Nix Flakes. Это позволяет упростить процесс сборки и управления зависимостями в проекте.