Radish alpha
H
rad:z3QDZAW2FAfuLvihrhiyDC9fAD8G9
HardenedBSD Package Manager
Radicle
Git
add: resolve shlibs and provides via symlink directory layout
Baptiste Daroussin committed 1 month ago
commit a8e6f140a240081e88372469a8805a2ed627dfe7
parent 51d1a4a
2 files changed +382 -2
modified libpkg/pkg_add.c
@@ -27,6 +27,8 @@
#include <time.h>
#include <xstring.h>

+
#include <dirent.h>
+

#include "pkg.h"
#include "private/event.h"
#include "private/utils.h"
@@ -1132,6 +1134,74 @@ append_pkg_if_newer(pkgs_t *localpkgs, struct pkg *p)
	return (true);
}

+
static charv_t system_shlibs_cache = vec_init();
+
static bool system_shlibs_scanned = false;
+

+
static charv_t *
+
get_system_shlibs(void)
+
{
+
	if (!system_shlibs_scanned) {
+
		scan_system_shlibs(&system_shlibs_cache, ctx.pkg_rootdir);
+
		system_shlibs_scanned = true;
+
	}
+
	return (&system_shlibs_cache);
+
}
+

+
/*
+
 * Select a provider package from a symlink directory.
+
 * dirpath: e.g. "/repo/shlibs/libfoo.so.1"
+
 * ext:     package extension including dot (e.g. ".pkg")
+
 *
+
 * Selection logic:
+
 * - Single entry → use it
+
 * - Multiple → sort alphabetically, pick first (lowest NN. prefix = highest priority)
+
 * Returns xasprintf'd full path or NULL.
+
 */
+
static char *
+
select_provider_from_dir(const char *dirpath, const char *ext)
+
{
+
	DIR *d = opendir(dirpath);
+
	if (d == NULL)
+
		return (NULL);
+

+
	charv_t entries = vec_init();
+
	struct dirent *de;
+
	while ((de = readdir(d)) != NULL) {
+
		if (de->d_name[0] == '.')
+
			continue;
+
		if (!str_ends_with(de->d_name, ext))
+
			continue;
+
		vec_push(&entries, xstrdup(de->d_name));
+
	}
+
	closedir(d);
+

+
	if (entries.len == 0) {
+
		vec_free(&entries);
+
		return (NULL);
+
	}
+

+
	char *selected = NULL;
+

+
	if (entries.len == 1) {
+
		selected = entries.d[0];
+
		entries.d[0] = NULL;
+
	} else {
+
		/* Sort alphabetically, pick first (lowest NN. prefix wins) */
+
		qsort(entries.d, entries.len, sizeof(char *), char_cmp);
+
		selected = entries.d[0];
+
		entries.d[0] = NULL;
+
	}
+

+
	char *result = NULL;
+
	if (selected != NULL) {
+
		xasprintf(&result, "%s/%s", dirpath, selected);
+
		free(selected);
+
	}
+

+
	vec_free_and_free(&entries, free);
+
	return (result);
+
}
+

static int
pkg_add_check_pkg_archive(struct pkgdb *db, struct pkg *pkg,
	const char *path, int flags, const char *location)
@@ -1250,6 +1320,80 @@ pkg_add_check_pkg_archive(struct pkgdb *db, struct pkg *pkg,
		}
	}

+
	/*
+
	 * Phase 2: Resolve shlibs_required via symlink directory layout.
+
	 * Look in basedir/../shlibs/<shlibname>/ for provider packages.
+
	 */
+
	if (!fromstdin && (flags & PKG_ADD_UPGRADE) == 0) {
+
		char parentdir[MAXPATHLEN];
+
		strlcpy(parentdir, bd, sizeof(parentdir));
+
		char *pslash = strrchr(parentdir, '/');
+
		if (pslash != NULL)
+
			*pslash = '\0';
+

+
		charv_t *system_shlibs = get_system_shlibs();
+

+
		vec_foreach(pkg->shlibs_required, si) {
+
			const char *shlibname = pkg->shlibs_required.d[si];
+

+
			if (charv_search(system_shlibs, shlibname) != NULL)
+
				continue;
+

+
			if (pkgdb_is_shlib_provided(db, shlibname))
+
				continue;
+

+
			char provdir[MAXPATHLEN];
+
			snprintf(provdir, sizeof(provdir), "%s/shlibs/%s",
+
			    parentdir, shlibname);
+
			char *provider = select_provider_from_dir(
+
			    provdir, ext);
+

+
			if (provider == NULL) {
+
				pkg_emit_error("Missing shlib %s required by %s",
+
				    pkg->shlibs_required.d[si], pkg->name);
+
				if ((flags & PKG_ADD_FORCE_MISSING) == 0)
+
					goto cleanup;
+
				continue;
+
			}
+
			ret = pkg_add(db, provider, PKG_ADD_AUTOMATIC,
+
			    location);
+
			free(provider);
+
			if (ret != EPKG_OK && ret != EPKG_INSTALLED)
+
				goto cleanup;
+
		}
+

+
		/*
+
		 * Phase 3: Resolve abstract requires via symlink directory
+
		 * layout.  Look in basedir/../provides/<label>/ for providers.
+
		 */
+
		vec_foreach(pkg->requires, ri) {
+
			const char *req = pkg->requires.d[ri];
+

+
			if (pkgdb_is_provided(db, req))
+
				continue;
+

+
			char provdir[MAXPATHLEN];
+
			snprintf(provdir, sizeof(provdir), "%s/provides/%s",
+
			    parentdir, req);
+
			char *provider = select_provider_from_dir(
+
			    provdir, ext);
+

+
			if (provider == NULL) {
+
				pkg_emit_error(
+
				    "Missing provide %s required by %s",
+
				    req, pkg->name);
+
				if ((flags & PKG_ADD_FORCE_MISSING) == 0)
+
					goto cleanup;
+
				continue;
+
			}
+
			ret = pkg_add(db, provider, PKG_ADD_AUTOMATIC,
+
			    location);
+
			free(provider);
+
			if (ret != EPKG_OK && ret != EPKG_INSTALLED)
+
				goto cleanup;
+
		}
+
	}
