commit: b6a8b93349b7f163cb8d35e19d2afcbb768f5ccc
Author: Michał Górny <mgorny <AT> gentoo <DOT> org>
AuthorDate: Sat Nov 29 13:30:29 2025 +0000
Commit: Michał Górny <mgorny <AT> gentoo <DOT> org>
CommitDate: Sat Nov 29 13:30:29 2025 +0000
URL: https://gitweb.gentoo.org/proj/steve.git/commit/?id=b6a8b933
Add the initial client version
Add a client that acquires a token and runs the specified command.
Signed-off-by: Michał Górny <mgorny <AT> gentoo.org>
meson.build | 20 +++++---
meson.options | 8 ++++
stevie.cxx | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 165 insertions(+), 6 deletions(-)
diff --git a/meson.build b/meson.build
index 2bc5c58..b0d8864 100644
--- a/meson.build
+++ b/meson.build
@@ -6,10 +6,18 @@ project('steve', 'cpp',
version = meson.project_version()
-fuse3 = dependency('fuse3')
-libevent = dependency('libevent', version: '>= 2')
+if get_option('client')
+ executable('stevie', ['stevie.cxx'],
+ cpp_args : [f'-DSTEVE_VERSION="@version@"'],
+ install : true)
+endif
-executable('steve', ['steve.cxx'],
- cpp_args : [f'-DSTEVE_VERSION="@version@"'],
- dependencies: [fuse3, libevent],
- install : true)
+if get_option('server')
+ fuse3 = dependency('fuse3')
+ libevent = dependency('libevent', version: '>= 2')
+
+ executable('steve', ['steve.cxx'],
+ cpp_args : [f'-DSTEVE_VERSION="@version@"'],
+ dependencies: [fuse3, libevent],
+ install : true)
+endif
diff --git a/meson.options b/meson.options
new file mode 100644
index 0000000..8a312bf
--- /dev/null
+++ b/meson.options
@@ -0,0 +1,8 @@
+option('client',
+ type: 'boolean',
+ value: true,
+ description: 'Build the client')
+option('server',
+ type: 'boolean',
+ value: true,
+ description: 'Build the server')
diff --git a/stevie.cxx b/stevie.cxx
new file mode 100644
index 0000000..28a0d88
--- /dev/null
+++ b/stevie.cxx
@@ -0,0 +1,143 @@
+/* Stevie, the client for stevie
+ * (c) 2025 Michał Górny
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Inspired by nixos-jobserver (draft) and guildmaster:
+ *
https://github.com/RaitoBezarius/nixpkgs/blob/e97220ecf1e8887b949e4e16547bf0334826d076/pkgs/by-name/ni/nixos-jobserver/nixos-jobserver.cpp#L213
+ * https://codeberg.org/amonakov/guildmaster/
+ */
+
+#include <cerrno>
+#include <cstring>
+#include <print>
+
+#include <fcntl.h>
+#include <getopt.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+struct token_guard {
+ int jobserver_fd;
+ char job_token;
+
+ ~token_guard() {
+ ssize_t wr;
+ while ((wr = write(jobserver_fd, &job_token, 1)) == -1 && errno
== EINTR);
+ if (wr == -1)
+ perror("Writing job token failed");
+ }
+};
+
+static int run_command(int jobserver_fd, char **argv, const char
*jobserver_path)
+{
+ char job_token;
+
+ ssize_t res;
+ while ((res = read(jobserver_fd, &job_token, 1)) == -1 && errno ==
EINTR);
+ if (res == 0) {
+ std::print("EOF while waiting for job token\n");
+ return 1;
+ }
+ if (res == -1) {
+ perror("Reading job token failed");
+ return 1;
+ }
+
+ pid_t pid = fork();
+ if (pid == 0) {
+ const char *old_makeflags = getenv("MAKEFLAGS");
+ std::string new_makeflags;
+ if (old_makeflags) {
+ new_makeflags = old_makeflags;
+ new_makeflags += ' ';
+ }
+ new_makeflags += "--jobserver-auth=fifo:";
+ new_makeflags += jobserver_path;
+ if (setenv("MAKEFLAGS", new_makeflags.c_str(), 1) == -1) {
+ std::print(stderr, "Unable to set MAKEFLAGS={}\n",
new_makeflags);
+ _exit(1);
+ }
+
+ execvp(argv[0], argv);
+ std::print("exec for {} failed: {}\n", argv[0],
strerror(errno));
+ _exit(1);
+ }
+
+ token_guard job_token_guard{jobserver_fd, job_token};
+ if (pid == -1) {
+ perror("Forking failed");
+ return 1;
+ }
+
+ int wret;
+ while ((res = waitpid(pid, &wret, 0)) == -1 && errno == EINTR);
+ if (res == -1) {
+ std::print("Waiting for PID {} failed: {}\n", pid,
strerror(errno));
+ return 1;
+ }
+
+ return WIFSIGNALED(wret) ? WTERMSIG(wret) + 128 : WEXITSTATUS(wret);
+}
+
+struct fd_guard {
+ int fd;
+ ~fd_guard() {
+ close(fd);
+ }
+};
+
+static constexpr char stevie_usage[] =
+ "usage: {} [options] <argv>...\n"
+ "\n"
+ "options:\n"
+ " --help, -h print this help message\n"
+ " --version, -V print version\n"
+ " --jobserver PATH,\n"
+ " -j PATH jobserver FIFO path (default:
/dev/steve)\n";
+
+static const struct option stevie_long_opts[] = {
+ {"help", no_argument, 0, 'h'},
+ {"version", no_argument, 0, 'V'},
+ {"jobserver", required_argument, 0, 'j'},
+ {},
+};
+
+static const char *stevie_short_opts = "+hVj:";
+
+int main(int argc, char **argv)
+{
+ const char *jobserver_path = "/dev/steve";
+
+ int opt;
+ while ((opt = getopt_long(argc, argv, stevie_short_opts,
+ stevie_long_opts, nullptr)) != -1) {
+ switch (opt) {
+ case 'h':
+ std::print(stevie_usage, argv[0]);
+ return 0;
+ case 'V':
+ std::print("stevie {}\n", STEVE_VERSION);
+ return 0;
+ case 'j':
+ jobserver_path = optarg;
+ break;
+ default:
+ std::print(stderr, stevie_usage, argv[0]);
+ return 1;
+ }
+ }
+
+ if (!argv[optind]) {
+ std::print(stderr, "{}: no command provided\n", argv[0]);
+ return 1;
+ }
+
+ int jobserver_fd = open(jobserver_path, O_RDWR | O_CLOEXEC);
+ if (jobserver_fd == -1) {
+ std::print(stderr, "unable to open {}: {}\n", jobserver_path,
strerror(errno));
+ return 1;
+ }
+ fd_guard jobserver_fd_guard{jobserver_fd};
+
+ return run_command(jobserver_fd, &argv[optind], jobserver_path);
+}