Skip to content

Instantly share code, notes, and snippets.

@ormaaj
Last active March 21, 2026 14:52
Show Gist options
  • Select an option

  • Save ormaaj/3d0fbe9a88a0c3dfdc4e4e49251d04e6 to your computer and use it in GitHub Desktop.

Select an option

Save ormaaj/3d0fbe9a88a0c3dfdc4e4e49251d04e6 to your computer and use it in GitHub Desktop.
build relative symlink trees using a source tree as a template
#!/usr/bin/python3

import os
import shutil
import logging
from pathlib import Path
from typing import Iterable

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("sync_engine")

class HotConfigSyncer:
    """
    Maps configuration from a source to a live destination.
    """

    def __init__(self, src_root: str | Path, dst_root: str | Path, folds: Iterable[str], masks: Iterable[str]):
        self.src_root = Path(src_root).resolve()
        self.dst_root = Path(dst_root).resolve()
        self.folds = list(folds)
        self.masks = list(masks)

    def _log(self, action: str, path: Path, details: str = ""):
        suffix = f" ({details})" if details else ""
        logger.info(f"[{action:^10}] {path}{suffix}")

    def _is_match(self, path: Path, patterns: list[str]) -> bool:
        """Match relative paths vs globs (e.g. 'postgresql-*', 'sub[gu]id')."""
        return any(path.match(p) for p in patterns)

    def _check_symlink(self, name: str, parent_fd: int, expected_target: str) -> bool:
        """Atomic symlink target compare."""
        try:
            fd = os.open(name, os.O_PATH | os.O_NOFOLLOW, dir_fd=parent_fd)
            try:
                return os.readlink("", dir_fd = fd) == expected_target
            finally:
                os.close(fd)
        except OSError:
            return False

    def _apply_fold(self, name: str, parent_fd: int, target: str, rel_path: Path):
        """Nuke and replace destination directory with a single symlink."""
        if self._check_symlink(name, parent_fd, target):
            self._log("UNCHANGED", rel_path, "already folded")
            return

        full_dst = self.dst_root / rel_path

        if full_dst.exists() or full_dst.is_symlink():
            if full_dst.is_dir() and not full_dst.is_symlink():
                shutil.rmtree(full_dst)
            else:
                os.unlink(name, dir_fd=parent_fd)

        self._log("FOLD", rel_path, f"-> {target}")
        os.symlink(target, name, dir_fd=parent_fd)

    def _apply_explode(self, name: str, parent_fd: int, rel_path: Path):
        """Ensure a real directory exists to traverse into."""
        try:
            os.mkdir(name, dir_fd=parent_fd)
            self._log("EXPLODE", rel_path, "created directory")
        except FileExistsError:
            full_dst = self.dst_root / rel_path
            if not full_dst.is_dir() or full_dst.is_symlink():
                self._log("TRANSITION", rel_path, "file -> directory")
                os.unlink(name, dir_fd=parent_fd)
                os.mkdir(name, dir_fd=parent_fd)

    def _apply_link(self, name: str, parent_fd: int, target: str, rel_path: Path):
        """Symlinks an individual file."""
        if self._check_symlink(name, parent_fd, target):
            return

        full_dst = self.dst_root / rel_path
        try:
            os.unlink(name, dir_fd=parent_fd)
        except FileNotFoundError:
            pass
        except IsADirectoryError:
            shutil.rmtree(full_dst)

        self._log("LINK", rel_path, f"-> {target}")
        os.symlink(target, name, dir_fd=parent_fd)

    def sync(self):
        """Execute config sync"""
        # Anchor destination root for relative fd operations
        dst_root_fd = os.open(self.dst_root, os.O_RDONLY | os.O_DIRECTORY)
        dst_fds = { Path("."): dst_root_fd }

        try:
            for root_str, dirs, files, _ in os.fwalk(self.src_root):
                root = Path(root_str)
                rel_root = root.relative_to(self.src_root)
                parent_dst_fd = dst_fds[rel_root]

                # Process directories in reverse for safe popping
                for i in range(len(dirs) - 1, -1, -1):
                    d_name = dirs[i]
                    rel_path = rel_root / d_name

                    if self._is_match(rel_path, self.masks):
                        self._log("MASKED", rel_path)
                        dirs.pop(i)
                        continue

                    target = os.path.relpath(root / d_name, self.dst_root / rel_root)

                    if self._is_match(rel_path, self.folds):
                        self._apply_fold(d_name, parent_dst_fd, target, rel_path)
                        dirs.pop(i)
                    else:
                        self._apply_explode(d_name, parent_dst_fd, rel_path)
                        # Open + persist the dirfd for its children
                        dst_fds[rel_path] = os.open(d_name, os.O_RDONLY | os.O_DIRECTORY, dir_fd=parent_dst_fd)

                for f_name in files:
                    rel_path = rel_root / f_name
                    if self._is_match(rel_path, self.masks):
                        self._log("MASKED", rel_path)
                        continue

                    target = os.path.relpath(root / f_name, self.dst_root / rel_root)
                    self._apply_link(f_name, parent_dst_fd, target, rel_path)

        finally:
            for fd in dst_fds.values():
                os.close(fd)

