devenv 0.6 实战解析:OCI 容器生成、即时 Shell 激活、Hosts 证书与 allowUnfree/overlays

原创2026-09-27 09:00:2463 阅读
文章标签:开发工具CLI

devenv 0.6 实战解析:OCI 容器生成、即时 Shell 激活、Hosts 证书与 allowUnfree/overlays

本文以 devenv 0.6 官方发布说明为骨架,结合当前仓库源码(容器模块实现、CLI 定义、mkcert 集成、hostctl 集成)与真实示例(examples/fly.io/devenv.nix),系统讲解该版本引入的四大能力:将 Nix 开发环境打包为 OCI 容器、基于缓存快照的即时 Shell 激活、声明式 Hosts 与本地 TLS 证书供给,以及 devenv.yaml 中新增的 allowUnfree 与 overlays 选项。读完你可以掌握 devenv container 的完整用法、direnv 缓存的正确配置方式,以及如何用两行配置把非自由软件和自定义 overlay 接入 pkgs。

版本背景:devenv 0.6 的四个核心改进

devenv 0.6 在约两个月的密集开发后发布,其改进全部来自现有用户的真实反馈,集中在以下四个方面:

  • 生成容器:新增 devenv container <name> 命令族,可以把 Nix 开发环境构建为 OCI 容器镜像并复制进 registry;
  • 即时 Shell 激活:修复 direnv 集成的缓存问题,环境只在配置变化时重建,否则秒级复用缓存快照;
  • Hosts 与证书供给:可以在 devenv.nix 中声明式配置 /etc/hosts 条目和本地 TLS 证书;
  • 新 devenv.yaml 选项:新增 allowUnfree(允许构建非自由软件)与 overlays(为输入接入 overlay)。

以下按原发布说明的脉络逐一展开,并补充当前仓库的源码级细节。

生成 OCI 容器:把开发环境打包成镜像

devenv shell 提供的是本机原生开发环境体验,而 devenv container <name> 则能把同样的环境(包括语言运行时、进程、证书等)打包成 OCI 容器,便于分发现成应用并部署到 fly.io 等平台。

命令族:build / copy / run

在 0.6 中,devenv container shell --docker-run 可以直接生成名为 shell 的容器、复制到本地 Docker daemon 并运行。当前仓库的 CLI 中,容器子命令演进为三个明确职责的命令(见 devenv/src/cli.rs):

  • devenv container build <name>:只构建容器镜像;
  • devenv container copy <name> [--registry <url>] [--copy-args <args>]:把镜像复制到指定 registry;
  • devenv container run <name> [--copy-args <args>]:构建并复制到本地 Docker daemon 后运行。

复制操作底层由 skopeo copy 完成。容器模块在 src/modules/containers.nix 中通过 nix2container 与 mk-shell-bin 两个输入(nix2container.buildImage + skopeo-nix2container)实现镜像构建,默认 registry 为 docker-daemon:,即直接落入本地 Docker daemon。

进入开发环境:生成一个 shell 容器

容器模块默认内置了名为 shell 的容器规格(见 src/modules/containers.nix):其 startupCommand 默认为 bash 本身,因此构建并运行它等价于"进入这个开发环境的交互式 shell"。

以一个 Ruby 项目为例,devenv.nix 只需声明项目名与语言版本:

{
  name = "simple-ruby-app";

  languages.ruby.enable = true;
  languages.ruby.version = "3.2.1";
}

0.6 中运行以下命令即可进入容器内的开发环境:

$ devenv container shell --docker-run
...
(devenv) bash-5.2# ruby --version
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-linux]

当前版本等价地使用 devenv container run shell。容器内的环境与本地 devenv shell 一致:mkEntrypoint 会先 source 环境的 envScript(对应 src/modules/containers.nix 中的 source ${shell.envScript}),再执行 startupCommand,从而保证 PATH、环境变量和语言运行时全部就位。

