From faeff38ca227294f000b236401d3126660befa38 Mon Sep 17 00:00:00 2001
From: Juan RP <xtraeme@voidlinux.eu>
Date: Sun, 18 Oct 2015 10:38:35 +0200
Subject: [PATCH] Alternatives framework (1/2) (WIP).

---
 ALTERNATIVES               | 246 ++++++++++++++++++++++++++++++++
 bin/xbps-create/main.c     |  78 ++++++++++-
 include/xbps.h.in          |  25 +++-
 lib/Makefile               |   1 +
 lib/package_alternatives.c | 280 +++++++++++++++++++++++++++++++++++++
 lib/package_remove.c       |   4 +
 lib/package_unpack.c       |   8 ++
 lib/transaction_commit.c   |   4 +
 8 files changed, 642 insertions(+), 4 deletions(-)
 create mode 100644 ALTERNATIVES
 create mode 100644 lib/package_alternatives.c

diff --git a/ALTERNATIVES b/ALTERNATIVES
new file mode 100644
index 00000000..89d666f8
--- /dev/null
+++ b/ALTERNATIVES
@@ -0,0 +1,246 @@
+The XBPS alternatives framework
+-------------------------------
+
+-------------------------------------------------
+Package metadata stored in packages (props.plist)
+-------------------------------------------------
+
+The 'nvi' pkg declares an alternative group named 'vi':
+
+...
+<key>alternatives</key>
+<dict>
+	<key>vi</key>
+	<array>
+		<string>/usr/bin/ex:/usr/bin/nvi</string>
+		<string>/usr/bin/vi:/usr/bin/nvi</string>
+		<string>/usr/share/man/man1/ex.1:/usr/share/man/man1/nvi.1</string>
+		<string>/usr/share/man/man1/vi.1:/usr/share/man/man1/nvi.1</string>
+	</array>
+	...
+</dict>
+...
+
+The strings need to follow the <symlink>:<target> convention, delimited by
+the ':' character.
+
+--------------------------------------------------------------------
+Package metadata stored in metadir (metadir/alternatives-0.48.plist)
+--------------------------------------------------------------------
+
+/var/db/xbps/alternatives-0.48.plist (dict):
+
+3 packages can provide the 'vi' alternative group: ex-vi, nvi or vim.
+The entry order determines the priority (first entry always wins).
+
+After running `xbps-install -Sy nvi ex-vi vim' the order is the following:
+
+...
+	<key>vi</key>
+	<array>
+		<string>nvi</string>
+		<string>ex-vi</string>
+		<string>vim</string>
+	</array>
+	...
+...
+
+If the 'nvi' pkg is removed, and there's no alternative set for `vi',
+the 'ex-vi' pkg now becomes the default alternative:
+
+...
+	<key>vi</key>
+	<array>
+		<string>ex-vi</string>
+		<string>vim</string>
+	</array>
+	...
+...
+
+The user now decides that 'vi' should be provided by the 'vim' pkg,
+so that the matching entry is put into the head:
+
+...
+	<key>vi</key>
+	<array>
+		<string>vim</string>
+		<string>ex-vi</string>
+	</array>
+	...
+...
+
+When no packages provide an alternative group, the alternative group and its associated symlinks
+will be completely removed.
+
+
+--------------------
+xbps-alternatives(1)
+--------------------
+
+- Listing all available alternatives and current state:
+
+	$ xbps-alternatives -l
+	libGL
+	  - libGL (current)
+	    - /usr/lib/libGL.so -> /usr/lib/libGL-mesa.so
+	    - /usr/lib/libGL.so.1 -> /usr/lib/libGL-mesa.so.1
+	    - /usr/lib/xorg/modules/extensions/libglx.so -> /usr/lib/xorg/modules/extensions/libglx-xorg.so
+	  - nvidia-libs
+	    - /usr/lib/libGL.so -> /usr/lib/libGL-nvidia.so.1
+	    - /usr/lib/libGL.so.1 -> /usr/lib/libGL-nvidia.so.1
+	    - /usr/lib/xorg/modules/extensions/libglx.so -> /usr/lib/xorg/modules/extensions/libglx-nvidia.so
+	  - catalyst-libs
+	    - /usr/lib/libGL.so -> /usr/lib/libGL-fglrx.so.1
+	    - /usr/lib/libGL.so.1 -> /usr/lib/libGL-fglrx.so.1
+	    - /usr/lib/xorg/modules/extensions/libglx.so -> /usr/lib/xorg/modules/extensions/libglx-fglrx.so
+	ntpd
+	  - openntpd (current)
+	    - /etc/sv/ntpd -> /etc/sv/openntpd
+	    - /usr/bin/ntpd -> /usr/bin/openntpd
+	    - /usr/share/man/man1/ntpd.1 -> /usr/share/man/man1/openntpd.1
+	  - ntp
+	    - /etc/sv/ntpd -> /etc/sv/isc-ntpd
+	    - /usr/bin/ntpd -> /usr/bin/isc-ntpd
+	    - /usr/share/man/man1/ntpd.1 -> /usr/share/man/man1/isc-ntpd.1
+	  - busybox
+	    - /etc/sv/ntpd -> /etc/sv/busybox-ntpd
+	    - /usr/bin/ntpd -> /usr/bin/busybox
+	sort
+	  - coreutils (current)
+	    - /usr/bin/sort -> /usr/bin/coreutils-sort
+	    - /usr/share/man/man1/sort.1 -> /usr/share/man/man1/coreutils-sort.1
+	  - busybox
+	    - /usr/bin/sort -> /usr/bin/busybox
+	vi
+	  - nvi (current)
+            - /usr/bin/ex -> /usr/bin/nvi
+            - /usr/bin/vi -> /usr/bin/nvi
+            - /usr/share/man/man1/ex.1 -> /usr/share/man/man1/nvi.1
+            - /usr/share/man/man1/vi.1 -> /usr/share/man/man1/nvi.1
+	  - vim
+            - /usr/bin/ex -> /usr/bin/vim-ex
+            - /usr/bin/vi -> /usr/bin/vim
+            - /usr/share/man/man1/ex.1 -> /usr/share/man/man1/vim-ex.1
+            - /usr/share/man/man1/vi.1 -> /usr/share/man/man1/vim.1
+
+	...
+
+- Listing available alternative groups of a package:
+
+	$ xbps-alternatives -l busybox
+	ntpd
+	  - /etc/sv/ntpd -> /etc/sv/busybox-ntpd
+	  - /usr/bin/ntpd -> /usr/bin/busybox
+	sort
+	  - /usr/bin/sort -> /usr/bin/busybox
+	...
+
+- Apply all alternative groups specified by the 'nvi' package (declares one group: vi):
+
+	$ xbps-alternatives -s nvi
+	Switched 'vi' alternatives group to 'nvi'.
+	Creating 'vi' alternatives group symlink: /usr/bin/ex -> /usr/bin/nvi
+	Creating 'vi' alternatives group symlink: /usr/bin/vi -> /usr/bin/nvi
+	Creating 'vi' alternatives group symlink: /usr/share/man/man1/ex.1 -> /usr/share/man/man1/nvi.1
+	Creating 'vi' alternatives group symlink: /usr/share/man/man1/vi.1 -> /usr/share/man/man1/nvi.1
+	$
+
+- Creating a specific alternative group of a package containing multiple groups:
+
+	$ xbps-alternatives -s busybox -g ntpd
+	Switched 'ntpd' alternatives group to 'busybox'.
+	Creating 'ntpd' alternatives group symlink: /etc/sv/ntpd -> /etc/sv/busybox-ntpd
+	Creating 'ntpd' alternatives group symlink: /usr/bin/ntpd -> /usr/bin/busybox
+	$
+
+---------------
+xbps-install(1)
+---------------
+
+Installing a package with an alternatives group for the first time:
+
+	$ xbps-install -Syv vim
+	Name Action    Version           New version            Download size
+	vim  install   -                 1.0_1                  -
+
+	Free space on disk:             49GB
+
+	[*] Downloading binary packages
+
+	[*] Verifying package integrity
+	vim-1.0_1: verifying SHA256 hash...
+
+	[*] Running transaction tasks
+	vim-1.0_1: unpacking ...
+	vim-1.0_1: unpacked file `./usr/bin/vim' (0 bytes)
+	vim-1.0_1: unpacked file `./usr/share/man/man1/vim.1' (0 bytes)
+	vim-1.0_1: registered 'vi' alternatives group
+	Creating 'vi' alternatives group symlink: /usr/bin/vi -> /usr/bin/vim
+	Creating 'vi' alternatives group symlink: /usr/bin/ex -> /usr/bin/vim
+	Creating 'vi' alternatives group symlink: /usr/bin/view -> /usr/bin/vim
+	Creating 'vi' alternatives group symlink: /usr/share/man/man1/ex.1 -> /usr/share/man/man1/vim.1
+	Creating 'vi' alternatives group symlink: /usr/share/man/man1/vi.1 -> /usr/share/man/man1/vim.1
+	Creating 'vi' alternatives group symlink: /usr/share/man/man1/view.1 -> /usr/share/man/man1/vim.1
+
+	[*] Configuring unpacked packages
+	vim-1.0_1: configuring ...
+	vim-1.0_1: installed successfully.
+
+	0 downloaded, 1 installed, 0 updated, 1 configured, 0 removed.
+	$
+
+Installing a package that provides a 'vi' alternatives group (nvi):
+
+	$ xbps-install -Syv nvi
+	Name Action    Version           New version            Download size
+	nvi  install   -                 1.0_1                  -
+
+	Free space on disk:             49GB
+
+	[*] Downloading binary packages
+
+	[*] Verifying package integrity
+	nvi-1.0_1: verifying SHA256 hash...
+
+	[*] Running transaction tasks
+	nvi-1.0_1: unpacking ...
+	nvi-1.0_1: unpacked file `./usr/bin/nvi' (0 bytes)
+	nvi-1.0_1: unpacked file `./usr/share/man/man1/nvi.1' (0 bytes)
+	nvi-1.0_1: registered 'vi' alternatives group
+
+	[*] Configuring unpacked packages
+	nvi-1.0_1: configuring ...
+	nvi-1.0_1: installed successfully.
+
+	0 downloaded, 1 installed, 0 updated, 1 configured, 0 removed.
+	$
+
+Removing a package that was the default provider of the 'vi' alternatives group (vim):
+
+	$ xbps-remove -yv vim
+	Name Action    Version           New version            Download size
+	vim  remove    1.0_1             -                      -
+
+	Free space on disk:             49GB
+
+	Removing `vim-1.0_1' ...
+	Removing 'vi' alternatives group symlink: /usr/bin/vi
+	Removing 'vi' alternatives group symlink: /usr/bin/ex
+	Removing 'vi' alternatives group symlink: /usr/bin/view
+	Removing 'vi' alternatives group symlink: /usr/share/man/man1/ex.1
+	Removing 'vi' alternatives group symlink: /usr/share/man/man1/vi.1
+	Removing 'vi' alternatives group symlink: /usr/share/man/man1/view.1
+	vim-1.0_1: unregistered 'vi' alternatives group
+	Switched 'vi' alternatives group to 'nvi'
+	Creating 'vi' alternatives group symlink: /usr/bin/vi -> /usr/bin/nvi
+	Creating 'vi' alternatives group symlink: /usr/bin/ex -> /usr/bin/nvi
+	Creating 'vi' alternatives group symlink: /usr/bin/view -> /usr/bin/nvi
+	Creating 'vi' alternatives group symlink: /usr/share/man/man1/ex.1 -> /usr/share/man/man1/nvi.1
+	Creating 'vi' alternatives group symlink: /usr/share/man/man1/vi.1 -> /usr/share/man/man1/nvi.1
+	Creating 'vi' alternatives group symlink: /usr/share/man/man1/view.1 -> /usr/share/man/man1/nvi.1
+	Removed file `/usr/share/man/man1/vim.1'
+	Removed file `/usr/bin/vim'
+	Removed `vim-1.0_1' successfully.
+
+	0 downloaded, 0 installed, 0 updated, 0 configured, 1 removed.
+	$
diff --git a/bin/xbps-create/main.c b/bin/xbps-create/main.c
index 5448b393..68b52566 100644
--- a/bin/xbps-create/main.c
+++ b/bin/xbps-create/main.c
@@ -102,6 +102,9 @@ usage(void)
 	" -s --desc           Short description (max 80 characters).\n"
 	" -t --tags           A list of tags/categories (blank separated list).\n"
 	" -V --version        Prints XBPS release version.\n"
