You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Reid 'arrdem' McKenzie e5b23aa480 [automated] Release version 0.2.3-1 2 days ago
src/python [automated] Release version 0.2.3-1 2 days ago
test/integration Implement single-file packages 2 days ago
tools Implement single-file packages 2 days ago
.envrc Create test helpers 2 weeks ago
.gitignore Extract cram from source@438df41 2 months ago
BUILD Extract cram from source@438df41 2 months ago
LICENSE Forgot the LICENSE file 2 months ago Extract cram from source@438df41 2 months ago
WORKSPACE Extract cram from source@438df41 2 months ago Fix the tests 2 days ago Automate releases 2 weeks ago


To force (people or things) into a place or container that is or appears to be too small to contain them.

An alternative to GNU Stow, more some notion of packages with dependencies and install scripts.

Think an Ansible, Puppet or even NixOS but anyarch and lite enough to check in with your dotfiles.


Cram operates on a directory of packages called packages.d/, and two directories of metapackages called profiles.d and hosts.d.


A Cram package consists of a directory containing a pkg.toml file with the following format -

version = 1

  # The package.require list names depended artifacts.
  name = "packages.d/some-other-package"

  # (optional) The list enumerates either
  # inline scripts or script files. These are run as a
  # package is 'built' before it is installed.
  run = "some-build-command"

  # (optional) Hook script(s) which occur before installation.
  run = "some-hook"

  # (optional) Override installation scrpt(s).
  # By default, everthing under the package directory
  # (the `pkg.toml` excepted) treated is as a file to be
  # installed and stow is emulated using symlinks.
  run = "some-install-command"

  # (optional) Hook script(s) which after installation.
  run = "some-other-hook"

To take a somewhat real example from my own dotfiles -

$ tree -a packages.d/tmux
├── pkg.toml
└── .tmux.conf

This TMUX package provides only my .tmux.conf file, and a stub pkg.toml that does nothing. A fancier setup could use pkg.toml to install TMUX either as a pre_install task or by using a separate TMUX package and providing the config in a profile.


Writing lots of packages gets cumbersome quickly, as does managing long lists of explicit dependencies. To try and manage this, Cram provides metapackages - packages which contain no stowable files, but instad contain subpackages.

To take a somewhat real example from my own dotfiles -

$ tree -a -L 1 profiles.d/macos
├── pkg.toml
├── emacs/
├── homebrew/
└── zsh/

The profiles.d/macos package depends AUTOMATICALLY on the contents of the profiles.d/macos/emacs, profiles.d/macos/homebrew and profiles.d/macos/zsh packages, which are normal packages. These sub-packages can have normal dependencies on other packages both within and without the profile and install files or run scripts.

Profiles allow users to write groups of related packages, especially configs, which go together and allows for scoped reuse of meaningful names.

Likewise the hosts.d/ tree allows users to store host-specific packages.


$ cram apply [--dry-run|--execute] [--optimize] [--require <package>] <configdir> <destdir>

The apply task applies a configuration to a destination directory. The most common uses of this would be --dry-run (the default), which functions as a diff or --execute ~/conf ~/ for emulating Stow and installing dotfiles.

By default cram installs two packages - profiles.d/default and hosts.d/$(hostname -s). This default can be overriden by providing --require <package> one or more times to enumerate specific packages to install.

Cram always reads the .cram.log state file and diffs the current state against the configured state. Files and directories no longer defined by the configured state are cleaned up automatically.

$ cram state <configdir>

The state task loads up and prints the .cram.log state file generated by any previous cram apply --execute so you can read a manifest of what cram thinks it did. This is useful because cram attempts to optimize repeated executions and implement change detection using the state file.

This cache can be busted if needed by using apply --execute --no-optimize, which will cause cram to take all actions it deems presently required. This can result in dangling symlinks in the filesystem.

$ cram list <configdir> [package]

The list task lists out all available packages (eg. packages, profiles, hosts, and subpackages) as a dependency graph. When provided a specific package, the details of that package (its requirements and installation task log) will be printed.


Copyright Reid 'arrdem' McKenzie, 15/02/2022.

Published under the terms of the Anticapitalist Software License (

Unlimited commercial licensing is available at nominal pricing.