The merge overwrote the target in place with `cat "$tmp" > path`, so a
crash mid-write could leave a truncated file, and an application that
saved its config while activation ran had that change silently
overwritten.
Snapshot the existing file, write the merged result to a temporary
file next to the resolved target, copy the existing mode (or apply
`mode` or the umask for a new file), and rename it into place. The
snapshot keeps the configured file name inside a temporary directory,
so a reader that detects the format from the extension still
recognizes it, even through a symlink to a differently named file.
Symlinked targets are written through and stay symlinks; a dangling
symlink creates its target, as the hand-written mergers this helper
replaced did. Activation fails without writing if the content, the
mode, or the symlink target changed since the snapshot, and refuses
targets inside the Nix store. A target that cannot be renamed over,
such as a file bind-mounted by impermanence, is written in place
instead. Temporary files are cleaned up by a trap scoped to a
subshell, since activation owns the top-level EXIT trap; this also
keeps the merge's shell variables out of the activation script.
The change check is not a lock: a write between the final check and
the rename is still lost. The documentation says so, along with the
attributes a rename does not preserve.
This follows the approach programs.vscode uses for its mutable user
settings.
Some applications store tokens in the files these merges manage, such
as gh's hosts.yml or Docker's config.json, so callers need a way to
create them as 600.
The new optional `mode` input applies only when the target does not
exist yet; existing files keep their mode. By default it is null and a
new file gets the permissions shell redirection would give it under the
activation's umask, as the hand-written mergers this helper replaces
did, instead of a fixed 644. A value that is not an octal string fails
evaluation, rather than the first activation on a machine where the
file does not exist yet.
jaq 3.1.1 reads TOML through toml-span, which does not deserialize
date or time values. An existing TOML file containing one makes the
merge refuse, so activation fails on every switch until the value is
removed. The file itself is left untouched.
Document the limitation and test that the refused merge leaves the
file byte-identical. If a later jaq parses these values, they would
pass through JSON as strings, so the test is expected to fail then and
prompt a review of the documented behavior.
`jaq --to yaml -c` writes the merged configuration in one-line flow
style, so a user's block-style YAML came back as `{a: {b: [x]}}`. Drop
the compact flag for every non-JSON format: YAML is now written in
block style, while TOML, CBOR, and XML output are byte-identical with
and without it.
Document that comments and formatting are not preserved. Test the
exact block-style output with nested mappings and a list, and that
merging the written file again leaves it unchanged.
The dry-run message interpolated the configured path into a
double-quoted string, and the parse error embedded an escaped path
inside one. A path containing a command substitution was therefore
executed while printing the message. Quote each complete message as a
single shell argument, as the verbose message already does, and test
both messages with such a path.
Pass JSON documents to jaq through process substitution instead of
--argjson. Linux limits a single argument to 128 KiB, so merging an
existing configuration file or Nix-declared settings above that size
failed activation with "Argument list too long".
Require exactly one JSON value in each input before merging, so
multi-value streams are still rejected as they were with --argjson.
Keep the merge operation on its own line inside that filter so a
trailing comment in it cannot comment out the closing parenthesis, and
name the config file when the merge fails.
Add regression coverage for large existing files, large Nix-declared
settings, an operation ending in a comment, and byte-preserving
rejection of multiple JSON values.
Add a reusable `lib.hm.generators.mkImpureConfigMerger` that generates a
bash snippet merging a Nix-generated config file into an existing user
config at activation time. It preserves interactive changes the user has
made while applying the Nix-declared settings on top.
The merge uses `jaq`, which handles JSON, YAML, TOML, and CBOR natively
through its `--from`/`--to` flags. An optional `reader` parameter allows
custom preprocessing, for example JSON5 files with comments.
The snippet follows the existing activation contract:
- `$DRY_RUN` prints what would be done and skips the merge entirely.
- `$VERBOSE` logs a merge message, customizable through `verboseMsg`.
- A missing or zero-byte file is treated as the `empty` value, kept in
memory so the target is only created once the merge is serialized.
- An existing file that fails to parse aborts activation with an error
instead of silently overwriting the user's settings with the generated
defaults. The reader's exit status is checked because it can emit valid
JSON before failing, as when jaq prints the first object of a truncated
stream.
- The result is written to a temporary file and moved into place only
after the merge succeeds. An existing file is overwritten in place to
keep its permissions and symlink target; a new file is created with
mode 644.
The helper is marked experimental: the activation contract edge cases are
still being discovered, so external flakes should pin this input.
Add a lib-level regression test covering the truncated-stream refusal
with byte preservation, first activation on missing JSON and TOML
targets, user-key preservation, and zero-byte files treated as empty.
Assisted-by: Goose + GLM 5.3 Flash
Forward converted values to the complete settings path so the standard
changed-option warning identifies the destination key. Preserve legacy
reads, priorities, defaults, and shadowed conversions.
Point mpdris2 at Library.music_dir directly and cover nested paths,
literal dotted keys, and the affected callers in warning tests. The
contributor guidelines now describe `to` as the settings path that
contains `key` instead of the settings root.
Document helper inputs, return values, and examples in the library format. Clarify legacy alias declarations and state-version warning modes without changing executable Nix.
Share raw overlay forwarding and effective-key detection for default-empty
legacy options. Keep application defaults and conversions in consumers.
Delegate aliases and diagnostics to doRename without rewrapping prepared
priorities. Cover ordering, composition, empty values, and source filenames
in the existing evaluations.
Add opt-in ordered forwarding to the settings rename helper so legacy and canonical list definitions retain priorities and mkBefore/mkAfter ordering.
Keep the default helper path unchanged and cover evaluation in module scopes without a warnings option.
The Putter tool is a Rust implementation of the file management
component of Home Manager. It takes a JSON manifest that indicates how
files should be symlinked and when the manifest is applied, does its
best to ensure that the file system reflect the manifest.
Putter offers some additional features that Home Manager does not
expose today, such as file copying (potentially recursive) and support
for overriding files within a recursive tree (e.g., inside a symlinked
tree one could ensure that a specific file is copies with specific
permissions).
This is considered highly experimental at the moment and to use Putter
one must set a hidden option.
ckgxrg was added to nixpkgs by NixOS/nixpkgs@ff7b6221c0. Regenerating all-maintainers.nix also restores the existing glmlm entry missing from the generated file.
Avoid repeated attribute lookups and unnecessary intermediate allocations in
DAG helpers. Cache DAG names while normalizing before-edges to improve topoSort
performance on large DAGs.
Co-authored-by: Eman Resu <78693624+quatquatt@users.noreply.github.com>
Add reusable Home Manager generator helpers for rendering freeform attrsets that contain lib.hm.dag entries.
This keeps ordering support available to JSON and future generated formats without forcing each module to carry its own DAG sorting boilerplate.
toHyprconf' tested every important prefix with a non-strict foldl that
always scanned the full list. lib.any expresses the same any-prefix-matches
intent and stops at the first match. Runs once per attribute in the
Hyprland generator.
Extract MCP-related helper functions (renderEnv, mkEnvFilesWrapper,
wrapEnvFilesCommand, addType, transformMcpServer) into a new shared
library module at modules/lib/mcp.nix.
- Add lib.hm.mcp for sharing transformMcpServer logic across consumers
- Add enabled/disabled conflict detection assertion
- Add tests for new library functions