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>.default
packages.<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 или соответствующей документации.