运行所有进程 / 单个进程 / 自定义二进制

除 shell 外,容器模块还默认内置了 processes 容器(src/modules/containers.nix),其 startupCommand 指向 config.procfileScript,即把 devenv.nix 中声明的全部进程(processes.*)作为容器入口启动。

你可以通过覆盖 startupCommand 来定制"单个进程"或"自定义产物"场景:

{ pkgs, ... }:
{
  name = "my-service";

  # 只启动一个进程
  startupCommand = "exec my-web-server";

  # 或者运行一个自定义构建产物(包路径、字符串或参数列表均可)
  # startupCommand = [ "-f" "/var/lib/haproxy/haproxy.cfg" ];
}

从源码看,startupCommand 支持 str、package 或字符串列表三种形态(src/modules/containers.nix);当传入包路径时会被写入镜像 Cmd。WorkingDir 默认是 /env,项目根目录会被复制到镜像内该目录(copyToRoot 默认值为整个 git 仓库,见 src/modules/containers.nix 与 mkHome 实现)。

把容器复制到镜像仓库

要把容器推送到远程 registry,只需提供 registry 地址。仓库自带的 fly.io 部署示例(examples/fly.io/devenv.nix)展示了完整做法——它定义了一个运行 flask 进程的容器,并配置 registry 与推送到 fly.io 所需的认证参数:

{
  languages.python.enable = true;

  packages = [ pythonPackages.flask ] ++ lib.optionals (!config.container.isBuilding) [ pkgs.flyctl ];

  processes.serve.exec = "exec flask --app hello run";

  containers.processes = {
    name = "simple-python-app";
    registry = "docker://registry.fly.io/";
    defaultCopyArgs = [
      "--dest-creds"
      ''x:"$(${lib.getExe pkgs.flyctl} auth token)"''
    ];
  };
}
  • registry 即 skopeo copy 的目标地址前缀,产物形如 <registry><name>:<version>;
  • defaultCopyArgs 作为 skopeo copy 的默认参数(这里是 --dest-creds),也可以在执行 devenv container copy 时用 --copy-args 覆盖;
  • 复制动作在 devenv 中被实现为一个内部任务 devenv:container:copy(见 src/modules/containers.nix),通过 DEVENV_TASK_INPUT 传入 copy 脚本、规格与参数。

根据构建目标条件化环境

构建容器时,devenv 会设置 container.isBuilding = true(通过环境变量 DEVENV_CONTAINER 判定,见 src/modules/containers.nix 与 L455-L457)。这让同一个 devenv.nix 可以针对"本机原生环境"与"容器目标"给出不同配置:

{ pkgs, config, lib, ... }:
{
  packages = [ pkgs.openssl ]
    ++ lib.optionals (!config.container.isBuilding) [ pkgs.git ];
}

上面的写法会在本机环境里额外安装 git,而构建容器时省略它。同样,容器构建时 devenv 会把 devenv.tmpdir、devenv.runtime、devenv.root 与 devenv.dotfile 全部重定向到镜像内路径(src/modules/containers.nix),保证容器内状态布局与原生环境自洽。

容器模块的完整配置项

围绕容器功能,当前仓库在 src/modules/containers.nix 中提供了以下可调选项(供 containers.<name>.* 使用):

选项 类型 默认值 说明
name str 项目名或 containers.mycontainer.name 容器名,缺省为 <项目名>-<容器名>
version str "latest" 镜像 tag
fromImage package / null null 基于已有 OCI 基础镜像(nix2container 的 pullImage)构建
copyToRoot path 或 path 列表 整个 git 仓库 复制进镜像根目录的路径
startupCommand str / package / list 视容器而定 容器入口命令(shell 容器默认 bash,processes 容器默认 procfile 脚本)
entrypoint list 内置 mkEntrypoint 容器 Entrypoint,会先 source 环境脚本
workingDir str /env 容器工作目录
registry str / null "docker-daemon:" skopeo copy 的目标 registry
defaultCopyArgs list of str [] 复制时的默认 skopeo copy 参数
maxLayers int / null 1 最大层数
enableLayerDeduplication bool true 层去重(基于 nix2container 的复用策略)
layers list 自动填充 细粒度自定义每一层的 deps、copyToRoot、perms 等

