summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--COPYING14
-rw-r--r--[l---------]Makefile48
-rw-r--r--[l---------]README.md103
-rw-r--r--config.mk.in198
-rwxr-xr-xdrain128
-rwxr-xr-xfill44
-rw-r--r--pristine-etc-keeper.service17
-rw-r--r--zz-pristine-etc-keeper-post-install.hook14
8 files changed, 366 insertions, 200 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..ee7d6a5
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,14 @@
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
+
diff --git a/Makefile b/Makefile
index 4ecdba5..fbde21c 120000..100644
--- a/Makefile
+++ b/Makefile
@@ -1 +1,47 @@
-build-aux/Makefile.README.mk \ No newline at end of file
+# Copyright 2016 Luke Shumaker
+# This work is free. You can redistribute it and/or modify it under the
+# terms of the Do What The Fuck You Want To Public License, Version 2,
+# as published by Sam Hocevar. See the COPYING file for more details.
+
+topsrcdir = .
+topoutdir = .
+include $(topsrcdir)/build-aux/Makefile.head.mk
+
+# The core of pristine-etc-keeper
+std.sys_files += /etc/etckeeper/pristine/drain
+std.sys_files += /etc/etckeeper/pristine/fill
+std.sys_files += /var/lib/pristine-etc/chroot.lock
+std.sys_files += /var/lib/pristine-etc/spool.lock
+
+$(DESTDIR)/etc/etckeeper/pristine/%: $(srcdir)/%
+ install -Dm755 $< $@
+$(DESTDIR)/var/lib/pristine-etc/%.lock:
+ mkdir -p $(@D)
+ touch $@
+
+# Convenience symlinks in bindir
+std.sys_files += /usr/bin/pristine-etc-keeper
+
+$(DESTDIR)/usr/bin/pristine-etc-keeper: $(DESTDIR)/etc/etckeeper/pristine/fill
+ mkdir -p $(@D)
+ ln -srfT $< $@
+
+# pacman integration
+std.sys_files += /usr/share/libalpm/hooks/zz-pristine-etc-keeper-post-install.hook
+
+$(DESTDIR)/usr/share/libalpm/hooks/%.hook: %.hook
+ install -Dm644 $< $@
+
+# systemd integration
+std.sys_files += /usr/lib/systemd/system/pristine-etc-keeper.service
+
+$(DESTDIR)/usr/lib/systemd/system/%: $(srcdir)/%
+ install -Dm644 $< $@
+
+# Documentation
+std.sys_files += /usr/share/doc/pristine-etc-keeper/README.md
+
+$(DESTDIR)/usr/share/doc/pristine-etc-keeper/%: %
+ install -Dm644 $< $@
+
+include $(topsrcdir)/build-aux/Makefile.tail.mk
diff --git a/README.md b/README.md
index 5e5ea4a..df4ae55 120000..100644
--- a/README.md
+++ b/README.md
@@ -1 +1,102 @@
-build-aux/Makefile.README.txt \ No newline at end of file
+The main functionality
+======================
+
+`pristine-etc-keeper` is a program that hooks into `etckeeper` to
+maintain a git branch that is a "pristine" version of `/etc`--just the
+raw files provided by installed packages.
+
+It makes several assumptions that make it a bit less general than
+etckeeper:
+ - the real directory being tracked by etckeeper is `/etc`
+ - the VCS using used is `git`
+ - the system package manager is `pacman`
+
+It works by asking `pacman` which packages are installed, and
+extracting `/etc` files from the cached copies of the package
+tarballs. A litteral pristine version of `/etc` lives at
+`/var/lib/pristine-etc/etc`.
+
+Invocation
+==========
+
+The normal operation of pristine-etc-keeper is for it to be called by
+etckeeper's post-install hook. You usually don't need to worry about
+it.
+
+However, sometimes it may be useful to call it from somewhere else.
+
+The `pristine-etc-keeper` program essentially has two modes of
+operation:
+
+ 1. With arguments: Run, and use "$*" for the commit message.
+
+ 2. No arguments: If a run was previously requested, but never
+ fulfilled; do that now.
+
+Really though, if it has arguments, it requests a run with that commit
+message; then, whether it requested a run or not, it launches another
+process to fulfill any outstanding requests.
+
+If you're thinking "What!? Why request runs? Why not just run
+directly? It's needless complexity!" Instinctively, I'd agree with
+that line of reasoning; but see "The spool" below for justification of
+the complexity.
+
+Implementation details
+======================
+
+The spool
+---------
+
+It would be really simple to just provide a program that you run when
+you want to update the pristine-etc. Unfortunately,
+pristine-etc-keeper is very slow, and this would make many tasks
+painful. So we run it asyncronously.
+
+But that opens a whole other slew of problems. What happens if we try
+to run it again while it is already running? The second instance
+should wait until the first instance is finished. But only one
+instance should be queued at a time; if a 3rd instance tries to start,
+it should just be discarded/merged with the one already waiting
+("coalescing").
+
+A simple thing to do would have been to write a daemon that is always
+running, waiting to receive a signal that it should run. I don't like
+that because I don't really want the process to be long lived, nor for
+it to be started at boot.
+
+But, let's take that idea, tweak it a bit.
+
+Let's conceptualize the "signal" as the invoker adding an item to a
+filesystem spool; each time the daemon runs a job, it empties the
+spool.
+
+Now, we modify this idea by saying that the daemon may exit when the
+spool is empty (ie, it has no work to do, and would just idle-wait
+until it is asked to run again). When the invoker adds an item to the
+spool, we simply have it ensure that the daemon is running.
+
+To translate this into the source code names, the invoker is "fill"
+because it adds an item to the spool, and the so-called-daemon that
+runs the jobs is "drain" because it drains the spool.
+
+Systemd integration
+-------------------
+
+In the above section, we conceptualized the "drain" program as a
+daemon, even though it isn't truly one. This conceptualization is
+useful in administration as well.
+
+So, if systemd is being used, we have it litterally show up as
+pristine-etc-keeper.service. This has a number of benefits:
+
+ - cgroup isolation
+ - uniform logging with other system services
+ - most importantly: systemd v230 and up won't kill a run in progress
+ when you log out
+
+But, if systemd wasn't used to boot the system, then it simply forks
+to the background.
+
+If you'd like to see similar integration with another service manager,
+patches welcome!
diff --git a/config.mk.in b/config.mk.in
deleted file mode 100644
index be95ab4..0000000
--- a/config.mk.in
+++ /dev/null
@@ -1,198 +0,0 @@
-# Copyright (C) 2016-2017 Luke Shumaker
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-# This is based on §7.2 "Makefile Conventions" of the July 25, 2016
-# release of the GNU Coding Standards.
-#
-# Grep for '^##' in this file to see which Autoconf macros it depends
-# on.
-
-dist.pkgname = @PACKAGE_TARNAME@
-gnuconf.pkgname = @PACKAGE_NAME@
-
-# 7.2.2: Utilities in Makefiles
-# -----------------------------
-
-# It's ok to hard-code these commands in rules, but who wants to
-# memorize the list of what's ok?
-
-## AC_PROG_AWK
-## AC_PROG_GREP
-## AC_PROG_EGREP
-## AC_PROG_SED
-
-AWK = @AWK@
-CAT = cat
-CMP = cmp
-CP = cp
-DIFF = diff
-ECHO = echo
-EGREP = @EGREP@
-EXPR = expr
-FALSE = false
-GREP = @GREP@
-INSTALL_INFO = install-info
-LN = ln
-LS = ls
-MKDIR = mkdir
-MV = mv
-PRINTF = printf
-PWD = pwd
-RM = rm
-RMDIR = rmdir
-SED = @SED@
-SLEEP = sleep
-SORT = sort
-TAR = tar
-TEST = test
-TOUCH = touch
-TR = tr
-TRUE = true
-
-# 7.2.2: Utilities in Makefiles/7.2.3: Variables for Specifying Commands
-# ----------------------------------------------------------------------
-
-# Standard user-configurable programs.
-#
-# The list of programs here is specified in §7.2.2, but the associated FLAGS
-# variables are specified in §7.2.3. I found it cleaner to list them together.
-
-## AC_PROG_INSTALL # @INSTALL@ @INSTALL_PROGRAM@ @INSTALL_SCRIPT@ @INSTALL_DATA@
-## AC_PROG_LEX # @LEX@ @LEXLIB@
-## AC_PROG_RANLIB
-## AC_PROG_YACC
-## AC_PROG_CC
-#
-# TODO: What causes Autoconf to define @AR@?
-
-AR = @AR@
-ARFLAGS =
-BISON = bison
-BISONFLAGS =
-CC = @CC@
-CFLAGS = @CFLAGS@ # CFLAGS instead of CCFLAGS
-FLEX = flex
-FLEXFLAGS =
-INSTALL = @INSTALL@
-# There is no INSTALLFLAGS[0]
-LD = ld
-LDFLAGS = @LDFLAGS@
-LDCONFIG = ldconfig # TODO[1]
-LDCONFIGFLAGS =
-LEX = @LEX@
-LFLAGS = #LFLAGS instead of LEXFLAGS
-#MAKE
-MAKEINFO = makeinfo
-MAKEINFOFLAGS =
-RANLIB = @RANLIB@
-RANLIBFLAGS =
-TEXI2DVI = texi2dvi
-TEXI2DVIFLAGS =
-YACC = @YACC@
-YFLAGS = # YFLAGS instead of YACCFLAGS
-
-CPPFLAGS = @CPPFLAGS@
-
-LN_S = @LN_S@
-
-CHGRP = chgrp
-CHGRPFLAGS =
-CHMOD = chmod
-CHMODFLAGS =
-CHOWN = chown
-CHOWNFLAGS =
-MKNOD = mknod
-MKNODFLAGS =
-
-# [0]: There is no INSTALLFLAGS because it would be inconsistent with how the
-# standards otherwise recommend using $(INSTALL); with INSTALL_PROGRAM and
-# INSTALL_DATA; which are specified in a way precluding the use of
-# INSTALLFLAGS. To have the variable, but to ignore it in the common case
-# would be confusing.
-#
-# [1]: The RANLIB and LDCONFIG variables need some extra smarts; §7.2.2 says:
-#
-# > When you use ranlib or ldconfig, you should make sure nothing bad
-# > happens if the system does not have the program in question. Arrange
-# > to ignore an error from that command, and print a message before the
-# > command to tell the user that failure of this command does not mean a
-# > problem. (The Autoconf ‘AC_PROG_RANLIB’ macro can help with this.)
-
-# 7.2.3: Variables for Specifying Commands
-# ----------------------------------------
-
-INSTALL_PROGRAM = @INSTALL_PROGRAM@
-INSTALL_DATA = @INSTALL_DATA@
-
-# 7.2.5: Variables for Installation Directories
-# ---------------------------------------------
-
-# Root for the installation
-prefix = @prefix@
-exec_prefix = @exec_prefix@
-# Executable programs
-bindir = @bindir@
-sbindir = @sbindir@
-libexecdir = @libexecdir@
-# Data files (Autoconf won't support runstatedir until version 2.70)
-datarootdir = @datarootdir@
-datadir = @datadir@
-sysconfdir = @sysconfdir@
-sharedstatedir = @sharedstatedir@
-localstatedir = @localstatedir@
-runstatedir = $(localstatedir)/run
-# Specific types of files
-includedir = @includedir@
-oldincludedir = @oldincludedir@
-docdir = @docdir@
-infodir = @infodir@
-htmldir = @htmldir@
-dvidir = @dvidir@
-pdfdir = @pdfdir@
-psdir = @psdir@
-libdir = @libdir@
-lispdir = $(datarootdir)/emacs/site-lisp
-localedir = @localedir@
-
-mandir = @mandir@
-man1dir = $(mandir)/man1
-man2dir = $(mandir)/man2
-man3dir = $(mandir)/man3
-man4dir = $(mandir)/man4
-man5dir = $(mandir)/man5
-man6dir = $(mandir)/man6
-man7dir = $(mandir)/man7
-man8dir = $(mandir)/man8
-
-manext = .1
-man1ext = .1
-man2ext = .2
-man3ext = .3
-man4ext = .4
-man5ext = .5
-man6ext = .6
-man7ext = .7
-man8ext = .8
-
-# 7.2.7: Install Command Categories
-# ---------------------------------
-
-PRE_INSTALL =
-POST_INSTALL =
-NORMAL_INSTALL =
-
-PRE_UNINSTALL =
-POST_UNINSTALL =
-NORMAL_UNINSTALL =
diff --git a/drain b/drain
new file mode 100755
index 0000000..c201727
--- /dev/null
+++ b/drain
@@ -0,0 +1,128 @@
+#!/usr/bin/env bash
+# Copyright 2016-2017 Luke Shumaker
+# This work is free. You can redistribute it and/or modify it under the
+# terms of the Do What The Fuck You Want To Public License, Version 2,
+# as published by Sam Hocevar. See the COPYING file for more details.
+
+# The user should not call this script directly.
+
+declare -r workdir=/var/lib/pristine-etc
+
+watchdirs=(
+ /etc
+ /usr/share/holo/files
+)
+readonly watchdirs
+
+pacman-watched-name-ver-dirs() {
+ local dir dirs
+ dirs=()
+ for dir in "${watchdirs[@]}"; do
+ if [[ -d "$dir" ]]; then
+ dirs+=("$dir")
+ fi
+ done
+ LC_ALL=C pacman -Qo "${dirs[@]}" | sed -r 's| is owned by | |' |
+ awk '{i=$2" "$3; a[i]=a[i]" "$1} END{for(i in a){print i " " a[i]}}'
+}
+
+pacman-all-name-arch() {
+ LC_ALL=C pacman -Qni | tr $'\n' $'\r' | sed 's/\r\r/\n/g' | sed -r 's|(.*\r)?Name\s*:\s*(\S+)(\r.*)?\rArchitecture\s*:\s*(\S+)\r.*|\2 \4|'
+}
+
+pacman-watched-name-arch-ver-dirs() {
+ join <(pacman-all-name-arch|sort) <(pacman-watched-name-ver-dirs|sort)
+}
+
+commit() (
+ msg="$1"
+
+ cd "$workdir"
+ if ! [[ -d etc.git ]]; then
+ mkdir -p chroot/etc
+ (cd chroot/etc && etckeeper init -d "$PWD")
+ mv chroot/etc/.git etc.git
+ fi
+ rm -rf chroot
+ mkdir chroot
+ cd chroot
+
+ err=false
+ files=()
+ while IFS=' ' read -r pkgname arch pkgver dirs; do
+ file=("/var/cache/pacman/pkg/$pkgname-$pkgver-$arch".pkg.tar.*)
+ if ! test -f "$file"; then
+ printf "ERROR: no cached package for %s %s %s\n" "$pkgname" "$pkgver" "$arch"
+ err=true
+ fi
+ files+=("$file $dirs")
+ done < <(pacman-watched-name-arch-ver-dirs)
+ if $err; then
+ return 1
+ fi
+ for filespec in "${files[@]}"; do
+ read file dirs_str <<<"$filespec"
+ read -a dirs <<<"$dirs_str"
+ printf " -> %s\n" "$file"
+ bsdtar xpvf "$file" "${dirs[@]#/}"
+ done
+
+ ln -srT ../etc.git etc/.git
+
+ if type holo &>/dev/null; then
+ mkdir -p -- run usr/lib
+ ln -sT /usr/lib/holo usr/lib/holo
+ if [ -f /etc/os-release ]; then
+ ln -sT /etc/os-release usr/lib/os-release
+ else
+ ln -sT /usr/lib/os-release usr/lib/os-release
+ fi
+ HOLO_ROOT_DIR=. holo apply
+ fi
+ cd etc/
+ etckeeper update-ignore -d "$PWD"
+ if etckeeper unclean -d "$PWD"; then
+ etckeeper commit -d "$PWD" "$msg"
+ fi
+)
+
+pull() (
+ cd /etc
+ git remote add pristine "${workdir}/chroot/etc" &>/dev/null || true
+ git fetch pristine
+)
+
+lock() {
+ local fd=$1
+ local file=$2
+ eval "exec $fd>"'"$file"'
+ flock "${@:3}" "$fd"
+}
+
+unlock() {
+ local fd=$1
+ exec {fd}>&-
+}
+
+main() {
+ set -e -o pipefail
+ umask 0022
+
+ if ! lock 7 "${workdir}/chroot.lock" -n; then
+ return 0
+ fi
+ while true; do
+ lock 8 "${workdir}/spool.lock"
+ if ! [[ -f "${workdir}/spool" ]]; then
+ return 0
+ fi
+ msg="$(cat "${workdir}/spool")"
+ rm -f "${workdir}/spool"
+ unlock 8
+
+ commit "$msg"
+ pull
+ done
+}
+
+main "$@"
diff --git a/fill b/fill
new file mode 100755
index 0000000..a4704e1
--- /dev/null
+++ b/fill
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# Copyright 2016 Luke Shumaker
+# This work is free. You can redistribute it and/or modify it under the
+# terms of the Do What The Fuck You Want To Public License, Version 2,
+# as published by Sam Hocevar. See the COPYING file for more details.
+
+declare -r workdir=/var/lib/pristine-etc
+declare -r bindir=/etc/etckeeper/pristine
+
+lock() {
+ local fd=$1
+ local file=$2
+ eval "exec $fd>"'"$file"'
+ flock "${@:3}" "$fd"
+}
+
+unlock() {
+ local fd=$1
+ exec {fd}>&-
+}
+
+drain() {
+ if [[ -d /run/systemd/system ]]; then
+ systemctl reset-failed pristine-etc-keeper.service &>/dev/null || true
+ systemctl start pristine-etc-keeper.service
+ else
+ nohup "${bindir}/drain" <&- &> /dev/null &
+ fi
+}
+
+main() {
+ set -e
+ umask 0022
+
+ if [[ $# -gt 0 ]]; then
+ lock 8 "${workdir}/spool.lock"
+ printf '%s\n' "$*" >> "${workdir}/spool"
+ unlock 8
+ fi
+
+ drain
+}
+
+main "$@"
diff --git a/pristine-etc-keeper.service b/pristine-etc-keeper.service
new file mode 100644
index 0000000..4cf4305
--- /dev/null
+++ b/pristine-etc-keeper.service
@@ -0,0 +1,17 @@
+# Copyright 2016 Luke Shumaker
+# This work is free. You can redistribute it and/or modify it under the
+# terms of the Do What The Fuck You Want To Public License, Version 2,
+# as published by Sam Hocevar. See the COPYING file for more details.
+[Unit]
+Description=Update pristine /etc directory
+Documentation=man:pristine-etc-keeper(8)
+DefaultDependencies=no
+Conflicts=shutdown.target
+After=local-fs.target time-sync.target
+Before=shutdown.target
+
+[Service]
+# Can't use Type=oneshot because that would block when starting
+Type=simple
+ExecStart=/etc/etckeeper/pristine/drain
+IOSchedulingClass=idle
diff --git a/zz-pristine-etc-keeper-post-install.hook b/zz-pristine-etc-keeper-post-install.hook
new file mode 100644
index 0000000..4e8b1c6
--- /dev/null
+++ b/zz-pristine-etc-keeper-post-install.hook
@@ -0,0 +1,14 @@
+# pristine-etc-keeper post-install hook for Pacman 5 and newer
+
+[Trigger]
+Operation = Install
+Operation = Upgrade
+Operation = Remove
+Type = File
+Target = etc/* usr/share/holo/files/*
+
+[Action]
+Description = pristine-etc-keeper: post-transaction commit
+When = PostTransaction
+Exec = /etc/etckeeper/pristine/fill 'pacman post-install'
+Depends = pristine-etc-keeper