Reid 'arrdem' McKenzie
||5 months ago|
|hosts.d/demo||5 months ago|
|packages.d||5 months ago|
|profiles.d||5 months ago|
|.gitignore||5 months ago|
|README.md||5 months ago|
|cram||5 months ago|
Hello and welcome to a workable demo repo of Cram; arrdem's personal dotfiles manager.
./cram file in this repo) is a tiny package manager.
Cram doesn't know anything about
$XDG_CONFIG_DIR or your OS's package manager or even dotfiles at all.
What cram does know about is
TO Cram, a package is a directory under
packages.d (this is hardcoded).
Such a directory may contain a
pkg.toml file, which as with other package file formats may describe dependencies, preparation, installation and post-install steps.
An example of such a file is as follows -
[cram] version = 1 [package] # The package.require list names depended artifacts. [[package.require]] name = "packages.d/some-other-package" # (optional) The package.build list enumerates either # inline scripts or script files. These are run as a # package is 'built' before it is installed. [[package.build]] run = "some-build-command" # (optional) Hook script(s) which occur before installation. [[package.pre_install]] 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. [[package.install]] run = "some-install-command" # (optional) Hook script(s) which after installation. [[package.post_install]] run = "some-other-hook"
Managing files with cram
Cram is used to "apply" changes to a directory under management.
The conventional incantation for this is
./cram apply ~/conf ~/, for managing a home directory or dotfiles.
Let's look at the usage -
$ ./cram apply --help Usage: __main__.py apply [OPTIONS] CONFDIR DESTDIR The entry point of cram. Options: --execute / --dry-run --force / --no-force --state-file PATH --optimize / --no-optimize --require TEXT --exec-idempotent / --exec-always --help Show this message and exit.
By default, Cram will "require" the following packages:
But you can override this by passing
For the purposes of this demo, we will just install the zsh package.
Don't worry, we aren't going to actually install anything here.
$ ./cram apply --dry-run --require packages.d/fake . ~/ 2022-07-28 22:25:26,521 - __main__ - WARNING - No previous statefile .cram.log - mkdir ~/.config - chmod ~/.config 16877 - mkdir ~/.config/fake - chmod ~/.config/fake 16877 - link ./packages.d/fake/.config/fake/b.conf ~/.config/fake/b.conf - link ./packages.d/fake/.config/fake/a.conf ~/.config/fake/a.conf
--dry-run (which is also the default behavior) instructed cram to figure out what to do, but not to do anything.
This is a changelog of commands which Cram is proposing to execute against your filesystem.
All of these commands are generated by the default
stow installer which the zsh package happens to use, and produce an installed state.
Were you to use
apply --execute, Cram would go ahead and make these changes.
No previous statefile warning is the secret sauce of Cram.
Cram works in terms not just of this log of what changes it will make, but in terms of a persisted log of what changes it has made.
This allows Cram to optimize repeated executions to remove installation steps that haven't changed, while still retaining a precise log of how to get where you are now from an empty slate.
This also allows Cram, unlike other dotfile managers, to clean up after itself.
Let's do a real demo of this.
$ ./cram apply --execute --require packages.d/fake . ~/ - mkdir ~/.config - chmod ~/.config 16877 - mkdir ~/.config/fake - chmod ~/.config/fake 16877 - link ./packages.d/fake/.config/fake/a.conf ~/.config/fake/a.conf - link ./packages.d/fake/.config/fake/b.conf ~/.config/fake/b.conf
So now we've got two files and a couple directories on the filesystem we may or may not want. We can see the record of this state as follows -
$ ./cram state . - mkdir ~/.config - chmod ~/.config 16877 - mkdir ~/.config/fake - chmod ~/.config/fake 16877 - link ./packages.d/fake/.config/fake/a.conf ~/.config/fake/a.conf - link ./packages.d/fake/.config/fake/b.conf ~/.config/fake/b.conf
Were you to delete a file, say
rm packages.d/fake/.config/a.conf and then inspect changes -
$ rm packages.d/fake/.config/a.conf $ ./cram apply --require packages.d/fake . ~/ - unlink ~/.config/fake/a.conf
What cram did here was compute what the install steps for the current state would be, compare that with the PREVIOUSLY EXECUTED steps, identify a file that is no longer to be installed, and include removing that file in the new plan.
And if we
apply --execute our changes, note that the state file DOES NOT include the
unlink cleanup instruction.
It only contains the steps required to produce the now-current state
$ ./cram apply --execute --require packages.d/fake . ~/ - unlink ~/.config/fake/a.conf $ ./cram state . - mkdir ~/.config - chmod ~/.config 16877 - mkdir ~/.config/fake - chmod ~/.config/fake 16877 - link ./packages.d/fake/.config/fake/b.conf ~/.config/fake/b.conf
Managing software with Cram
Okay so that gives you a good idea of how to manage files with Cram packages. Software is a bit trickier. Let's look at how homebrew would be installed -
$ ./cram list . packages.d/homebrew packages.d/homebrew: (PackageV1) requires: log: - exec /tmp ('/bin/sh', PosixPath('/tmp/stow/e5d3a54761ee43023832d565e11ec4661b84f4ec66629042674b6658993e8cb8.sh'))
Not super helpful - let's take a look at the
$ cat packages.d/homebrew/pkg.toml [cram] version = 1 [package] require =  [[package.install]] run = "[ ! -e /opt/homebrew/bin/brew ] && /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
Managing larger configurations with cram
We've hinted a couple times at the
package.require feature of Cram packages.
But that doesn't tell you too much about how to organize larger configurations.
A profile is a directory under
hosts.d which may but need not have a
pkg.toml specifying requirements.
Where a package is fundamentally a set of installation directives, a profile is a group of packages.
profiles.d/emacs/doom-emacs is a package of configurations specific to the Emacs package.
The tricky bit is that a profile IMPLICITLY requires all its subpackages.
This is useful for profile and host specific packages - you don't have to have a bunch of
macos-foo packages running around, they could live in
profiles.d/macos/* and then a given MacOS host can depend on
profiles.d/macos to grab all the relevant configuration.
hosts.d/demo host provides an example of this pattern by depending on the
work profiles as meta-packages.
To see how the
demo host would be installed,
./cram apply --require profiles.d/default --require hosts.d/demo . ~/ would do the trick.
Limitations of Cram
- Cram does not do static analysis or sandboxing of your
run =scripts and assumes they're idempotent.
- Cram does not a templating engine.
- Cram does not have an inventory or data system.
- It can be difficult to
- Cram does not have a conditional dependency system - such as detecting the platform OS and varying requirements accordingly.
- Cram is not recursive and does not support nested transactions. It's a one-shot system.
- Cram assumes that if you have files or directories which CONFLICT with an
apply --executeand are not managed that you mean to delete them.
- Cram can't tell that there are unmanaged files or directories alongside a directory you're removing from config.
- Cram can't (easily) create symlinks that aren't pointers back to packaged files.
- Cram has no concept of a package manager or providers; can't abstract over where you're going to get
- Cram has no pattern for depending on remote packages. Everything must be vendored into your repo.
- Cram cannot be configured. You get
hosts.d. That's it.
- Cram cannot be configured. There is no
host.d/default, you get
host.d/$HOSTNAMEor an error.
- Cram cannot be configured and uses
- Cram cannot be configured and uses a new
/bin/shfor each script; this prevents
export PATH=from functioning and may impede installing user-local package managers or leveraging user-local software.
- Cram uses
.cram.logas a state file, not
~/.local/cram/cram.logor some other harder to
git clean -fdxstate.
- Cram maybe should know something about the XDG directory conventions and common dotfiles.
- There is no
cram packagecommand for turning a directory tree into a cram package.
- There is no
cram checkcommand for self-validating the state of your config repo.
- There is no
cram untrackedcommand for identifying cram-unmanaged files inhabiting cram-managed directories.
- The name
cramis contended and a new one may be chosen.
- The concept of a
profileis somewhat backwards - a real
groupmay be introduced with
profilesbecoming configuration packages.
- Cram will probably break while you use it, be prepared to
unzip ./cramand edit the source.
cram fmtdirective produces pretty garbage output.
Copyright Reid 'arrdem' McKenzie, 28/07/2022.
Published under the terms of the Anticapitalist Software License (https://anticapitalist.software).
Unlimited commercial licensing is available at nominal pricing.