ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

devenv 服务模块开发指南:从零创建可复用、可测试的 Nix 服务模块

devenv 服务模块开发指南:从零创建可复用、可测试的 Nix 服务模块 开发工具CLI【免费下载链接】devenvFast, Declarative, Reproducible, and Composable Developer Environments using Nix项目地址https://gitcode.com/gh_mirrors/de/devenv点击查看免费下载本文基于 devenv 仓库中.agents/skills/create-service/SKILL.md的规范完整讲解如何在src/modules/services/下开发一个符合 devenv 模块体系约定的服务模块。你将学会服务模块的标准骨架、动态端口分配、Unix Socket 优先策略、就绪探针Readiness Probe、Socket 激活、systemd notify/watchdog 集成、任务编排与配置文件生成等全部实战要点并配合仓库内redis.nix、minio.nix、memcached.nix、postgres.nix、rustfs.nix等真实模块源码逐行印证实现原理。服务模块是什么devenv 通过 Nix 模块系统把跑一个中间件/数据库/对象存储封装成声明式配置用户只需在devenv.nix里写一行services.redis.enable true;devenv 就会自动完成包安装、端口分配、环境变量导出、进程托管、就绪探测与数据目录初始化。服务模块的开发地点是src/modules/services/name.nix。该目录下的每个.nix文件都会被自动发现并加载——在 src/modules/top-level.nix 中listEntries通过builtins.readDir遍历目录并生成导入列表listEntries path: map (name: path /${name}) (builtins.attrNames (builtins.readDir path));随后在模块列表里追加 (listEntries ./services)也就是说只要把name.nix放进src/modules/services/它就会被自动引入模块系统无需任何注册清单。仓库中当前已有 40 余个服务模块覆盖 redis.nix、postgres.nix、minio.nix、kafka.nix、mongodb.nix、mysql.nix 等主流中间件是编写新模块时的最佳参照物。开发流程总览按 SKILL.md 的约定开发一个新服务模块遵循四步流程调研服务本身确认默认端口、nixpkgs 中的包名通常是pkgs.name、配置文件格式、是否支持 socket activation、是否支持 systemd notify/watchdog阅读现有模块作为参照SKILL.md 明确建议——memcached.nix是最简参照、redis.nix是中等复杂度参照、minio.nix是复杂模块参照创建src/modules/services/name.nix遵循下述模式文件自动发现无需注册在tests/下为该模块补充测试用devenv test验证。其中第 1 步的调研结论直接决定你在端口分配 vs Socket 激活、TCP 探测 vs notify 通知之间选择哪条实现路径是整个模块设计的分水岭。模块骨架一个最小可用的服务模块SKILL.md 给出了所有服务模块共用的标准骨架。完整继承如下含注释说明{ pkgs, lib, config, ... }: let cfg config.services.name; types lib.types; # Port allocation basePort cfg.port; allocatedPort config.processes.name.ports.main.value; in { imports [ # Backward compat: only add if migrating from old top-level options # (lib.mkRenamedOptionModule [ name enable ] [ services name enable ]) ]; options.services.name { enable lib.mkEnableOption human-readable description; package lib.mkOption { type types.package; description Which package of name to use; default pkgs.name; defaultText lib.literalExpression pkgs.name; }; bind lib.mkOption { type types.nullOr types.str; default 127.0.0.1; description The IP interface to bind to. null means all interfaces. ; }; port lib.mkOption { type types.port; default default-port; description The TCP port to accept connections.; }; # Add service-specific options here }; config lib.mkIf cfg.enable { packages [ cfg.package ]; env.NAME_PORT allocatedPort; processes.name { ports.main.allocate basePort; exec exec ${cfg.package}/bin/binary args; # Only needed for non-TCP health checks (see Readiness Probes below) # ready { ... }; }; }; }骨架各部分的职责拆解options.services.name定义模块对外暴露的 Nix 选项。四个必选选项——enable开关、package可替换的包默认pkgs.name、bind绑定 IPnull表示所有网卡、porttypes.port类型约束 0–65535。之后可以继续追加服务专属选项如extraConfig、settings、buckets等。config lib.mkIf cfg.enable { ... }仅在启用时才激活配置。典型配置包括把包加入packages进入 shell 的 PATH、导出NAME_PORT环境变量、注册processes.name进程定义。imports中的lib.mkRenamedOptionModule只在从旧版顶层选项迁移时才需要。例如 redis.nix 用lib.mkRenamedOptionModule [ redis enable ] [ services redis enable ]让老配置无缝升级到新命名空间。以最简模块 memcached.nix 为例其核心配置如下可作为最小骨架 自定义选项的对照processes.memcached { ports.main.allocate basePort; exec exec ${cfg.package}/bin/memcached --port${toString allocatedPort} --listen${cfg.bind} ${lib.concatStringsSep cfg.startArgs}; ready { exec echo -e stats\nquit | ${pkgs.netcat}/bin/nc ${cfg.bind} ${toString allocatedPort} /dev/null 21 ; initial_delay 2; probe_timeout 4; failure_threshold 5; }; };注意exec一律以exec前缀直接替换 shell 进程进入目标二进制这是 devenv 进程模型的硬性约定见下文进程 exec 约定。Unix Socket 优先策略SKILL.md 的核心设计主张之一只要服务支持 Unix socket就应默认用 Unix socket 而非 TCP 端口。理由有三点更快绕过 TCP/IP 协议栈与回环接口开销避免端口冲突本地多个项目同时跑同一个服务也不会抢端口更安全socket 文件通过文件权限控制访问天然只对本机开放。推荐做法是用DEVENV_RUNTIME作为 socket 文件目录对外暴露$NAME_UNIX_SOCKET环境变量仅在用户显式配置端口时才回退到 TCP。redis.nix正是这个模式的范本# Port allocation (port 0 means unix socket only) basePort cfg.port; allocatedPort if cfg.port 0 then 0 else config.processes.redis.ports.main.value; REDIS_UNIX_SOCKET ${config.env.DEVENV_RUNTIME}/redis.sock;其port选项的文档也明确说明了0 仅 Unix socket的语义redis.nixport mkOption { type types.port; default 6379; description The TCP port to accept connections. If port 0 is specified, Redis will not listen on a TCP socket and a unix socket file will be found at $REDIS_UNIX_SOCKET. ; };生成的redis.conf会根据端口是否为 0 写入对应的unixsocket配置并配套unixsocketperm 700权限位redis.nixredisConfig pkgs.writeText redis.conf port ${toString allocatedPort} ${optionalString (cfg.bind ! null) bind ${cfg.bind}} ${optionalString (allocatedPort 0) unixsocket ${REDIS_UNIX_SOCKET}} ${optionalString (allocatedPort 0) unixsocketperm 700} ${cfg.extraConfig} ;环境变量导出也跟随 socket 模式仅在纯 socket 模式下才设置REDIS_UNIX_SOCKETredis.nixenv { REDISDATA config.env.DEVENV_STATE /redis; REDIS_UNIX_SOCKET if allocatedPort 0 then REDIS_UNIX_SOCKET else null; };另一个 Unix socket 优先的范例是 postgres.nix其listen_addresses默认值为空字符串即默认只监听 Unix socket并把PGHOST指向DEVENV_RUNTIME/postgresenv.PGHOST let parsedAddress headWithDefault null (parseListenAddresses cfg.listen_addresses); host if cfg.listen_addresses ! then parsedAddress else runtimeDir; in lib.mkDefault host;只有用户显式设置listen_addresses时才走 TCP 并触发端口分配processes.postgres { ports lib.mkIf (cfg.listen_addresses ! ) { main.allocate basePort; }; ... };动态端口分配绝不硬编码端口单一端口SKILL.md 强调永远使用动态端口分配系统绝不硬编码端口。原因是多项目并行开发时同一个默认端口如 6379、5432会被多个 devenv 环境争抢。devenv 的端口系统会从allocate指定的基准端口开始自增探测直到找到空闲端口并把最终结果通过只读选项value暴露basePort cfg.port; allocatedPort config.processes.name.ports.main.value; # ... processes.name.ports.main.allocate basePort;这套机制的底层实现在 src/modules/processes.nix 的mkPortType中allocate是用户提供的基准端口value是只读的已解析端口其默认值由 devenv 的allocatePortprimopdevenvPrimops.allocatePort processName portName basePort计算产生。注意这里传入了进程名与端口名是为了在多次 evaluation 之间获得稳定的缓存 key——端口分配结果会被缓存避免反复运行导致端口漂移。进程定义中所有用到端口的地方都必须引用allocatedPort而非cfg.port例如 memcached 的启动命令memcached.nixexec exec ${cfg.package}/bin/memcached --port${toString allocatedPort} --listen${cfg.bind} ${lib.concatStringsSep cfg.startArgs};多个命名端口一个服务监听多个端口如 API 管理台 gRPC时使用命名端口allocatedHttpPort config.processes.name.ports.http.value; allocatedGrpcPort config.processes.name.ports.grpc.value; # ... processes.name.ports.http.allocate baseHttpPort; processes.name.ports.grpc.allocate baseGrpcPort;仓库中的真实案例是 minio.nix它用api和console两个命名端口先从listenAddress/consoleAddress格式127.0.0.1:9000中解析出基准端口再通过命名端口分配baseApiPort parsePort cfg.listenAddress; baseConsolePort parsePort cfg.consoleAddress; allocatedApiPort config.processes.minio.ports.api.value; allocatedConsolePort config.processes.minio.ports.console.value;同样rustfs.nix 也采用apiconsole双命名端口模式并把分配后的端口分别导出为RUSTFS_PORT与RUSTFS_CONSOLE_PORT环境变量。数据与运行时目录约定服务需要落盘的数据必须放在 devenv 的标准路径之下保证与项目生命周期绑定、可被devenv gc清理env.NAME_DATA config.env.DEVENV_STATE /name; # persistent data env.NAME_RUNTIME config.env.DEVENV_RUNTIME /name; # runtime/socket files两个目录的语义区别很关键DEVENV_STATE持久化数据目录如数据库文件、上传对象。redis.nix中的REDISDATA即指向DEVENV_STATE /redisredis.nixminio 的数据目录MINIO_DATA_DIR为DEVENV_STATE /minio/data配置目录MINIO_CONFIG_DIR为DEVENV_STATE /minio/configminio.nixpostgres 的PGDATA为DEVENV_STATE /postgrespostgres.nix。DEVENV_RUNTIME临时运行时文件如 socket 文件、PID 文件重启后不应依赖。redis.sock、postgres 的unix_socket_directories都放在这里。启动脚本中创建目录redis 的启动脚本会在启动前确保数据目录存在同时保持exec语义redis.nixstartScript pkgs.writeShellScriptBin start-redis set -euo pipefail if [[ ! -d $REDISDATA ]]; then mkdir -p $REDISDATA fi exec ${cfg.package}/bin/redis-server ${redisConfig} --daemonize no --dir $REDISDATA ;关键点有两个一是--daemonize no服务必须以前台进程方式运行因为进程管理器需要直接持有这个进程二是最后用exec替换为服务二进制避免多余的 shell 包装层。Readiness Probes进程就绪探测默认行为TCP 自动探测SKILL.md 明确指出原生进程管理器会自动为第一个分配的端口或监听 socket 创建 TCP 就绪探针。因此对绝大多数 TCP 服务来说不需要显式写ready块——只要分配了端口devenv 就会在进程真正可以接受连接后才标记为 ready。这也意味着服务端口的exec命令必须让服务自己监听端口或依赖 socket activation 由 supervisor 预创建 socket而不是依赖脚本模拟端口。自定义探测三种场景只有当默认的端口探测不足以表达服务真正可用时才需要显式ready块。SKILL.md 给出三种典型场景① HTTP 健康端点ready.http.get { host cfg.bind; port allocatedPort; path /health; };仓库真实案例是 rustfs.nix其/health探测直接绑定分配后的 API 端口ready.http.get { host cfg.bind; port allocatedApiPort; path /health; };② CLI 工具探测ready.exec ${cfg.package}/bin/client ping;例如redis-cli ping、pg_isready。redis 模块在 socket 模式与 TCP 模式下分别选用不同的 ping 命令redis.nixtcpPing ${cfg.package}/bin/redis-cli -p ${toString allocatedPort} ping; unixSocketPing ${cfg.package}/bin/redis-cli -s ${REDIS_UNIX_SOCKET} ping;③ 多步/初始化校验ready.exec配合一段脚本验证超出端口可用性的初始化状态。postgres 的探针是最复杂的范例postgres.nix先检查初始化标记文件$PGDATA/.devenv_initialized再用pg_isready加一次真实SELECT 1查询确认不仅能连接、还能执行查询ready { exec if [[ -f $PGDATA/.devenv_initialized ]]; then ${postgresPkg}/bin/pg_isready -d template1 \ ${postgresPkg}/bin/psql -c SELECT 1 template1 /dev/null 21 else echo Waiting for PostgreSQL initialization to complete... 21 exit 1 fi ; initial_delay 2; probe_timeout 4; failure_threshold 5; };探针可调参数ready块的所有可调参数定义在 src/modules/lib/ready.nix完整参数如下参数默认值含义execnull执行的 shell 命令退出码 0 视为就绪http.getnullHTTP GET 探测含host、port、path默认/、scheme默认httpnotifyfalse使用 systemd notify 协议就绪见下文initial_delay0首次探测前等待秒数period10两次探测间隔秒数probe_timeout1单次探测超时秒数timeoutnull整体就绪期限秒null表示无期限success_threshold1连续成功多少次才标记就绪failure_threshold3连续失败多少次标记不健康仓库中各模块常见的调优组合是initial_delay 2; probe_timeout 4; failure_threshold 5;见 redis、memcached、postgres给服务留出启动冷启动时间同时容忍单次抖动。Socket Activation彻底消除启动竞态为什么优先 Socket Activation当服务支持 systemd socket activationLISTEN_FDS/LISTEN_PID时SKILL.md 建议优先于端口分配使用它。收益有二消除竞态socket 由 supervisor 在进程启动之前创建并监听客户端永远不会撞上端口还没起来的窗口零停机重启重启进程期间旧 socket 持续接受连接新进程直接接管。配置方式processes.name { exec exec ${cfg.package}/bin/binary args; listen [ # TCP socket { name http; kind tcp; address ${cfg.bind}:${toString allocatedPort}; } # Unix socket { name main; kind unix_stream; path ${config.env.DEVENV_RUNTIME}/name/name.sock; mode 384; } # 0o600 ]; };listen列表的每个元素由 src/modules/lib/listen.nix 定义支持字段namesocket 名称如http、adminkindtcp或unix_streamaddressTCP 地址127.0.0.1:8080TCP socket 必填pathUnix socket 路径unix_stream必填backlog监听队列长度默认128modeUnix socket 文件权限如384即0o600仅unix_stream适用。supervisor 侧的底层实现socket activation 的底层实现在 devenv-processes/src/socket_activation.rs。关键机制L100-L116supervisor 启动前先bindlisten创建好 socket且默认设置FD_CLOEXEC防止泄漏给未激活的子进程子进程内文件描述符被映射到SD_LISTEN_FDS_START 3起的标准位置3、4、5…进程启动前设置LISTEN_FDS数量环境变量L251-L254并在pre_exec中清除FD_CLOEXECLISTEN_PID也一并注入完全对齐 systemd 规范测试 L578-L579 专门断言SD_LISTEN_FDS_START必须等于 3。因此supervisor 会对 TCP 监听 socket 自动执行就绪探测其优先级高于已分配端口——服务一进程启动socket 早已在监听readiness 几乎瞬时达成。如何判断目标服务是否支持 socket activationSKILL.md 给出的方法很直接查阅服务文档中是否有LISTEN_FDS或SD_LISTEN_FDS_START关键字Nginx、Caddy、PostgreSQL 等现代服务器普遍支持。Systemd Notify 与 WatchdogNotify精确的就绪信号当服务支持sd_notify(3)协议时可用它替代 TCP 探测做精确就绪判定进程收到NOTIFY_SOCKET环境变量在完全初始化完成后发送READY1processes.name { exec exec ${cfg.package}/bin/binary args; ready.notify true; };实现层面devenv 进程管理器在 devenv-processes/src/command.rs 中为进程注入NOTIFY_SOCKET环境变量就绪状态机定义在 supervisor_state.rs进程先处于Spawned尚未收到 READY 或 WATCHDOG收到READY1后转入 Ready 状态。Watchdog卡死自动重启对长时间运行且支持 watchdog 的服务开启 watchdog 后 supervisor 能检测到进程卡死并自动重启。进程必须周期性发送WATCHDOG1心跳processes.name { exec exec ${cfg.package}/bin/binary args; ready.notify true; watchdog { usec 30000000; # 30 seconds require_ready true; # only enforce after READY1 }; };watchdog选项定义于 src/modules/processes.nixusecwatchdog 间隔微秒例如30000000表示 30 秒require_ready默认true表示仅在收到READY1之后才强制执行 watchdog 超时判定devenv-processes/src/config.rs 中同样有该语义。对应地WATCHDOG_USEC环境变量由 command.rs 注入进程供sd_notify客户端计算心跳周期。判断服务是否支持的标准是文档中是否有Typenotify、WatchdogSec或sd_notify字样。初始化与清理用 Tasks 而不是包装启动脚本为什么用 TasksSKILL.md 给出了一个容易被忽略但非常重要的约定服务需要初始化创建数据目录、初始化数据库等或清理时用 tasks而不是把初始化逻辑包进进程的 exec 启动脚本里。理由缓存task 有缓存机制不会每次devenv up都重复执行DAG 顺序task 通过before/after形成依赖图保证正确的执行顺序TUI 可见task 在 devenv TUI 中可见便于观察与调试。而进程的exec应该保持直接exec进服务二进制的纯净形式不做多余包装。标准写法tasks.devenv:name:setup { exec mkdir -p $NAME_DATA # any other initialization ; before [ devenv:processes:name ]; };before [ devenv:processes:name ]保证任务在进程启动前完成。真实案例是 rustfs.nixtasks.devenv:rustfs:setup { exec mkdir -p $RUSTFS_DATA_DIR ; before [ devenv:processes:rustfs ]; };task 与进程的自动联动从模块系统看每个进程都会被自动生成为一个名为devenv:processes:name的 tasksrc/modules/processes.nix类型为process并继承after/before/ready/restart/listen/ports/watchdog等全部进程配置。因此before [ devenv:processes:name ]的语义就是本 task 必须在该进程 task 之前运行完。任务间依赖后缀规则见 src/modules/tasks.nixtaskstarted等待任务开始执行task或taskready等待任务就绪/健康进程默认tasksucceeded等待任务成功退出oneshot 任务默认taskcompleted等待任务结束、不要求退出码软依赖。对进程依赖同样支持started、ready、completed后缀例如after [ devenv:processes:postgres ]。配置文件生成pkgs.writeText与pkgs.formats服务模块经常需要生成配置文件。SKILL.md 给出两种方式都基于 Nix 包机制生成的文件在/nix/store中路径作为只读输入传入进程天然不可变、可复现。纯文本配置适合redis.conf、mosquitto.conf这类简单格式可用pkgs.writeText拼接并支持透传用户的extraConfigconfigFile pkgs.writeText name.conf port ${toString allocatedPort} ${cfg.extraConfig} ;redis 的配置即此模式redis.nix把动态分配的端口、bind、Unix socket 路径与用户追加的extraConfig合成为最终redis.conf。mosquitto 同样如此mosquitto.nixconfigFile pkgs.writeText mosquitto.conf allow_anonymous true listener ${toString allocatedPort}${lib.optionalString (cfg.bind ! null) ${cfg.bind}} persistence false log_dest stderr ${cfg.extraConfig} ;postgres 更进一步用settings属性集 类型感知的序列化函数生成postgresql.confpostgres.nix布尔值转yes/no、字符串自动加单引号并转义configFile pkgs.writeText postgresql.conf (lib.concatStringsSep \n (lib.mapAttrsToList (n: v: ${n} ${toStr v}) cfg.settings));结构化配置INI / JSON / YAML适合复杂配置使用pkgs.formats从 Nix 属性集生成用户通过settings选项获得类型安全的配置体验format pkgs.formats.ini { }; configFile format.generate name.conf cfg.settings;minio 用pkgs.formats.json生成mc客户端配置minio.nix默认注入一个localalias 指向 devenv 里的 minio 服务用户在clientConfig里覆盖clientWrapper pkgs.writeShellScriptBin mc mkdir -p $MINIO_CLIENT_CONFIG_DIR install -m 0644 \ ${json.generate mc-config.json cfg.clientConfig} \ $MINIO_CLIENT_CONFIG_DIR/config.json exec ${cfg.clientPackage}/bin/mc --config-dir $MINIO_CLIENT_CONFIG_DIR $ ;其他关键约定与进阶技巧向后兼容的选项迁移新模块若是从旧版顶层选项迁移而来务必在imports中提供重命名桥接。几乎所有仓库模块都遵循此模式例如 adminer.niximports [ (lib.mkRenamedOptionModule [ adminer enable ] [ services adminer enable ]) ];进程 exec 约定processes.name.exec应始终是exec 服务二进制的直接启动形式。这是 devenv 进程模型的硬性约定——devenv-processes/src/command.rs 会以bash -c exec方式启动命令并保留exec的进程替换语义确保信号SIGINT/SIGTERM能直达服务进程本体。postgres 还通过shutdown.signal 2SIGINT自定义优雅关停信号因为 PostgreSQL 对 SIGINT 做 fast shutdownpostgres.nix。复杂初始化postgres 范本postgres 模块展示了一次性初始化 进程启动分离的完整范式postgres.nixsetupScript负责首次运行时initdb、复制配置文件、创建初始数据库并写入.devenv_initialized标记startScript每次都先跑 setup内部幂等判断已初始化则跳过再exec postgresstartScript pkgs.writeShellScriptBin start-postgres set -euo pipefail mkdir -p ${q runtimeDir} ${setupScript}/bin/setup-postgres exec ${postgresPkg}/bin/postgres ;这种初始化脚本幂等 exec 纯净启动的组合比把初始化塞进 exec 包装脚本更符合 devenv 的进程/任务模型。多阶段启动与等待循环minio 的afterStart选项展示了服务启动后再执行用户脚本的另一种实现minio.nix后台启动服务、轮询mc admin info等待就绪、执行用户脚本、最后wait保持前台。这种写法适合需要在服务可用后再做一次性配置如设置 bucket 匿名访问的场景但注意它比 task 方案更命令式——SKILL.md 推荐的 task 方案在缓存与 TUI 可见性上更优。断言校验配置合法性模块可以在config中用assertions校验用户配置的非法组合。minio 断言afterStart依赖localaliasminio.nixpostgres 断言pass必须配userpostgres.nixassertions lib.concatMap (database: [ { assertion database.pass ! null - database.user ! null; message services.postgres.initialDatabases: database ${database.name} has pass set but not user. Setting pass requires user.; } ]) cfg.initialDatabases;这类断言在 evaluation 阶段就会报错把错误挡在运行前。完整示例综合所有约定的新模块将上述约定综合起来一个功能完整的服务模块应具备选项定义、动态端口、数据/运行时目录、就绪探测、可选的 socket activation 或 notify/watchdog、初始化 task、配置文件生成。下面是一个整合了 SKILL.md 全部核心模式的完整模板以示例服务example为例{ pkgs, lib, config, ... }: let cfg config.services.example; types lib.types; basePort cfg.port; allocatedPort config.processes.example.ports.main.value; configFile pkgs.writeText example.conf bind ${cfg.bind} port ${toString allocatedPort} ${cfg.extraConfig} ; in { options.services.example { enable lib.mkEnableOption example service; package lib.mkOption { type types.package; default pkgs.example; defaultText lib.literalExpression pkgs.example; description Which package of example to use; }; bind lib.mkOption { type types.nullOr types.str; default 127.0.0.1; description The IP interface to bind to. null means all interfaces. ; }; port lib.mkOption { type types.port; default 9000; description The TCP port to accept connections.; }; extraConfig lib.mkOption { type types.lines; default ; description Additional text appended to the config file.; }; }; config lib.mkIf cfg.enable { packages [ cfg.package ]; env { EXAMPLE_PORT allocatedPort; EXAMPLE_DATA config.env.DEVENV_STATE /example; # persistent data EXAMPLE_RUNTIME config.env.DEVENV_RUNTIME /example; # sockets runtime files }; tasks.devenv:example:setup { exec mkdir -p $EXAMPLE_DATA ; before [ devenv:processes:example ]; }; processes.example { ports.main.allocate basePort; exec exec ${cfg.package}/bin/example -c ${configFile}; # 仅当需要自定义健康检查时 # ready.http.get { host cfg.bind; port allocatedPort; path /health; }; # 或 socket activation # listen [ # { name main; kind tcp; address ${cfg.bind}:${toString allocatedPort}; } # ]; # 或 notify # ready.notify true; # watchdog { usec 30000000; require_ready true; }; }; }; }将这个文件放入src/modules/services/example.nix它即被自动发现随后在tests/下新增对应测试目录用devenv test验证模块行为仓库中 tests/ 下每个子目录就是一个可独立运行的测试环境如 tests/kafka/devenv.nix 即针对services.kafka的集成测试。测试验证SKILL.md 的流程第 4 步要求在tests/下为模块添加测试。devenv 的测试体系以每个测试 一个含devenv.nix的目录组织测试内通过enterTest写断言脚本。例如 tests/process-compose-shortlived/devenv.nix 展示了典型的断言模式启动短生命周期进程、轮询状态直到Completed、校验退出码为 0。针对新服务模块建议至少覆盖默认启动启用服务后能通过端口/socket 连上端口分配两个项目同时启用不会端口冲突参考 tests/process-port-allocation-two-repos/Unix socket 模式若支持port 0时 socket 文件出现在$DEVENV_RUNTIME下、TCP 不再监听就绪探测自定义ready探针在服务真正就绪前保持等待不提前标记 ready初始化 task首次运行执行初始化重复运行不重复执行task 缓存。小结本文完整覆盖了 SKILL.md 定义的 devenv 服务模块开发规范模块自动发现机制、标准骨架与四个必选选项、Unix Socket 优先策略、动态端口分配含命名端口与稳定缓存、DEVENV_STATE/DEVENV_RUNTIME目录约定、默认 TCP 探测与三种自定义就绪探针、socket activation 的底层 FD 传递原理、systemd notify/watchdog 集成、task 驱动的初始化编排、以及基于pkgs.writeText/pkgs.formats的配置文件生成。在动手实现新服务前务必先完成调研四问——默认端口、nixpkgs 包名、配置格式、是否支持 socket activation 与 sd_notify——再按简单参照memcached.nix、中等参照redis.nix、复杂参照minio.nix的顺序阅读仓库源码即可快速产出符合 devenv 生态规范、可复用、可测试的服务模块。赞分享开发工具CLI【免费下载链接】devenvFast, Declarative, Reproducible, and Composable Developer Environments using Nix项目地址https://gitcode.com/gh_mirrors/de/devenv点击查看免费下载相关推荐home-manager模块开发入门从零构建自定义Nix服务home manager模块开发入门从零构建自定义Nix服务 引言为什么需要自定义模块 在Linux用户环境管理中你是否曾遇到过这些痛点系统服务配置分配置管理CLIdevenv 中的 Cassandra 服务用 Nix 声明式搭建可复现的 Cassandra 开发环境devenv 中的 Cassandra 服务用 Nix 声明式搭建可复现的 Cassandra 开发环境 本文讲解如何在 devenv基于 Nix 的声明式开发工具CLIdevenv 服务模块实战用 Nix 声明式配置 WireMock 模拟 HTTP 接口devenv 服务模块实战用 Nix 声明式配置 WireMock 模拟 HTTP 接口 本文介绍如何在 devenv基于 Nix 的声明式、可复现开发环境开发工具CLI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表