[0x01]:nix之路——一个软件包的艺术之旅

上一篇文章简单介绍了一下nix核心在做些什么,似乎有些空中楼阁。这次我希望能够通过一个具体的包,让你感受一下,为什么nix这个家伙即使在如此离经叛道的情况下,经过二十多年的发展,狂揽一大批忠实用户和开发者。 当然,你需要先安装nix,并开启nix-command以及flakes两个特性,因为篇幅问题,这部分我就不太赘述了,直接参考determinate systems的文档好了。 本文将带你以最快的速度理解nix的flake机制,准备好了吗? hello nix 在我们之前有关“究极构建系统”的设想中,将构建过程抽象成了一个函数,参数是构建依赖,返回值是构建结果。而nix实质上,就是一个函数式的构建系统,函数nix的一等公民。接收参数,返回结果: 有关上面的代码,你需要理解以下关键内容,更多有关nix语法的介绍可以参考nixos-cn的文档 声明了一个函数,以:为分割,前面为参数,后面为返回值 函数接收一个属性集作为参数,其中只有nixpkgs_path一个属性,当不传递任何参数时,改属性的默认值为fetchGit函数的返回结果 let … in是一个语法,作用是在一个在有限作用域内声明绑定关系,便于在后面反复使用 import关键字的作用是从一个路径导入函数,如果路径是一个目录,则导入default.nix nixpkgs是一个使用nix语言编写的代码仓库,其中包含了超过12万个包的构建代码 那这个函数做了什么呢?其实什么也没做什么复杂的事情: 使用git下载一个目录 将目录导入为函数,调用得到pkgs 返回pkgs中的hello 这个函数可以说是最简单的构建函数,什么也不做,只把构建参数直接返回。调用函数也很简单: 看我们的函数返回了一个什么,.drv?不要怕,如果你读过上一篇【nix之路——究极构建系统】,这个东西就是之前提及的.bld。 使用nix derivation show可以将这个文件转换为json查看: nixpkgs仓库比较大,下载需要多等待一会儿 当然,直接指定绝对路径也可以: 所以说,pkgs.hello实际上对应一个.drv文件,这个文件包含了能够构建这个包的所有信息。那我们如何最终构建他呢?也很简单: 构建完成后,默认会在当前目录建立一个指向产物目录的软连接,让我们可以快速使用 当然,以上只是为了让你更好的理解nix代码和产物的关系,实际上用下面的命令可以构建出一样的结果: hello flake.nix 也许你发现了,上面我们每次调用default.nix的时候,都要重新下载nixpkgs。而且,函数参数的默认值是一个下载函数,感觉哪里怪怪的。 其实nix提供了一种更好的的,导入外部nix代码依赖的机制,叫做flake,不同于default.nix,当指定一个目录时,flake.nix是默认的入口文件。他的格式也很简单: 这个文件核心在于inputs以及outputs两部分 inputs inputs声明了需要的外部nix代码仓库地址,地址可以是git仓库、本地路径、压缩包、压缩包下载链接等等 outputs outputs很显然是一个函数,就像上面我们写的default.nix一样,他接收一个属性集作为参数,属性来自inputs中的声明,最后返回一个属性集。 也许你注意到了,不同于我们自己写的default.nix,这里的参数多出了一个self属性,我们可以简单模拟一下flake.nix的执行过程: 等等等等,发生了什么?这里的fix函数其实是nix语言中的一个常用trick,称为fix point,既然你早晚要面对他,我决定在这里直接告诉你它的存在。 不过,我并不打算在这里过分展开fix point的原理,有关fix point的原理,我看过比较好的介绍是akavel的这篇文章,你可以选择去深入了解一下。 如果你暂时还不想知道fix干了什么,只需要知道它的效果就可以了,在这里我们尝试使用我们的call_flake.nix调用一下flake.nix,看看发生了什么: 可以看到,如果我们在ouptuts的返回值中导出self,这个self就不断指向返回结果自己。这样可以让你很方便的在outputs的实现中,引用自己对外导出的部分。 好了,小插曲到此为止,还是让我们看一下如何正常操作flake.nix吧: 首先,使用nix flake show可以查看flake中outputs函数的返回结果,但默认支持的类型有限,其他不支持展示的属性只能展示为unknown: 如果你想知道flake的详细返回内容是什么怎么办?你可以调用nix repl,使用:lf来调用flake.nix,获取返回值: nix_repl_flake 一个软件包的艺术之旅 我们现在知道flake机制的运作原理,以及如何将gnu hello作为packages.${system}.hello作为flake的返回值返回,那我们都可以对这个包做些什么操作呢? 首先,我们可以使用nix build快速构建一个flake仓库导出的包,构建后默认会在当前目录建立一个指向构建产物的软链接: 你也可以选择使用nix run直接运行它,运行前他也会检查是否构建过,如果没构建会先构建再执行: 也可以使用nix derivation show查看这个包对应的.drv文件: 你也可以将某个包,以及他的所有运行时依赖拷贝到别处,只需要一个内核就可以运行,可以使用chroot验证一下: 当然,nixpkgs本身也是一个flake仓库,nixpkgs仓库中任何包都可以用上面的方式操作: ...