def main():
    syncer = HotConfigSyncer(
        src_root="/dotfiles/common/etc",
        dst_root="/etc",
        folds=[
            "NetworkManager", "portage", "ssh", "syslog-ng", "local.d", "X11"
        ],
        masks=[
            "nsswitch.conf", "*shadow", "group", "passwd", "sub[gu]id",
            "postgresql-*", "chromium", "bluetooth", "conky", "cups", "opt"
        ]
    )

    syncer.sync()

main()
[  MASKED  ] chromium
[  MASKED  ] opt
[  MASKED  ] cups
[   FOLD   ] NetworkManager (-> ../dotfiles/common/etc/NetworkManager)
[ EXPLODE  ] network (created directory)
[ EXPLODE  ] mplayer (created directory)
[ EXPLODE  ] openal (created directory)
[  MASKED  ] bluetooth
[  MASKED  ] postgresql-9.6
[ EXPLODE  ] layman (created directory)
[   FOLD   ] local.d (-> ../dotfiles/common/etc/local.d)
[   FOLD   ] X11 (-> ../dotfiles/common/etc/X11)
[ EXPLODE  ] polipo (created directory)
[  MASKED  ] conky
[   FOLD   ] syslog-ng (-> ../dotfiles/common/etc/syslog-ng)
[   FOLD   ] ssh (-> ../dotfiles/common/etc/ssh)
[   FOLD   ] portage (-> ../dotfiles/common/etc/portage)
[ EXPLODE  ] samba (created directory)
[   LINK   ] eix-sync.conf (-> ../dotfiles/common/etc/eix-sync.conf)
[   LINK   ] fstab (-> ../dotfiles/common/etc/fstab)
[   LINK   ] login.defs (-> ../dotfiles/common/etc/login.defs)
[   LINK   ] dracut.conf (-> ../dotfiles/common/etc/dracut.conf)
[   LINK   ] ntp.conf (-> ../dotfiles/common/etc/ntp.conf)
[   LINK   ] timezone (-> ../dotfiles/common/etc/timezone)
[   LINK   ] libao.conf (-> ../dotfiles/common/etc/libao.conf)
[   LINK   ] rc.conf (-> ../dotfiles/common/etc/rc.conf)
[  MASKED  ] subuid
[  MASKED  ] subgid
[   LINK   ] host.conf (-> ../dotfiles/common/etc/host.conf)
[   LINK   ] localtime (-> ../dotfiles/common/etc/localtime)
[  MASKED  ] nsswitch.conf
[   LINK   ] sddm.conf (-> ../dotfiles/common/etc/sddm.conf)
[   LINK   ] xorg.conf (-> ../dotfiles/common/etc/xorg.conf)
[   LINK   ] exports (-> ../dotfiles/common/etc/exports)
[   LINK   ] fuse.conf (-> ../dotfiles/common/etc/fuse.conf)
[   LINK   ] xattr.conf (-> ../dotfiles/common/etc/xattr.conf)
[   LINK   ] shells (-> ../dotfiles/common/etc/shells)
[   LINK   ] sudoers (-> ../dotfiles/common/etc/sudoers)
[   LINK   ] profile2 (-> ../dotfiles/common/etc/profile2)
[   LINK   ] profile (-> ../dotfiles/common/etc/profile)
[   LINK   ] sysctl.conf (-> ../dotfiles/common/etc/sysctl.conf)
[  MASKED  ] passwd
[  MASKED  ] group
[  MASKED  ] gshadow
[   LINK   ] locale.gen (-> ../dotfiles/common/etc/locale.gen)
[   LINK   ] inittab (-> ../dotfiles/common/etc/inittab)
[  MASKED  ] shadow
[   LINK   ] gitconfig (-> ../dotfiles/common/etc/gitconfig)
[   LINK   ] resolv.conf (-> ../dotfiles/common/etc/resolv.conf)
[   LINK   ] bash/bashrc (-> ../../dotfiles/common/etc/bash/bashrc)
[   LINK   ] bash/completion.blacklist (-> ../../dotfiles/common/etc/bash/completion.blacklist)
[   LINK   ] bash/._cfg0000_bashrc (-> ../../dotfiles/common/etc/bash/._cfg0000_bashrc)
[   LINK   ] bash/._cfg0001_bashrc (-> ../../dotfiles/common/etc/bash/._cfg0001_bashrc)
[   LINK   ] bash/._cfg0000_bash_logout (-> ../../dotfiles/common/etc/bash/._cfg0000_bash_logout)
[   LINK   ] bash/bashrc.d/._cfg0000_bash_completion.sh (-> ../../../dotfiles/common/etc/bash/bashrc.d/._cfg0000_bash_completion.sh)
[   LINK   ] bash/bashrc.d/kitty.bash (-> ../../../dotfiles/common/etc/bash/bashrc.d/kitty.bash)
[   LINK   ] bash/bashrc.d/.keep_app-shells_bash-0 (-> ../../../dotfiles/common/etc/bash/bashrc.d/.keep_app-shells_bash-0)
[   LINK   ] bash/bashrc.d/._cfg0000_10-gentoo-title.bash (-> ../../../dotfiles/common/etc/bash/bashrc.d/._cfg0000_10-gentoo-title.bash)
[   LINK   ] bash/bashrc.d/._cfg0001_10-gentoo-title.bash (-> ../../../dotfiles/common/etc/bash/bashrc.d/._cfg0001_10-gentoo-title.bash)
[   LINK   ] bash/bashrc.d/._cfg0000_10-gentoo-color.bash (-> ../../../dotfiles/common/etc/bash/bashrc.d/._cfg0000_10-gentoo-color.bash)
[   LINK   ] samba/.smb.conf.swp (-> ../../dotfiles/common/etc/samba/.smb.conf.swp)
[   LINK   ] samba/smb.conf (-> ../../dotfiles/common/etc/samba/smb.conf)
[   LINK   ] conf.d/zram-init (-> ../../dotfiles/common/etc/conf.d/zram-init)
[   LINK   ] conf.d/net (-> ../../dotfiles/common/etc/conf.d/net)
[   LINK   ] conf.d/nfsmount (-> ../../dotfiles/common/etc/conf.d/nfsmount)
[   LINK   ] conf.d/ntp-client (-> ../../dotfiles/common/etc/conf.d/ntp-client)
[   LINK   ] conf.d/ntpd (-> ../../dotfiles/common/etc/conf.d/ntpd)
[   LINK   ] conf.d/nfs (-> ../../dotfiles/common/etc/conf.d/nfs)
[   LINK   ] conf.d/consolefont (-> ../../dotfiles/common/etc/conf.d/consolefont)
[  MASKED  ] conf.d/postgresql-9.5
[   LINK   ] conf.d/keymaps (-> ../../dotfiles/common/etc/conf.d/keymaps)
[  MASKED  ] conf.d/postgresql-9.6
[   LINK   ] conf.d/postgresql (-> ../../dotfiles/common/etc/conf.d/postgresql)
[   LINK   ] polipo/config.direct (-> ../../dotfiles/common/etc/polipo/config.direct)
[   LINK   ] polipo/config.tor (-> ../../dotfiles/common/etc/polipo/config.tor)
[   LINK   ] grub.d/41_custom (-> ../../dotfiles/common/etc/grub.d/41_custom)
[   LINK   ] grub.d/40_custom (-> ../../dotfiles/common/etc/grub.d/40_custom)
[   LINK   ] grub.d/30_os-prober (-> ../../dotfiles/common/etc/grub.d/30_os-prober)
[   LINK   ] grub.d/20_linux_xen (-> ../../dotfiles/common/etc/grub.d/20_linux_xen)
[   LINK   ] grub.d/10_linux (-> ../../dotfiles/common/etc/grub.d/10_linux)
[   LINK   ] grub.d/00_header (-> ../../dotfiles/common/etc/grub.d/00_header)
[   LINK   ] grub.d/README (-> ../../dotfiles/common/etc/grub.d/README)
[   LINK   ] env.d/999custom (-> ../../dotfiles/common/etc/env.d/999custom)
[   LINK   ] env.d/02locale (-> ../../dotfiles/common/etc/env.d/02locale)
[   LINK   ] env.d/50baselayout (-> ../../dotfiles/common/etc/env.d/50baselayout)
[ EXPLODE  ] polkit-1/conf.d (created directory)
[   LINK   ] polkit-1/rules.d/49-polkit-pkla-compat.rules (-> ../../../dotfiles/common/etc/polkit-1/rules.d/49-polkit-pkla-compat.rules)
[   LINK   ] polkit-1/rules.d/50-default.rules (-> ../../../dotfiles/common/etc/polkit-1/rules.d/50-default.rules)
[   LINK   ] polkit-1/rules.d/10-local.rules (-> ../../../dotfiles/common/etc/polkit-1/rules.d/10-local.rules)
[   LINK   ] polkit-1/conf.d/10-local.conf (-> ../../../dotfiles/common/etc/polkit-1/conf.d/10-local.conf)
[   LINK   ] layman/layman.cfg (-> ../../dotfiles/common/etc/layman/layman.cfg)
[   LINK   ] udev/rules.d/99-myrules.rules (-> ../../../dotfiles/common/etc/udev/rules.d/99-myrules.rules)
[   LINK   ] openal/alsoft.conf (-> ../../dotfiles/common/etc/openal/alsoft.conf)
[   LINK   ] mplayer/mplayer.conf (-> ../../dotfiles/common/etc/mplayer/mplayer.conf)
[ EXPLODE  ] network/if-down.d (created directory)
[ EXPLODE  ] network/if-pre-up.d (created directory)
[ EXPLODE  ] network/if-post-down.d (created directory)
[ EXPLODE  ] network/interfaces.d (created directory)
[ EXPLODE  ] network/if-up.d (created directory)
[   LINK   ] network/interfaces (-> ../../dotfiles/common/etc/network/interfaces)
[   LINK   ] network/ifupdown-ng.conf (-> ../../dotfiles/common/etc/network/ifupdown-ng.conf)
[   LINK   ] network/if-up.d/avahi-autoipd (-> ../../../dotfiles/common/etc/network/if-up.d/avahi-autoipd)
[   LINK   ] network/if-up.d/ethtool (-> ../../../dotfiles/common/etc/network/if-up.d/ethtool)
[   LINK   ] network/if-up.d/wpasupplicant (-> ../../../dotfiles/common/etc/network/if-up.d/wpasupplicant)
[   LINK   ] network/interfaces.d/setup (-> ../../../dotfiles/common/etc/network/interfaces.d/setup)
[   LINK   ] network/if-post-down.d/wireless-tools (-> ../../../dotfiles/common/etc/network/if-post-down.d/wireless-tools)
[   LINK   ] network/if-post-down.d/wpasupplicant (-> ../../../dotfiles/common/etc/network/if-post-down.d/wpasupplicant)
[   LINK   ] network/if-post-down.d/vde2 (-> ../../../dotfiles/common/etc/network/if-post-down.d/vde2)
[   LINK   ] network/if-post-down.d/bridge (-> ../../../dotfiles/common/etc/network/if-post-down.d/bridge)
[   LINK   ] network/if-pre-up.d/ethtool (-> ../../../dotfiles/common/etc/network/if-pre-up.d/ethtool)
[   LINK   ] network/if-pre-up.d/wireless-tools (-> ../../../dotfiles/common/etc/network/if-pre-up.d/wireless-tools)
[   LINK   ] network/if-pre-up.d/wpasupplicant (-> ../../../dotfiles/common/etc/network/if-pre-up.d/wpasupplicant)
[   LINK   ] network/if-pre-up.d/vde2 (-> ../../../dotfiles/common/etc/network/if-pre-up.d/vde2)
[   LINK   ] network/if-pre-up.d/bridge (-> ../../../dotfiles/common/etc/network/if-pre-up.d/bridge)
[   LINK   ] network/if-down.d/avahi-autoipd (-> ../../../dotfiles/common/etc/network/if-down.d/avahi-autoipd)
[   LINK   ] network/if-down.d/wpasupplicant (-> ../../../dotfiles/common/etc/network/if-down.d/wpasupplicant)
[   LINK   ] network/if-down.d/bridge (-> ../../../dotfiles/common/etc/network/if-down.d/bridge)
[   LINK   ] security/passwdqc.conf (-> ../../dotfiles/common/etc/security/passwdqc.conf)
[   LINK   ] eixrc/00-eixrc (-> ../../dotfiles/common/etc/eixrc/00-eixrc)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment