#file-synchronization #config-file #yaml-config #unison #binary-file #sync #bidirectional

app allenap-unison-confgen

Generate Unison configuration files from a YAML description

3 unstable releases

0.2.0 Jun 8, 2024
0.1.1 May 11, 2024
0.1.0 May 10, 2024

#276 in Configuration

Custom license

24KB
208 lines

Allenap's Unison Configuration Generator

This helps me generate my Unison configuration files.

Background

Unison does bidirectional file synchronisation. I write a configuration file, run unison name-of-config-file, and Unison synchronises files between here and there. The there can be another directory, or it can be another machine reachable over SSH. This is typically how I use it.

I have a few machines that I keep in sync. I don't have a central server that serves as a source of truth, so I'm usually syncing back and forth from a few different machines. One day I'll run unison on machine A to sync to machine B, and another day I'll do the reverse, then also to machine C, and combinations of those. It follows that I want to have the same configuration on every machine – so I use Unison itself to sync those configuration files around.

I also have different sets of files that I want to sync. I want to sync dotfiles up to my server in the cloud, but not my financial records. I want to sync my coding projects between my laptop and my Linux machine under the desk, but not my photos.

Principle of operation

This script takes a single configuration file – read from stdin – that describes all the hosts that I have. For example:

# schemes.yaml
hosts:
  alice:
    hostname: alice.example.com
    home: /home/gavin
    sets:
      - common
      - financial
      - github
  bob:
    hostname: bob.example.org
    home: /Users/gavin
    sets:
      - common
      - github
      - photos
  carol:
    hostname: carol.example.net
    home: /home/gavin
    sets:
      - common
      - financial
      - photos

This configuration file describes three hosts – alice, bob, and carol – and the sets of files that each syncs.

When I run unison-confgen < schemes.yaml on alice, two configuration files are generated:

  • alice-bob.prf
  • alice-carol.prf

These contain standard Unison configuration directives to sync between hosts for the sets of files that those hosts have in common.

Now I can type unison alice-bob or unison alice-carol to:

  • sync the common and github sets between alice and bob, or
  • sync the common and financial sets between alice and carol.

Likewise, bob will have bob-alice.prf and bob-carol.prf, and carol will have carol-alice.prf and carol-bob.prf.

What's in a set?

A set file lives in a sets subdirectory. It's named exactly the same as the name of the set; there's no file extension. It contains Unison configuration directives. It doesn't have to describe a set of files, but typically it might look like:

# sets/github
path = GitHub
ignore = Regex GitHub/.*/target
ignore = Name .overmind.sock

Paths, like GitHub above, are resolved relative to the home setting from schemes.yaml.

These set files are included verbatim in the generated configuration files, with one addition: one can use an include directive to include another set file:

# sets/all-code
include github
include gitlab
include projects

Usage

I put the unison-confgen binary on PATH – typically because I've used cargo install to install it, and Cargo's bin directory is already on my PATH.

Then, in ~/.unison, I have the etc/schemes.yaml configuration file, a sets subdirectory, and a Makefile to drive it:

.PHONY: all
all: clean gen

.PHONY: gen
gen:
	@type unison-confgen >/dev/null || cargo install allenap-unison-confgen
	unison-confgen < etc/schemes.yaml

.PHONY: clean
clean:
	@$(RM) $(wildcard *.prf)

.PHONY: complete
complete: clean gen
	@echo $(basename $(wildcard *.prf))

Running make -C ~/.unison clears away all .prf files and regenerates them.

The complete target is useful for shell completion. I use Bash with the following code in my ~/.bashrc:

# Unison stuff
# shellcheck disable=SC2317
_unison() {
    local cur words
    # The current word due for completion.
    cur=${COMP_WORDS[COMP_CWORD]}
    # Non-local array storing the possible completions.
    COMPREPLY=()
    case "${cur}" in
        -*)
            words="$(unison -help | awk '/^ -/ { print $1 }')"
            ;;
        *)
            words="$(make -sC ~/.unison complete)"
            ;;
    esac
    mapfile -t COMPREPLY < <(compgen -W "${words}" -- "${cur}")
    return 0
}

for _unison in $(compgen -c unison)
do
    complete -F _unison "$_unison"
done
unset _unison

# Override local hostname. Useful when WiFi publishes a different
# local domain (<cough>FritzBox</cough>), for example.
if [ -f ~/.unison/hostname ]
then
    read -r UNISONLOCALHOSTNAME < ~/.unison/hostname &&
        export UNISONLOCALHOSTNAME
fi

That's all. It's simple but effective; I've been using it for years. Recently I rewrote it in Rust – from Python – and this is the first time I've published it. Since it does all I need, I probably won't work much on it. Well, maybe a rainy day will see me:

  • Add a proper command-line parser;
  • Move the shell completion code into the Rust binary;
  • Eliminate the need for that Makefile.

But I'm not holding my breath.

Have fun!

License

GNU General Public License 3.0 (or later). See LICENSE.

Dependencies

~3–26MB
~385K SLoC