Radish alpha
H
rad:z3QDZAW2FAfuLvihrhiyDC9fAD8G9
HardenedBSD Package Manager
Radicle
Git
lua: add pkg.exec_capture, a pkg.exec version capturing stdout/stderr
Baptiste Daroussin committed 22 days ago
commit bd9cf035609b0f533645495daea418fe0088b4b2
parent c2de9ad
6 files changed +164 -2
modified docs/pkg-lua-script.5
@@ -11,7 +11,7 @@
.\"    notice, this list of conditions and the following disclaimer in the
.\"    documentation and/or other materials provided with the distribution.
.\"
-
.Dd December 12, 2025
+
.Dd April 14, 2026
.Dt PKG-LUA-SCRIPT 5
.Os
.Sh NAME
@@ -165,6 +165,27 @@ expected in the following form:
.Bro command, arg1, arg2, arg3, ...
.Brc
.Sc
+
.Pp
+
The command inherits the parent's standard output and standard error.
+
.It Ft "stdout, stderr, exitcode" Fn pkg.exec_capture arguments
+
Will execute the command
+
.Ar arguments
+
expected in the same form as
+
.Fn pkg.exec ,
+
capturing its standard output and standard error separately.
+
.Pp
+
Returns three values:
+
.Va stdout
+
(string containing the captured standard output),
+
.Va stderr
+
(string containing the captured standard error), and
+
.Va exitcode
+
(integer exit status of the command).
+
If the command cannot be started, the first two return values are
+
.Dv nil
+
and
+
.Va exitcode
+
contains the error number.
.It Ft res Fn pkg.readdir path
Will return an
.Va ipair
modified libpkg/lua.c
@@ -190,6 +190,107 @@ lua_exec(lua_State *L)
}

int
+
lua_exec_capture(lua_State *L)
+
{
+
	int r, pstat;
+
	posix_spawn_file_actions_t action;
+
	int stdout_pipe[2] = {-1, -1};
+
	int stderr_pipe[2] = {-1, -1};
+
	pid_t pid;
+
	const char **argv;
+
	int n = lua_gettop(L);
+
	char buf[BUFSIZ];
+
	ssize_t nread;
+
	xstring *out = NULL;
+
	xstring *err = NULL;
+

+
	luaL_argcheck(L, n == 1, n > 1 ? 2 : n,
+
	    "pkg.exec_capture takes exactly one argument");
+

+
#ifdef HAVE_CAPSICUM
+
	unsigned int capmode;
+
	if (cap_getmode(&capmode) == 0 && capmode > 0) {
+
		return (luaL_error(L,
+
		    "pkg.exec_capture not available in sandbox"));
+
	}
+
#endif
+
	if (pipe(stdout_pipe) < 0)
+
		return (luaL_error(L, "pipe: %s", strerror(errno)));
+
	if (pipe(stderr_pipe) < 0) {
+
		close(stdout_pipe[0]);
+
		close(stdout_pipe[1]);
+
		return (luaL_error(L, "pipe: %s", strerror(errno)));
+
	}
+

+
	posix_spawn_file_actions_init(&action);
+
	posix_spawn_file_actions_adddup2(&action, stdout_pipe[1],
+
	    STDOUT_FILENO);
+
	posix_spawn_file_actions_addclose(&action, stdout_pipe[0]);
+
	posix_spawn_file_actions_adddup2(&action, stderr_pipe[1],
+
	    STDERR_FILENO);
+
	posix_spawn_file_actions_addclose(&action, stderr_pipe[0]);
+

+
	argv = luaL_checkarraystrings(L, 1);
+
	if (0 != (r = posix_spawnp(&pid, argv[0], &action, NULL,
+
		(char*const*)argv, environ))) {
+
		close(stdout_pipe[0]);
+
		close(stdout_pipe[1]);
+
		close(stderr_pipe[0]);
+
		close(stderr_pipe[1]);
+
		posix_spawn_file_actions_destroy(&action);
+
		lua_pushnil(L);
+
		lua_pushnil(L);
+
		lua_pushinteger(L, r);
+
		return 3;
+
	}
+
	posix_spawn_file_actions_destroy(&action);
+
	close(stdout_pipe[1]);
+
	close(stderr_pipe[1]);
+

+
	out = xstring_new();
+
	while ((nread = read(stdout_pipe[0], buf, sizeof(buf))) != 0) {
+
		if (nread < 0) {
+
			if (errno == EINTR)
+
				continue;
+
			break;
+
		}
+
		fwrite(buf, 1, nread, out->fp);
+
	}
+
	close(stdout_pipe[0]);
+

+
	err = xstring_new();
+
	while ((nread = read(stderr_pipe[0], buf, sizeof(buf))) != 0) {
+
		if (nread < 0) {
+
			if (errno == EINTR)
+
				continue;
+
			break;
+
		}
+
		fwrite(buf, 1, nread, err->fp);
+
	}
+
	close(stderr_pipe[0]);
+

+
	while (waitpid(pid, &pstat, 0) == -1) {
+
		if (errno != EINTR) {
+
			xstring_free(out);
+
			xstring_free(err);
+
			lua_pushnil(L);
+
			lua_pushnil(L);
+
			lua_pushinteger(L, -1);
+
			return 3;
+
		}
+
	}
+

+
	fflush(out->fp);
+
	fflush(err->fp);
+
	lua_pushstring(L, out->buf);
+
	lua_pushstring(L, err->buf);
+
	lua_pushinteger(L, WEXITSTATUS(pstat));
+
	xstring_free(out);
+
	xstring_free(err);
+
	return 3;
+
}
+

