Flox:用 Manifest、Activation 与 Generation 交付可移植环境
开发者进入 Flox 交互 Shell 后,项目 alias、编译器和数据库客户端都可用;自动化改成 flox activate -- ./release.sh 后,alias 消失了,脚本还读到了宿主机没有声明的工具。另一个同事从远端拉取同名环境,包版本一致,服务却因为 CPU system 不受支持而没有启动。故障不在“Flox 有没有安装成功”,而在 manifest、lock、激活入口和目标 system 没有被当成同一份运行合同。
Flox 在项目 .flox 中保存环境声明与解析快照,底层仍由 Nix Store 提供包闭包。它可以启动交互 Shell、直接执行命令、管理本地服务、组合多个环境并把引用推送到 FloxHub;这些能力共享同一环境模型,却有不同的 profile、信任、认证和清理语义。激活也不是容器,宿主内核、网络、文件系统、设备和未清理的环境变量仍然存在。
安装包可能重配已有 Nix
Flox 支持 x86_64-linux、aarch64-linux、x86_64-darwin 和 aarch64-darwin。Windows 应在 WSL2 中使用;原生 Windows 不属于 manifest 的 system 集合。macOS 可使用 pkg 或 Homebrew,Linux 有 Debian、RPM 与 Nix/NixOS 入口,具体命令应从 Flox 安装页选择并核对目标架构。
下载 pkg、deb 或 rpm 时先从安装页取得对应发布物与 SHA256/SHA512 文件,在离线校验通过后再交给包管理器。已有 Nix 且需要保留组织配置的机器,可使用 Nix Generic 路径;它要求 Nix 版本满足当前安装页基线,并显式合并 Flox cache 的 substituter 与公钥,不能用覆盖式写入破坏原有私有 cache。macOS 使用 Homebrew 的最小入口是:
brew install flox
flox --version
command -v floxDebian/RPM 发布物会注册后续升级的软件源;这意味着安装不仅改变当前二进制,还增加了未来更新入口。企业镜像应固定发布物摘要、仓库签名键和升级窗口,不要把 brew upgrade、apt upgrade 或 dnf update 留给开发者在项目激活时自动触发。
原生安装包不只是把一个二进制放进用户目录。它可能撤销已有 Nix shell 初始化、覆盖 /etc/nix/nix.conf、转换为 multi-user Nix 并重配 nix-daemon。已经运营 Nix 的团队先在 VM 对比安装前后的配置,或者采用文档中的 Nix Generic 安装保留控制权。安装后记录:
flox --version
nix --version
uname -s
uname -m
nix show-config | grep -E '^(substituters|trusted-public-keys|trusted-users)'如果安装后其他 Nix 项目突然无法拉取私有 cache,第一证据不是 Flox manifest,而是 /etc/nix/nix.conf、daemon 状态和 substituter 信任 diff。恢复时按安装前备份还原 Nix 配置并重启 daemon,不能只卸载 Flox CLI 就假设主机改动已经撤销。
.flox 里有声明,也有工具管理的状态
在仓库根目录创建 path environment:
mkdir flox-lab && cd flox-lab
git init
flox init
flox install nodejs_20 ripgrep
flox list -a用户维护 .flox/env/manifest.toml,Flox 管理 .flox/env/manifest.lock 与 env.json,自定义 Nix expression 放在 .flox/pkgs。path environment 可以整体随 Git 提交;不要手工编辑 lock 或依赖内部 metadata 字段作为集成接口。
一个小型项目可以从下面的 manifest 开始:
schema-version = "1.13.0"
[install]
node.pkg-path = "nodejs_20"
rg.pkg-path = "ripgrep"
[vars]
APP_ENV = "development"
[hook]
on-activate = '''
export PROJECT_ROOT="${FLOX_ENV_PROJECT:-$PWD}"
'''
[profile]
bash = '''
alias project-test='npm test'
'''
[services.demo-api]
command = '''
node -e "require('http').createServer((req,res) => res.end('ok')).listen(4320, '127.0.0.1')"
'''
[options]
systems = ["x86_64-linux", "aarch64-linux", "x86_64-darwin", "aarch64-darwin"]新功能可能要求更高 schema-version;minimum-cli-version 对旧 CLI 只是告警,不一定阻断。因此 CI 仍要显式比较批准的 Flox 发布线,并让 flox 自己校验、迁移和重锁 manifest。旧 hook.script 已被 [profile] 取代,新配置不要继续复制弃用字段。
manifest 是意图,lock 保存解析出的版本、依赖、system、catalog revision 和 outputs。outputs 决定是否把 bin、dev、man 等闭包带进环境,直接影响下载与磁盘;pkg-group 可让一组包共享兼容 catalog revision 并成组升级;priority 用于处理合并后的路径冲突。把 manifest 与 lock 一起提交,才能在相同 system 上避免每次重新解析。
四种激活入口不是同一种 Shell
交互入口最适合日常开发:
flox activate
node --version
rg --version
command -v node
printf '%s\n' "$APP_ENV"
exit预期 Node 与 ripgrep 来自 /nix/store/...,APP_ENV 为 development,退出后宿主 PATH 恢复。flox activate -c 'project-test' 通过 shell 命令执行并加载对应 profile,适合依赖 alias 或 shell function 的任务。自动化更适合直接 exec:
flox activate -- node --version
flox activate -- bash -c 'type project-test || true'直接 exec 不运行 [profile],第二条命令预期找不到 project-test。这不是激活失败,而是入口语义不同。第四种入口是输出 activation script 并 source 到当前 shell,它会修改当前会话,适合必须保留 shell 状态的场景;退出时要执行对应 deactivate 流程,团队脚本不要在不受控的父 shell 中静默 source。
hook.on-activate 在可预测的 Bash 中运行。并发 shell 通常复用第一次 activation 捕获的变量而不重复执行 hook,但 flox services start 等命令可能建立临时 activation 并再次触发它;官方也明确提示这种复用行为未来可能变化。hook 因而必须幂等、不能假设 cwd,也不要执行不可重复的迁移。需要持久状态时写入有项目归属和锁机制的目录,不能依赖“第二个 shell 恰好不重跑”来保证唯一性。
正反实验要证明 lock 与宿主输入各自负责什么
从干净 checkout 保留当前 .flox/env/manifest.lock,先验证激活不会静默重锁:
flox activate -- bash -c 'node --version; command -v node; rg --version'
git diff --exit-code -- .flox/env/manifest.lock相同 system 上应得到 lock 记录的版本,路径进入 Nix Store,激活不应偷偷改写 lock。复制环境到另一台受支持的同 system 主机时,版本与 outputs 应符合 lock;同时记录实际 Store path,只有解析到同一 derivation 时才应期待路径一致。
反向实验在副本中执行升级或删除 lock 后重锁,再比较:
cp .flox/env/manifest.lock /tmp/manifest.lock.saved
flox upgrade
git diff -- .flox/env/manifest.lock
cp /tmp/manifest.lock.saved .flox/env/manifest.lock
flox activate -- node --version新 lock 可能改变版本、catalog revision、outputs 和闭包;恢复旧 lock 后应回到原解析。若 catalog 已不能重新解析某个旧版本,已有 lock 仍可能继续工作,但未来重锁会失败。这说明 lock、可用 binary cache 与内部归档是三层持久性,不能互相替代。
再在宿主提供一个未声明变量和命令,进入普通 activation 观察它们仍可能可见。Flox 把环境路径前置,并不会默认清空宿主。需要验证构建输入时,应使用 build 的 sandbox 语义,而不是从激活成功推断环境已经隔离。
dev 与 run 模式决定闭包暴露多少
默认 dev 模式会暴露开发依赖和语言变量;run 模式只暴露请求包及 man page,更适合作为应用运行入口。组合多个环境时,高优先级 manifest 会覆盖选项,一个 include 的 mode 也可能影响最终结果,所以顶层 manifest 应显式声明团队期望的模式。
收敛闭包前先跑项目编译、测试和启动探针。若 run 模式出现头文件缺失、编译器不可见或语言变量消失,说明任务仍依赖开发闭包;不要简单把全部 outputs 加回来,而应判断它究竟是构建任务还是运行任务。闭包越大,首次下载、Store 占用、漏洞扫描面和 cache 成本都越高。
服务的生命周期绑定 activation 引用
[services] 定义本地进程,不创建容器。显式启动服务比缩写更适合团队脚本,也能把“启动环境”和“启动有副作用的进程”分开:
flox services start demo-api
curl --fail http://127.0.0.1:4320/
flox services status
flox services logs demo-api预期响应为 ok,状态显示服务运行。services start 在没有现存 activation 时会创建临时 activation,并使用当前最新环境构建;如果 shell 在修改 manifest 前已经激活,服务和 shell 可能观察到不同变量。修改配置后应重新激活再启动服务,并把 flox services logs demo-api 与实际端口探针一起作为故障证据。
独立执行 flox services start 后,退出普通 shell 不能作为停止证据,CI 与退出流程必须显式清理:
flox services stop demo-api
curl --fail http://127.0.0.1:4320/最后一次请求应连接失败。另一种入口 flox activate -s 会把服务生命周期绑定到 activation,最后一个持有该环境的 activation 退出后才清理这批服务。若设置 services.auto-start = true,任何 activation 都可能启动服务,CI 和工具探针也会产生副作用;可用 flox activate --no-start-services -- <command> 为无副作用任务兜底。自动启动前应确认端口、数据目录与并发环境不会互相污染。
会自行 daemonize 的程序需要 is-daemon 与 shutdown.command。故意省略后,activation 退出而端口仍存在,就是残留证据;先用 PID、端口和服务日志确认归属,再补 shutdown 并复验。清理不能使用模糊的进程名全局杀进程,因为同一开发机可能有其他仓库的同名服务。
服务变量可以写在服务私有配置中,按 systems 限制平台。数据库一类有状态服务还要把数据目录独立声明:Flox 负责进程生命周期,不会自动回滚 schema 或删除数据。停进程、备份或迁移数据、删除项目缓存是不同动作。
组合环境先合并 Manifest,再锁定最终结果
include 可以指向本地目录或 FloxHub remote。Flox 会先按优先级合并 manifests,再锁定合成结果,不是把每个环境的 lock 机械拼接。后列 include 优先,顶层 manifest 最高;install、vars 与 services 的冲突项整体覆盖,hook 与 profile 追加。当前组合模型不能从低优先级环境删除一个条目,只能覆盖或增加,因此“公共基线太大”不能靠子项目减法修复。
远端 include 不会自动漂移到最新版本,执行 flox include upgrade 才会拉取新 manifest 并重锁。升级时评审合并后的 lock,而不只评审上游仓库 diff。出现命令冲突时,检查包 priority、include 顺序和最终 flox list -a;出现 hook 重复执行时,检查追加规则,不要只改顶层 hook 期待覆盖上游。
Build 默认允许宿主输入,pure 也有平台差异
[build.<name>] 在类似 activation 的 Bash 环境执行,产物写入 $out,项目旁生成 result-<name> 链接。默认 sandbox 关闭,构建可读取宿主和增量缓存,因此“使用 Flox build”本身不等于可复现构建。
可以设计一个正反实验:仓库提交 tracked.txt,另放未跟踪的 local.txt,build 同时尝试读取两者和 $HOME 文件。默认模式可能全部读到;pure 模式只复制 Git tracked 输入并收紧外部访问,未跟踪文件与仓库外文件应失败。Linux pure 会禁网,macOS pure 仍可能联网。重复构建后比较 $out 内容摘要可以发现漂移,但不能据此跨平台承诺逐字节一致。
新发布线提供 warn、enforce 与 sandbox-allow 等控制时,应同时固定最低 CLI/schema,并在 Linux 与 macOS 分别验证。warn 只给证据而不阻断,真正的供应链门禁要使用能让 CI 非零退出的模式。构建结果、增量缓存和 result-* 链接都计入容量,不应只统计 .flox 目录。
本地 Git 分享与 FloxHub 是两条发布路径
path environment 随 Git 分享不需要 FloxHub 登录,适合仓库拥有 manifest 与 lock 的团队。FloxHub 提供远程引用、generation 和组织共享,需要 flox auth login,并依赖 api.flox.dev、hub.flox.dev 与 auth.flox.dev 的 HTTPS。代理、企业 CA 或非交互环境缺认证时,常见表现是本地 activate 成功而 push、pull 或 remote activate 失败。
第一次推送不含 secret 的 path environment 时,FloxHub 保存 manifest 与 lock,本地 .flox 则转成指向该远端 revision 的引用;之后每次成功 push 都形成新的 generation:
flox auth login
flox push
flox generations list
flox generations history另一目录 pull 会保留上游关系,pull --copy 则创建断开上游的副本;flox activate -r owner/name 操作 FloxHub 环境在本机的持久缓存副本,离线可用性取决于该 revision 的 manifest、lock 与 Nix 闭包是否都已缓存。远端与本地都改动后会发生分叉,普通 push 应拒绝或提示冲突。--force 会覆盖远端状态,只能在确认 owner、generation 与恢复点后使用,不能成为自动化默认参数。
激活非本人维护的远程环境会执行其中的 hook,先核对 owner、manifest、lock 和 generation,再响应 trust 提示。FloxHub 的私有环境、私有包、publish、自动升级和企业部署属于托管或商业能力,成本与配额从 Flox 价格页按采用时状态核对;开源 CLI 能运行本地环境,并不代表这些远程能力离线可用或无需治理。
Secret 留在外部存储,Manifest 只保留取值逻辑
manifest 不应保存 secret 值,也不能假设 FloxHub 会替它安全托管。hook.on-activate 可以调用 macOS Keychain、1Password、Vault 或云 Secret Manager,只把用途、键名和取值逻辑纳入版本控制。非交互 CI 使用独立服务身份,限制读取路径与有效期,并在日志中屏蔽命令回显。
取出的 secret 若进入环境变量,仍可能被子进程、崩溃转储和调试日志读取。更敏感的证书和私钥应由启动包装器写入仓库与 .flox 之外、仅当前用户可读的临时目录,并用 trap 或受控的 deactivate/服务停止流程保证删除;不要把 Secret 放进 $FLOX_ENV_CACHE、build 输入或 result-*。hook 不得打印值。外部 PR、远程环境和不受信 include 在获得 secret 前必须完成代码审查,否则 activation 就是任意代码执行入口。
Cache、Catalog 与磁盘要分开治理
binary cache 命中决定首次激活是下载还是本地构建。可用 FLOX_MAX_PARALLEL_DOWNLOADS 限制并发下载,避免共享出口被一次冷启动打满;需要关闭基础命令遥测时设置 FLOX_DISABLE_METRICS=true。容量观察至少包含 Nix Store、Flox 环境缓存、build result,以及 macOS containerize 使用的 flox-nix volume。
flox delete 删除指定环境的数据;flox gc 清理已删除环境遗留的 Flox 数据,并运行 Nix Store GC,二者不是同一个动作。flox gc 没有只预览候选的 dry-run 选项,执行前要退出 activation、停止服务、确认删除目标并记录 Store 与缓存占用;共享主机还要先核对其他 profile 与作业引用。GC 后的下一次 cache miss 会重新产生下载或构建成本,删除项目目录本身也不等于已经释放 Store 空间。
Base Catalog 的旧 metadata 会按保留策略轮换。已有 lock 保存解析 metadata,通常能继续使用;将来对旧版本重新 update 却可能无法再次解析。长期维护不能只留 manifest,至少同时保存 lock,并保证关键闭包在可信 cache 中可获得;强监管项目还要按内部制品策略归档来源与签名证据。
OCI 导出不会自动补齐运行时治理
flox containerize 可把环境输出为 OCI image,写入 tar、标准输出或受支持 runtime。Linux 可以直接导出后由 Docker/Podman 加载;macOS 依赖外部 runtime、代理容器和 flox-nix volume。容器入口执行类似 activation 的逻辑,但宿主文件、服务数据、secret、UID、端口和网络不会因为环境可激活就自动迁移正确。
containerize.config 中 user、cmd、ports、volumes、working-dir、labels 和 signal 等能力仍有实验性边界。采用时锁定 Flox CLI/schema,检查生成镜像的架构、入口、运行用户和层内容,并在容器内执行与 flox activate -- <probe> 相同的版本与变量探针。生产运行仍需独立处理镜像最小化、漏洞、签名、网络、volume、凭证和回滚。
升级与退出以 Generation、Lock 和资源引用为锚点
升级包、include 或 Flox CLI 时,本地 path environment 先保存 Git revision、manifest 与 lock,再在分支中重锁,从冷缓存执行激活、直接 exec、profile 差异、项目测试和服务退出实验。若新 lock 改变闭包、目标 system 缺包或 hook 行为异常,从版本控制恢复旧 manifest/lock。
FloxHub environment 才以 generation 为远端回退锚点。先用 flox generations list 和 flox generations history 记录当前关系,再执行 flox generations switch <generation> 精确切换,或用 flox generations rollback 回到“上一次 live generation”。rollback 不是简单的 N-1:经历过 switch 或 rollback 后,历史可能形成非线性关系。切换会恢复该 generation 的 manifest 与 lock 并新增历史记录,但不会回滚服务写入的数据库、外部缓存或已发布制品。恢复后必须重新激活、重启服务并执行项目探针。
不要把本地 lock、远端 generation 和 Nix binary cache 当成同一种快照,也不要只降级 CLI。lock 固定解析,generation 管理远端 live 状态,cache 保存可取的闭包;三者缺一都可能让“配置已回退”停在纸面上,新 schema 或服务数据也可能已经不兼容。
删除本地环境前先退出所有 activation、确认服务停止、保存需要的代码与数据,再执行会要求确认的 flox delete;只有在核对共享 Store 引用后才执行 flox gc,自动化不要默认附加跳过确认的 --force。远程退出还要断开 include/upstream、撤销账号与自动化凭证、按 FloxHub 当前管理入口删除不再使用的远程环境,并确认组织可见性和 generation 不再提供旧入口。pull --copy 适合迁移时切断上游,但副本随后由新 owner 负责升级和安全修复。
团队治理需要明确三类 owner:应用团队维护 manifest、hook、profile 和服务,平台团队维护 Flox/Nix 基线、cache、CI 与 system 矩阵,安全团队维护远程信任、账号和 secret 身份。持续观察冷启动与热启动、Store 增量、generation 分叉、残留服务、旧 lock 重解析失败和远程席位成本。可移植性的可信结论来自这些证据在目标 system 上持续成立,而不是来自环境名称在多台机器上相同。