+	" --alternatives      List of available alternatives this pkg provides.\n"
+	"                     This expects a blank separated list of <name>:<symlink>:<target>, e.g\n"
+	"                     'vi:/usr/bin/vi:/usr/bin/vim foo:/usr/bin/foo:/usr/bin/blah'.\n"
 	" --build-options     A string with the used build options.\n"
 	" --compression       Compression format: none, gzip, bzip2, xz (default).\n"
 	" --shlib-provides    List of provided shared libraries (blank separated list,\n"
@@ -162,6 +165,72 @@ out:
 	xbps_object_release(array);
 }
 
+static void
+process_one_alternative(const char *altgrname, const char *val)
+{
+	xbps_dictionary_t d;
+	xbps_array_t a;
+	char *altfiles;
+	bool alloc = false;
+
+	if ((d = xbps_dictionary_get(pkg_propsd, "alternatives")) == NULL) {
+		d = xbps_dictionary_create();
+		assert(d);
+		alloc = true;
+	}
+	if ((a = xbps_dictionary_get(d, altgrname)) == NULL) {
+		a = xbps_array_create();
+		assert(a);
+	}
+	altfiles = strchr(val, ':') + 1;
+	assert(altfiles);
+
+	xbps_array_add_cstring(a, altfiles);
+	xbps_dictionary_set(d, altgrname, a);
+	xbps_dictionary_set(pkg_propsd, "alternatives", d);
+
+	if (alloc) {
+		xbps_object_release(a);
+		xbps_object_release(d);
+	}
+}
+
+
+static void
+process_dict_of_arrays(const char *key, const char *val)
+{
+	char *altgrname, *args, *p, *saveptr;
+
+	assert(key);
+
+	if (val == NULL)
+		return;
+
+	args = strdup(val);
+	assert(args);
+
+	if (strchr(args, ' ') == NULL) {
+		altgrname = strtok(args, ":");
+		assert(altgrname);
+		process_one_alternative(altgrname, val);
+		goto out;
+	}
+
+	for ((p = strtok_r(args, " ", &saveptr)); p;
+	     (p = strtok_r(NULL, " ", &saveptr))) {
+		char *b;
+
+		b = strdup(p);
+		assert(b);
+		altgrname = strtok(b, ":");
+		assert(altgrname);
+		process_one_alternative(altgrname, p);
+		free(b);
+	}
+out:
+	free(args);
+}
+
 static void
 process_file(const char *file, const char *key)
 {
@@ -615,6 +684,7 @@ main(int argc, char **argv)
 		{ "shlib-requires", required_argument, NULL, '1' },
 		{ "build-options", required_argument, NULL, '2' },
 		{ "compression", required_argument, NULL, '3' },
+		{ "alternatives", required_argument, NULL, '4' },
 		{ NULL, 0, NULL, 0 }
 	};
 	struct archive *ar;