December 4, 2025 · 1 min

[0x01]:nix之路——究极构建系统

构建的本质 让我们先忘掉nix,假设我们现在要设计一个究极的构建系统,能构建计算机领域的任何东西。为了实现这个伟大的目标,我们首先要思考一下,什么是“构建” 拿最简单的hello world举例。我们写完了hello world的源码,这个源码是如何变为产物的? $ gcc hello.c -o hello 似乎很简单,只要有一个源码,有一个构建工具,就可以输出最终产物: ELF是构建产物,那压缩包呢?似乎也是一个道理,只不过构建依赖中有压缩工具而已 压缩包是构建产物,那容器镜像呢?好像也一样,无非也就是多引入几个工具,里面包含一个rootfs,生成一个特殊格式的压缩包而已 容器镜像是构建产物,操作系统呢?好像也差不多,只不过是依赖多了一些。 可以说掌握了构建的本质,就掌握了能够构建所有东西的理论基础 hello world! 你可能说,问题才没那么简单!让我们拿最简单的hello world程序举例 #include <stdio.h> int main() { printf("hello world\n"); return 0; } stdio.h哪里来呢,这个应该算作什么呢?然后我们看一下hello的动态链接情况 $ ldd hello linux-vdso.so.1 (0x00007ffcda3d9000) libc.so.6 => /lib64/libc.so.6 (0x00007b5716402000) /lib64/ld-linux-x86-64.so.2 (0x00007b5716606000) libc.so.6以及ld-linux-x86-64.so.2算作什么呢?似乎不只运行阶段,我们构建的时候也需要这些文件 好好好,让我们弥补一下: 在上面这张图中,我将gcc也算作了构建依赖,使用bash脚本作为构建器,其中可能使用mkdir之类的命令,所以说可能也需要coreutils作为构建依赖,实际问题可能会更复杂,但总体来说都能总结为(源码+依赖 -> 产物) 究极的构建系统 很好,很好,很好。现在我们既理解了构建的本质,也有了hello world的实际例子,理论与实践相结合,现在离目标的“究极构建系统”就差一个程序员了。 Meta Builder (v0.0.1) 实现似乎也很简单,把构建过程封装成一个函数,成果物就是函数的返回值: // ... seed = downloadUrl("https://example.com/bootstrap-seed.tar.gz") (gcc,make,glibc,autoconf,m4,bash,coreutils) := mkPackage(seed) // 源码来源无所谓,只要能返回值类型一致就可以 hello_src = downloadUrl("mirror://gnu/hello/hello-2.12.2.tar.gz") hello := mkPackage(hello_src,gcc,bash,coreutils,make,autoconf) // 当然,动态链接库依赖也没问题 libanything_src = downloadUrl("...") libanything = mkPackage(libanything_src,...) anything_src = downloadUrl("...") anything = mkPackage(anything_src,libanything,...) // 究极构建系统,当然要可以自举了 bootstrap_seed = mkPackage(...) 以上代码描绘了这样一套系统: ...

November 22, 2025 · 2 min

[0x00]:nix之路——为什么我要学习nix

为什么我要学习nix 还记得大学开学拿到自己第一台笔记本电脑时候,当时兴高采烈的打算装上visual studio开始探索计算机世界,但反反复复失败之后,我最后决定把windows干掉安装linux。 接下来,一台笔记本,一个Linux系统的配置就伴随了我整个大学时光。 当然,在使用期间免不了喜欢配置各种桌面系统、探索各种命令行软件、重装各种发行版。常见的比如ubuntu、fedora、archlinux、opensuse、Elementary OS,还有少见一点的例如Qubes OS、clear os、fedora silverblue,可以说各式各样的发行版都玩过一遍。 整个过程下来,我逐渐感受到,不同发行版之间的区别,无非也就是包管理的区别和其中维护的包集合数量/质量之间的区别。 然而在当时尝试的众多发行版中,有这样一个发行版,每次都让我望而却步,却又充满了好奇,那就是nixos。nixos很不一样,他用的nix包管理与其说是一个包管理,不如说是一个终极的构建工具,而nixos恰好是nix打出来的一种产物而已。 虽然现在nix的github主页的介绍中写着"Nix, the purely functional package manager",不过我觉得这个介绍可能过于狭隘,最开始Eelco Dolstra的论文题目是"The Purely Functional Software Deployment Model",两个表述之间,孰强孰弱立马见分晓。 不过就像我前面所说,当时我只是知道有这么一个神奇的发行版,使用非FHS的目录结构,其他什么“可重现”,“原子升级”,“声明式”之类词汇虽然挺起来高大上,但是这和我又有什么关系呢?毕竟这是一个正常ELF文件都没办法在上面跑的操作系统。 若干年后,在我经历了各种交叉编译、静态链接、各种操作系统适配问题、容器镜像构建问题之后,我一步一步的走向nix。并且笃定的认为nix正在成为计算机领域的下一代构建技术,并且将会杀死发行版这个概念。 对于我来说,学习nix的过程从来不是容易的。在2018年左右,我第一次尝试安装nixos,当时觉得自己连archlinux安装都没什么问题,无非也就是照着官网的文档一步一步来,但很快我就发现,在nixos中似乎常规的linux技巧都失效了,一切都被一个巨大的配置文档所支配,我完全无法理解背后到底发生了什么。 后来,尝试去读nix pills文档,读Eelco Dolstra的论文,尝试理解nix的基本概念,但也一知半解。再后来nix推出了flake,虽然我一向喜欢一些高概念的事物,但是nix相关的概念之复杂,最开始真的让我无从下手。然而这一切直到我开始阅读nixpkgs代码,以及一点一点开始编写nix代码之后才缓解,终于能将各种基本概念映射到实际了。 而这个系列(如果顺利的话),将介绍nix的一些基本概念,后续可能还会加上一些基本的使用场景和使用技巧,帮助还没有接触nix或者已经接触nix但对其原理还不太清楚的人更快的进入nix的领域。我希望通过这个系列文章能够让你认识到nix的本质,或者说让你更接近nix的本质,而不是被表面上眼花缭乱的封装搞得晕头转向,就像我最开始了解nix时的那样。

November 22, 2025 · 1 min

Nix and Content Address

