[pacman-dev] [PATCH v4] libalpm: Add support for trigger dropins

Daan De Meyer daan.j.demeyer at gmail.com
Sat Aug 22 13:12:45 UTC 2020


In some scenarios, instead of adding their own hooks, packages want to
augment hooks already installed by other packages with extra triggers.
Currently, this requires modifying the existing hook file which is error
prone and hard to manage for package managers.

A concrete example where packages would want to extend an existing hook
is using systemd's kernel-install script on Arch Linux. An example
pacman hook for kernel-install could be the following:

```
[Trigger]
Operation = Install
Operation = Upgrade
Type = Path
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/kernel/install.d/*

[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = systemd

[Action]
Description = Adding kernel and initramfs images to /boot...
When = PostTransaction
Exec = /etc/pacman.d/scripts/mkosi-kernel-add
NeedsTargets
```

This hook would run on installation and upgrades of kernel packages and
every package that installs kernel-install scripts (which includes
dracut and mkinitcpio). It's also fairly generic in that it doesn't
include specifics of other packages except its source package (and
implicitly the install directory of kernel packages).

However, both mkinitcpio and dracut use the concept of hooks that
can be installed by other packages to extend their functionality. In
mkinitcpio these are stored in /usr/lib/initcpio. When a package is
installed that installs extra hooks for mkinitcpio, the kernel-install
hook will not trigger. We can fix this by adding /usr/lib/initcpio/* to
the kernel-install pacman hook but this requires either modifying the
kernel-install hook when installing mkinitpcio or adding all possible
directories for all possible packages that want to add triggers to the
kernel-install hook in the systemd package itself. Neither of these are
attractive solutions. The first one isn't because we'd have to modify
the hook when installing packages, the second isn't because it would be
a huge maintenance burden and doesn't include for AUR or private
repository packages.

Instead, this commit adds support for drop-in directories that allow
extra triggers to be added in separate files. When parsing hooks, we now
also look in a .d directory for extra triggers (for mkinitcpio.hook, we
look in mkinitcpio.hook.d). Trigger files are required to have the
.trigger extension and follow the same ini format as regular hooks
files. However, only [Trigger] sections are allowed.

Drop-in directories for a hook can be put in any of the existing hook
search directories. The same override rules that apply for normal hooks
files apply for drop-in files as well.

With this feature, packages can now install extra triggers for an
existing hook into the trigger dropin directory for that package. These
extra triggers can be tracked by the package manager and uninstalled
along with the package.

Signed-off-by: Daan De Meyer <daan.j.demeyer at gmail.com>
---
 doc/alpm-hooks.5.asciidoc |   9 +-
 lib/libalpm/hook.c        | 267 ++++++++++++++++++++++++++++++++++++--
 lib/libalpm/hook.h        |   2 +
 3 files changed, 268 insertions(+), 10 deletions(-)

diff --git a/doc/alpm-hooks.5.asciidoc b/doc/alpm-hooks.5.asciidoc
index 916d43bb..caa55a79 100644
--- a/doc/alpm-hooks.5.asciidoc
+++ b/doc/alpm-hooks.5.asciidoc
@@ -38,6 +38,12 @@ linkman:pacman.conf[5] (the default is +{sysconfdir}/pacman.d/hooks+).  The
 file names are required to have the suffix ".hook".  Hooks are run in
 alphabetical order of their file name, where the ordering ignores the suffix.
 
+Extra triggers can be added in drop-in directories in the configured hook
+directories. Drop-in directories for a hook must be named after the hook with
+the ".d" suffix added. For example, "dracut.hook.d" would be the drop-in
+directory for "dracut.hook". Trigger files in drop-in directories are required
+to have the suffix ".trigger" and can only contain '[Trigger]' sections.
+
 TRIGGERS
 --------
 
@@ -96,7 +102,8 @@ OVERRIDING HOOKS
 
 Hooks may be overridden by placing a file with the same name in a higher
 priority hook directory.  Hooks may be disabled by overriding them with
-a symlink to '/dev/null'.
+a symlink to '/dev/null'. The same logic applies to trigger files in drop-in
+directories.
 
 EXAMPLES
 --------
diff --git a/lib/libalpm/hook.c b/lib/libalpm/hook.c
index aca8707e..19963dab 100644
--- a/lib/libalpm/hook.c
+++ b/lib/libalpm/hook.c
@@ -44,6 +44,7 @@ struct _alpm_trigger_t {
 	enum _alpm_hook_op_t op;
 	enum _alpm_trigger_type_t type;
 	alpm_list_t *targets;
+	char *name;
 };
 
 struct _alpm_hook_t {
@@ -66,6 +67,7 @@ static void _alpm_trigger_free(struct _alpm_trigger_t *trigger)
 {
 	if(trigger) {
 		FREELIST(trigger->targets);
+		free(trigger->name);
 		free(trigger);
 	}
 }
@@ -146,16 +148,16 @@ static int _alpm_hook_validate(alpm_handle_t *handle,
 	return ret;
 }
 
-static int _alpm_hook_parse_cb(const char *file, int line,
+#define error(...) _alpm_log(handle, ALPM_LOG_ERROR, __VA_ARGS__); return 1;
+#define warning(...) _alpm_log(handle, ALPM_LOG_WARNING, __VA_ARGS__);
+
+static int _alpm_trigger_parse_cb(const char *file, int line,
 		const char *section, char *key, char *value, void *data)
 {
 	struct _alpm_hook_cb_ctx *ctx = data;
 	alpm_handle_t *handle = ctx->handle;
 	struct _alpm_hook_t *hook = ctx->hook;
 
-#define error(...) _alpm_log(handle, ALPM_LOG_ERROR, __VA_ARGS__); return 1;
-#define warning(...) _alpm_log(handle, ALPM_LOG_WARNING, __VA_ARGS__);
-
 	if(!section && !key) {
 		error(_("error while reading hook %s: %s\n"), file, strerror(errno));
 	} else if(!section) {
@@ -166,8 +168,6 @@ static int _alpm_hook_parse_cb(const char *file, int line,
 			struct _alpm_trigger_t *t;
 			CALLOC(t, sizeof(struct _alpm_trigger_t), 1, return 1);
 			hook->triggers = alpm_list_add(hook->triggers, t);
-		} else if(strcmp(section, "Action") == 0) {
-			/* no special processing required */
 		} else {
 			error(_("hook %s line %d: invalid section %s\n"), file, line, section);
 		}