@@ -624,7 +694,7 @@ main(int argc, char **argv)
 	const char *conflicts, *deps, *homepage, *license, *maint, *bwith;
 	const char *provides, *pkgver, *replaces, *reverts, *desc, *ldesc;
 	const char *arch, *config_files, *mutable_files, *version;
-	const char *buildopts, *shlib_provides, *shlib_requires;
+	const char *buildopts, *shlib_provides, *shlib_requires, *alternatives;
 	const char *compression, *tags = NULL, *srcrevs = NULL;
 	char *pkgname, *binpkg, *tname, *p, cwd[PATH_MAX-1];
 	bool quiet = false, preserve = false;
@@ -634,7 +704,7 @@ main(int argc, char **argv)
 	arch = conflicts = deps = homepage = license = maint = compression = NULL;
 	provides = pkgver = replaces = reverts = desc = ldesc = bwith = NULL;
 	buildopts = config_files = mutable_files = shlib_provides = NULL;
-	shlib_requires = NULL;
+	alternatives = shlib_requires = NULL;
 
 	while ((c = getopt_long(argc, argv, shortopts, longopts, NULL)) != -1) {
 		if (optarg && strcmp(optarg, "") == 0)
@@ -716,6 +786,9 @@ main(int argc, char **argv)
 		case '3':
 			compression = optarg;
 			break;
+		case '4':
+			alternatives = optarg;
+			break;
 		case '?':
 		default:
 			usage();
@@ -798,6 +871,7 @@ main(int argc, char **argv)
 	process_array("reverts", reverts);
 	process_array("shlib-provides", shlib_provides);
 	process_array("shlib-requires", shlib_requires);
+	process_dict_of_arrays("alternatives", alternatives);
 
 	/* save cwd */
 	memset(&cwd, 0, sizeof(cwd));
diff --git a/include/xbps.h.in b/include/xbps.h.in
index 3cda9ddd..c632dfab 100644
--- a/include/xbps.h.in
+++ b/include/xbps.h.in
@@ -48,7 +48,7 @@
  *
  * This header documents the full API for the XBPS Library.
  */
-#define XBPS_API_VERSION	"20150603"
+#define XBPS_API_VERSION	"20151018"
 
 #ifndef XBPS_VERSION
  #define XBPS_VERSION		"UNSET"
@@ -96,6 +96,12 @@
  */
 #define XBPS_PKGDB		"pkgdb-0.38.plist"
 
+/**
+ * @def XBPS_ALTERNATIVES
+ * Filename for the package alternatives database.
+ */
+#define XBPS_ALTERNATIVES	"alternatives-0.48.plist"
+
 /**
  * @def XBPS_PKGPROPS
  * Filename for package metadata property list.
@@ -280,6 +286,11 @@ extern "C" {
  * - XBPS_STATE_INVALID_DEP: package has an invalid dependency.
  * - XBPS_STATE_SHOW_INSTALL_MSG: package must show a post-install message.
  * - XBPS_STATE_SHOW_REMOVE_MSG: package must show a pre-remove message.
+ * - XBPS_STATE_ALTGROUP_ADDED: package has registered an alternative group.
+ * - XBPS_STATE_ALTGROUP_REMOVED: package has unregistered an alternative group.
+ * - XBPS_STATE_ALTGROUP_SWITCHED: alternative group has been switched.
+ * - XBPS_STATE_ALTGROUP_LINK_ADDED: link added by an alternative group.
+ * - XBPS_STATE_ALTGROUP_LINK_REMOVED: link removed by an alternative group.
  * - XBPS_STATE_UNPACK_FILE_PRESERVED: package unpack preserved a file.
  * - XBPS_STATE_PKGDB: pkgdb upgrade in progress.
  * - XBPS_STATE_PKGDB_DONE: pkgdb has been upgraded successfully.
@@ -328,7 +339,12 @@ typedef enum xbps_state {
 	XBPS_STATE_UNPACK_FILE_PRESERVED,
 	XBPS_STATE_PKGDB,
 	XBPS_STATE_PKGDB_DONE,
-	XBPS_STATE_TRANS_ADDPKG
+	XBPS_STATE_TRANS_ADDPKG,
+	XBPS_STATE_ALTGROUP_ADDED,
+	XBPS_STATE_ALTGROUP_REMOVED,
+	XBPS_STATE_ALTGROUP_SWITCHED,
+	XBPS_STATE_ALTGROUP_LINK_ADDED,
+	XBPS_STATE_ALTGROUP_LINK_REMOVED
 } xbps_state_t;
 
 /**
@@ -514,6 +530,7 @@ struct xbps_handle {
 	 */
 	xbps_dictionary_t pkgdb_revdeps;
 	xbps_dictionary_t vpkgd;
+	xbps_dictionary_t alternatives;
 	/**
 	 * @var pkgdb
 	 *
@@ -629,6 +646,10 @@ void xbps_dbg_printf_append(struct xbps_handle *, const char *, ...);
 void xbps_error_printf(const char *, ...);
 void xbps_warn_printf(const char *, ...);
 
+int xbps_alternatives_flush(struct xbps_handle *xhp);
+int xbps_alternatives_register(struct xbps_handle *, xbps_dictionary_t);
+int xbps_alternatives_unregister(struct xbps_handle *, xbps_dictionary_t);
+
 /**
  * Initialize the XBPS library with the following steps:
  *
diff --git a/lib/Makefile b/lib/Makefile
index 3ef2a237..571e621b 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -42,6 +42,7 @@ OBJS += plist.o plist_find.o plist_match.o archive.o
 OBJS += plist_remove.o plist_fetch.o util.o util_hash.o 
 OBJS += repo.o repo_pkgdeps.o repo_sync.o
 OBJS += rpool.o cb_util.o proplib_wrapper.o
+OBJS += package_alternatives.o
 OBJS += $(EXTOBJS) $(COMPAT_SRCS)
 
 .PHONY: all
diff --git a/lib/package_alternatives.c b/lib/package_alternatives.c
new file mode 100644
index 00000000..40858436
--- /dev/null
+++ b/lib/package_alternatives.c
@@ -0,0 +1,280 @@
+/*-
+ * Copyright (c) 2015 Juan Romero Pardines.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#include "xbps_api_impl.h"
+
+/*
+ * Alternatives framework for xbps.
+ */
+static char *
+left(const char *str)
+{
+	char *p;
+	size_t len;
+
+	p = strdup(str);
+	len = strlen(p) - strlen(strchr(p, ':'));
+	p[len] = '\0';
+
+	return p;
+}
+
+static const char *
+right(const char *str)
+{
+	return strchr(str, ':') + 1;
+}
+
+#if 0
+static int
+make_symlink(const char *target, const char *link)
+{
+	char *t, *l, *tdir, *ldir;
+
+	tdir = strdup(target);
+	assert(tdir);
+	ldir = strdup(link);
+	assert(ldir);
+
+}
+#endif
+
+static void
+xbps_alternatives_init(struct xbps_handle *xhp)
+{
+	char *plist;
+
+	if (xbps_object_type(xhp->alternatives) == XBPS_TYPE_DICTIONARY)
+		return;
+
+	plist = xbps_xasprintf("%s/%s", xhp->metadir, XBPS_ALTERNATIVES);
+	xhp->alternatives = xbps_dictionary_internalize_from_file(plist);
+	free(plist);
+
+	if (xhp->alternatives == NULL)
+		xhp->alternatives = xbps_dictionary_create();
+}
+
+int
+xbps_alternatives_flush(struct xbps_handle *xhp)
+{
+	char *plist;
+
+	if (xbps_object_type(xhp->alternatives) != XBPS_TYPE_DICTIONARY)
+		return 0;
+
+	/* ... and then write dictionary to disk */
+	plist = xbps_xasprintf("%s/%s", xhp->metadir, XBPS_ALTERNATIVES);
+	if (!xbps_dictionary_externalize_to_file(xhp->alternatives, plist)) {
+		free(plist);
+		return EINVAL;
+	}
+	free(plist);
+
+	return 0;
+}
+
+static int
+remove_symlinks(struct xbps_handle *xhp, xbps_array_t a, const char *grname)
+{
+	unsigned int i, cnt;
+
+	cnt = xbps_array_count(a);
+	for (i = 0; i < cnt; i++) {
+		xbps_string_t str;
+		char *l, *lnk;
+
+		str = xbps_array_get(a, i);
+		l = left(xbps_string_cstring_nocopy(str));
+		assert(l);
+		lnk = xbps_xasprintf("%s%s", xhp->rootdir, l);
+		xbps_set_cb_state(xhp, XBPS_STATE_ALTGROUP_LINK_REMOVED, 0, NULL,
+		    "Removing '%s' alternatives group symlink: %s", grname, l);
+		unlink(lnk);
+		free(lnk);
+		free(l);
+	}
+
+	return 0;
+}
+
+static int
+create_symlinks(struct xbps_handle *xhp, xbps_array_t a, const char *grname)
+{
+	unsigned int i, cnt;
+
+	cnt = xbps_array_count(a);
+	for (i = 0; i < cnt; i++) {
+		xbps_string_t str;
+		char *l, *lnk;
+		const char *tgt;
+
+		str = xbps_array_get(a, i);
+		l = left(xbps_string_cstring_nocopy(str));
+		assert(l);
+		tgt = right(xbps_string_cstring_nocopy(str));
+		assert(tgt);
+		lnk = xbps_xasprintf("%s%s", xhp->rootdir, l);
+		xbps_set_cb_state(xhp, XBPS_STATE_ALTGROUP_LINK_ADDED, 0, NULL,
+		    "Creating '%s' alternatives group symlink: %s -> %s", grname, l, tgt);
+		unlink(lnk);
+		symlink(tgt, lnk);
+		free(lnk);
+		free(l);
+	}
+
+	return 0;
+}
+
+int
+xbps_alternatives_unregister(struct xbps_handle *xhp, xbps_dictionary_t pkgd)
+{
+	xbps_array_t allkeys;
+	xbps_dictionary_t alternatives;
+	const char *pkgver;
+	char *pkgname;
+	int rv = 0;
+
+	alternatives = xbps_dictionary_get(pkgd, "alternatives");
+	if (!xbps_dictionary_count(alternatives))
+		return 0;
+
+	xbps_alternatives_init(xhp);
+
+	xbps_dictionary_get_cstring_nocopy(pkgd, "pkgver", &pkgver);
+	if ((pkgname = xbps_pkg_name(pkgver)) == NULL)
+		return EINVAL;
+
+	allkeys = xbps_dictionary_all_keys(alternatives);
+	for (unsigned int i = 0; i < xbps_array_count(allkeys); i++) {
+		xbps_array_t array;
+		xbps_object_t keysym;
+		const char *first = NULL, *keyname;
+
+		keysym = xbps_array_get(allkeys, i);
+		keyname = xbps_dictionary_keysym_cstring_nocopy(keysym);
+
+		array = xbps_dictionary_get(xhp->alternatives, keyname);
+		if (array == NULL)
+			continue;
+
+		xbps_array_get_cstring_nocopy(array, 0, &first);
+		if (strcmp(pkgname, first) == 0) {
+			/* this pkg is the current alternative for this group */
+			rv = remove_symlinks(xhp,
+				xbps_dictionary_get(alternatives, keyname),
+				keyname);
+			if (rv != 0)
+				break;
+		}
+		xbps_set_cb_state(xhp, XBPS_STATE_ALTGROUP_REMOVED, 0, NULL,
+		    "%s: unregistered '%s' alternatives group", pkgver, keyname);
+		xbps_remove_string_from_array(array, pkgname);
+		if (xbps_array_count(array) == 0) {
+			xbps_dictionary_remove(xhp->alternatives, keyname);
+		} else {
+			xbps_dictionary_t curpkgd;
+
+			first = NULL;
+			xbps_array_get_cstring_nocopy(array, 0, &first);
+			curpkgd = xbps_pkgdb_get_pkg(xhp, first);
+			assert(curpkgd);
+			xbps_set_cb_state(xhp, XBPS_STATE_ALTGROUP_SWITCHED, 0, NULL,
+			    "Switched '%s' alternatives group to '%s'", keyname, first);
+			alternatives = xbps_dictionary_get(curpkgd, "alternatives");
+			rv = create_symlinks(xhp,
+				xbps_dictionary_get(alternatives, keyname),
+				keyname);
+			if (rv != 0)
+				break;
+		}
+
+	}
+	xbps_object_release(allkeys);
+	free(pkgname);
+
+	return rv;
+}
+
+int
+xbps_alternatives_register(struct xbps_handle *xhp, xbps_dictionary_t pkgd)
+{
+	xbps_array_t allkeys;
+	xbps_dictionary_t alternatives;
+	const char *pkgver;
+	char *pkgname;
+	int rv = 0;
+
+	alternatives = xbps_dictionary_get(pkgd, "alternatives");
+	if (!xbps_dictionary_count(alternatives))
+		return 0;
+
+	xbps_alternatives_init(xhp);
+
+	xbps_dictionary_get_cstring_nocopy(pkgd, "pkgver", &pkgver);
+	pkgname = xbps_pkg_name(pkgver);
+	if (pkgname == NULL)
+		return EINVAL;
+
+	allkeys = xbps_dictionary_all_keys(alternatives);
+	for (unsigned int i = 0; i < xbps_array_count(allkeys); i++) {
+		xbps_array_t array;
+		xbps_object_t keysym;
+		const char *keyname;
+		bool alloc = false;
+
+		keysym = xbps_array_get(allkeys, i);
+		keyname = xbps_dictionary_keysym_cstring_nocopy(keysym);
+
+		array = xbps_dictionary_get(xhp->alternatives, keyname);
+		if (array == NULL) {
+			alloc = true;
+			array = xbps_array_create();
+		}
+		xbps_array_add_cstring(array, pkgname);
+		xbps_dictionary_set(xhp->alternatives, keyname, array);
+		xbps_set_cb_state(xhp, XBPS_STATE_ALTGROUP_ADDED, 0, NULL,
+		    "%s: registered '%s' alternatives group", pkgver, keyname);
+		if (alloc) {
+			/* apply alternatives for this group */
+			rv = create_symlinks(xhp,
+				xbps_dictionary_get(alternatives, keyname),
+				keyname);
+			xbps_object_release(array);
+			if (rv != 0)
+				break;
+		}
+	}
+	xbps_object_release(allkeys);
+	free(pkgname);
+
+	return rv;
+}
diff --git a/lib/package_remove.c b/lib/package_remove.c
index a3d1741c..288fd3f8 100644
--- a/lib/package_remove.c
+++ b/lib/package_remove.c
@@ -318,6 +318,10 @@ xbps_remove_pkg(struct xbps_handle *xhp, const char *pkgver, bool update)
 	if ((rv = xbps_cb_message(xhp, pkgd, "remove-msg")) != 0)
 		goto out;
 
+	/* unregister alternatives */
+	if ((rv = xbps_alternatives_unregister(xhp, pkgd)) != 0)
+		goto out;
+
 	/*
 	 * If updating a package, we just need to execute the current
 	 * pre-remove action target and we are done. Its files will be
diff --git a/lib/package_unpack.c b/lib/package_unpack.c
index 94d7d97b..0fb96d7b 100644
--- a/lib/package_unpack.c
+++ b/lib/package_unpack.c
@@ -622,6 +622,14 @@ xbps_unpack_binary_pkg(struct xbps_handle *xhp, xbps_dictionary_t pkg_repod)
 		    "%s: [unpack] failed to set state to unpacked: %s",
 		    pkgver, strerror(rv));
 	}
+	/* register alternatives */
+	if ((rv = xbps_alternatives_register(xhp, pkg_repod)) != 0) {
+		xbps_set_cb_state(xhp, XBPS_STATE_UNPACK_FAIL,
+		    rv, pkgver,
+		    "%s: [unpack] failed to register alternatives: %s",
+		    pkgver, strerror(rv));
+	}
+
 out:
 	if (pkg_fd != -1)
 		close(pkg_fd);
diff --git a/lib/transaction_commit.c b/lib/transaction_commit.c
index b018e5d3..d50c7bd1 100644
--- a/lib/transaction_commit.c
+++ b/lib/transaction_commit.c
@@ -350,6 +350,10 @@ xbps_transaction_commit(struct xbps_handle *xhp)
 			goto out;
 		}
 	}
+	/* flush changes to the alternatives framework */
+	if ((rv = xbps_alternatives_flush(xhp)) != 0)
+		goto out;
+
 	/* if there are no packages to install or update we are done */
 	if (!xbps_dictionary_get(xhp->transd, "total-update-pkgs") &&
 	    !xbps_dictionary_get(xhp->transd, "total-install-pkgs"))