Nix目前的构建和发布模型 derivation outputs and output paths Nix软件构建以Derivation为核心,Derivation有以下核心属性: Derivation表示了软件的构建过程; Derivation可以依赖其他Derivation; Derivation执行后具有副作用,会在/nix/store下生成路径为xxx-name.drv的文件,其中xxx为该Derivation的唯一标识符,在任何电脑中都一致; xxx-name.drv可以被进一步构建成产物,并且产物的路径存储在xxx-name.drv中,在构建前就可以知道,此处我们称为outPath; outPath路径中也存在唯一标识,同一个Derrvation产生的.drv文件中的outPath一定相同; 由于构建前就知道产物路径,引用某个derivation时,只要检查其中存储的outPath存不存在,就可以判定需不需要重复编译; 不同机器可以通过共享outPath来共享构建缓存; 这里存在两个目录,一个是.drv的目录,一个是outPath的目录,两者的路径中都有唯一标识符,且路径都根据Derivation的参数计算得来,outPath存储在.drv中。 构建过程 nix包管理的软件构建过程用伪码表示大致如下: derivation = getDerivationFrom(nixpkgs, package_name) drv = derivatoin() # 此处存在副作用,derivation函数调用后会生成.drv文件 if checkOutPathExist(drv.outPath): pass # 如果产物目录已经存在,就不用重复构建了 else: drv.build() # drv.build()会将软件构建后放到drv.outPath中 binary cache引入后: derivation = getDerivationFrom(nixpkgs, package_name) drv = derivatoin() if checkOutPathExist(drv.outPath): pass else: if fetchOutPathFromBinaryCache(drv.outPath): pass # 尝试从BinaryCache服务器中下载对应的包,放到outPath中 else: drv.build() Input-Address Model 总体来说,上面我们描述的目前Nix的做法是将软件构建软件的过程(包括输入)抽象为一个哈希值,这个唯一哈希值可以代表软件的某个特定状态,然后用这个哈希值来索引构建后的软件。 在构建过程当中,任何输入的改变必然会导致输出的路径发生变化: 过度重复构建 你可能已经意识到了一些不对,在正常的软件构建过程当中,输入的改变不一定会导致输出不同,比如说代码路径下面增加了一些文档,或者说构建流程发生了改变。 然而在Nix目前的构建模型中,输入的任何变化都会导致输出路径的变化,但现实当中这样往往会导致一些不好的副作用。 例如go语言的源码中使用perl来做代码单元测试,原则上来说,使用什么版本的perl做单元测试,或者是否做单元测试,都不会改变构建产物。我们可以认为,源码中不是所有改动都会影响产物。但在nix中,如果perl包发生了改变,而go又依赖perl做单元测试,那么go的产物路径就会发生变化,进而需要重新编译。再进一步,所有依赖go的包也需要重新编译,导致了很多没有必要的重复编译。 Content-Address Model 解决上述问题的方式思路也很简单,就是使用content address的方式来确定构建缓存的实际目录。 所谓content address,与input address相对。在input address的模型中,用于索引value的key与value内容无关; 但在content address的模型中,用于索引value的key是根据value的内容计算得来。content address最简单的例子就是使用文件的哈希值来索引文件,给定文件的哈希值,返回文件内容; 而input address典型例子就是给定文件的文件名,返回文件的内容; ...

December 8, 2023 · 1 min

Way to Nix 1