+
int
lua_pkg_copy(lua_State *L)
{
	int n = lua_gettop(L);
modified libpkg/lua_scripts.c
@@ -66,6 +66,7 @@ pkg_lua_script_run(struct pkg * const pkg, pkg_lua_script type, bool upgrade)
				{ "stat", lua_stat },
				{ "readdir", lua_readdir },
				{ "exec", lua_exec },
+
				{ "exec_capture", lua_exec_capture },
				{ "symlink", lua_pkg_symlink },
				{ "metalog_copy", lua_metalog_copy },
				{ NULL, NULL },
modified libpkg/private/lua.h
@@ -35,6 +35,7 @@ int lua_pkg_symlink(lua_State *L);
int lua_prefix_path(lua_State *L);
int lua_metalog_copy(lua_State *L);
int lua_exec(lua_State *L);
+
int lua_exec_capture(lua_State *L);
void lua_override_ios(lua_State *L, bool);
int lua_stat(lua_State *L);
int lua_readdir(lua_State *L);
modified libpkg/triggers.c
@@ -465,6 +465,7 @@ trigger_execute_lua_common(const char *script, bool sandbox, pkghash *args,
			{ "stat", lua_stat },
			{ "readdir", lua_readdir },
			{ "exec", lua_exec },
+
			{ "exec_capture", lua_exec_capture },
			{ "symlink", lua_pkg_symlink },
			{ NULL, NULL },
		};
modified tests/frontend/lua.sh
@@ -20,7 +20,8 @@ tests_init \
	script_sample_exists \
	script_stat \
	script_arguments \
-
	script_metalog_add
+
	script_metalog_add \
+
	script_exec_capture

script_arguments_body() {
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg "test" "test" "1" "/"
@@ -780,3 +781,39 @@ OUTPUT="./plop type=file uname=root gname=wheel mode=440
			-qfy ${TMPDIR}/test-1.pkg
	atf_check -o inline:"${OUTPUT}" cat METALOG
}
+

+
script_exec_capture_body() {
+
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg "test" "test" "1" "/"
+
	cat << EOF >> test.ucl
+
lua_scripts: {
+
  post-install: [ <<EOS
+
-- Test capturing stdout
+
local out, err, code = pkg.exec_capture({"/bin/echo", "hello world"})
+
pkg.print_msg("stdout:" .. out)
+
pkg.print_msg("exitcode:" .. tostring(code))
+

+
-- Test capturing stderr
+
local out2, err2, code2 = pkg.exec_capture({"/bin/sh", "-c", "echo errmsg >&2"})
+
pkg.print_msg("stderr:" .. err2)
+

+
-- Test exit code
+
local out3, err3, code3 = pkg.exec_capture({"/bin/sh", "-c", "exit 42"})
+
pkg.print_msg("code:" .. tostring(code3))
+
EOS
+
, ]
+
}
+
EOF
+

+
	atf_check \
+
		-o empty \
+
		-e empty \
+
		-s exit:0 \
+
		pkg create -M test.ucl
+

+
	mkdir ${TMPDIR}/target
+
	atf_check \
+
		-o inline:"stdout:hello world\n\nexitcode:0\nstderr:errmsg\n\ncode:42\n" \
+
		-e empty \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR=/dev/null -r ${TMPDIR}/target install -qfy ${TMPDIR}/test-1.pkg
+
}