nixos-and-flakes-book/docs/zh/development/intro.md

447 lines
16 KiB
Markdown
Raw Normal View History

2023-06-30 11:00:03 +02:00
# 在 NixOS 上进行开发工作
2023-06-27 04:53:11 +02:00
由于 NixOS 自身可复现的特性,它非常适合用于搭建开发环境。
但是如果你想直接将在其他发行版上的环境搭建经验用在 NixOS 上,可能会遇到许多问题,因为 NixOS 有自己的一套逻辑在,下面我们先对此稍作说明。
在 NixOS 上,全局环境中只建议安装一些通用的工具,比如 `git`、`vim`、`emacs`、`tmux`、`zsh` 等等,而各语言的开发环境,最好是每个项目都有一个独立的环境。
为了简便,你也可以考虑提前为常用语言创建一些通用的开发环境,在需要时切换进去。
总而言之NixOS 上的开发环境不应该装在全局,应该是一个个独立的项目环境,或许还带有一些通用的语言环境,但是它们都是完全隔离的,不会相互影响。
在本章中我们先学习一下 Nix Flakes 开发环境的实现原理,后面的章节再按使用场景介绍一些更具体的内容。
## 通过 `nix shell` 创建开发环境
在 NixOS 上,最简单的创建开发环境的方法是使用 `nix shell`,它会创建一个含有指定 Nix 包的 shell 环境。
示例:
```shell
# hello 不存在
hello
hello: command not found
# 进入到一个含有 hello 与 cowsay 的 shell 环境
# 可以指定多个包,用空格分隔
nix shell nixpkgs#hello nixpkgs#cowsay
# hello 可以用了
hello
Hello, world!
# cowsay 也可以用了
cowsay "Hello, world!"
_______
< hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
```
`nix shell` 非常适合用于临时试用一些软件包或者快速创建一个干净的环境。
2023-06-30 11:00:03 +02:00
## 创建与使用开发环境
2023-06-27 04:53:11 +02:00
`nix shell` 用起来非常简单,但它并不够灵活,对于更复杂的开发环境管理,我们需要使用 `pkgs.mkShell``nix develop`
2023-06-27 04:53:11 +02:00
在 Nix Flakes 中,我们可以通过 `pkgs.mkShell { ... }` 来定义一个项目环境,通过 `nix develop` 来打开一个该开发环境的交互式 Bash Shell.
为了更好的使用上述两个功能,我们先来看看它们的原理。
2023-12-09 16:18:43 +01:00
[`pkgs.mkShell` 的源码](https://github.com/NixOS/nixpkgs/blob/nixos-23.05/pkgs/build-support/mkshell/default.nix)如下:
2023-06-27 04:53:11 +02:00
```nix
{ lib, stdenv, buildEnv }:
# A special kind of derivation that is only meant to be consumed by the
# nix-shell.
{ name ? "nix-shell"
, # a list of packages to add to the shell environment
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" ];
# ......
# when distributed building is enabled, prefer to build locally
preferLocalBuild = true;
} // rest)
```
可以看到 `pkgs.mkShell { ... }` 本质上就是一个特殊的 DerivationNix 包),它的 `name` `buildInputs` 等参数都是可自定义的,而 `shellHook` 则是一个特殊的参数,它会在 `nix develop` 进入该环境时被执行。
如下是一份 `flake.nix` 文件,它定义了一个 nodejs 18 的开发环境:
```nix
{
description = "A Nix-flake-based Node.js development environment";
inputs = {
2023-12-05 03:39:10 +01:00
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
2023-06-27 04:53:11 +02:00
};
outputs = { self , nixpkgs ,... }: let
# system should match the system you are running on
# system = "x86_64-linux";
system = "x86_64-darwin";
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 {
# create an environment with nodejs-18_x, pnpm, and yarn
packages = with pkgs; [
node2nix
nodejs
pnpm
yarn
];
shellHook = ''
echo "node `${pkgs.nodejs}/bin/node --version`"
'';
};
};
}
```
建个空文件夹,将上面的配置保存为 `flake.nix`,然后执行 `nix develop`(或者更精确点,可以用 `nix develop .#default`),首先会打印出当前 nodejs 的版本,之后 `node` `pnpm` `yarn` 等命令就都能正常使用了。
2023-07-20 05:50:26 +02:00
2023-08-19 10:20:18 +02:00
## 在开发环境中使用 zsh/fish 等其他 shell
2023-07-20 05:50:26 +02:00
`pkgs.mkShell` 默认使用 `bash`,但是你也可以通过在 `shellHook` 中添加 `exec <your-shell>` 来使用 `zsh` 或者 `fish` 等其他 shell。
示例如下:
```nix
{
description = "A Nix-flake-based Node.js development environment";
inputs = {
2023-12-05 03:39:10 +01:00
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
2023-07-20 05:50:26 +02:00
};
outputs = { self , nixpkgs ,... }: let
# system should match the system you are running on
# system = "x86_64-linux";
system = "x86_64-darwin";
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 {
# create an environment with nodejs-18_x, pnpm, and yarn
packages = with pkgs; [
node2nix
nodejs
pnpm
yarn
nushell
];
shellHook = ''
echo "node `${pkgs.nodejs}/bin/node --version`"
exec nu
'';
};
};
}
```
使用上面的 `flake.nix` 配置,`nix develop` 将进入一个 nodejs 18 的开发环境,同时使用 `nushell` 作为交互式 shell.
## 通过 `pkgs.runCommand` 创建开发环境
`pkgs.mkShell` 创建的 derivation 不能直接使用,必须通过 `nix develop` 进入到该环境中。
实际上我们也可以通过 `pkgs.stdenv.mkDerivation` 来创建一个包含所需软件包的 shell wrapper, 这样就能直接通过执行运行该 wrapper 来进入到该环境中。
直接使用 `mkDerivation` 略显繁琐Nixpkgs 提供了一些更简单的函数来帮助我们创建这类 wrapper比如 `pkgs.runCommand`.
示例:
```nix
{
description = "A Nix-flake-based Node.js development environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
};
outputs = { self , nixpkgs ,... }: let
# system should match the system you are running on
# system = "x86_64-linux";
system = "x86_64-darwin";
in {
packages."${system}".dev = let
pkgs = import nixpkgs {
inherit system;
overlays = [
(self: super: rec {
nodejs = super.nodejs_20;
pnpm = super.nodePackages.pnpm;
})
];
};
packages = with pkgs; [
nodejs
pnpm
nushell
];
in pkgs.runCommand "dev-shell" {
# Dependencies that should exist in the runtime environment
buildInputs = packages;
# Dependencies that should only exist in the build environment
nativeBuildInputs = [ pkgs.makeWrapper ];
} ''
mkdir -p $out/bin/
ln -s ${pkgs.nushell}/bin/nu $out/bin/dev-shell
wrapProgram $out/bin/dev-shell --prefix PATH : ${pkgs.lib.makeBinPath packages}
'';
};
}
```
然后执行 `nix run .#dev`,就能进入一个 nushell session可以在其中正常使用 `node` `pnpm` 命令,且 node 版本为 20.
相关源代码:
- [pkgs/build-support/trivial-builders/default.nix - runCommand](https://github.com/NixOS/nixpkgs/blob/nixos-23.11/pkgs/build-support/trivial-builders/default.nix#L21-L49)
- [pkgs/build-support/build-support/setup-hooks/make-wrapper.sh](https://github.com/NixOS/nixpkgs/blob/nixos-23.11/pkgs/build-support/setup-hooks/make-wrapper.sh)
2023-06-30 11:00:03 +02:00
## 进入任何 Nix 包的构建环境
2023-06-27 04:53:11 +02:00
现在再来看看 `nix develop`,先读下 `nix develop --help` 输出的帮助文档:
```
Name
nix develop - run a bash shell that provides the build environment of a derivation
Synopsis
nix develop [option...] installable
# ......
```
可以看到 `nix develop` 接受的参数是 `installable`,这说明我们可以通过它进入任何一个 installable 的 Nix 包的开发环境,而不仅仅是 `pkgs.mkShell` 创建的环境。
默认情况下,`nix develop` 命令会尝试 flake outputs 中的如下属性:
- `devShells.<system>.default`
- `packages.<system>.default`
而如果我们通过 `nix develop /path/to/flake#<name>` 来指定了 flake 包地址以及 flake output name那么 `nix develop` 命令会尝试 flake outputs 中的如下属性:
- `devShells.<system>.<name>`
- `packages.<system>.<name>`
- `legacyPackages.<system>.<name>`
现在来尝试一下,首先测试下,确认我当前环境中没有 `c++` `g++` 这这些编译相关的命令:
```shell
ryan in 🌐 aquamarine in ~
c++
c++: command not found
ryan in 🌐 aquamarine in ~
g++
g++: command not found
```
现在通过 `nix develop` 进入到 `hello` 的构建环境,然后再次测试下:
```shell
# 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` `CXXCPP` 环境变量已经被设置好了,而且 `c++` `g++` 等命令也可以正常使用了。
此外我们还可以正常调用 `hello` 这个 Nix 包的各构建阶段命令:
> 提前说明下,一个 Nix 包的所有构建阶段及其默认的执行顺序为:`$prePhases unpackPhase patchPhase $preConfigurePhases configurePhase $preBuildPhases buildPhase checkPhase $preInstallPhases installPhase fixupPhase installCheckPhase $preDistPhases distPhase $postPhases`
```shell
# 解压源码包
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/
# generate 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
# build the package
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'
# run the built program
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 包的构建过程,或者在某个 Nix 包的构建环境中执行一些命令。
## `nix build`
`nix build` 用于构建一个软件包,并在当前目录下创建一个名为 `result` 的符号链接,链接到该构建结果。
一个示例:
```bash
# 构建 `nixpkgs` flake 中的 `ponysay` 这个包
nix build "nixpkgs#ponysay"
# 使用构建出来的 ponysay 命令
./result/bin/ponysay 'hey buddy!'
____________
< hey buddy! >
------------
\
\
\
▄▄ ▄▄ ▄ ▄
▀▄▄▄█▄▄▄▄▄█▄▄▄
▀▄███▄▄██▄██▄▄██
▄██▄███▄▄██▄▄▄█▄██
█▄█▄██▄█████████▄██
▄▄█▄█▄▄▄▄▄████████
▀▀▀▄█▄█▄█▄▄▄▄▄█████ ▄ ▄
▀▄████▄▄▄█▄█▄▄██ ▄▄▄▄▄█▄▄▄
█▄██▄▄▄▄███▄▄▄██ ▄▄▄▄▄▄▄▄▄█▄▄
▀▄▄██████▄▄▄████ █████████████
▀▀▀▀▀█████▄▄ ▄▄▄▄▄▄▄▄▄▄██▄█▄▄▀
██▄███▄▄▄▄█▄▄▀ ███▄█▄▄▄█▀
█▄██▄▄▄▄▄████ ███████▄██
█▄███▄▄█████ ▀███▄█████▄
██████▀▄▄▄█▄█ █▄██▄▄█▄█▄
███████ ███████ ▀████▄████
▀▀█▄▄▄▀ ▀▀█▄▄▄▀ ▀██▄▄██▀█
▀ ▀▀█
2023-06-27 04:53:11 +02:00
```
## 其他命令
2023-06-27 04:53:11 +02:00
其他还有些 `nix flake init` 之类的命令,请自行查阅 [New Nix Commands][New Nix Commands] 学习研究,这里就不详细介绍了。
2023-06-27 04:53:11 +02:00
## References
- [pkgs.mkShell - nixpkgs manual](https://nixos.org/manual/nixpkgs/stable/#sec-pkgs-mkShell)
- [A minimal nix-shell](https://fzakaria.com/2021/08/02/a-minimal-nix-shell.html)
2023-06-30 11:00:03 +02:00
- [One too many shell, Clearing up with nix' shells nix shell and nix-shell - Yannik Sander](https://blog.ysndr.de/posts/guides/2021-12-01-nix-shells/)
- [Shell Scripts - NixOS Wiki](https://nixos.wiki/wiki/Shell_Scripts)
[New Nix Commands]: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix.html