最近学习了一些Nix相关的东西,把家里大多数机器都切到了nixos上,感觉很不错。但是由于nix文档比较差,写自己配置的时候,很多时候需要看代码,或者抄别人的配置一点点凑,过程当中遇到了不少问题。也学习了不少Nix的基本概念,于是记录一下相关的概念和技术以免忘记。 前阵子看过NickCao的前些年的一个视频,非常有条例的介绍了一些nix的基本概念,有兴趣可以去看一下:【金枪鱼之夜:Nix - 从构建系统到配置管理-哔哩哔哩】 Nix as a build systems 构建系统一般就是指从源码生成产物的一套工具生态集合,就拿Makefile举例,几乎所有的构建过程都可以描述成如下形式的嵌套结构: 构建目标: 构建原料 构建过程 围绕着gnu make,有一套完整的,有着悠久历史的构建系统,如GNU Autoconf,Cmake等,许多现代语言也包含了自己的构建系统,如rust的cargo,go的build,js的npm等等。也会有许多项目选择多种构建系统嵌套,如Cmake生成makefile调用Cargo来进行项目构建。 Derivation 也许nix里最重要的概念就是Derivation了,它与nix中的很多概念相互关联,并作为纽带。可以说理解了Derivation,至少就理解了Nix的半壁江山。 首先, derivition是nix中的一个内置函数,代码实现位于(https://github.com/NixOS/nix/blob/188c803ddb5e63b243ddb84eba9b70e45475b7ea/src/libexpr/primops/derivation.nix#L2),实际调用的是内部C++函数prim_derivationStrict: nix-repl> derivation «lambda @ /builtin/derivation.nix:5:1» 与其他构建系统类似,derivation包含了构建原料,构建目标以及构建过程,调用这个函数必须有name、system以及builder三个必选参数,其他可选参数可参考Nix Reference Manual: nix-repl> derivation {name = "target_name";system="x86_64-linux"; builder="builder_binary";} «derivation /nix/store/6z9jj5khn7j3xi2fv8fibpzj6gnq4iz4-target_name.drv» 值得注意的是,derivation函数存在一个副作用,即在/nix/store/目录下生成一个以.drv为后缀的文件,并且文件名中包含了一个类似哈希的字符串,保证路径的唯一性。derivation有如下属性: 同样参数的derivation函数调用后生成的文件,路径一定是一样的(在哈希算法的保证下,可以假定不同参数调用derivation产生的drv文件路径一定是不同的) derivation中包含的所有参数必须为字面量,或预先可以确定哈希值的固定内容(Fixed-Output Derivations) derivation可以互相依赖,一个derivation可以依赖另外一个derivation derivation可以被执行,类似Makefile一样可以被构建 derivation会被在隔离的环境执行,其中没有类似/bin/bash之类可以预先假定存在的路径,能且只能通过derivation依赖的方式在构建过程当中引入其他软件 derivation执行后会产生两个关键参数,out.out以及out.outPath,一个存储了drv的路径,一个存储了构建产物的路径 nix-repl> d = derivation {name = "target_name";system="x86_64-linux"; builder="builder_binary";} nix-repl> d.out.out «derivation /nix/store/6z9jj5khn7j3xi2fv8fibpzj6gnq4iz4-target_name.drv» nix-repl> d.out.outPath "/nix/store/pd5l9rzb613v5lv4c6q2m0c81zd9w3l6-target_name" nix-repl> d.out.out == d true nix-repl> d.out == d true 以上属性组合的结果,使得nix构建系统存在如下特性: 任何软件的derivation在/nix/store中的路径包含了唯一哈希值,这个路径实际上代表了产物是用何种输入源,以何种构建方式,配置何种构建参数所构建出来的结果,且由于derivation之间互相依赖,整颗依赖树的任何一个环节发生了变化,重新构建后,derivation路径相应也会发生变化 derivation既可以描述软件的构建过程,又可以描述多种软件组合称为操作系统的过程,进一步说,如果把操作系统当成目标产物,通过derivation的嵌套,nix可以描述出整个操作系统的构建过程,且在保证可完全可复现。 Nix as programing language 与其他构建系统采用的语法风格不同的是,Nix采用了函数式语法,且没有类型系统,这也是Nix看起来比较吓人的主要原因 ...

December 1, 2023 · 2 min

叛逃

所谓叛逃 仔细想一想,自从上大学开始,我用linux做为日常工作和学习的系统也有将近六年了。 这六年来,常见的Linux发行版基本都装了个遍,常见的桌面环境和窗口管理器也基本用了个遍。 不过随着时间流逝,我逐渐发现,桌面环境折腾无数遍,其实实际在用的图形界面软件只有三个:emacs、chrome、alacritty。 更抽象一点的话,其实就是一个编辑器,一个浏览器还有一个终端模拟器。其他软件使用频率都非常低,最近两年内,我逐渐发现自己打开文件管理器的频率也越来越低,以至于文件管理器对于我日常的电脑使用来说,也成为了一个没有必要的软件。 铺垫了这么多,其实我就是想说,爷叛逃了,到MacOS。 目的地 其实也没啥道理,最近想买一个笔记本,看了一圈。intel 11代摆大烂,12代功耗爆炸;AMD 6000系的笔记本,续航稍微好一些,但GPU硬件设计有问题。仔细想了一想,苹果的笔记本成了这两年唯一没重大问题,没开过什么倒车的设备了。 所以,Macbook Air m2 16+512,请: 基本配置 终端应用 brew install p7zip starship rustup-init tmux go coreutils grep startship bottom zoxide fzf ripgrep shellcheck starship hugo fd 7z pandoc neofetch 大概先装这么多吧,zsh配置后续再迁移过来 GUI 写这篇文章的时候突然恍然大悟,原来我能在这台电脑上装qq和微信,突然间一股惆怅的情绪涌上心头 其实也没啥好装的,浏览器暂时看看safari表现怎么样了,之后如果不成的话再装chrome。 文本编辑器的话,还是当之无愧的emacs。虽然在macos上的这个版本没有native compile,但是感觉响应速度没有什么明显区别: brew install --cask emacs

August 7, 2022 · 1 min

Template Post

segment segment segment segment Text style This text is in italics. And so is this text. This text is in bold. And so is this text. This text is in both. As is this! And this! Paragraph This is a paragraph. I’m typing in a paragraph isn’t this fun? Now I’m in paragraph 2. I’m still in paragraph 2 too! I’m in paragraph three! This is a block quote. You can either manually wrap your lines and put a > before every line or you can let your lines get really long and wrap on their own. It doesn’t make a difference so long as they start with a >. ...

August 4, 2022 · 1 min