即时 Shell 激活:环境只构建一次,其余时间用缓存快照

对于大型 monorepo,开发环境可能达到数 GB 体量,激活耗时数秒。0.6 的目标很明确:环境只在内容发生变化时才构建;未变化时,直接使用缓存快照实现"即时"激活。

通过 direnv 集成,该版本把缓存机制做对了:缓存会正常命中,并且它还会监听每一个 import 的文件变化,一旦 devenv.nix / devenv.yaml 或其任何被引入的模块发生变化,环境才被重新构建。

在使用时需要注意迁移事项(同样适用于团队其他成员):

  • 重新运行 devenv init 或手动更新到最新版 .envrc(模板位于 devenv/init/envrc);
  • 团队所有成员统一升级到 devenv 0.6,避免缓存格式不一致。

仓库中的 monorepo 测试(tests/monorepo)验证了多项目共享输入、分别激活的场景,是理解该特性适用面的参考样例。Hook 相关的实现与测试位于 devenv/src/commands/hook.rs,其中 hook-should-activate 在每个提示符时被调用,以确定当前目录是否需要激活。

声明式 Hosts 与本地 TLS 证书

0.6 允许把 hosts 条目和证书直接写进 devenv.nix,并在运行 devenv up 启动进程时自动完成本地供给:

{ pkgs, config, ... }:

{
  certificates = [
    "example.com"
  ];

  hosts."example.com" = "127.0.0.1";

  services.caddy.enable = true;
  services.caddy.virtualHosts."example.com" = {
    extraConfig = ''
      tls ${config.env.DEVENV_STATE}/mkcert/example.com.pem ${config.env.DEVENV_STATE}/mkcert/example.com-key.pem
      respond "Hello, world!"
    '';
  };
}
  • hosts."example.com" = "127.0.0.1" 会把该条目写入 /etc/hosts。底层由 hostctl 集成模块 实现:它把所有条目按 IP 分组、哈希后通过 hostctl replace 写入(native 进程管理器下作为 devenv:hostctl:setup 任务执行,见 src/modules/integrations/hostctl.nix)。由于需要写 /etc/hosts,模块会优先使用 sudo,必要时提示手动添加条目;
  • certificates 为域名列表(支持 *.example.com 通配),底层由 mkcert 集成模块 实现:首次运行 mkcert -install 生成本地 CA,再为列表中的每个域名签发证书到 $DEVENV_STATE/mkcert/(见 src/modules/integrations/mkcert.nix)。证书是否重新生成由域名列表的 SHA256 哈希决定,配置未变则直接复用;
  • config.env.DEVENV_STATE 是 devenv 的状态目录,mkcert 模块同时设置了 env.CAROOT、env.DEVENV_MKCERT 与 env.NODE_EXTRA_CA_CERTS(src/modules/integrations/mkcert.nix),让 curl、Node.js 等客户端都能信任本地 CA;
  • 在 Caddy 示例中,TLS 证书路径直接引用 $DEVENV_STATE/mkcert/ 下生成的 .pem 与 -key.pem,从而让本地服务跑在真正的 HTTPS 上。

certificates 与 hosts 选项均在对应集成模块中声明(certificates 支持自定义 certFile/keyFile 文件名,见 src/modules/integrations/mkcert.nix;hosts 支持值为字符串或 IP 列表,见 src/modules/integrations/hostctl.nix)。

allowUnfree 与 overlays:两个新 devenv.yaml 选项

allowUnfree:允许非自由软件

devenv.yaml 中设置 allowUnfree: true 后,构建系统会放行许可证受限的软件包:

