Development Environments on NixOS
Возможность NixOS гарантировать одинаковые результаты сборки делает ее идеальной для разработки ПО, однако стоит учитывать ряд отличий от более традиционных дистрибутивов.
На NixOS глобально/системно рекомендуется ставить тулзы "общего назначения", вроде git, vim, emacs, tmux или zsh, в то время как софт, нужный для разработки конктетных проектов на конкретных ЯП должен жить в соответствующих development environment-ах, изолированных от основной системы и друг от друга.
В следующих частях статьи будут описана работа с development environment-ами под NixOS.
Создаем Development Environment
Development environment создается с помощью pkgs.mkShell { ... }, после чего можно открыть изолированный для проекта bash через nix develop.
Посмотрим на сырцы pkgs.mkShell, чтобы разобраться в его работе:
{ lib, stdenv, buildEnv }:
# особый derivation, работающий только с nix-shell.
{ name ? "nix-shell"
, # список пакетов, нужных для конкретного проекта
packages ? [ ]
, # propagate all the inputs from the given derivations
inputsFrom ? [ ]
, buildInputs ? [ ]
, nativeBuildInputs ? [ ]
, propagatedBuildInputs ? [ ]
, propagatedNativeBuildInputs ? [ ]
, ...
}@attrs:
let
mergeInputs = name:
(attrs.${name} or [ ]) ++
(lib.subtractLists inputsFrom (lib.flatten (lib.catAttrs name inputsFrom)));
rest = builtins.removeAttrs attrs [
"name"
"packages"
"inputsFrom"
"buildInputs"
"nativeBuildInputs"
"propagatedBuildInputs"
"propagatedNativeBuildInputs"
"shellHook"
];
in
stdenv.mkDerivation ({
inherit name;
buildInputs = mergeInputs "buildInputs";
nativeBuildInputs = packages ++ (mergeInputs "nativeBuildInputs");
propagatedBuildInputs = mergeInputs "propagatedBuildInputs";
propagatedNativeBuildInputs = mergeInputs "propagatedNativeBuildInputs";
shellHook = lib.concatStringsSep "\n" (lib.catAttrs "shellHook"
(lib.reverseList inputsFrom ++ [ attrs ]));
phases = [ "buildPhase" ];
# ......
# если включена распределенная сборка (на нескольких машинах), предпочитаем билдить локально
preferLocalBuild = true;
} // rest)pkgs.mkShell { ... } - особый тип derivation. name, buildInputs и т.д. - изменяемые пользователем параметры, а в shellHook пишется то, что будет запущено при входе в окружение через nix develop.
Небольшой flake.nix, в котором описан development environment с Node.js 18:
{
description = "Development environment под Node.js на флейках";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05";
};
outputs = { self , nixpkgs ,... }: let
# system должен соответствовать архитектуре исопльзуемой машины
# system = "x86_64-darwin";
system = "x86_64-linux";
in {
devShells."${system}".default = let
pkgs = import nixpkgs {
inherit system;
overlays = [
(self: super: rec {
nodejs = super.nodejs-18_x;
pnpm = super.nodePackages.pnpm;
yarn = (super.yarn.override { inherit nodejs; });
})
];
};
in pkgs.mkShell {
# создаем environment c nodejs-18_x, pnpm и yarn
packages = with pkgs; [
node2nix
nodejs
pnpm
yarn
];
shellHook = ''
echo "node `${pkgs.nodejs}/bin/node --version`"
'';
};
};
}Кладем flake.nix в новую директорию, запускаем nix develop (так же сработает nix develop .#default, см 13 строку примера), оказываемся в development environment с nodejs 18 и пакетниками npm, pnpm, yarn. Также благодаря команде в shellHook выводится инфа о версии nodejs.
Используем zsh/fish/... вместо bash
По дефолту pkgs.mkShell открывает bash, однако это недоразумение можно исправить, закинув exec <твой-шелл> в shellHook:
{
description = "A Nix-flake-based Node.js development environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05";
};
outputs = { self , nixpkgs ,... }: let
# system должен соответствовать архитектуре исопльзуемой машины
# system = "x86_64-darwin";
system = "x86_64-linux";
in {
devShells."${system}".default = let
pkgs = import nixpkgs {
inherit system;
overlays = [
(self: super: rec {
nodejs = super.nodejs-18_x;
pnpm = super.nodePackages.pnpm;
yarn = (super.yarn.override { inherit nodejs; });
})
];
};
in pkgs.mkShell {
# создаем environment c nodejs-18_x, pnpm и yarn
packages = with pkgs; [
node2nix
nodejs
pnpm
yarn
nushell # также хотим нюшелл
];
shellHook = ''
echo "node `${pkgs.nodejs}/bin/node --version`"
exec nu # запускаем нюшелл
'';
};
};
}Вуаля, при запуске nix develop попадаем в REPL nushell.
Заходим в сборочный environment любого Nix-пакета
А теперь можно взглянуть на описание nix develop, nix develop --help:
Name
nix develop - запускает bash с окрежением сборки derivation-а
Synopsis
nix develop [option...] installable
# ......installable тут означает, что nix develop может зайти не только в результат pkgs.mkShell, но в окружение для сборки любого пакета, который можно установить.
По дефолту nix develop ищет что-то из следующих атрибутов в outputs флейка (лежащего в текущей директории):
примечание:
system= архитектура текущей системы, напримерx86_64-linux
devShells.<system>.defaultpackages.<system>.default
Если указать путь к флейку и имя аутпута через nix develop /path/to/flake#<имя>, список станет таким:
devShells.<system>.<имя>packages.<system>.<имя>legacyPackages.<system>.<имя>
Проверяем. Сейчас у нас нет доступа к c++/g++:
ryan in 🌐 aquamarine in ~
› c++
c++: command not found
ryan in 🌐 aquamarine in ~
› g++
g++: command not foundТеперь с помощью nix develop сходим в сборочный цех проги hello из nixpkgs:
# login to the build environment of the package `hello`
ryan in 🌐 aquamarine in ~
› nix develop nixpkgs#hello
ryan in 🌐 aquamarine in ~ via ❄️ impure (hello-2.12.1-env)
› env | grep CXX
CXX=g++
ryan in 🌐 aquamarine in ~ via ❄️ impure (hello-2.12.1-env)
› c++ --version
g++ (GCC) 12.3.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
ryan in 🌐 aquamarine in ~ via ❄️ impure (hello-2.12.1-env)
› g++ --version
g++ (GCC) 12.3.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.Видим установленную переменную окружения CXX и наличие c++ и g++.
Плюсом можно пройтись по разным стадиям сборки hello:
Пакеты в Nix проходят следующие старии сборки (в этом порядке):
$prePhases unpackPhase patchPhase $preConfigurePhases configurePhase $preBuildPhases buildPhase checkPhase $preInstallPhases installPhase fixupPhase installCheckPhase $preDistPhases distPhase $postPhases
# распаковываем исходники проги
ryan in 🌐 aquamarine in /tmp/xxx via ❄️ impure (hello-2.12.1-env)
› unpackPhase
unpacking source archive /nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz
source root is hello-2.12.1
setting SOURCE_DATE_EPOCH to timestamp 1653865426 of file hello-2.12.1/ChangeLog
ryan in 🌐 aquamarine in /tmp/xxx via ❄️ impure (hello-2.12.1-env)
› ls
hello-2.12.1
ryan in 🌐 aquamarine in /tmp/xxx via ❄️ impure (hello-2.12.1-env)
› cd hello-2.12.1/
# генерируем Makefile
ryan in 🌐 aquamarine in /tmp/xxx/hello-2.12.1 via ❄️ impure (hello-2.12.1-env)
› configurePhase
configure flags: --prefix=/tmp/xxx/outputs/out --prefix=/tmp/xxx/outputs/out
checking for a BSD-compatible install... /nix/store/02dr9ymdqpkb75vf0v1z2l91z2q3izy9-coreutils-9.3/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /nix/store/02dr9ymdqpkb75vf0v1z2l91z2q3izy9-coreutils-9.3/bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for gcc... gcc
# ......
checking that generated files are newer than configure... done
configure: creating ./config.status
config.status: creating Makefile
config.status: creating po/Makefile.in
config.status: creating config.h
config.status: config.h is unchanged
config.status: executing depfiles commands
config.status: executing po-directories commands
config.status: creating po/POTFILES
config.status: creating po/Makefile
# собираем
ryan in 🌐 aquamarine in /tmp/xxx/hello-2.12.1 via C v12.3.0-gcc via ❄️ impure (hello-2.12.1-env) took 2s
› buildPhase
build flags: SHELL=/run/current-system/sw/bin/bash
make all-recursive
make[1]: Entering directory '/tmp/xxx/hello-2.12.1'
# ......
ranlib lib/libhello.a
gcc -g -O2 -o hello src/hello.o ./lib/libhello.a
make[2]: Leaving directory '/tmp/xxx/hello-2.12.1'
make[1]: Leaving directory '/tmp/xxx/hello-2.12.1'
# запускаем собранную программу
ryan in 🌐 aquamarine in /tmp/xxx/hello-2.12.1 via C v12.3.0-gcc via ❄️ impure (hello-2.12.1-env)
› ./hello
Hello, world!Таким образом можно, например, дебажить сборку пакетов или вносить какие-нибудь изменения в процесс.
nix build
nix build собирает пакет и делает симлинк result из /nix/store/куда-там-собрался-пакет в текущую директорию:
# собираем 'ponysay' из 'nixpkgs'
nix build "nixpkgs#ponysay"
# пользуем собранный 'ponysay'
› ./result/bin/ponysay 'hey buddy!'
____________
< hey buddy! >
------------
\
\
\
▄▄ ▄▄ ▄ ▄
▀▄▄▄█▄▄▄▄▄█▄▄▄
▀▄███▄▄██▄██▄▄██
▄██▄███▄▄██▄▄▄█▄██
█▄█▄██▄█████████▄██
▄▄█▄█▄▄▄▄▄████████
▀▀▀▄█▄█▄█▄▄▄▄▄█████ ▄ ▄
▀▄████▄▄▄█▄█▄▄██ ▄▄▄▄▄█▄▄▄
█▄██▄▄▄▄███▄▄▄██ ▄▄▄▄▄▄▄▄▄█▄▄
▀▄▄██████▄▄▄████ █████████████
▀▀▀▀▀█████▄▄ ▄▄▄▄▄▄▄▄▄▄██▄█▄▄▀
██▄███▄▄▄▄█▄▄▀ ███▄█▄▄▄█▀
█▄██▄▄▄▄▄████ ███████▄██
█▄███▄▄█████ ▀███▄█████▄
██████▀▄▄▄█▄█ █▄██▄▄█▄█▄
███████ ███████ ▀████▄████
▀▀█▄▄▄▀ ▀▀█▄▄▄▀ ▀██▄▄██▀█
▀ ▀▀█Другие команды
О других командах nix, вроде nix flake init, можно подробнее узнать из New Nix Commands или соответствующей документации.