A minimal script that deploys and undeploys file trees via symlink or copy without configuration.
# Deploy SOURCE tree to DESTINATION via symlink.
# Symlinks to directories in SOURCE are dereferenced recursively.
emplacetree ln SOURCE DESTINATION
# Deploy SOURCE tree to DESTINATION by copying.
# Symlinks to directories in SOURCE are dereferenced recursively.
emplacetree cp SOURCE DESTINATION
# If any node of DESTINATION corresponding to a node in SOURCE has changed, then
# do nothing and report the difference. Otherwise...
# Remove every corresponding node from DESTINATION and prune empty directories
# recursively.
emplacetree rm SOURCE DESTINATION
# List relative paths to leaves in SOURCE.
emplacetree ls SOURCE
There is zero configuration involved, beyond choosing whether to symlink or copy the files. Behaviour is unapologetically opinionated, chosen to be as simple as I could imagine.
I used stow and then xstow for years to deploy my dotfiles, but kept running into issues because their behaviour was often more complicated than I needed (not to mention leading to outstanding bugs). I understand that working with symlinks requires some care, especially if you want to optimise. However, for me, this nuance came at the cost of me not being confident that the tool would do what I wanted, or safely. I have designed this tool to be as minimal as possible, such that it is able to deploy my nix-derived dotfiles and not necessarily more.
To elaborate, here were my main priorities when designing the tool:
- Deploy
SOURCE
to aDESTINATION
that may contain additional files not inSOURCE
. - Refuse to clobber anything. Refuse to delete anything that does not appear in
SOURCE
. - When a conflict does occur, do nothing else.
- Always behave the same, regardless of whether a target directory already
exists in
DESTINATION
. - Clearly differentiate between directory-like and leaf-like files. Deployed directories should be ordinary directories (never symlinks) owned by the current user. Deployed symlinks to leaves should be absolute symlinks.
- Both deploy and undeploy operations should be idempotent.
Since a major goal of this script was to require and allow zero configuration, some opinionated decisions had to be made. This section attempts to make clear what those decisions were and why I made them.
Do we recursively dereference symlinks to directories?
Yes.
In order to deploy alongside an already nonempty DESTINATION
tree,
clearly we must dereference symlinks to directories sometimes. In order to have
deterministic behaviour that does not depend on whether destination directories
already exist, we must always dereference.
Relative or absolute symlinks?
Absolute.
Why not? Absolute symlinks are easier to inspect, especially when
SOURCE
and DESTINATION
are disjoint. The only time I personally prefer
relative symlinks is when I'm linking within a Git repository. But why would
anyone clone a tree within a Git repository? If you have a good reason, let me
know!
How to handle conflicts?
Do not clobber, unless the destination is equivalent to its source.
Obviously, we want the default behaviour to avoid clobbering files. One possibility was to never clobber, even if the destination is semantically equivalent to the source. I decided instead to proceed successfully if the destination is equivalent, in order to make the tool idempotent.
Let the user exclude files?
No.
That would be configuration, and we refuse to support configuration. If you wish
to avoid deploying some files, then you must remove them from the SOURCE
tree.
Must the destination root
DESTINATION
exist?
No, create DESTINATION
if it does not exist.
This avoids treating the root directory as a special case; in general, we create
any path components as necessary. One downside is that it does not prevent you
from making a little typo and deploying the entire tree to a new erroneous path.
On the other hand, such an error is easily corrected by running emplacetree rm
on the mistaken root.
- The canonical path of a given path is obtained by following symlinks in
every component of the path until no initial segment of the path is a symlink.
We use the implementation
realpath -e
. - Two paths are equivalent when their canonical paths are equal or they have identical contents.
- A leaf is a node whose canonical path is a regular file. That is, it is a regular file or a symlink to one and not a directory, symlink to directory, nor special file.
- A directory-like node is path whose canonical path is a directory, i.e. a directory or symlink to one.
- Given a path
R/P
inside a tree, whereR
isSOURCE
orDESTINATION
, we callP
the relative path. - For a pair
(SOURCE/P, DESTINATION/P)
, we call the former the source and the latter the destination.
All commands run the following checks before proceeding:
- If
SOURCE
orDESTINATION
exists but is not directory-like, then abort with status4
. - If the destination of any directory-like source exists but is not a directory,
then abort with status
3
. - If the destination of any leaf exists but is not equivalent to its source,
then abort with status
2
.
Symlink DESTINATION
leaves to SOURCE
leaves.
- For every directory-like source
SOURCE/P
that does not already exist, create the directoryDESTINATION/P
as the current user. - For every source leaf
SOURCE/P
, create a symlink pointing fromDESTINATION/P
toSOURCE/P
.
Copy SOURCE
leaves to DESTINATION
leaves.
- For every directory-like source
SOURCE/P
that does not already exist, create the directoryDESTINATION/P
as the current user. - For every source leaf
SOURCE/P
, copySOURCE/P
toDESTINATION/P
. Preserve mode, but set the destination's ownership to the current user and group.
Remove the largest partial embedding of SOURCE
from DESTINATION
.
- For every source leaf
SOURCE/P
, removeDESTINATION/P
. - For every directory-like source
SOURCE/P
, removeDESTINATION/P
if and only if it is empty.
List leaves.
- Report the relative paths of all leaves in
SOURCE
in a\n
-delimited list. - No path in the list is empty.
Tip: If you desire full paths instead, then do something like
emplacetree ls SOURCE | rargs echo SOURCE/{}
Dependencies:
The preferred use is as a nix flake. Therefore, the file emplacetree.sh
is
written under the assumption that you build the script using nixpkgs
writeShellApplication
. However, one should be able to
run the script with bash, assuming you have the dependencies in your path.
As of yet, I have been lazy about testing. So YMMV. Please feel free to report issues.
- The script is technically not atomic.
emplacetree rm
prunes below the destination root if empty.