@@ -205,6 +205,37 @@ static int _alpm_hook_parse_cb(const char *file, int line,
 		} else {
 			error(_("hook %s line %d: invalid option %s\n"), file, line, key);
 		}
+	}
+
+	return 0;
+}
+
+static int _alpm_hook_parse_cb(const char *file, int line,
+		const char *section, char *key, char *value, void *data)
+{
+	struct _alpm_hook_cb_ctx *ctx = data;
+	alpm_handle_t *handle = ctx->handle;
+	struct _alpm_hook_t *hook = ctx->hook;
+
+	if(!section && !key) {
+		error(_("error while reading hook %s: %s\n"), file, strerror(errno));
+	} else if(!section) {
+		error(_("hook %s line %d: invalid option %s\n"), file, line, key);
+	} else if(!key) {
+		/* beginning a new section */
+		if(strcmp(section, "Trigger") == 0) {
+			if (_alpm_trigger_parse_cb(file, line, section, key, value, data) != 0) {
+				return 1;
+			}
+		} else if(strcmp(section, "Action") == 0) {
+			/* no special processing required */
+		} else {
+			error(_("hook %s line %d: invalid section %s\n"), file, line, section);
+		}
+	} else if(strcmp(section, "Trigger") == 0) {
+		if (_alpm_trigger_parse_cb(file, line, section, key, value, data) != 0) {
+			return 1;
+		}
 	} else if(strcmp(section, "Action") == 0) {
 		if(strcmp(key, "When") == 0) {
 			if(hook->when != 0) {
@@ -249,12 +280,12 @@ static int _alpm_hook_parse_cb(const char *file, int line,
 		}
 	}
 
-#undef error
-#undef warning
-
 	return 0;
 }
 
+#undef error
+#undef warning
+
 static int _alpm_hook_trigger_match_file(alpm_handle_t *handle,
 		struct _alpm_hook_t *hook, struct _alpm_trigger_t *t)
 {
@@ -466,6 +497,17 @@ static alpm_list_t *find_hook(alpm_list_t *haystack, const void *needle)
 	return NULL;
 }
 
+static alpm_list_t *find_trigger(alpm_list_t *haystack, const void *needle) {
+	while (haystack) {
+		struct _alpm_trigger_t *t = haystack->data;
+		if (t && t->name && strcmp(t->name, needle) == 0)  {
+			return haystack;
+		}
+		haystack = haystack->next;
+	}
+	return NULL;
+}
+
 static ssize_t _alpm_hook_feed_targets(char *buf, ssize_t needed, alpm_list_t **pos)
 {
 	size_t remaining = needed, written = 0;;
@@ -529,6 +571,209 @@ static int _alpm_hook_run_hook(alpm_handle_t *handle, struct _alpm_hook_t *hook)
 	}
 }
 
+static int _alpm_hook_parse_drop_in_one(alpm_handle_t *handle,
+		struct _alpm_hook_t *hook, const char *dir)
+{
+	char path[PATH_MAX];
+	size_t dirlen;
+	struct dirent *entry;
+	DIR *d;
+	size_t suflen = strlen(ALPM_TRIGGER_SUFFIX);
+	int ret = 0;
+
+	if((dirlen = strlen(dir)) + 1 >= PATH_MAX) {
+		_alpm_log(handle, ALPM_LOG_ERROR,
+				_("could not open drop-in directory: %s: %s\n"), dir,
+				strerror(ENAMETOOLONG));
+		return -1;
+	}
+	memcpy(path, dir, dirlen + 1);
+
+	if(!(d = opendir(path))) {
+		if(errno == ENOENT) {
+			return 0;
+		} else {
+			_alpm_log(handle, ALPM_LOG_ERROR,
+					_("could not open drop-in directory: %s: %s\n"), path,
+					strerror(errno));
+			return -1;
+		}
+	}
+
+	while((errno = 0, entry = readdir(d))) {
+		struct _alpm_hook_cb_ctx ctx = { handle, hook };
+		size_t name_len;
+		struct stat buf;
+
+		if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
+			continue;
+		}
+
+		if((name_len = strlen(entry->d_name)) >= PATH_MAX - dirlen - 1) {
+			_alpm_log(handle, ALPM_LOG_ERROR, _("could not open file: %s%s: %s\n"),
+					path, entry->d_name, strerror(ENAMETOOLONG));
+			ret = -1;
+			continue;
+		}
+
+		/* Make sure path ends with a '/'. */
+		if (dirlen > 0 && path[dirlen - 1] != '/') {
+			path[dirlen++] = '/';
+		}
+
+		memcpy(path + dirlen, entry->d_name, name_len + 1);
+
+		if(name_len < suflen
+				|| strcmp(entry->d_name + name_len - suflen, ALPM_TRIGGER_SUFFIX) != 0) {
+			_alpm_log(handle, ALPM_LOG_DEBUG, "skipping non-trigger file %s\n",
+					path);
+			continue;
+		}
+
+		if(find_trigger(hook->triggers, entry->d_name)) {
+			_alpm_log(handle, ALPM_LOG_DEBUG,
+					"skipping overridden trigger %s\n", path);
+			continue;
+		}
+
+		if(stat(path, &buf) != 0) {
+			_alpm_log(handle, ALPM_LOG_ERROR,
+					_("could not stat trigger file %s: %s\n"), path,
+					strerror(errno));
+			ret = -1;
+			continue;
+		}
+
+		if(S_ISDIR(buf.st_mode)) {
+			_alpm_log(handle, ALPM_LOG_DEBUG, "skipping directory %s\n", path);
+			continue;
+		}
+
+		_alpm_log(handle, ALPM_LOG_DEBUG, "parsing trigger file %s\n", path);
+
+		if(parse_ini(path, _alpm_trigger_parse_cb, &ctx) != 0
+			|| _alpm_hook_validate(handle, ctx.hook, path)) {
+			_alpm_log(handle, ALPM_LOG_ERROR,
+					_("parsing trigger file %s failed\n"), path);
+			ret = -1;
+			continue;
+		}
+
+		STRDUP(hook->name, entry->d_name, ret = -1);
+	}
+	if(errno != 0) {
+		_alpm_log(handle, ALPM_LOG_ERROR, _("could not read directory: %s: %s\n"),
+				dir, strerror(errno));
+		ret = -1;
+	}
+
+	closedir(d);
+
+	return ret;
+}
+
+static int _alpm_hook_parse_drop_ins(alpm_handle_t *handle, alpm_list_t *hooks)
+{
+	alpm_list_t *i;
+	size_t suflen = strlen(ALPM_DROP_IN_SUFFIX);
+	int ret = 0;
+
+	for(i = alpm_list_last(handle->hookdirs); i; i = alpm_list_previous(i)) {
+		char path[PATH_MAX];
+		size_t dirlen;
+		struct dirent *entry;
+		DIR *d;
+
+		if((dirlen = strlen(i->data)) >= PATH_MAX) {
+			_alpm_log(handle, ALPM_LOG_ERROR, _("could not open file: %s: %s\n"),
+					(char *)i->data, strerror(ENAMETOOLONG));
+			ret = -1;
+			continue;
+		}
+		memcpy(path, i->data, dirlen + 1);
+
+		if(!(d = opendir(path))) {
+			if(errno == ENOENT) {
+				continue;
+			} else {
+				_alpm_log(handle, ALPM_LOG_ERROR,
+						_("could not open directory: %s: %s\n"), path,
+						strerror(errno));
+				ret = -1;
+				continue;
+			}
+		}
+
+		while((errno = 0, entry = readdir(d))) {
+			alpm_list_t *j;
+			struct stat buf;
+			size_t name_len;
+
+			if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
+				continue;
+			}
+
+			if((name_len = strlen(entry->d_name)) >= PATH_MAX - dirlen) {
+				_alpm_log(handle, ALPM_LOG_ERROR, _("could not open file: %s%s: %s\n"),
+						path, entry->d_name, strerror(ENAMETOOLONG));
+				ret = -1;
+				continue;
+			}
+			memcpy(path + dirlen, entry->d_name, name_len + 1);
+
+			if(name_len < suflen
+					|| strcmp(entry->d_name + name_len - suflen, ALPM_DROP_IN_SUFFIX) != 0) {
+				_alpm_log(handle, ALPM_LOG_DEBUG,
+						"skipping non-drop-in directory %s\n", path);
+				continue;
+			}
+
+			entry->d_name[name_len - 2] = '\0';
+			j = find_hook(hooks, entry->d_name);
+			entry->d_name[name_len - 2] = '.';
+
+			if (!j) {
+				_alpm_log(handle, ALPM_LOG_DEBUG,
+						"skipping drop-in directory %s without a corresponding hook",
+						path);
+				continue;
+			}
+
+			if(stat(path, &buf) != 0) {
+				_alpm_log(handle, ALPM_LOG_ERROR,
+						_("could not stat drop-in directory %s: %s\n"), path,
+						strerror(errno));
+				ret = -1;
+				continue;
+			}
+
+			if(!S_ISDIR(buf.st_mode)) {
+				_alpm_log(handle, ALPM_LOG_DEBUG, "skipping file %s\n", path);
+				continue;
+			}
+
+			_alpm_log(handle, ALPM_LOG_DEBUG, "parsing drop-in directory %s\n", path);
+
+			if (_alpm_hook_parse_drop_in_one(handle, j->data, path) != 0) {
+				_alpm_log(handle, ALPM_LOG_ERROR,
+						 _("failed to read drop-in files in drop-in directory %s"),
+						 path);
+				ret = -1;
+				continue;
+			}
+		}
+		if(errno != 0) {
+			_alpm_log(handle, ALPM_LOG_ERROR, _("could not read directory: %s: %s\n"),
+					(char *) i->data, strerror(errno));
+			ret = -1;
+		}
+
+		closedir(d);
+	}
+
+	return ret;
+}
+
 int _alpm_hook_run(alpm_handle_t *handle, alpm_hook_when_t when)
 {
 	alpm_event_hook_t event = { .when = when };
@@ -626,6 +871,10 @@ int _alpm_hook_run(alpm_handle_t *handle, alpm_hook_when_t when)
 		closedir(d);
 	}
 
+	if (_alpm_hook_parse_drop_ins(handle, hooks) != 0) {
+		ret = -1;
+	}
+
 	if(ret != 0 && when == ALPM_HOOK_PRE_TRANSACTION) {
 		goto cleanup;
 	}
diff --git a/lib/libalpm/hook.h b/lib/libalpm/hook.h
index ff5de4f2..961d3238 100644
--- a/lib/libalpm/hook.h
+++ b/lib/libalpm/hook.h
@@ -23,6 +23,8 @@
 #include "alpm.h"
 
 #define ALPM_HOOK_SUFFIX ".hook"
+#define ALPM_DROP_IN_SUFFIX ".hook.d"
+#define ALPM_TRIGGER_SUFFIX ".trigger"
 
 int _alpm_hook_run(alpm_handle_t *handle, alpm_hook_when_t when);
 
-- 
2.28.0


More information about the pacman-dev mailing list