+

	retcode = EPKG_OK;
cleanup:
	pkg_emit_add_deps_finished(pkg);
modified tests/frontend/add.sh
@@ -14,8 +14,14 @@ tests_init \
		add_no_version \
		add_no_version_multi \
		add_deps_multi \
-
		add_wrong_version
-
		#add_require
+
		add_wrong_version \
+
		add_shlib_provider \
+
		add_shlib_priority \
+
		add_shlib_missing \
+
		add_shlib_accept_missing \
+
		add_shlib_already_installed \
+
		add_provides_requires \
+
		add_shlib_stdin_skip

initialize_pkg() {
	touch a
@@ -310,3 +316,233 @@ EOF
	atf_check -o ignore -s exit:0 \
		pkg add final-1.pkg
}
+

+
# Helper: create shared libraries for shlib tests.
+
# Creates libtest.so.1 (provider) and libconsumer.so.1 (requires libtest.so.1).
+
create_shlibs() {
+
	touch empty.c
+
	cc -shared -Wl,-soname=libtest.so.1 empty.c -o libtest.so.1
+
	ln -s libtest.so.1 libtest.so
+
	cc -shared -Wl,-soname=libconsumer.so.1 empty.c -o libconsumer.so.1 -L. -ltest
+
}
+

+
# Helper: set up the symlink directory layout for shlib tests.
+
# Creates real shared libs, packages in All/, and symlink dir.
+
setup_provider_layout() {
+
	atf_skip_on Darwin "The macOS linker uses different flags"
+
	mkdir -p All
+
	create_shlibs
+

+
	# Provider package: contains libtest.so.1
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg provider provider 1 /usr/local
+
	cat << EOF >> provider.ucl
+
files: {
+
	$(pwd)/libtest.so.1: "",
+
}
+
EOF
+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg create -M provider.ucl
+
	mv provider-1.pkg All/
+

+
	# Consumer package: contains libconsumer.so.1 (requires libtest.so.1)
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg consumer consumer 1 /usr/local
+
	cat << EOF >> consumer.ucl
+
files: {
+
	$(pwd)/libconsumer.so.1: "",
+
}
+
EOF
+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg create -M consumer.ucl
+
	mv consumer-1.pkg All/
+

+
	# Set up symlink directory
+
	mkdir -p shlibs/libtest.so.1
+
	ln -s ../../All/provider-1.pkg shlibs/libtest.so.1/provider.pkg
+
}
+

+

+
add_shlib_provider_body() {
+
	setup_provider_layout
+

+
	atf_check \
+
		-o match:"Installing provider-1" \
+
		-o match:"Installing consumer-1" \
+
		-e empty \
+
		-s exit:0 \
+
		pkg add $(pwd)/All/consumer-1.pkg
+

+
	# Verify both packages are installed
+
	atf_check -o inline:"consumer\nprovider\n" -e empty -s exit:0 \
+
		pkg query -a "%n"
+

+
	# Verify provider was installed as automatic
+
	atf_check -o inline:"1\n" -e empty -s exit:0 \
+
		pkg query "%a" provider
+
}
+

+

