hack.foo
Guides

Dependency installer protocol

Ordinary dependency-cache labels select a volume; they do not serialize installers or certify its contents. hack.dependencies.cache-protocol: locked-v1 opts one initializer into a stricter, cooperative protocol. Adopt it only for an immutable dependency payload. Keep worktree-specific source generation in a separate, always-run step with its own output directory.

services:
  install:
    image: your-pinned-installer-image
    platform: linux/arm64
    entrypoint: []
    command: ["sh", "/source/install-dependencies.sh"]
    labels:
      hack.dependencies.cache-volume: dependencies
      hack.dependencies.lockfiles: bun.lock
      hack.dependencies.runtime-files: package.json,scripts/install-dependencies.sh
      hack.dependencies.cache-protocol: locked-v1
      hack.dependencies.cache-generation: "0"
      hack.dependencies.cache-verify: '["test", "-s", "/deps/expected-output"]'
    volumes:
      - ../scripts:/source:ro
      - dependencies:/deps
  app:
    image: your-application-image
    depends_on:
      install:
        condition: service_completed_successfully
    volumes:
      - dependencies:/deps:ro
volumes:
  dependencies: {}

Replace the example commands, output check, image and platform with your actual installer contract. Pin the image by digest. The initializer needs /bin/sh, flock, cmp, env, sleep, mkdir and mv, and permission to write the cache root. Hack generates and mounts a read-only wrapper; it preserves the explicit command argv. Inherited image entrypoints, string commands, multiple writers for one volume, external/named volumes, Compose indirection (include, extends, volumes_from) and unresolved runtime configuration are unsupported. Replacing the initializer argv through hack run is rejected before cache access. Do not change the generated files under .hack/.internal yourself.

This first version runs initializer and verifier with a clean environment: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin and HOME=/tmp. It does not forward image, host, branch or Hack overlay variables. Explicit initializer environment and env_file settings are refused. Credential- or environment-dependent installers are therefore not supported by this protocol yet; retain their existing setup instead of opting in. This prevents untracked runtime environment changes from producing different payloads under one ready marker.

The initializer must run synchronously, keep the inherited lock descriptor open, and write only its declared payload. It must not background writers or alter the protocol's .hack-dependency-cache-v1 metadata. The output verifier must be synchronous and return nonzero unless the complete payload is usable. A trivial file check is only an example: verify the real required dependencies and generated artifacts. All consumers must directly require successful initializer completion and mount this volume read-only. This is a cooperative contract, not a sandbox against malicious commands or out-of-band volume writers.

State and recovery

During detached startup and restart, Hack observes opted-in initializers and reports waiting, installing, verifying, ready, or failed on stderr. waiting means waiting for the shared-cache lock; Compose's own output describes container launch. ready describes the dependency payload, not application readiness. JSON command output remains on stdout.

Observation uses a project-filtered Docker event stream and bounded initializer log streams only while Compose startup runs. It accepts fixed protocol phase records and discards other installer output. Progress is diagnostic: unavailable observation does not change Compose's result or startup budget. Ordinary, non-opted-in installers retain their existing Compose output. Very short-lived phases can finish before observation attaches. Phase records are cooperative diagnostics, not independent proof of payload or application health.

The wrapper waits up to 60 seconds for a kernel-managed exclusive lock. It never unlinks the lock or steals it based on age. It records an attempt before running the initializer, then runs the verifier. Only two successful exits allow an identity-bound readiness marker to be renamed into place. A matching ready marker skips this explicitly declared initializer; it does not skip other services or certify worktree-specific generation.

Give a separate generation service its own per-instance output volume. Both that service and the application must directly depend on successful initializer completion and mount dependencies read-only; the application should also depend on successful generation. Generation-only source inputs should not change the dependency fingerprint. Verify regeneration through full startup/restart or run the generator explicitly: scoped commands and a one-off hack run may skip dependencies, so they are not an unconditional generation hook.

An unsuccessful or interrupted attempt has no valid ready marker. Subsequent starts refuse to reuse or mutate that incomplete generation. Diagnose the failure, then increment hack.dependencies.cache-generation in the tracked Compose file and rerun hack up --detach. This selects a fresh cache volume; failed and existing ready generations are retained. Do not delete protocol markers to force reuse. Already-running containers retain their original mounts until restarted.

The fingerprint includes the protocol, generation, initializer argv, verifier and working directory along with the normal cache inputs. It still does not resolve mutable image tags, inspect arbitrary build contexts or fully evaluate later Compose overrides. Include relevant source inputs explicitly; do not override protected mounts, gates, commands or environment after validation.

The atomic rename publishes readiness, not an atomic replacement of the entire dependency directory. The protocol does not claim power-loss durability, external writer protection or correctness of an inadequate verifier. The first implementation also does not reclaim old generations automatically. See the CLI cache and startup documentation.


Source: v5 (next.14) · docs/guides/dependency-cache-protocol.md.

On this page