allowUnfree: true
inputs:
  nixpkgs:
    url: github:NixOS/nixpkgs/nixpkgs-unstable
  rust-overlay:
    url: github:oxalica/rust-overlay
    overlays:
      - default

从后端实现看(devenv-nix-backend/src/backend.rs),allowUnfree 会生成一个 allowUnfreePredicate:当其为真时谓词恒真(放行一切包);否则若配置了 permitted_unfree_packages,则只放行白名单内的包,并对白名单外的非自由包抛出包含"允许方案"提示的错误(建议在 devenv.yaml 中设置 allow_unfree: true 或 nixpkgs.permitted_unfree_packages)。相关配置解析见 devenv-core/src/config.rs 与 devenv-core/src/nix_args.rs。

overlays:把输入接入 pkgs

同一份配置中,inputs.rust-overlay.overlays = <a href="https://link.gitcode.com/i/24d722feea569461844600b63be193d5" target="_blank"> "default" ] 会把 rust-overlay 的 default overlay 自动接入 pkgs。这解决了两个常见痛点:无需手写 imports/overlay 样板,也无需在 devenv.nix 里手动 merge overlay;声明后 overlay 内的包(如最新 Rust 工具链)直接可用。overlays 选项的定义见 [devenv-core/src/config.rs。

同样的写法适用于任意 flake 输入——只要输入导出了 overlay 属性,overlays 列表即可把它应用到 nixpkgs 上。

语言支持更新

0.6 对各语言模块做了大量增强,以下是本次新增或改进的语言(对应模块位于 src/modules/languages):

  • Python:新增 virtualenv 创建与 poetry 支持;
  • Ruby:一等支持设置 version 或 versionFile;
  • Go:获得显著改进;
  • PHP:一等支持设置版本,便于配置扩展;
  • Scala:允许更换包,JDK 过旧时可选用 scala-cli;
  • R:新增指定包的选项;
  • Rust:可为 darwin frameworks 查找 headers;
  • OCaml:允许使用不同版本的 OCaml;
  • Tex Live:新增支持;
  • Swift:新增支持;
  • Raku:新增支持;
  • Gawk:新增支持;
  • Racket:新增支持;
  • Dart:新增支持;
  • Julia:新增支持;
  • Crystal:新增支持;
  • Unison:新增支持;
  • Zig:新增支持;
  • Deno:新增支持。

服务支持更新

进程与服务侧同样有更新(对应模块位于 src/modules/services):

  • Cassandra:新增;
  • CouchDB:新增;
  • MariaDB:修正了用户与数据库的处理逻辑;
  • MinIO:现在可指定要供给的 buckets。

修复与其他改进

  • process-compose:更快的关闭、默认失败重启、正确转义环境变量;
  • 模块中支持 assertions;
  • 修复 overmind root 问题;
  • 使 devenv info 的输出可由 devenv 模块插拔(当前 devenv info 命令定义见 devenv/src/commands/info.rs);
  • 扩充 flake 指南;
  • 缺失时自动设置 LOCALE_ARCHIVE;
  • 大量 option 文档修复;
  • 修复 starship 使用自定义配置时的集成问题;
  • 在严格 bash 模式下测试 direnv 集成;
  • 为 flakes 集成添加 devenv shim。

从当前仓库结构看,0.6 引入的这些能力(容器生成、缓存激活、hosts/证书、allowUnfree/overlays)均已固化为稳定的模块与命令实现,并持续演进:容器模块在 src/modules/containers.nix 中维护,CLI 命令族在 devenv/src/cli.rs 中定义,hosts 与证书分别由 hostctl 与 mkcert 集成模块支撑。你可以直接按本文示例在本地项目 devenv.nix / devenv.yaml 中复现这些配置,再以 devenv container build|copy|run、devenv up 与 direnv 完成实战验证。

登录后查看全文
devenv