summaryrefslogtreecommitdiff
path: root/corebinutils/nproc
diff options
context:
space:
mode:
Diffstat (limited to 'corebinutils/nproc')
-rw-r--r--corebinutils/nproc/.gitignore25
-rw-r--r--corebinutils/nproc/GNUmakefile35
-rw-r--r--corebinutils/nproc/LICENSE26
-rw-r--r--corebinutils/nproc/LICENSES/BSD-2-Clause.txt9
-rw-r--r--corebinutils/nproc/README.md26
-rw-r--r--corebinutils/nproc/nproc.162
-rw-r--r--corebinutils/nproc/nproc.c343
-rw-r--r--corebinutils/nproc/tests/test.sh368
8 files changed, 894 insertions, 0 deletions
diff --git a/corebinutils/nproc/.gitignore b/corebinutils/nproc/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/corebinutils/nproc/.gitignore
@@ -0,0 +1,25 @@
+*.a
+*.core
+*.lo
+*.nossppico
+*.o
+*.orig
+*.pico
+*.pieo
+*.po
+*.rej
+*.so
+*.so.[0-9]*
+*.sw[nop]
+*~
+.*DS_Store
+.cache
+.clangd
+.ccls-cache
+.depend*
+compile_commands.json
+compile_commands.events.json
+tags
+build/
+out/
+.linux-obj/
diff --git a/corebinutils/nproc/GNUmakefile b/corebinutils/nproc/GNUmakefile
new file mode 100644
index 0000000000..a745eaf001
--- /dev/null
+++ b/corebinutils/nproc/GNUmakefile
@@ -0,0 +1,35 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS += -D_GNU_SOURCE=1
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/nproc
+OBJS := $(OBJDIR)/nproc.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/nproc.o: $(CURDIR)/nproc.c | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/nproc.c" -o "$@"
+
+test: $(TARGET)
+ CC="$(CC)" NPROC_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/nproc/LICENSE b/corebinutils/nproc/LICENSE
new file mode 100644
index 0000000000..f538ffcee4
--- /dev/null
+++ b/corebinutils/nproc/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2023 Mateusz Guzik
+All rights reserved.
+
+Copyright (c) 2026
+ Project Tick. 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 REGENTS AND CONTRIBUTORS ``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 REGENTS OR CONTRIBUTORS 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.
diff --git a/corebinutils/nproc/LICENSES/BSD-2-Clause.txt b/corebinutils/nproc/LICENSES/BSD-2-Clause.txt
new file mode 100644
index 0000000000..5f662b354c
--- /dev/null
+++ b/corebinutils/nproc/LICENSES/BSD-2-Clause.txt
@@ -0,0 +1,9 @@
+Copyright (c) <year> <owner>
+
+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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.
diff --git a/corebinutils/nproc/README.md b/corebinutils/nproc/README.md
new file mode 100644
index 0000000000..64f48bae87
--- /dev/null
+++ b/corebinutils/nproc/README.md
@@ -0,0 +1,26 @@
+# nproc
+
+Standalone musl-libc-friendly Linux port of FreeBSD `nproc` for Project Tick BSD/Linux Distribution.
+
+## Build
+
+```sh
+gmake -f GNUmakefile
+gmake -f GNUmakefile CC=musl-gcc
+```
+
+## Test
+
+```sh
+gmake -f GNUmakefile test
+gmake -f GNUmakefile test CC=musl-gcc
+```
+
+## Notes
+
+- Port strategy is Linux-native API translation, not preservation of FreeBSD `cpuset(2)` ABI.
+- Default output is mapped to `sched_getaffinity(2)` for the calling thread and counts the CPUs in the returned affinity mask.
+- `--all` is mapped to `sysconf(_SC_NPROCESSORS_ONLN)` because `nproc.1` defines it as the number of processors currently online.
+- `--ignore=count` is parsed strictly as an unsigned decimal integer and the result is clamped to a minimum of `1`, matching the documented semantics.
+- The affinity mask buffer grows dynamically until `sched_getaffinity(2)` accepts it, so the port does not silently truncate systems with more CPUs than a fixed `cpu_set_t` can describe.
+- Intentionally unsupported semantic: Linux CPU quota throttling such as cgroup `cpu.max` is not converted into fractional processor counts. This port reports schedulable CPUs from affinity and the online CPU count because that is the kernel API described by the manpage.
diff --git a/corebinutils/nproc/nproc.1 b/corebinutils/nproc/nproc.1
new file mode 100644
index 0000000000..f3f23190e2
--- /dev/null
+++ b/corebinutils/nproc/nproc.1
@@ -0,0 +1,62 @@
+.\"-
+.\" * Copyright (c) 2023 Piotr Paweł Stefaniak
+.\"
+.\" * SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd June 2, 2023
+.Dt NPROC 1
+.Os
+.Sh NAME
+.Nm nproc
+.Nd print the number of processors
+.Sh SYNOPSIS
+.Nm
+.Op Fl -all
+.Op Fl -ignore Ns = Ns Ar count
+.Nm Fl -help
+.Nm Fl -version
+.Sh DESCRIPTION
+The
+.Nm
+utility is used to print the number of processors limited to the
+.Xr sched_getaffinity 2
+mask of the current process, unless the
+.Fl -all
+flag is specified.
+.Pp
+The available flags are:
+.Bl -tag -width Ds
+.It Fl -all
+Count all processors currently online.
+.It Fl -ignore Ns = Ns Ar count
+The result is decreased by
+.Ar count ,
+but never below 1.
+.It Fl -version
+Print the current program version and exit.
+Do not use this option.
+.It Fl -help
+Print usage information and exit.
+.El
+.Sh COMPATIBILITY
+This program is intended to be compatible with nproc as found in GNU coreutils.
+.Sh BUGS
+The Linux port reports discrete CPUs from the current affinity mask and from
+.Dv _SC_NPROCESSORS_ONLN .
+It does not try to derive fractional processor counts from cgroup CPU quota
+controllers such as
+.Pa cpu.max .
+.Sh SEE ALSO
+.Xr cpuset 1 ,
+.Xr sched_getaffinity 2
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Fx 13.2 .
+.Sh AUTHORS
+.An -nosplit
+.An Mateusz Guzik Aq Mt mjg@FreeBSD.org
+wrote the program and
+.An Piotr Paweł Stefaniak Aq Mt pstef@FreeBSD.org
+wrote this page.
diff --git a/corebinutils/nproc/nproc.c b/corebinutils/nproc/nproc.c
new file mode 100644
index 0000000000..c2e01e421e
--- /dev/null
+++ b/corebinutils/nproc/nproc.c
@@ -0,0 +1,343 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 Mateusz Guzik
+ * Copyright (c) 2026 Project Tick
+ *
+ * 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 REGENTS AND CONTRIBUTORS ``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 REGENTS OR CONTRIBUTORS 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 <ctype.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <sched.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+enum command_mode {
+ MODE_RUN,
+ MODE_HELP,
+ MODE_VERSION
+};
+
+struct options {
+ enum command_mode mode;
+ bool all;
+ uintmax_t ignore;
+};
+
+static const char *progname = "nproc";
+
+static void die(const char *fmt, ...) __attribute__((__noreturn__, format(printf, 1, 2)));
+static void die_errno(const char *what) __attribute__((__noreturn__));
+static void help(FILE *stream);
+static size_t available_processors(void);
+static size_t count_set_bits(const unsigned char *mask, size_t mask_size);
+static size_t initial_mask_size(void);
+static size_t online_processors(void);
+static void parse_args(int argc, char **argv, struct options *options);
+static uintmax_t parse_ignore_count(const char *text);
+static size_t subtract_ignore(size_t count, uintmax_t ignore);
+static void usage_error(const char *fmt, ...) __attribute__((__noreturn__, format(printf, 1, 2)));
+static void version(void);
+
+int
+main(int argc, char **argv)
+{
+ struct options options;
+ size_t count;
+
+ if (argv[0] != NULL && argv[0][0] != '\0') {
+ const char *slash;
+
+ slash = strrchr(argv[0], '/');
+ progname = (slash != NULL && slash[1] != '\0') ? slash + 1 : argv[0];
+ }
+
+ parse_args(argc, argv, &options);
+
+ switch (options.mode) {
+ case MODE_HELP:
+ help(stderr);
+ return (EXIT_SUCCESS);
+ case MODE_VERSION:
+ version();
+ return (EXIT_SUCCESS);
+ case MODE_RUN:
+ break;
+ }
+
+ count = options.all ? online_processors() : available_processors();
+ count = subtract_ignore(count, options.ignore);
+
+ if (printf("%zu\n", count) < 0)
+ die_errno("stdout");
+
+ return (EXIT_SUCCESS);
+}
+
+static void
+parse_args(int argc, char **argv, struct options *options)
+{
+ int i;
+
+ memset(options, 0, sizeof(*options));
+ options->mode = MODE_RUN;
+
+ for (i = 1; i < argc; i++) {
+ const char *arg;
+
+ arg = argv[i];
+ if (strcmp(arg, "--") == 0) {
+ i++;
+ break;
+ }
+ if (strcmp(arg, "--all") == 0) {
+ options->all = true;
+ continue;
+ }
+ if (strcmp(arg, "--help") == 0) {
+ if (argc != 2)
+ usage_error("option --help must be used alone");
+ options->mode = MODE_HELP;
+ return;
+ }
+ if (strcmp(arg, "--version") == 0) {
+ if (argc != 2)
+ usage_error("option --version must be used alone");
+ options->mode = MODE_VERSION;
+ return;
+ }
+ if (strcmp(arg, "--ignore") == 0) {
+ if (i + 1 >= argc)
+ usage_error("option requires an argument: --ignore");
+ i++;
+ options->ignore = parse_ignore_count(argv[i]);
+ continue;
+ }
+ if (strncmp(arg, "--ignore=", 9) == 0) {
+ options->ignore = parse_ignore_count(arg + 9);
+ continue;
+ }
+ if (arg[0] == '-') {
+ usage_error("unknown option: %s", arg);
+ }
+ break;
+ }
+
+ if (i < argc)
+ usage_error("unexpected operand: %s", argv[i]);
+}
+
+static uintmax_t
+parse_ignore_count(const char *text)
+{
+ char *end;
+ uintmax_t value;
+ const unsigned char *p;
+
+ if (text[0] == '\0')
+ die("invalid ignore count: %s", text);
+
+ for (p = (const unsigned char *)text; *p != '\0'; p++) {
+ if (!isdigit(*p))
+ die("invalid ignore count: %s", text);
+ }
+
+ errno = 0;
+ value = strtoumax(text, &end, 10);
+ if (errno == ERANGE || *end != '\0')
+ die("invalid ignore count: %s", text);
+ return (value);
+}
+
+static size_t
+online_processors(void)
+{
+ long value;
+
+ errno = 0;
+ value = sysconf(_SC_NPROCESSORS_ONLN);
+ if (value < 1) {
+ if (errno != 0)
+ die_errno("sysconf(_SC_NPROCESSORS_ONLN)");
+ die("sysconf(_SC_NPROCESSORS_ONLN) returned %ld", value);
+ }
+
+ return ((size_t)value);
+}
+
+static size_t
+initial_mask_size(void)
+{
+ long configured;
+ size_t bits;
+ size_t bytes;
+
+ errno = 0;
+ configured = sysconf(_SC_NPROCESSORS_CONF);
+ if (configured < 1 || configured > LONG_MAX / 2) {
+ if (errno != 0)
+ return (128);
+ return (128);
+ }
+
+ bits = (size_t)configured;
+ if (bits < CHAR_BIT)
+ bits = CHAR_BIT;
+
+ bytes = bits / CHAR_BIT;
+ if ((bits % CHAR_BIT) != 0)
+ bytes++;
+ if (bytes < sizeof(unsigned long))
+ bytes = sizeof(unsigned long);
+ return (bytes);
+}
+
+static size_t
+available_processors(void)
+{
+ unsigned char *mask;
+ size_t count;
+ size_t mask_size;
+
+ mask_size = initial_mask_size();
+
+ for (;;) {
+ int saved_errno;
+
+ mask = calloc(1, mask_size);
+ if (mask == NULL)
+ die_errno("calloc");
+
+ if (sched_getaffinity(0, mask_size, (cpu_set_t *)(void *)mask) == 0) {
+ count = count_set_bits(mask, mask_size);
+ free(mask);
+ if (count == 0)
+ die("sched_getaffinity returned an empty CPU mask");
+ return (count);
+ }
+
+ saved_errno = errno;
+ free(mask);
+ if (saved_errno != EINVAL) {
+ errno = saved_errno;
+ die_errno("sched_getaffinity");
+ }
+ if (mask_size > SIZE_MAX / 2)
+ die("sched_getaffinity CPU mask is too large");
+ mask_size *= 2;
+ }
+}
+
+static size_t
+count_set_bits(const unsigned char *mask, size_t mask_size)
+{
+ size_t count;
+ size_t i;
+
+ count = 0;
+ for (i = 0; i < mask_size; i++) {
+ unsigned int bits;
+
+ bits = mask[i];
+ while (bits != 0) {
+ count++;
+ bits &= bits - 1;
+ }
+ }
+
+ return (count);
+}
+
+static size_t
+subtract_ignore(size_t count, uintmax_t ignore)
+{
+ if (ignore >= count)
+ return (1);
+ return (count - (size_t)ignore);
+}
+
+static void
+help(FILE *stream)
+{
+ if (fprintf(stream,
+ "usage: %s [--all] [--ignore=count]\n"
+ " %s --help\n"
+ " %s --version\n",
+ progname, progname, progname) < 0) {
+ if (stream == stdout)
+ die_errno("stdout");
+ die_errno("stderr");
+ }
+}
+
+static void
+version(void)
+{
+ if (printf("%s (neither_GNU nor_coreutils) 8.32\n", progname) < 0)
+ die_errno("stdout");
+}
+
+static void
+usage_error(const char *fmt, ...)
+{
+ va_list ap;
+
+ fprintf(stderr, "%s: ", progname);
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ fputc('\n', stderr);
+ help(stderr);
+ exit(EXIT_FAILURE);
+}
+
+static void
+die(const char *fmt, ...)
+{
+ va_list ap;
+
+ fprintf(stderr, "%s: ", progname);
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ fputc('\n', stderr);
+ exit(EXIT_FAILURE);
+}
+
+static void
+die_errno(const char *what)
+{
+ fprintf(stderr, "%s: %s: %s\n", progname, what, strerror(errno));
+ exit(EXIT_FAILURE);
+}
diff --git a/corebinutils/nproc/tests/test.sh b/corebinutils/nproc/tests/test.sh
new file mode 100644
index 0000000000..6349577600
--- /dev/null
+++ b/corebinutils/nproc/tests/test.sh
@@ -0,0 +1,368 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+NPROC_BIN=${NPROC_BIN:-"$ROOT/out/nproc"}
+CC=${CC:-cc}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/nproc-test.XXXXXX")
+HELPER_C="$WORKDIR/affinity-helper.c"
+HELPER_BIN="$WORKDIR/affinity-helper"
+STDOUT_FILE="$WORKDIR/stdout"
+STDERR_FILE="$WORKDIR/stderr"
+trap 'rm -rf "$WORKDIR"' EXIT INT TERM
+
+export LC_ALL=C
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+assert_eq() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" != "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected ---" >&2
+ printf '%s' "$expected" >&2
+ printf '\n%s\n' "--- actual ---" >&2
+ printf '%s' "$actual" >&2
+ printf '\n' >&2
+ exit 1
+ fi
+}
+
+assert_match() {
+ name=$1
+ text=$2
+ pattern=$3
+ printf '%s\n' "$text" | grep -Eq "$pattern" || fail "$name"
+}
+
+assert_status() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" -ne "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "expected status: $expected" >&2
+ printf '%s\n' "actual status: $actual" >&2
+ exit 1
+ fi
+}
+
+assert_empty() {
+ name=$1
+ text=$2
+ if [ -n "$text" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected empty ---" >&2
+ printf '%s\n' "--- actual ---" >&2
+ printf '%s' "$text" >&2
+ printf '\n' >&2
+ exit 1
+ fi
+}
+
+run_capture() {
+ if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
+ LAST_STATUS=0
+ else
+ LAST_STATUS=$?
+ fi
+
+ LAST_STDOUT=$(cat "$STDOUT_FILE")
+ LAST_STDERR=$(cat "$STDERR_FILE")
+}
+
+build_helper() {
+ cat >"$HELPER_C" <<'EOF'
+#define _GNU_SOURCE 1
+
+#include <errno.h>
+#include <limits.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static cpu_set_t *
+read_affinity(size_t *setsize, int *cpu_count)
+{
+ cpu_set_t *mask;
+ long configured;
+
+ configured = sysconf(_SC_NPROCESSORS_CONF);
+ if (configured < 1 || configured > INT_MAX / 2)
+ *cpu_count = 128;
+ else
+ *cpu_count = (int)configured;
+
+ if (*cpu_count < 1)
+ *cpu_count = 1;
+
+ for (;;) {
+ int saved_errno;
+
+ mask = CPU_ALLOC((size_t)*cpu_count);
+ if (mask == NULL) {
+ perror("CPU_ALLOC");
+ exit(1);
+ }
+ *setsize = CPU_ALLOC_SIZE((size_t)*cpu_count);
+ CPU_ZERO_S(*setsize, mask);
+
+ if (sched_getaffinity(0, *setsize, mask) == 0)
+ return (mask);
+
+ saved_errno = errno;
+ CPU_FREE(mask);
+ if (saved_errno != EINVAL) {
+ errno = saved_errno;
+ perror("sched_getaffinity");
+ exit(1);
+ }
+ if (*cpu_count > INT_MAX / 2) {
+ fprintf(stderr, "affinity mask too large\n");
+ exit(1);
+ }
+ *cpu_count *= 2;
+ }
+}
+
+static int
+online_processors(void)
+{
+ long value;
+
+ value = sysconf(_SC_NPROCESSORS_ONLN);
+ if (value < 1) {
+ fprintf(stderr, "invalid online cpu count\n");
+ exit(1);
+ }
+ return ((int)value);
+}
+
+static int
+subset_size_for_mask(const cpu_set_t *mask, size_t setsize)
+{
+ int available;
+
+ available = CPU_COUNT_S(setsize, mask);
+ if (available <= 1)
+ return (0);
+ return (available - 1);
+}
+
+static void
+fill_subset(cpu_set_t *subset, size_t setsize, const cpu_set_t *mask, int subset_size,
+ int cpu_count)
+{
+ int cpu;
+ int chosen;
+
+ CPU_ZERO_S(setsize, subset);
+ chosen = 0;
+ for (cpu = 0; cpu < cpu_count && chosen < subset_size; cpu++) {
+ if (!CPU_ISSET_S(cpu, setsize, mask))
+ continue;
+ CPU_SET_S(cpu, setsize, subset);
+ chosen++;
+ }
+ if (chosen != subset_size) {
+ fprintf(stderr, "failed to build subset mask\n");
+ exit(1);
+ }
+}
+
+int
+main(int argc, char **argv)
+{
+ cpu_set_t *mask;
+ cpu_set_t *subset;
+ size_t setsize;
+ int cpu_count;
+ int subset_size;
+
+ mask = read_affinity(&setsize, &cpu_count);
+ subset_size = subset_size_for_mask(mask, setsize);
+
+ if (argc == 2 && strcmp(argv[1], "counts") == 0) {
+ printf("available=%d\n", CPU_COUNT_S(setsize, mask));
+ printf("online=%d\n", online_processors());
+ printf("subset=%d\n", subset_size);
+ CPU_FREE(mask);
+ return (0);
+ }
+
+ if (argc >= 2 && strcmp(argv[1], "run-subset") == 0) {
+ if (argc < 3) {
+ fprintf(stderr, "run-subset requires a command\n");
+ CPU_FREE(mask);
+ return (1);
+ }
+ if (subset_size == 0) {
+ CPU_FREE(mask);
+ return (2);
+ }
+
+ subset = CPU_ALLOC((size_t)cpu_count);
+ if (subset == NULL) {
+ perror("CPU_ALLOC");
+ CPU_FREE(mask);
+ return (1);
+ }
+ fill_subset(subset, setsize, mask, subset_size, cpu_count);
+ if (sched_setaffinity(0, setsize, subset) != 0) {
+ perror("sched_setaffinity");
+ CPU_FREE(subset);
+ CPU_FREE(mask);
+ return (1);
+ }
+ CPU_FREE(subset);
+ CPU_FREE(mask);
+ execvp(argv[2], &argv[2]);
+ perror("execvp");
+ return (1);
+ }
+
+ fprintf(stderr, "usage: affinity-helper counts | run-subset command [args ...]\n");
+ CPU_FREE(mask);
+ return (1);
+}
+EOF
+
+ "$CC" -D_GNU_SOURCE=1 -O2 -std=c17 -Wall -Wextra -Werror \
+ "$HELPER_C" -o "$HELPER_BIN" || fail "failed to build helper with $CC"
+}
+
+extract_value() {
+ name=$1
+ text=$2
+ value=$(printf '%s\n' "$text" | sed -n "s/^$name=//p")
+ [ -n "$value" ] || fail "missing helper value: $name"
+ printf '%s' "$value"
+}
+
+[ -x "$NPROC_BIN" ] || fail "missing binary: $NPROC_BIN"
+build_helper
+
+counts_output=$("$HELPER_BIN" counts)
+available_count=$(extract_value available "$counts_output")
+online_count=$(extract_value online "$counts_output")
+subset_count=$(extract_value subset "$counts_output")
+
+run_capture "$NPROC_BIN"
+assert_status "default status" 0 "$LAST_STATUS"
+assert_eq "default stdout" "$available_count" "$LAST_STDOUT"
+assert_empty "default stderr" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --all
+assert_status "all status" 0 "$LAST_STATUS"
+assert_eq "all stdout" "$online_count" "$LAST_STDOUT"
+assert_empty "all stderr" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --ignore=0
+assert_status "ignore zero status" 0 "$LAST_STATUS"
+assert_eq "ignore zero stdout" "$available_count" "$LAST_STDOUT"
+assert_empty "ignore zero stderr" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --ignore 0
+assert_status "ignore split status" 0 "$LAST_STATUS"
+assert_eq "ignore split stdout" "$available_count" "$LAST_STDOUT"
+assert_empty "ignore split stderr" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --ignore="$available_count"
+assert_status "ignore saturates status" 0 "$LAST_STATUS"
+assert_eq "ignore saturates stdout" "1" "$LAST_STDOUT"
+assert_empty "ignore saturates stderr" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --all --ignore="$online_count"
+assert_status "all ignore saturates status" 0 "$LAST_STATUS"
+assert_eq "all ignore saturates stdout" "1" "$LAST_STDOUT"
+assert_empty "all ignore saturates stderr" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --
+assert_status "double dash status" 0 "$LAST_STATUS"
+assert_eq "double dash stdout" "$available_count" "$LAST_STDOUT"
+assert_empty "double dash stderr" "$LAST_STDERR"
+
+if [ "$subset_count" -gt 0 ]; then
+ run_capture "$HELPER_BIN" run-subset "$NPROC_BIN"
+ assert_status "subset affinity status" 0 "$LAST_STATUS"
+ assert_eq "subset affinity stdout" "$subset_count" "$LAST_STDOUT"
+ assert_empty "subset affinity stderr" "$LAST_STDERR"
+fi
+
+help_text="usage: nproc [--all] [--ignore=count]
+ nproc --help
+ nproc --version"
+
+run_capture "$NPROC_BIN" --help
+assert_status "help status" 0 "$LAST_STATUS"
+assert_empty "help stdout" "$LAST_STDOUT"
+assert_eq "help stderr" "$help_text" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --version
+assert_status "version status" 0 "$LAST_STATUS"
+assert_match "version stdout" "$LAST_STDOUT" '^nproc \(neither_GNU nor_coreutils\) 8\.32$'
+assert_empty "version stderr" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --ignore=abc
+assert_status "invalid ignore alpha status" 1 "$LAST_STATUS"
+assert_empty "invalid ignore alpha stdout" "$LAST_STDOUT"
+assert_eq "invalid ignore alpha stderr" \
+ "nproc: invalid ignore count: abc" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --ignore=-1
+assert_status "invalid ignore negative status" 1 "$LAST_STATUS"
+assert_empty "invalid ignore negative stdout" "$LAST_STDOUT"
+assert_eq "invalid ignore negative stderr" \
+ "nproc: invalid ignore count: -1" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --ignore=
+assert_status "invalid ignore empty status" 1 "$LAST_STATUS"
+assert_empty "invalid ignore empty stdout" "$LAST_STDOUT"
+assert_eq "invalid ignore empty stderr" \
+ "nproc: invalid ignore count: " "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --ignore
+assert_status "missing ignore status" 1 "$LAST_STATUS"
+assert_empty "missing ignore stdout" "$LAST_STDOUT"
+assert_eq "missing ignore stderr" \
+ "nproc: option requires an argument: --ignore
+usage: nproc [--all] [--ignore=count]
+ nproc --help
+ nproc --version" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --bogus
+assert_status "unknown option status" 1 "$LAST_STATUS"
+assert_empty "unknown option stdout" "$LAST_STDOUT"
+assert_eq "unknown option stderr" \
+ "nproc: unknown option: --bogus
+usage: nproc [--all] [--ignore=count]
+ nproc --help
+ nproc --version" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" extra
+assert_status "unexpected operand status" 1 "$LAST_STATUS"
+assert_empty "unexpected operand stdout" "$LAST_STDOUT"
+assert_eq "unexpected operand stderr" \
+ "nproc: unexpected operand: extra
+usage: nproc [--all] [--ignore=count]
+ nproc --help
+ nproc --version" "$LAST_STDERR"
+
+run_capture "$NPROC_BIN" --help --all
+assert_status "help exclusive status" 1 "$LAST_STATUS"
+assert_empty "help exclusive stdout" "$LAST_STDOUT"
+assert_eq "help exclusive stderr" \
+ "nproc: option --help must be used alone
+usage: nproc [--all] [--ignore=count]
+ nproc --help
+ nproc --version" "$LAST_STDERR"
+
+printf '%s\n' "PASS"