Ergonomic overrides for Nixpkgs

Ergonomic overrides for Nixpkgs

Nixpkgs 的人体工程学覆盖(Overrides)

I created a new override-utils package for simplifying Nixpkgs overrides/overlays, which is part of a broader project of mine to improve the usability of Nixpkgs. This post will focus more on the big picture but I’ll also motivate the override-utils package.

我创建了一个新的 override-utils 包,旨在简化 Nixpkgs 的覆盖(overrides/overlays)。这是我旨在提升 Nixpkgs 可用性的更宏大项目的一部分。本文将更多地关注宏观愿景,同时也会阐述开发 override-utils 包的动机。

Background

背景

Several years ago I wrote “The hard part of type-checking Nix”, which was my take on the usability issues plaguing the Nix ecosystem. The relevant excerpt from that post is:

几年前,我写过一篇名为《类型检查 Nix 的难点》(The hard part of type-checking Nix)的文章,表达了我对困扰 Nix 生态系统的可用性问题的看法。该文章的相关摘录如下:

The fundamental problem that plagues all type-checking attempts for Nix is that nobody actually uses Nix the language at any significant scale. Instead, the community has adopted two sub-languages embedded within Nix for programming “in the large”:

困扰所有 Nix 类型检查尝试的根本问题在于,没有人真正大规模地使用 Nix 语言本身。相反,社区为了进行“大规模”编程,在 Nix 内部采用了两种嵌入式子语言:

  • Nixpkgs overlays: This is an embedded language that simulates object-oriented programming with inheritance / late binding / dynamic scope (depending on how you think about it).

  • NixOS modules: This is an embedded language that roughly emulates Terraform.

  • Nixpkgs overlays:这是一种嵌入式语言,通过继承/后期绑定/动态作用域(取决于你如何理解)来模拟面向对象编程。

  • NixOS modules:这是一种大致模拟 Terraform 的嵌入式语言。

Carefully note that these are not language features built into Nix; rather they are embedded domain-specific languages implemented within Nix. Consequently, a type-checker for “Nix the language” is not necessarily equipped to type-check these two sub-languages.

请注意,这些并不是 Nix 内置的语言特性;它们是实现于 Nix 内部的嵌入式领域特定语言(DSL)。因此,针对“Nix 语言”的类型检查器并不一定具备对这两种子语言进行类型检查的能力。

In other words, the problem with Nix isn’t that Nix doesn’t have types; it’s that even if Nix had types they’d be just as impenetrable as current stack traces because Nix is operating at the wrong level of abstraction. We need to design better abstractions before we can build a type system that works well in inexpert hands.

换句话说,Nix 的问题不在于它没有类型;而在于即使 Nix 有了类型,它们也会像当前的堆栈跟踪一样难以理解,因为 Nix 运行在错误的抽象层级上。在构建一个能让非专家也能良好使用的类型系统之前,我们需要设计更好的抽象。

Design

设计

To this end, I sat down and asked myself: “if I could build a programming language purpose-built for working with Nixpkgs, what would that ideal language look like?”. I figured this would be a useful thought experiment, but also I have enough experience with implementing programming languages that I could perhaps build out a proof of concept if I thought it were compelling enough.

为此,我坐下来问自己:“如果我能构建一种专门用于处理 Nixpkgs 的编程语言,那这种理想的语言会是什么样子?”我认为这是一个有用的思想实验,而且我拥有足够的编程语言实现经验,如果我觉得足够有吸引力,或许可以构建出一个概念验证。

While designing this “Nixpkgs language” I realized that I struggle most with override functions and overlays so I started there. To illustrate what I mean, suppose that I needed to add the libvirt package as a native dependency to Haskell’s libvirt-hs package. I’d have to do something like this:

在设计这种“Nixpkgs 语言”时,我意识到我最头疼的是覆盖函数(override functions)和 overlays,所以我从这里入手。为了说明我的意思,假设我需要将 libvirt 包作为 Haskell 的 libvirt-hs 包的本地依赖项添加进去。我必须这样做:

final: prev: {
  haskellPackages = prev.haskellPackages (old: {
    overrides = hfinal: hprev: {
      libvirt-hs = final.haskell.lib.overrideCabal hprev.libvirt-hs (old: {
        libraryPkgconfigDepends = (old.libraryPkgconfigDepends or []) ++ [ final.libvirt ];
      });
    };
  });
}

Gross. Moreover, it’s an even bigger pain if I want to do this for a non-default GHC version.

太恶心了。而且,如果我想对非默认的 GHC 版本执行此操作,那就更痛苦了。

I’ve used Nixpkgs long enough that I’m used to this sort of thing by now, but this is not the sort of user experience that I would confidently recommend to a coworker if they were on the fence about Nix. This poor user experience is (in my view) a big contributor to why Nix gets consistently sidelined as companies grow.

我使用 Nixpkgs 的时间足够长,以至于现在已经习惯了这种方式,但这绝不是那种我会自信地推荐给还在犹豫是否使用 Nix 的同事的用户体验。在我看来,这种糟糕的用户体验是 Nix 在公司规模扩大时不断被边缘化的重要原因。

So I asked myself: how would I have preferred to write that last example? The answer I converged upon was something along these lines:

所以我问自己:我更希望如何编写上一个示例?我最终得出的答案大致如下:

haskell.packages.ghc98.override.overrides = 
  libvirt-hs.overrideCabal.libraryPkgconfigDepends ++= [ libvirt ];

There’s plenty we can workshop there, but I still think something like that would be much less intimidating to a newcomer. Additionally, that syntax is much more autocomplete-friendly!

虽然还有很多细节可以打磨,但我仍然认为这样的语法对新手来说会少很多畏难情绪。此外,这种语法对自动补全更加友好!

Moreover, this simpler syntax also suggests a simpler type system. Instead of thinking in terms of override functions and overlays I believe we should just be thinking in terms of attribute paths and safe operations on those attribute paths. Structuring all overrides in that way would greatly simplify the type system (both the implementation and the user experience).

此外,这种更简单的语法也暗示了一个更简单的类型系统。我认为我们不应该从覆盖函数和 overlays 的角度去思考,而应该从属性路径(attribute paths)以及对这些路径的安全操作角度去思考。以这种方式构建所有覆盖将极大地简化类型系统(无论是实现层面还是用户体验层面)。

Implementation

实现

I have not yet built any such programming language. What I did do, though, is to release an override-utils package that approximates that idealized interface in pure Nix. I created this package as a starting point to prove out the idea before even thinking about embarking on a larger programming language project.

我还没有构建任何这样的编程语言。但我所做的是发布了一个 override-utils 包,它在纯 Nix 中近似实现了那种理想化的接口。我创建这个包是作为一个起点,在考虑开启更大的编程语言项目之前,先验证这个想法。

For example, using override-utils the above example would be written as:

例如,使用 override-utils,上面的示例可以写成:

final: override {
  haskell.packages.ghc98.override.overrides = set (hfinal: override {
    libvirt-hs.overrideCabal.libraryPkgconfigDepends = append [ final.libvirt ];
  });
}

… which is already pretty similar to the original idealized interface I proposed, but there are a few important differences that I want to dig into.

……这已经和我最初提出的理想化接口非常相似了,但还有一些重要的区别,我想深入探讨一下。