# How twist.nix Generates and Manages flake.lock Files: A Complete Technical Guide

> Discover how twist.nix generates and manages flake.lock files. Learn the technical details of its artifact creation and intelligent lock file merging for your Nix projects.

- Repository: [Emacs Twist/twist.nix](https://github.com/emacs-twist/twist.nix)
- Tags: deep-dive
- Published: 2026-03-01

---

**twist.nix does not write `flake.lock` files directly; instead it generates `flake.nix` and `archive.lock` artefacts, then invokes the Nix CLI to create or update the lock file while intelligently merging existing entries to preserve local packages.**

The `emacs-twist/twist.nix` repository provides a deterministic, code-first approach to managing Emacs configurations through Nix flakes. Understanding how twist.nix handles **flake.lock generation** is essential for maintaining reproducible builds without manually editing lock file contents.

## The Three-Stage Lock File Workflow

The twist.nix implementation splits **flake.lock management** into three distinct stages orchestrated through `pkgs/emacs/lock/default.nix`.

### Stage 1: Generating Lock Artefacts

The `generateLockFiles` function (defined in `pkgs/emacs/lock/default.nix`) builds a minimal Nix derivation that stages the required input files. This process utilizes `write-lock-1.nix` to prepare `flake.nix` and `archive.lock` for placement in the user-specified lock directory. The actual file system operations are handled by the Bash script [`pkgs/emacs/lock/write-lock-2.bash`](https://github.com/emacs-twist/twist.nix/blob/main/pkgs/emacs/lock/write-lock-2.bash), which copies these generated files into the target location before any locking occurs.

### Stage 2: Invoking the Nix CLI

After the artefacts are in place, the system executes a **post-command** (configured in `default.nix` at lines 107‑108) that defaults to `nix flake lock`. The `runNix` function inside [`write-lock-2.bash`](https://github.com/emacs-twist/twist.nix/blob/main/write-lock-2.bash) executes this command, which creates the actual `flake.lock` file recording the exact revisions of all inputs declared in the generated `flake.nix`.

### Stage 3: Merging with Existing Lock Files

When a lock directory already contains a `flake.lock`, twist.nix does not overwrite it blindly. The `pkgs/emacs/lock/flake-lock.nix` module reads the existing lock using `prev = lib.importJSON flakeLockFile;` (lines 10‑34). If the lock is **version 7**, it merges the newly generated nodes with the old ones while preserving any inputs that are **not** declared in the freshly generated `flake.nix`—specifically local packages that exist outside the twist.nix generation scope. Unsupported lock versions trigger an explicit error.

## Core Implementation Files

### High-Level Interface (`pkgs/emacs/default.nix`)

The primary entry points for **lock directory generation** reside here:

- **`generateLockDir`** — A convenience attribute built from `generateLockFiles` with `flakeNix = true` and `archiveLock = true`, returning a `writerScript` that users can invoke directly.
- **`makeApps { lockDirName }`** — Exposes two executable apps:
  - **`lock`** — Writes `flake.nix` and `archive.lock`, then runs `nix flake lock`.
  - **`update`** — Writes only `archive.lock` (intended for runs that later call `nix flake update`).

Both apps are constructed by the helper `asAppWritingToRelativeDir`, which wraps the writer script into a runnable Nix application.

### Lock Writer Logic (`pkgs/emacs/lock/default.nix`)

This file implements the `generateLockFiles` function that coordinates the lock generation pipeline. It wires together `write-lock-1.nix` for file staging and [`write-lock-2.bash`](https://github.com/emacs-twist/twist.nix/blob/main/write-lock-2.bash) for execution, while configuring the `postCommand` that triggers the actual Nix locking mechanism.

### The Bash Runner ([`pkgs/emacs/lock/write-lock-2.bash`](https://github.com/emacs-twist/twist.nix/blob/main/pkgs/emacs/lock/write-lock-2.bash))

The `runNix` function in this script handles the execution of `nix flake lock` or `nix flake update`. It respects command-line flags such as `--force` and `--no-update-flake-lock`, providing fine-grained control over when the lock file gets refreshed.

### The Merge Module (`pkgs/emacs/lock/flake-lock.nix`)

This module contains the logic for preserving existing lock state:

```nix
prev = lib.importJSON flakeLockFile;
...
if prev.version == 7
then version7
else throw "Unsupported flake.lock version ${prev.version}";

```

The `version7` implementation merges `prev.nodes` with `newNodeAttrs` (the freshly generated package set), ensuring that nodes corresponding to local packages survive the merge even when those packages are intentionally omitted from the generated `flake.nix`.

## Practical Usage Examples

### Creating a New Lock Directory

Define a flake output that uses `makeApps` to expose the generation command:

```nix

# flake.nix

{
  description = "My twist-based Emacs configuration";

  outputs = { self, nixpkgs, ... }:
    let
      twist = import (builtins.fetchGit {
        url = "https://github.com/emacs-twist/twist.nix";
        rev = "master";
      }) { inherit nixpkgs; };
    in {
      apps.x86_64-linux.generate-twist-lock = twist.emacs.makeApps {
        lockDirName = "twist-lock";
      }.lock;
    };
}

```

Execute the generator:

```bash
nix run .#generate-twist-lock

```

This writes `flake.nix` and `archive.lock` into `./twist-lock`, automatically runs `nix flake lock` there, and produces `./twist-lock/flake.lock`.

### Updating Existing Locks

Use the `update` app to regenerate only the `archive.lock`, then manually refresh the flake inputs:

```bash

# Regenerate archive.lock only

nix run .#generate-twist-lock.update

# Refresh flake.lock with newest inputs

nix flake update ./twist-lock

```

The **lock** app performs both steps in sequence (generating files and locking), while the **update** app prepares the directory for a separate `nix flake update` call.

## How the Merge Strategy Preserves Local Packages

According to the `emacs-twist/twist.nix` source code, the **merge logic** in `flake-lock.nix` specifically addresses the scenario where a user has local packages that are not part of the twist.nix package set. These packages appear as inputs in the existing `flake.lock` but are absent from the generated `flake.nix`.

The merging algorithm:

1. Loads the existing lock into `prev`
2. Generates new node attributes (`newNodeAttrs`) for the current twist.nix package set
3. Performs a version‑aware merge (only version 7 is supported)
4. Retains nodes from `prev` that do not conflict with `newNodeAttrs`, ensuring local dependencies remain locked at their existing revisions

This prevents spurious changes to the lock file when the user has custom local inputs alongside twist.nix-managed Emacs packages.

## Summary

- **twist.nix** generates `flake.nix` and `archive.lock` artefacts rather than writing `flake.lock` directly.
- The **lock generation** pipeline is implemented across `pkgs/emacs/lock/default.nix`, `write-lock-1.nix`, and [`write-lock-2.bash`](https://github.com/emacs-twist/twist.nix/blob/main/write-lock-2.bash).
- The Nix CLI (`nix flake lock`) creates the actual lock file after twist.nix stages the necessary inputs.
- **`flake-lock.nix`** merges existing version 7 locks to preserve local package entries that are not declared in the generated configuration.
- **`makeApps`** provides both `lock` and `update` apps for different maintenance workflows.

## Frequently Asked Questions

### Does twist.nix directly modify flake.lock files?

No. According to the `emacs-twist/twist.nix` source code, the system generates `flake.nix` and `archive.lock` files and then invokes the Nix CLI (via the `postCommand` mechanism) to create or update `flake.lock`. The twist.nix code never directly serializes or edits the JSON structure of the lock file itself.

### How does twist.nix handle existing flake.lock entries?

When a `flake.lock` already exists in the target directory, the `pkgs/emacs/lock/flake-lock.nix` module imports the existing JSON and merges its nodes with the newly generated set. This preserves entries for local packages that are intentionally omitted from the twist.nix-generated `flake.nix`, ensuring those dependencies remain locked at their existing revisions while updating only the twist-managed inputs.

### What is the difference between the lock and update apps?

The **`lock`** app (accessed via `.lock` on the result of `makeApps`) writes both `flake.nix` and `archive.lock` to the target directory and then immediately executes `nix flake lock` to generate a fresh lock file. The **`update`** app writes only `archive.lock`, preparing the directory for a subsequent manual or automated `nix flake update` call that refreshes input sources without regenerating the entire flake structure.

### Which flake.lock versions does twist.nix support?

Only **version 7** is supported. The `flake-lock.nix` module explicitly checks `prev.version` and throws an error if the existing lock file uses any other version format, ensuring predictable merge behavior and preventing corruption of lock files with incompatible schemas.