+
add_shlib_priority_body() {
+
	atf_skip_on Darwin "The macOS linker uses different flags"
+
	mkdir -p All
+
	create_shlibs
+

+
	# Create two provider packages (both provide libtest.so.1)
+
	for p in alpha bravo; do
+
		atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg ${p} ${p} 1 /usr/local
+
		cat << EOF >> ${p}.ucl
+
files: {
+
	$(pwd)/libtest.so.1: "",
+
}
+
EOF
+
		atf_check -o ignore -e empty -s exit:0 \
+
			pkg create -M ${p}.ucl
+
		mv ${p}-1.pkg All/
+
	done
+

+
	# Consumer with libconsumer.so.1 (requires libtest.so.1)
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg consumer consumer 1 /usr/local
+
	cat << EOF >> consumer.ucl
+
files: {
+
	$(pwd)/libconsumer.so.1: "",
+
}
+
EOF
+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg create -M consumer.ucl
+
	mv consumer-1.pkg All/
+

+
	# Symlink dir with priority prefixes: 00. = highest priority
+
	mkdir -p shlibs/libtest.so.1
+
	ln -s ../../All/bravo-1.pkg shlibs/libtest.so.1/00.bravo.pkg
+
	ln -s ../../All/alpha-1.pkg shlibs/libtest.so.1/01.alpha.pkg
+

+
	# 00.bravo.pkg sorts first alphabetically → bravo wins
+
	atf_check \
+
		-o match:"Installing bravo-1" \
+
		-o match:"Installing consumer-1" \
+
		-o not-match:"Installing alpha-1" \
+
		-e empty \
+
		-s exit:0 \
+
		pkg add $(pwd)/All/consumer-1.pkg
+
}
+

+
add_shlib_missing_body() {
+
	atf_skip_on Darwin "The macOS linker uses different flags"
+
	mkdir -p All
+
	create_shlibs
+

+
	# Consumer with libconsumer.so.1 (requires libtest.so.1)
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg consumer consumer 1 /usr/local
+
	cat << EOF >> consumer.ucl
+
files: {
+
	$(pwd)/libconsumer.so.1: "",
+
}
+
EOF
+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg create -M consumer.ucl
+
	mv consumer-1.pkg All/
+

+
	# No shlibs/ directory at all → should fail
+
	atf_check \
+
		-o ignore \
+
		-e match:"Missing shlib libtest.so.1 required by consumer" \
+
		-s exit:1 \
+
		pkg add $(pwd)/All/consumer-1.pkg
+
}
+

+
add_shlib_accept_missing_body() {
+
	atf_skip_on Darwin "The macOS linker uses different flags"
+
	mkdir -p All
+
	create_shlibs
+

+
	# Consumer with libconsumer.so.1 (requires libtest.so.1)
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg consumer consumer 1 /usr/local
+
	cat << EOF >> consumer.ucl
+
files: {
+
	$(pwd)/libconsumer.so.1: "",
+
}
+
EOF
+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg create -M consumer.ucl
+
	mv consumer-1.pkg All/
+

+
	# With -M (accept missing), should succeed despite missing shlib
+
	atf_check \
+
		-o match:"Installing consumer-1" \
+
		-e match:"Missing shlib libtest.so.1 required by consumer" \
+
		-s exit:0 \
+
		pkg add -M $(pwd)/All/consumer-1.pkg
+
}
+

+
add_shlib_already_installed_body() {
+
	setup_provider_layout
+

+
	# Pre-install the provider
+
	atf_check \
+
		-o match:"Installing provider-1" \
+
		-e empty \
+
		-s exit:0 \
+
		pkg add $(pwd)/All/provider-1.pkg
+

+
	# Now install the consumer; provider shlib is already satisfied
+
	# so provider should NOT be re-installed
+
	atf_check \
+
		-o match:"Installing consumer-1" \
+
		-o not-match:"Installing provider-1" \
+
		-e empty \
+
		-s exit:0 \
+
		pkg add $(pwd)/All/consumer-1.pkg
+
}
+

+
add_provides_requires_body() {
+
	mkdir -p All
+

+
	# Provider with abstract provides
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg myprovider myprovider 1 /usr/local
+
	cat << EOF >> myprovider.ucl
+
provides: [
+
	"vi-editor",
+
]
+
EOF
+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg create -M myprovider.ucl
+
	mv myprovider-1.pkg All/
+

+
	# Consumer with abstract requires
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg consumer consumer 1 /usr/local
+
	cat << EOF >> consumer.ucl
+
requires: [
+
	"vi-editor",
+
]
+
EOF
+
	atf_check -o ignore -e empty -s exit:0 \
+
		pkg create -M consumer.ucl
+
	mv consumer-1.pkg All/
+

+
	# Set up provides symlink directory
+
	mkdir -p provides/vi-editor
+
	ln -s ../../All/myprovider-1.pkg provides/vi-editor/myprovider.pkg
+

+
	atf_check \
+
		-o match:"Installing myprovider-1" \
+
		-o match:"Installing consumer-1" \
+
		-e empty \
+
		-s exit:0 \
+
		pkg add $(pwd)/All/consumer-1.pkg
+

+
	# Verify both installed
+
	atf_check -o inline:"consumer\nmyprovider\n" -e empty -s exit:0 \
+
		pkg query -a "%n"
+
}
+

+
add_shlib_stdin_skip_body() {
+
	setup_provider_layout
+

+
	# From stdin, shlib resolution should be skipped entirely
+
	# (no directory context to search), so it should succeed
+
	# without trying to resolve shlibs
+
	atf_check \
+
		-o match:"Installing consumer-1" \
+
		-e empty \
+
		-s exit:0 \
+
		pkg add - < All/consumer-1.pkg
+
}