summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/codeql.yml9
-rw-r--r--.github/dependabot.yml6
-rwxr-xr-x.github/diag.sh16
-rw-r--r--.github/workflows/ci.yml107
-rw-r--r--.github/workflows/codecov.yml4
-rw-r--r--.github/workflows/codeql.yml6
-rw-r--r--Makefile167
-rw-r--r--README.md4
-rw-r--r--bench/bench.sh10
-rwxr-xr-xbuild/cc.sh (renamed from config/cc.sh)0
-rw-r--r--build/config.mk62
-rwxr-xr-xbuild/define-if.sh (renamed from config/cc-define.sh)11
-rw-r--r--build/deps.mk18
-rw-r--r--build/empty.c (renamed from config/empty.c)0
-rw-r--r--build/exports.mk (renamed from config/exports.mk)0
-rw-r--r--build/flags.mk (renamed from config/flags.mk)27
-rw-r--r--build/has/--st-birthtim.c9
-rw-r--r--build/has/acl-get-entry.c11
-rw-r--r--build/has/acl-get-file.c11
-rw-r--r--build/has/acl-get-tag-type.c13
-rw-r--r--build/has/acl-is-trivial-np.c (renamed from config/acl-is-trivial-np.c)1
-rw-r--r--build/has/acl-trivial.c8
-rw-r--r--build/has/aligned-alloc.c (renamed from config/aligned-alloc.c)0
-rw-r--r--build/has/attribute-format-syslog.c13
-rw-r--r--build/has/confstr.c (renamed from config/confstr.c)0
-rw-r--r--build/has/extattr-get-file.c10
-rw-r--r--build/has/extattr-get-link.c10
-rw-r--r--build/has/extattr-list-file.c10
-rw-r--r--build/has/extattr-list-link.c10
-rw-r--r--build/has/fdclosedir.c (renamed from config/fdclosedir.c)0
-rw-r--r--build/has/getdents.c (renamed from config/getdents.c)5
-rw-r--r--build/has/getdents64-syscall.c (renamed from config/getdents64-syscall.c)5
-rw-r--r--build/has/getdents64.c (renamed from config/getdents64.c)5
-rw-r--r--build/has/getmntent-1.c9
-rw-r--r--build/has/getmntent-2.c10
-rw-r--r--build/has/getmntinfo.c10
-rw-r--r--build/has/getprogname-gnu.c (renamed from config/getprogname-gnu.c)0
-rw-r--r--build/has/getprogname.c (renamed from config/getprogname.c)0
-rw-r--r--build/has/max-align-t.c8
-rw-r--r--build/has/pipe2.c (renamed from config/pipe2.c)0
-rw-r--r--build/has/posix-getdents.c9
-rw-r--r--build/has/posix-spawn-addfchdir-np.c (renamed from config/posix-spawn-addfchdir-np.c)0
-rw-r--r--build/has/posix-spawn-addfchdir.c (renamed from config/posix-spawn-addfchdir.c)0
-rw-r--r--build/has/st-acmtim.c (renamed from config/st-acmtim.c)0
-rw-r--r--build/has/st-acmtimespec.c (renamed from config/st-acmtimespec.c)0
-rw-r--r--build/has/st-birthtim.c (renamed from config/st-birthtim.c)0
-rw-r--r--build/has/st-birthtimespec.c (renamed from config/st-birthtimespec.c)0
-rw-r--r--build/has/st-flags.c (renamed from config/st-flags.c)0
-rw-r--r--build/has/statx-syscall.c (renamed from config/statx-syscall.c)0
-rw-r--r--build/has/statx.c (renamed from config/statx.c)0
-rw-r--r--build/has/strerror-l.c (renamed from config/strerror-l.c)0
-rw-r--r--build/has/strerror-r-gnu.c (renamed from config/strerror-r-gnu.c)0
-rw-r--r--build/has/strerror-r-posix.c (renamed from config/strerror-r-posix.c)0
-rw-r--r--build/has/string-to-flags.c9
-rw-r--r--build/has/strtofflags.c9
-rw-r--r--build/has/timegm.c9
-rw-r--r--build/has/tm-gmtoff.c (renamed from config/tm-gmtoff.c)0
-rw-r--r--build/has/uselocale.c (renamed from config/uselocale.c)0
-rw-r--r--build/header.mk74
-rwxr-xr-xbuild/msg-if.sh21
-rwxr-xr-xbuild/msg.sh62
-rwxr-xr-xbuild/pkgconf.sh (renamed from config/pkgconf.sh)14
-rw-r--r--build/pkgs.mk33
-rw-r--r--build/prelude.mk124
-rw-r--r--build/use/libacl.c (renamed from config/libacl.c)0
-rw-r--r--build/use/libcap.c (renamed from config/libcap.c)0
-rw-r--r--build/use/libselinux.c (renamed from config/libselinux.c)0
-rw-r--r--build/use/liburing.c (renamed from config/liburing.c)0
-rw-r--r--build/use/oniguruma.c (renamed from config/oniguruma.c)0
-rw-r--r--config/config.mk78
-rw-r--r--config/deps.mk18
-rw-r--r--config/header.mk60
-rw-r--r--config/pkg.mk23
-rwxr-xr-xconfig/pkg.sh26
-rw-r--r--config/pkgs.mk17
-rw-r--r--config/prelude.mk170
-rwxr-xr-xconfigure137
-rw-r--r--docs/BUILDING.md211
-rw-r--r--docs/CHANGELOG.md30
-rw-r--r--src/atomic.h33
-rw-r--r--src/bar.c137
-rw-r--r--src/bfstd.c99
-rw-r--r--src/bfstd.h5
-rw-r--r--src/bftw.c6
-rw-r--r--src/bftw.h4
-rw-r--r--src/color.c50
-rw-r--r--src/color.h7
-rw-r--r--src/ctx.c104
-rw-r--r--src/diag.c4
-rw-r--r--src/diag.h43
-rw-r--r--src/dir.c16
-rw-r--r--src/dir.h9
-rw-r--r--src/dstring.c5
-rw-r--r--src/dstring.h4
-rw-r--r--src/eval.c42
-rw-r--r--src/exec.c6
-rw-r--r--src/fsade.c127
-rw-r--r--src/fsade.h9
-rw-r--r--src/ioq.c5
-rw-r--r--src/main.c1
-rw-r--r--src/mtab.c9
-rw-r--r--src/opt.c6
-rw-r--r--src/prelude.h59
-rw-r--r--src/printf.c118
-rw-r--r--src/sighook.c600
-rw-r--r--src/sighook.h73
-rw-r--r--src/stat.c37
-rw-r--r--src/xspawn.c37
-rw-r--r--src/xtime.c40
-rw-r--r--tests/bfstd.c4
-rw-r--r--tests/common/empty.out (renamed from tests/gnu/empty.out)0
-rw-r--r--tests/common/empty.sh (renamed from tests/gnu/empty.sh)0
-rw-r--r--tests/common/empty_error.out3
-rw-r--r--tests/common/empty_error.sh7
-rw-r--r--tests/common/empty_special.out (renamed from tests/gnu/empty_special.out)0
-rw-r--r--tests/common/empty_special.sh (renamed from tests/gnu/empty_special.sh)0
-rw-r--r--tests/ioq.c6
-rw-r--r--tests/main.c6
-rw-r--r--tests/sighook.c97
-rw-r--r--tests/tests.h18
-rw-r--r--tests/util.sh9
-rw-r--r--tests/xspawn.c53
-rw-r--r--tests/xtime.c20
-rw-r--r--tests/xtouch.c2
124 files changed, 2557 insertions, 1102 deletions
diff --git a/.github/codeql.yml b/.github/codeql.yml
new file mode 100644
index 0000000..6ff8337
--- /dev/null
+++ b/.github/codeql.yml
@@ -0,0 +1,9 @@
+query-filters:
+ - exclude:
+ id: cpp/commented-out-code
+ - exclude:
+ id: cpp/long-switch
+ - exclude:
+ id: cpp/loop-variable-changed
+ - exclude:
+ id: cpp/poorly-documented-function
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..5ace460
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/diag.sh b/.github/diag.sh
new file mode 100755
index 0000000..942487a
--- /dev/null
+++ b/.github/diag.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Convert compiler diagnostics to GitHub Actions messages
+# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message
+
+set -eu
+
+filter() {
+ sed -E 's/^(([^:]*):([^:]*):([^:]*): (warning|error): (.*))$/::\5 file=\2,line=\3,col=\4,title=Compiler \5::\6\
+\1/'
+}
+
+"$@" 2> >(filter >&2) | filter
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3ad924f..8011224 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,7 +39,12 @@ jobs:
- name: Run tests
run: |
- make -j$(nproc) distcheck
+ .github/diag.sh make -j$(nproc) distcheck
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-config.log
+ path: distcheck-*/gen/config.log
macos:
name: macOS
@@ -58,7 +63,7 @@ jobs:
- name: Run tests
run: |
jobs=$(sysctl -n hw.ncpu)
- make -j$jobs distcheck
+ .github/diag.sh make -j$jobs distcheck
freebsd:
name: FreeBSD
@@ -69,27 +74,25 @@ jobs:
- uses: actions/checkout@v4
- name: Run tests
- uses: vmactions/freebsd-vm@v1
+ uses: cross-platform-actions/action@v0.24.0
with:
- release: "14.0"
- usesh: true
- copyback: false
+ operating_system: freebsd
+ version: "14.0"
- prepare: |
- pkg install -y \
+ run: |
+ sudo pkg install -y \
bash \
expect \
oniguruma \
pkgconf \
- sudo \
tcl-wrapper
- pw useradd -n action -m -G wheel -s /usr/local/bin/bash
- echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers
- mount -t fdescfs none /dev/fd
+ sudo mount -t fdescfs none /dev/fd
+ .github/diag.sh make -j$(nproc) distcheck
- run: |
- chown -R action:action .
- sudo -u action make -j$(nproc) distcheck
+ - uses: actions/upload-artifact@v4
+ with:
+ name: freebsd-config.log
+ path: distcheck-*/gen/config.log
openbsd:
name: OpenBSD
@@ -100,27 +103,25 @@ jobs:
- uses: actions/checkout@v4
- name: Run tests
- uses: vmactions/openbsd-vm@v1
+ uses: cross-platform-actions/action@v0.24.0
with:
- release: "7.5"
- usesh: true
- copyback: false
+ operating_system: openbsd
+ version: "7.5"
- prepare: |
- pkg_add \
+ run: |
+ sudo pkg_add \
bash \
expect \
gmake \
oniguruma
- adduser -group USER -batch action wheel </dev/null
- cp /etc/examples/doas.conf /etc/doas.conf
- echo "permit nopass keepenv :wheel" >>/etc/doas.conf
-
- run: |
- chown -R action:action .
jobs=$(sysctl -n hw.ncpu)
- doas -u action gmake config
- doas -u action gmake -j$jobs check TEST_FLAGS="--sudo=doas --verbose=skipped"
+ ./configure MAKE=gmake
+ .github/diag.sh gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: openbsd-config.log
+ path: gen/config.log
netbsd:
name: NetBSD
@@ -131,30 +132,26 @@ jobs:
- uses: actions/checkout@v4
- name: Run tests
- uses: vmactions/netbsd-vm@v1
+ uses: cross-platform-actions/action@v0.24.0
with:
- release: "10.0"
- usesh: true
- copyback: false
+ operating_system: netbsd
+ version: "10.0"
- prepare: |
+ run: |
PATH="/sbin:/usr/sbin:$PATH"
- pkg_add \
+ sudo pkgin -y install \
bash \
- clang \
oniguruma \
pkgconf \
- sudo \
tcl-expect
- useradd -m -G wheel -g =uid action
- echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/pkg/etc/sudoers
-
- run: |
- PATH="/sbin:/usr/sbin:$PATH"
- chown -R action:action .
jobs=$(sysctl -n hw.ncpu)
- sudo -u action make config CC=clang
- sudo -u action make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+ ./configure
+ .github/diag.sh make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: netbsd-config.log
+ path: gen/config.log
dragonflybsd:
name: DragonFly BSD
@@ -169,7 +166,6 @@ jobs:
with:
release: "6.4.0"
usesh: true
- copyback: false
prepare: |
pkg install -y \
@@ -185,8 +181,13 @@ jobs:
run: |
chown -R action:action .
jobs=$(sysctl -n hw.ncpu)
- sudo -u action make config
- sudo -u action make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+ sudo -u action ./configure
+ sudo -u action .github/diag.sh make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dragonfly-config.log
+ path: gen/config.log
omnios:
name: OmniOS
@@ -201,7 +202,6 @@ jobs:
with:
release: "r151048"
usesh: true
- copyback: false
prepare: |
pkg install \
@@ -218,5 +218,10 @@ jobs:
PATH="/usr/xpg4/bin:$PATH"
chown -R action:staff .
jobs=$(getconf NPROCESSORS_ONLN)
- sudo -u action gmake config
- sudo -u action gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+ sudo -u action ./configure MAKE=gmake
+ sudo -u action .github/diag.sh gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: omnios-config.log
+ path: gen/config.log
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index 2abe531..6aaace6 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -25,11 +25,11 @@ jobs:
- name: Generate coverage
run: |
- make config GCOV=y
+ ./configure --enable-gcov
make -j$(nproc) check TEST_FLAGS="--sudo"
gcov -abcfpu obj/*/*.o
- - uses: codecov/codecov-action@v3
+ - uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index a2c224a..a0b8fe3 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -39,15 +39,19 @@ jobs:
libonig-dev \
liburing-dev
+ - name: Configure
+ run: |
+ ./configure
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: cpp
queries: +security-and-quality
+ config-file: .github/codeql.yml
- name: Build
run: |
- make config
make -j$(nproc) all
- name: Perform CodeQL Analysis
diff --git a/Makefile b/Makefile
index 981c322..b31d6c5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,111 +1,99 @@
# Copyright © Tavian Barnes <tavianator@tavianator.com>
# SPDX-License-Identifier: 0BSD
-# This Makefile implements the configuration and build steps for bfs. It is
-# portable to both GNU make and most BSD make implementations. To build bfs,
-# run
+# To build bfs, run
#
-# $ make config
+# $ ./configure
# $ make
# Utilities and GNU/BSD portability
-include config/prelude.mk
+include build/prelude.mk
# The default build target
default: bfs
.PHONY: default
# Include the generated build config, if it exists
--include ${CONFIG}
+-include gen/config.mk
-## Configuration phase (`make config`)
-
-# The configuration goal itself
-config::
- @+${MAKE} -sf config/config.mk
+## Configuration phase (`./configure`)
# bfs used to have flag-like targets (`make release`, `make asan ubsan`, etc.).
# Direct users to the new configuration system.
asan lsan msan tsan ubsan gcov lint release::
- @printf 'error: `%s %s` is no longer supported. ' "${MAKE}" $@ >&2
- @printf 'Use `%s config %s=y` instead.\n' "${MAKE}" $$(echo $@ | tr 'a-z' 'A-Z') >&2
+ @printf 'error: `%s %s` is no longer supported. Use `./configure --enable-%s` instead.\n' \
+ "${MAKE}" $@ $@ >&2
@false
-# Print an error if `make` is run before `make config`
-${CONFIG}::
+# Print an error if `make` is run before `./configure`
+gen/config.mk::
@if ! [ -e $@ ]; then \
- printf 'error: You must run `%s config` before `%s`.\n' "${MAKE}" "${MAKE}" >&2; \
+ printf 'error: You must run `./configure` before `%s`.\n' "${MAKE}" >&2; \
false; \
fi
## Build phase (`make`)
# The main binary
-bfs: ${BIN}/bfs
+bfs: bin/bfs
.PHONY: bfs
# All binaries
BINS := \
- ${BIN}/bfs \
- ${BIN}/tests/mksock \
- ${BIN}/tests/units \
- ${BIN}/tests/xspawnee \
- ${BIN}/tests/xtouch
+ bin/bfs \
+ bin/tests/mksock \
+ bin/tests/units \
+ bin/tests/xspawnee \
+ bin/tests/xtouch
all: ${BINS}
.PHONY: all
-# Group relevant flags together
-ALL_CFLAGS = ${CPPFLAGS} ${CFLAGS} ${DEPFLAGS}
-ALL_LDFLAGS = ${CFLAGS} ${LDFLAGS}
-
# The main binary
-${BIN}/bfs: ${LIBBFS} ${OBJ}/src/main.o
+bin/bfs: ${LIBBFS} obj/src/main.o
${BINS}:
@${MKDIR} ${@D}
- +${MSG} "[ LD ] ${TGT}" ${CC} ${ALL_LDFLAGS} ${.ALLSRC} ${LDLIBS} -o $@
+ +${MSG} "[ LD ] $@" ${CC} ${CFLAGS} ${LDFLAGS} ${.ALLSRC} ${LDLIBS} -o $@
${POSTLINK}
# Get the .c file for a .o file
-_CSRC = ${@:${OBJ}/%.o=%.c}
-CSRC = ${_CSRC:gen/%=${GEN}/%}
+CSRC = ${@:obj/%.o=%.c}
-# Depend on ${CONFIG} to make sure `make config` runs first, and to rebuild when
-# the configuration changes
-${OBJS}: ${CONFIG}
+# Rebuild when the configuration changes
+${OBJS}: gen/config.mk
@${MKDIR} ${@D}
- ${MSG} "[ CC ] ${_CSRC}" ${CC} ${ALL_CFLAGS} -c ${CSRC} -o $@
+ ${MSG} "[ CC ] ${CSRC}" ${CC} ${CPPFLAGS} ${CFLAGS} -c ${CSRC} -o $@
# Save the version number to this file, but only update version.c if it changes
-${GEN}/version.c.new::
+gen/version.c.new::
@${MKDIR} ${@D}
@printf 'const char bfs_version[] = "' >$@
@if [ "$$VERSION" ]; then \
printf '%s' "$$VERSION"; \
- elif test -d .git && command -v git >/dev/null 2>&1; then \
- git describe --always --dirty; \
+ elif test -e src/../.git && command -v git >/dev/null 2>&1; then \
+ git -C src/.. describe --always --dirty; \
else \
- echo "3.1.3"; \
+ echo "3.2"; \
fi | tr -d '\n' >>$@
@printf '";\n' >>$@
-${GEN}/version.c: ${GEN}/version.c.new
+gen/version.c: gen/version.c.new
@test -e $@ && cmp -s $@ ${.ALLSRC} && rm ${.ALLSRC} || mv ${.ALLSRC} $@
-${OBJ}/gen/version.o: ${GEN}/version.c
+obj/gen/version.o: gen/version.c
## Test phase (`make check`)
# Unit test binaries
UTEST_BINS := \
- ${BIN}/tests/units \
- ${BIN}/tests/xspawnee
+ bin/tests/units \
+ bin/tests/xspawnee
# Integration test binaries
ITEST_BINS := \
- ${BIN}/tests/mksock \
- ${BIN}/tests/xtouch
+ bin/tests/mksock \
+ bin/tests/xtouch
# Build (but don't run) test binaries
tests: ${UTEST_BINS} ${ITEST_BINS}
@@ -117,45 +105,45 @@ check: unit-tests integration-tests
# Run the unit tests
unit-tests: ${UTEST_BINS}
- ${MSG} "[TEST] tests/units" ${BIN}/tests/units
+ ${MSG} "[TEST] tests/units" bin/tests/units
.PHONY: unit-tests
-${BIN}/tests/units: \
+bin/tests/units: \
${UNIT_OBJS} \
${LIBBFS}
-${BIN}/tests/xspawnee: \
- ${OBJ}/tests/xspawnee.o
+bin/tests/xspawnee: \
+ obj/tests/xspawnee.o
# The different flag combinations we check
INTEGRATIONS := default dfs ids eds j1 j2 j3 s
INTEGRATION_TESTS := ${INTEGRATIONS:%=check-%}
# Check just `bfs`
-check-default: ${BIN}/bfs ${ITEST_BINS}
+check-default: bin/bfs ${ITEST_BINS}
+${MSG} "[TEST] bfs" \
- ./tests/tests.sh --make="${MAKE}" --bfs="${BIN}/bfs" ${TEST_FLAGS}
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs" ${TEST_FLAGS}
# Check the different search strategies
-check-dfs check-ids check-eds: ${BIN}/bfs ${ITEST_BINS}
+check-dfs check-ids check-eds: bin/bfs ${ITEST_BINS}
+${MSG} "[TEST] bfs -S ${@:check-%=%}" \
- ./tests/tests.sh --make="${MAKE}" --bfs="${BIN}/bfs -S ${@:check-%=%}" ${TEST_FLAGS}
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs -S ${@:check-%=%}" ${TEST_FLAGS}
# Check various flags
-check-j1 check-j2 check-j3 check-s: ${BIN}/bfs ${ITEST_BINS}
+check-j1 check-j2 check-j3 check-s: bin/bfs ${ITEST_BINS}
+${MSG} "[TEST] bfs -${@:check-%=%}" \
- ./tests/tests.sh --make="${MAKE}" --bfs="${BIN}/bfs -${@:check-%=%}" ${TEST_FLAGS}
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs -${@:check-%=%}" ${TEST_FLAGS}
# Run the integration tests
integration-tests: ${INTEGRATION_TESTS}
.PHONY: integration-tests
-${BIN}/tests/mksock: \
- ${OBJ}/tests/mksock.o \
+bin/tests/mksock: \
+ obj/tests/mksock.o \
${LIBBFS}
-${BIN}/tests/xtouch: \
- ${OBJ}/tests/xtouch.o \
+bin/tests/xtouch: \
+ obj/tests/xtouch.o \
${LIBBFS}
# `make distcheck` configurations
@@ -168,23 +156,25 @@ DISTCHECKS := \
# Test multiple configurations
distcheck:
- @+${MAKE} -s distcheck-asan
- @+test "$$(uname)" = Darwin || ${MAKE} -s distcheck-msan
- @+${MAKE} -s distcheck-tsan
- @+test "$$(uname)-$$(uname -m)" != Linux-x86_64 || ${MAKE} -s distcheck-m32
- @+${MAKE} -s distcheck-release
+ @+${MAKE} distcheck-asan
+ @+test "$$(uname)" = Darwin || ${MAKE} distcheck-msan
+ @+test "$$(uname)" = FreeBSD || ${MAKE} distcheck-tsan
+ @+test "$$(uname)-$$(uname -m)" != Linux-x86_64 || ${MAKE} distcheck-m32
+ @+${MAKE} distcheck-release
.PHONY: distcheck
# Per-distcheck configuration
-DISTCHECK_CONFIG_asan := ASAN=y UBSAN=y
-DISTCHECK_CONFIG_msan := MSAN=y UBSAN=y CC=clang
-DISTCHECK_CONFIG_tsan := TSAN=y UBSAN=y CC=clang
+DISTCHECK_CONFIG_asan := --enable-asan --enable-ubsan
+DISTCHECK_CONFIG_msan := --enable-msan --enable-ubsan CC=clang
+DISTCHECK_CONFIG_tsan := --enable-tsan --enable-ubsan CC=clang
DISTCHECK_CONFIG_m32 := EXTRA_CFLAGS="-m32" PKG_CONFIG_LIBDIR=/usr/lib32/pkgconfig
-DISTCHECK_CONFIG_release := RELEASE=y
+DISTCHECK_CONFIG_release := --enable-release
${DISTCHECKS}::
- +${MAKE} -rs BUILDDIR=${BUILDDIR}/$@ config ${DISTCHECK_CONFIG_${@:distcheck-%=%}}
- +${MAKE} -s BUILDDIR=${BUILDDIR}/$@ check TEST_FLAGS="--sudo --verbose=skipped"
+ @${MKDIR} $@
+ @+cd $@ \
+ && ../configure ${DISTCHECK_CONFIG_${@:distcheck-%=%}} \
+ && ${MAKE} check TEST_FLAGS="--sudo --verbose=skipped"
## Packaging (`make install`)
@@ -193,44 +183,49 @@ DEST_MANDIR := ${DESTDIR}${MANDIR}
install::
${Q}${MKDIR} ${DEST_PREFIX}/bin
- ${MSG} "[INSTALL] bin/bfs" \
- ${INSTALL} -m755 ${BIN}/bfs ${DEST_PREFIX}/bin/bfs
+ ${MSG} "[INST] bin/bfs" \
+ ${INSTALL} -m755 bin/bfs ${DEST_PREFIX}/bin/bfs
${Q}${MKDIR} ${DEST_MANDIR}/man1
- ${MSG} "[INSTALL] man/man1/bfs.1" \
+ ${MSG} "[INST] man/man1/bfs.1" \
${INSTALL} -m644 docs/bfs.1 ${DEST_MANDIR}/man1/bfs.1
${Q}${MKDIR} ${DEST_PREFIX}/share/bash-completion/completions
- ${MSG} "[INSTALL] completions/bfs.bash" \
+ ${MSG} "[INST] completions/bfs.bash" \
${INSTALL} -m644 completions/bfs.bash ${DEST_PREFIX}/share/bash-completion/completions/bfs
${Q}${MKDIR} ${DEST_PREFIX}/share/zsh/site-functions
- ${MSG} "[INSTALL] completions/bfs.zsh" \
+ ${MSG} "[INST] completions/bfs.zsh" \
${INSTALL} -m644 completions/bfs.zsh ${DEST_PREFIX}/share/zsh/site-functions/_bfs
${Q}${MKDIR} ${DEST_PREFIX}/share/fish/vendor_completions.d
- ${MSG} "[INSTALL] completions/bfs.fish" \
+ ${MSG} "[INST] completions/bfs.fish" \
${INSTALL} -m644 completions/bfs.fish ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
uninstall::
- ${RM} ${DEST_PREFIX}/share/bash-completion/completions/bfs
- ${RM} ${DEST_PREFIX}/share/zsh/site-functions/_bfs
- ${RM} ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
- ${RM} ${DEST_MANDIR}/man1/bfs.1
- ${RM} ${DEST_PREFIX}/bin/bfs
+ ${MSG} "[ RM ] completions/bfs.bash" \
+ ${RM} ${DEST_PREFIX}/share/bash-completion/completions/bfs
+ ${MSG} "[ RM ] completions/bfs.zsh" \
+ ${RM} ${DEST_PREFIX}/share/zsh/site-functions/_bfs
+ ${MSG} "[ RM ] completions/bfs.fish" \
+ ${RM} ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
+ ${MSG} "[ RM ] man/man1/bfs.1" \
+ ${RM} ${DEST_MANDIR}/man1/bfs.1
+ ${MSG} "[ RM ] bin/bfs" \
+ ${RM} ${DEST_PREFIX}/bin/bfs
# Check that `make install` works and `make uninstall` removes everything
check-install::
- +${MAKE} install DESTDIR=${BUILDDIR}/pkg
- +${MAKE} uninstall DESTDIR=${BUILDDIR}/pkg
- ${BIN}/bfs ${BUILDDIR}/pkg -not -type d -print -exit 1
- ${RM} -r ${BUILDDIR}/pkg
+ +${MAKE} install DESTDIR=pkg
+ +${MAKE} uninstall DESTDIR=pkg
+ bin/bfs pkg -not -type d -print -exit 1
+ ${RM} -r pkg
## Cleanup (`make clean`)
# Clean all build products
clean::
${MSG} "[ RM ] bin obj" \
- ${RM} -r ${BIN} ${OBJ}
+ ${RM} -r bin obj
# Clean everything, including generated files
distclean: clean
${MSG} "[ RM ] gen" \
- ${RM} -r ${GEN} ${DISTCHECKS}
+ ${RM} -r gen ${DISTCHECKS}
.PHONY: distclean
diff --git a/README.md b/README.md
index b95c16b..b9aad82 100644
--- a/README.md
+++ b/README.md
@@ -333,7 +333,7 @@ Once you have the dependencies, you can build <code>bfs</code>.
Download one of the [releases](https://github.com/tavianator/bfs/releases) or clone the [git repo](https://github.com/tavianator/bfs).
Then run
- $ make config
+ $ ./configure
$ make
This will build the `./bin/bfs` binary.
@@ -343,7 +343,7 @@ Run the test suite to make sure it works correctly:
If you're interested in speed, you may want to build the release version instead:
- $ make config RELEASE=y
+ $ ./configure --enable-release
$ make
Finally, if you want to install it globally, run
diff --git a/bench/bench.sh b/bench/bench.sh
index e4b5511..cf1ae49 100644
--- a/bench/bench.sh
+++ b/bench/bench.sh
@@ -221,7 +221,8 @@ setup() {
fi
echo "Building bfs ..."
- as-user make -s -j"$nproc" release all
+ as-user ./configure --enable-release
+ as-user make -s -j"$nproc" all
as-user mkdir -p bench/corpus
@@ -253,7 +254,12 @@ setup() {
echo "Building bfs $commit ..."
cd "$worktree"
as-user git checkout -qd "$commit" --
- as-user make -s -j"$nproc" release
+ if [ -e configure ]; then
+ as-user ./configure --enable-release
+ as-user make -s -j"$nproc"
+ else
+ as-user make -s -j"$nproc" release
+ fi
if [ -e ./bin/bfs ]; then
as-user cp ./bin/bfs "$bin/bfs-$commit"
else
diff --git a/config/cc.sh b/build/cc.sh
index 45d51ca..45d51ca 100755
--- a/config/cc.sh
+++ b/build/cc.sh
diff --git a/build/config.mk b/build/config.mk
new file mode 100644
index 0000000..24873ec
--- /dev/null
+++ b/build/config.mk
@@ -0,0 +1,62 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that implements `./configure`
+
+include build/prelude.mk
+include build/exports.mk
+
+# All configuration steps
+config: gen/config.mk gen/config.h
+.PHONY: config
+
+# Makefile fragments generated by `./configure`
+MKS := \
+ gen/vars.mk \
+ gen/flags.mk \
+ gen/deps.mk \
+ gen/pkgs.mk
+
+# The main configuration file, which includes the others
+gen/config.mk: ${MKS}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf 'include %s\n' ${.ALLSRC} >>$@
+ ${VCAT} gen/config.mk
+.PHONY: gen/config.mk
+
+# Saves the configurable variables
+gen/vars.mk::
+ @${MKDIR} ${@D}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf 'PREFIX := %s\n' "$$XPREFIX" >>$@
+ @printf 'MANDIR := %s\n' "$$XMANDIR" >>$@
+ @printf 'OS := %s\n' "$${OS:-$$(uname)}" >>$@
+ @printf 'CC := %s\n' "$$XCC" >>$@
+ @printf 'INSTALL := %s\n' "$$XINSTALL" >>$@
+ @printf 'MKDIR := %s\n' "$$XMKDIR" >>$@
+ @printf 'PKG_CONFIG := %s\n' "$$XPKG_CONFIG" >>$@
+ @printf 'RM := %s\n' "$$XRM" >>$@
+ ${VCAT} $@
+
+# Sets the build flags. This depends on vars.mk and uses a recursive make so
+# that the default flags can depend on variables like ${OS}.
+gen/flags.mk: gen/vars.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/flags.mk $@
+.PHONY: gen/flags.mk
+
+# Check for dependency generation support
+gen/deps.mk: gen/flags.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/deps.mk $@
+.PHONY: gen/deps.mk
+
+# Auto-detect dependencies and their build flags
+gen/pkgs.mk: gen/flags.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/pkgs.mk $@
+.PHONY: gen/pkgs.mk
+
+# Compile-time feature detection
+gen/config.h: gen/config.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/header.mk $@
+.PHONY: gen/config.h
diff --git a/config/cc-define.sh b/build/define-if.sh
index edb5c87..295ead8 100755
--- a/config/cc-define.sh
+++ b/build/define-if.sh
@@ -3,17 +3,18 @@
# Copyright © Tavian Barnes <tavianator@tavianator.com>
# SPDX-License-Identifier: 0BSD
-# Output a C preprocessor definition based on whether a C source file could be
-# compiled successfully
+# Output a C preprocessor definition based on whether a command succeeds
set -eu
-SLUG="${1#config/}"
+SLUG="${1#build/}"
SLUG="${SLUG%.c}"
-MACRO="BFS_HAS_$(printf '%s' "$SLUG" | tr 'a-z-' 'A-Z_')"
+MACRO="BFS_$(printf '%s' "$SLUG" | tr '/a-z-' '_A-Z_')"
+shift
-if config/cc.sh "$1"; then
+if "$@"; then
printf '#define %s true\n' "$MACRO"
else
printf '#define %s false\n' "$MACRO"
+ exit 1
fi
diff --git a/build/deps.mk b/build/deps.mk
new file mode 100644
index 0000000..3db62b6
--- /dev/null
+++ b/build/deps.mk
@@ -0,0 +1,18 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/deps.mk
+
+include build/prelude.mk
+include gen/vars.mk
+include gen/flags.mk
+include build/exports.mk
+
+gen/deps.mk::
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @if build/cc.sh -MD -MP -MF /dev/null build/empty.c; then \
+ printf 'CPPFLAGS += -MD -MP\n'; \
+ fi >>$@ 2>$@.log
+ ${VCAT} $@
+ @printf -- '-include %s\n' ${OBJS:.o=.d} >>$@
diff --git a/config/empty.c b/build/empty.c
index 4fa9a5b..4fa9a5b 100644
--- a/config/empty.c
+++ b/build/empty.c
diff --git a/config/exports.mk b/build/exports.mk
index ed19134..ed19134 100644
--- a/config/exports.mk
+++ b/build/exports.mk
diff --git a/config/flags.mk b/build/flags.mk
index 8a2e50e..c911b22 100644
--- a/config/flags.mk
+++ b/build/flags.mk
@@ -3,8 +3,8 @@
# Makefile that generates gen/flags.mk
-include config/prelude.mk
-include ${GEN}/vars.mk
+include build/prelude.mk
+include gen/vars.mk
# Configurable flags
CPPFLAGS ?=
@@ -29,7 +29,7 @@ export XLDLIBS=${LDLIBS}
# Immutable flags
export BFS_CPPFLAGS= \
-Isrc \
- -I${GEN} \
+ -Igen \
-D__EXTENSIONS__ \
-D_ATFILE_SOURCE \
-D_BSD_SOURCE \
@@ -45,7 +45,7 @@ export BFS_CFLAGS= -std=c17 -pthread
LDLIBS,DragonFly := -lposix1e
LDLIBS,Linux := -lrt
LDLIBS,NetBSD := -lutil
-LDLIBS,SunOS := -lsocket -lnsl
+LDLIBS,SunOS := -lsec -lsocket -lnsl
export BFS_LDLIBS=${LDLIBS,${OS}}
# Build profiles
@@ -75,12 +75,13 @@ export TSAN_CFLAGS=${TSAN_CFLAGS,${_TSAN}}
export UBSAN_CFLAGS=${UBSAN_CFLAGS,${_UBSAN}}
SAN_CFLAGS,y := -fno-sanitize-recover=all
-NO_SAN := ${NOR,${_ASAN},${_LSAN},${_MSAN},${_TSAN},${_UBSAN}}
-SAN := ${NOT,${NO_SAN}}
+INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_UBSAN}}
+SAN := ${NOT,${INSANE}}
export SAN_CFLAGS=${SAN_CFLAGS,${SAN}}
# MSAN and TSAN both need all code to be instrumented
-NOLIBS ?= ${NOT,${NOR,${_MSAN},${_TSAN}}}
+YESLIBS := ${NOT,${_MSAN}${_TSAN}}
+NOLIBS ?= ${NOT,${YESLIBS}}
export XNOLIBS=${NOLIBS}
# gcov only intercepts fork()/exec() with -std=gnu*
@@ -100,14 +101,14 @@ export RELEASE_CPPFLAGS=${RELEASE_CPPFLAGS,${_RELEASE}}
export RELEASE_CFLAGS=${RELEASE_CFLAGS,${_RELEASE}}
# Set a variable
-SETVAR = printf '%s := %s\n' >>$@
+SETVAR = @printf '%s := %s\n' >>$@
# Append to a variable, if non-empty
-APPEND = append() { test -z "$$2" || printf '%s += %s\n' "$$1" "$$2" >>$@; }; append
+APPEND = @append() { test -z "$$2" || printf '%s += %s\n' "$$1" "$$2" >>$@; }; append
-${GEN}/flags.mk::
- ${MSG} "[ GEN] ${TGT}"
- printf '# %s\n' "${TGT}" >$@
+gen/flags.mk::
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
${SETVAR} CPPFLAGS "$$BFS_CPPFLAGS"
${APPEND} CPPFLAGS "$$TSAN_CPPFLAGS"
${APPEND} CPPFLAGS "$$LINT_CPPFLAGS"
@@ -131,5 +132,5 @@ ${GEN}/flags.mk::
${APPEND} LDLIBS "$$EXTRA_LDLIBS"
${APPEND} LDLIBS "$$BFS_LDLIBS"
${SETVAR} NOLIBS "$$XNOLIBS"
- test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@
+ @test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@
${VCAT} $@
diff --git a/build/has/--st-birthtim.c b/build/has/--st-birthtim.c
new file mode 100644
index 0000000..4da621f
--- /dev/null
+++ b/build/has/--st-birthtim.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ return sb.__st_birthtim.tv_sec;
+}
diff --git a/build/has/acl-get-entry.c b/build/has/acl-get-entry.c
new file mode 100644
index 0000000..1e7f473
--- /dev/null
+++ b/build/has/acl-get-entry.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT);
+ acl_entry_t entry;
+ return acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
+}
diff --git a/build/has/acl-get-file.c b/build/has/acl-get-file.c
new file mode 100644
index 0000000..0b76ee2
--- /dev/null
+++ b/build/has/acl-get-file.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT);
+ return acl == (acl_t)NULL;
+}
diff --git a/build/has/acl-get-tag-type.c b/build/has/acl-get-tag-type.c
new file mode 100644
index 0000000..67b7d37
--- /dev/null
+++ b/build/has/acl-get-tag-type.c
@@ -0,0 +1,13 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <string.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_entry_t entry;
+ memset(&entry, 0, sizeof(entry));
+ acl_tag_t tag;
+ return acl_get_tag_type(entry, &tag);
+}
diff --git a/config/acl-is-trivial-np.c b/build/has/acl-is-trivial-np.c
index 4178238..9ca9fc7 100644
--- a/config/acl-is-trivial-np.c
+++ b/build/has/acl-is-trivial-np.c
@@ -1,6 +1,7 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include <sys/types.h>
#include <sys/acl.h>
int main(void) {
diff --git a/build/has/acl-trivial.c b/build/has/acl-trivial.c
new file mode 100644
index 0000000..7efc838
--- /dev/null
+++ b/build/has/acl-trivial.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/acl.h>
+
+int main(void) {
+ return acl_trivial(".");
+}
diff --git a/config/aligned-alloc.c b/build/has/aligned-alloc.c
index 4460038..4460038 100644
--- a/config/aligned-alloc.c
+++ b/build/has/aligned-alloc.c
diff --git a/build/has/attribute-format-syslog.c b/build/has/attribute-format-syslog.c
new file mode 100644
index 0000000..ce988e5
--- /dev/null
+++ b/build/has/attribute-format-syslog.c
@@ -0,0 +1,13 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdio.h>
+
+__attribute__((format(syslog, 1, 2)))
+static int foo(const char *format, ...) {
+ return 0;
+}
+
+int main(void) {
+ return foo("%s: %m\n", "main()");
+}
diff --git a/config/confstr.c b/build/has/confstr.c
index 58280b4..58280b4 100644
--- a/config/confstr.c
+++ b/build/has/confstr.c
diff --git a/build/has/extattr-get-file.c b/build/has/extattr-get-file.c
new file mode 100644
index 0000000..ac9cf96
--- /dev/null
+++ b/build/has/extattr-get-file.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_get_file("file", EXTATTR_NAMESPACE_USER, "xattr", NULL, 0);
+}
diff --git a/build/has/extattr-get-link.c b/build/has/extattr-get-link.c
new file mode 100644
index 0000000..c35be5b
--- /dev/null
+++ b/build/has/extattr-get-link.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_get_link("link", EXTATTR_NAMESPACE_USER, "xattr", NULL, 0);
+}
diff --git a/build/has/extattr-list-file.c b/build/has/extattr-list-file.c
new file mode 100644
index 0000000..e68a8bb
--- /dev/null
+++ b/build/has/extattr-list-file.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_list_file("file", EXTATTR_NAMESPACE_USER, NULL, 0);
+}
diff --git a/build/has/extattr-list-link.c b/build/has/extattr-list-link.c
new file mode 100644
index 0000000..49f0ec2
--- /dev/null
+++ b/build/has/extattr-list-link.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_list_link("link", EXTATTR_NAMESPACE_USER, NULL, 0);
+}
diff --git a/config/fdclosedir.c b/build/has/fdclosedir.c
index f4ad1f5..f4ad1f5 100644
--- a/config/fdclosedir.c
+++ b/build/has/fdclosedir.c
diff --git a/config/getdents.c b/build/has/getdents.c
index d0d4228..579898f 100644
--- a/config/getdents.c
+++ b/build/has/getdents.c
@@ -4,7 +4,6 @@
#include <dirent.h>
int main(void) {
- struct dirent de;
- getdents(3, &de, 1024);
- return 0;
+ char buf[1024];
+ return getdents(3, (void *)buf, sizeof(buf));
}
diff --git a/config/getdents64-syscall.c b/build/has/getdents64-syscall.c
index 4838c14..7642d93 100644
--- a/config/getdents64-syscall.c
+++ b/build/has/getdents64-syscall.c
@@ -6,7 +6,6 @@
#include <unistd.h>
int main(void) {
- struct dirent64 de;
- syscall(SYS_getdents64, 3, &de, 1024);
- return 0;
+ char buf[1024];
+ return syscall(SYS_getdents64, 3, (void *)buf, sizeof(buf));
}
diff --git a/config/getdents64.c b/build/has/getdents64.c
index 1abf36d..d8e8062 100644
--- a/config/getdents64.c
+++ b/build/has/getdents64.c
@@ -4,7 +4,6 @@
#include <dirent.h>
int main(void) {
- struct dirent64 de;
- getdents64(3, &de, 1024);
- return 0;
+ char buf[1024];
+ return getdents64(3, (void *)buf, sizeof(buf));
}
diff --git a/build/has/getmntent-1.c b/build/has/getmntent-1.c
new file mode 100644
index 0000000..9854dcd
--- /dev/null
+++ b/build/has/getmntent-1.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <mntent.h>
+#include <stdio.h>
+
+int main(void) {
+ return !getmntent(stdin);
+}
diff --git a/build/has/getmntent-2.c b/build/has/getmntent-2.c
new file mode 100644
index 0000000..71f0220
--- /dev/null
+++ b/build/has/getmntent-2.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdio.h>
+#include <sys/mnttab.h>
+
+int main(void) {
+ struct mnttab mnt;
+ return getmntent(stdin, &mnt);
+}
diff --git a/build/has/getmntinfo.c b/build/has/getmntinfo.c
new file mode 100644
index 0000000..90ef5fb
--- /dev/null
+++ b/build/has/getmntinfo.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/mount.h>
+
+int main(void) {
+ return getmntinfo(NULL, MNT_WAIT);
+}
diff --git a/config/getprogname-gnu.c b/build/has/getprogname-gnu.c
index 6b97c5e..6b97c5e 100644
--- a/config/getprogname-gnu.c
+++ b/build/has/getprogname-gnu.c
diff --git a/config/getprogname.c b/build/has/getprogname.c
index 83dc8e8..83dc8e8 100644
--- a/config/getprogname.c
+++ b/build/has/getprogname.c
diff --git a/build/has/max-align-t.c b/build/has/max-align-t.c
new file mode 100644
index 0000000..96165ce
--- /dev/null
+++ b/build/has/max-align-t.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+
+int main(void) {
+ return _Alignof(max_align_t);
+}
diff --git a/config/pipe2.c b/build/has/pipe2.c
index 4cb43b5..4cb43b5 100644
--- a/config/pipe2.c
+++ b/build/has/pipe2.c
diff --git a/build/has/posix-getdents.c b/build/has/posix-getdents.c
new file mode 100644
index 0000000..f74bbe5
--- /dev/null
+++ b/build/has/posix-getdents.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ char buf[1024];
+ return posix_getdents(3, (void *)buf, sizeof(buf), 0);
+}
diff --git a/config/posix-spawn-addfchdir-np.c b/build/has/posix-spawn-addfchdir-np.c
index b870a53..b870a53 100644
--- a/config/posix-spawn-addfchdir-np.c
+++ b/build/has/posix-spawn-addfchdir-np.c
diff --git a/config/posix-spawn-addfchdir.c b/build/has/posix-spawn-addfchdir.c
index c52ff81..c52ff81 100644
--- a/config/posix-spawn-addfchdir.c
+++ b/build/has/posix-spawn-addfchdir.c
diff --git a/config/st-acmtim.c b/build/has/st-acmtim.c
index d687ab0..d687ab0 100644
--- a/config/st-acmtim.c
+++ b/build/has/st-acmtim.c
diff --git a/config/st-acmtimespec.c b/build/has/st-acmtimespec.c
index f747bc0..f747bc0 100644
--- a/config/st-acmtimespec.c
+++ b/build/has/st-acmtimespec.c
diff --git a/config/st-birthtim.c b/build/has/st-birthtim.c
index 4964571..4964571 100644
--- a/config/st-birthtim.c
+++ b/build/has/st-birthtim.c
diff --git a/config/st-birthtimespec.c b/build/has/st-birthtimespec.c
index 91a613f..91a613f 100644
--- a/config/st-birthtimespec.c
+++ b/build/has/st-birthtimespec.c
diff --git a/config/st-flags.c b/build/has/st-flags.c
index b1d0c32..b1d0c32 100644
--- a/config/st-flags.c
+++ b/build/has/st-flags.c
diff --git a/config/statx-syscall.c b/build/has/statx-syscall.c
index 87ec869..87ec869 100644
--- a/config/statx-syscall.c
+++ b/build/has/statx-syscall.c
diff --git a/config/statx.c b/build/has/statx.c
index 65f1674..65f1674 100644
--- a/config/statx.c
+++ b/build/has/statx.c
diff --git a/config/strerror-l.c b/build/has/strerror-l.c
index 3dcc4d7..3dcc4d7 100644
--- a/config/strerror-l.c
+++ b/build/has/strerror-l.c
diff --git a/config/strerror-r-gnu.c b/build/has/strerror-r-gnu.c
index 26ca0ee..26ca0ee 100644
--- a/config/strerror-r-gnu.c
+++ b/build/has/strerror-r-gnu.c
diff --git a/config/strerror-r-posix.c b/build/has/strerror-r-posix.c
index 41b2d30..41b2d30 100644
--- a/config/strerror-r-posix.c
+++ b/build/has/strerror-r-posix.c
diff --git a/build/has/string-to-flags.c b/build/has/string-to-flags.c
new file mode 100644
index 0000000..027d72c
--- /dev/null
+++ b/build/has/string-to-flags.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <util.h>
+
+int main(void) {
+ return string_to_flags(NULL, NULL, NULL);
+}
diff --git a/build/has/strtofflags.c b/build/has/strtofflags.c
new file mode 100644
index 0000000..73ecbcb
--- /dev/null
+++ b/build/has/strtofflags.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <unistd.h>
+
+int main(void) {
+ return strtofflags(NULL, NULL, NULL);
+}
diff --git a/build/has/timegm.c b/build/has/timegm.c
new file mode 100644
index 0000000..6e2d155
--- /dev/null
+++ b/build/has/timegm.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <time.h>
+
+int main(void) {
+ struct tm tm = {0};
+ return (int)timegm(&tm);
+}
diff --git a/config/tm-gmtoff.c b/build/has/tm-gmtoff.c
index 543df48..543df48 100644
--- a/config/tm-gmtoff.c
+++ b/build/has/tm-gmtoff.c
diff --git a/config/uselocale.c b/build/has/uselocale.c
index a712ff8..a712ff8 100644
--- a/config/uselocale.c
+++ b/build/has/uselocale.c
diff --git a/build/header.mk b/build/header.mk
new file mode 100644
index 0000000..a449b77
--- /dev/null
+++ b/build/header.mk
@@ -0,0 +1,74 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/config.h
+
+include build/prelude.mk
+include gen/config.mk
+include build/exports.mk
+
+# All header fragments we generate
+HEADERS := \
+ gen/has/--st-birthtim.h \
+ gen/has/acl-get-entry.h \
+ gen/has/acl-get-file.h \
+ gen/has/acl-get-tag-type.h \
+ gen/has/acl-is-trivial-np.h \
+ gen/has/acl-trivial.h \
+ gen/has/aligned-alloc.h \
+ gen/has/confstr.h \
+ gen/has/extattr-get-file.h \
+ gen/has/extattr-get-link.h \
+ gen/has/extattr-list-file.h \
+ gen/has/extattr-list-link.h \
+ gen/has/fdclosedir.h \
+ gen/has/getdents.h \
+ gen/has/getdents64-syscall.h \
+ gen/has/getdents64.h \
+ gen/has/getmntent-1.h \
+ gen/has/getmntent-2.h \
+ gen/has/getmntinfo.h \
+ gen/has/getprogname-gnu.h \
+ gen/has/getprogname.h \
+ gen/has/max-align-t.h \
+ gen/has/pipe2.h \
+ gen/has/posix-getdents.h \
+ gen/has/posix-spawn-addfchdir-np.h \
+ gen/has/posix-spawn-addfchdir.h \
+ gen/has/st-acmtim.h \
+ gen/has/st-acmtimespec.h \
+ gen/has/st-birthtim.h \
+ gen/has/st-birthtimespec.h \
+ gen/has/st-flags.h \
+ gen/has/statx-syscall.h \
+ gen/has/statx.h \
+ gen/has/strerror-l.h \
+ gen/has/strerror-r-gnu.h \
+ gen/has/strerror-r-posix.h \
+ gen/has/string-to-flags.h \
+ gen/has/strtofflags.h \
+ gen/has/timegm.h \
+ gen/has/tm-gmtoff.h \
+ gen/has/uselocale.h
+
+# Previously generated by pkgs.mk
+PKG_HEADERS := ${ALL_PKGS:%=gen/use/%.h}
+
+gen/config.h: ${PKG_HEADERS} ${HEADERS}
+ ${MSG} "[ GEN] $@"
+ @printf '// %s\n' "$@" >$@
+ @printf '#ifndef BFS_CONFIG_H\n' >>$@
+ @printf '#define BFS_CONFIG_H\n' >>$@
+ @cat ${.ALLSRC} >>$@
+ @printf '#endif // BFS_CONFIG_H\n' >>$@
+ @cat ${.ALLSRC:%=%.log} >gen/config.log
+ ${VCAT} $@
+.PHONY: gen/config.h
+
+# The short name of the config test
+SLUG = ${@:gen/%.h=%}
+
+${HEADERS}::
+ @${MKDIR} ${@D}
+ @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0
diff --git a/build/msg-if.sh b/build/msg-if.sh
new file mode 100755
index 0000000..8112aea
--- /dev/null
+++ b/build/msg-if.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Print a success/failure indicator from a makefile:
+#
+# $ ./configure
+# [ CC ] use/liburing.c ✘
+# [ CC ] use/oniguruma.c ✔
+
+set -eu
+
+MSG="$1"
+shift
+
+if "$@"; then
+ build/msg.sh "$(printf '%-37s ✔' "$MSG")"
+else
+ build/msg.sh "$(printf '%-37s ✘' "$MSG")"
+fi
diff --git a/build/msg.sh b/build/msg.sh
new file mode 100755
index 0000000..a7da31b
--- /dev/null
+++ b/build/msg.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Print a message from a makefile:
+#
+# $ make -s
+# $ make
+# [ CC ] src/main.c
+# $ make V=1
+# cc -Isrc -Igen -D...
+
+set -eu
+
+# Get the $MAKEFLAGS from the top-level make invocation
+MFLAGS="${XMAKEFLAGS-${MAKEFLAGS-}}"
+
+# Check if make should be quiet (make -s)
+is_quiet() {
+ # GNU make puts single-letter flags in the first word of $MAKEFLAGS,
+ # without a leading dash
+ case "${MFLAGS%% *}" in
+ -*) : ;;
+ *s*) return 0 ;;
+ esac
+
+ # BSD make puts each flag separately like -r -s -j 48
+ for flag in $MFLAGS; do
+ case "$flag" in
+ # Ignore things like --jobserver-auth
+ --*) continue ;;
+ # Skip variable assignments
+ *=*) break ;;
+ -*s*) return 0 ;;
+ esac
+ done
+
+ return 1
+}
+
+# Check if make should be loud (make V=1)
+is_loud() {
+ test "$XV"
+}
+
+MSG="$1"
+shift
+
+if ! is_quiet && ! is_loud; then
+ printf '%s\n' "$MSG"
+fi
+
+if [ $# -eq 0 ]; then
+ exit
+fi
+
+if is_loud; then
+ printf '%s\n' "$*"
+fi
+
+"$@"
diff --git a/config/pkgconf.sh b/build/pkgconf.sh
index 2dbad76..96e4bf1 100755
--- a/config/pkgconf.sh
+++ b/build/pkgconf.sh
@@ -26,10 +26,22 @@ esac
if [ -z "$MODE" ]; then
# Check whether the libraries exist at all
for LIB; do
+ # Check ${USE_$LIB}
+ USE_LIB="USE_$(printf '%s' "$LIB" | tr 'a-z-' 'A-Z_')"
+ eval "USE=\"\${$USE_LIB:-}\""
+ case "$USE" in
+ y|1)
+ continue
+ ;;
+ n|0)
+ exit 1
+ ;;
+ esac
+
CFLAGS=$("$0" --cflags "$LIB") || exit 1
LDFLAGS=$("$0" --ldflags "$LIB") || exit 1
LDLIBS=$("$0" --ldlibs "$LIB") || exit 1
- config/cc.sh $CFLAGS $LDFLAGS config/$LIB.c $LDLIBS || exit 1
+ build/cc.sh $CFLAGS $LDFLAGS build/use/$LIB.c $LDLIBS || exit 1
done
fi
diff --git a/build/pkgs.mk b/build/pkgs.mk
new file mode 100644
index 0000000..39b550d
--- /dev/null
+++ b/build/pkgs.mk
@@ -0,0 +1,33 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/pkgs.mk
+
+include build/prelude.mk
+include gen/vars.mk
+include gen/flags.mk
+include build/exports.mk
+
+HEADERS := ${ALL_PKGS:%=gen/use/%.h}
+
+gen/pkgs.mk: ${HEADERS}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @gen() { \
+ printf 'PKGS := %s\n' "$$*"; \
+ printf 'CFLAGS += %s\n' "$$(build/pkgconf.sh --cflags "$$@")"; \
+ printf 'LDFLAGS += %s\n' "$$(build/pkgconf.sh --ldflags "$$@")"; \
+ printf 'LDLIBS := %s $${LDLIBS}\n' "$$(build/pkgconf.sh --ldlibs "$$@")"; \
+ }; \
+ gen $$(grep -l ' true$$' ${.ALLSRC} | sed 's|.*/\(.*\)\.h|\1|') >>$@
+ ${VCAT} $@
+
+.PHONY: gen/pkgs.mk
+
+# Convert gen/use/foo.h to foo
+PKG = ${@:gen/use/%.h=%}
+
+${HEADERS}::
+ @${MKDIR} ${@D}
+ @build/define-if.sh use/${PKG} build/pkgconf.sh ${PKG} >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] use/${PKG}.c" test $$? -eq 0;
diff --git a/build/prelude.mk b/build/prelude.mk
new file mode 100644
index 0000000..d0968ea
--- /dev/null
+++ b/build/prelude.mk
@@ -0,0 +1,124 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Common makefile utilities. Compatible with both GNU make and most BSD makes.
+
+# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
+.OBJDIR: .
+
+# We don't use any suffix rules
+.SUFFIXES:
+
+# GNU make has $^ for the full list of targets, while BSD make has $> and the
+# long-form ${.ALLSRC}. We could write $^ $> to get them both, but that would
+# break if one of them implemented support for the other. So instead, bring
+# BSD's ${.ALLSRC} to GNU.
+.ALLSRC ?= $^
+
+# Installation paths
+DESTDIR ?=
+PREFIX ?= /usr
+MANDIR ?= ${PREFIX}/share/man
+
+# Configurable executables
+CC ?= cc
+INSTALL ?= install
+MKDIR ?= mkdir -p
+PKG_CONFIG ?= pkg-config
+RM ?= rm -f
+
+# GNU and BSD make have incompatible syntax for conditionals, but we can do a
+# lot with just nested variable expansion. We use "y" as the canonical
+# truthy value, and "" (the empty string) as the canonical falsey value.
+#
+# To normalize a boolean, use ${TRUTHY,${VAR}}, which expands like this:
+#
+# VAR=y ${TRUTHY,${VAR}} => ${TRUTHY,y} => y
+# VAR=1 ${TRUTHY,${VAR}} => ${TRUTHY,1} => y
+# VAR=n ${TRUTHY,${VAR}} => ${TRUTHY,n} => [empty]
+# VAR=other ${TRUTHY,${VAR}} => ${TRUTHY,other} => [empty]
+# VAR= ${TRUTHY,${VAR}} => ${TRUTHY,} => [emtpy]
+#
+# Inspired by https://github.com/wahern/autoguess
+TRUTHY,y := y
+TRUTHY,1 := y
+
+# Boolean operators are also implemented with nested expansion
+NOT, := y
+
+# Normalize ${V} to either "y" or ""
+export XV=${TRUTHY,${V}}
+
+# Suppress output unless V=1
+Q, := @
+Q := ${Q,${XV}}
+
+# Show full commands with `make V=1`, otherwise short summaries
+MSG = @build/msg.sh
+
+# cat a file if V=1
+VCAT,y := @cat
+VCAT, := @:
+VCAT := ${VCAT,${XV}}
+
+# All external dependencies
+ALL_PKGS := \
+ libacl \
+ libcap \
+ libselinux \
+ liburing \
+ oniguruma
+
+# List all object files here, as they're needed by both `./configure` and `make`
+
+# All object files except the entry point
+LIBBFS := \
+ obj/src/alloc.o \
+ obj/src/bar.o \
+ obj/src/bfstd.o \
+ obj/src/bftw.o \
+ obj/src/color.o \
+ obj/src/ctx.o \
+ obj/src/diag.o \
+ obj/src/dir.o \
+ obj/src/dstring.o \
+ obj/src/eval.o \
+ obj/src/exec.o \
+ obj/src/expr.o \
+ obj/src/fsade.o \
+ obj/src/ioq.o \
+ obj/src/mtab.o \
+ obj/src/opt.o \
+ obj/src/parse.o \
+ obj/src/printf.o \
+ obj/src/pwcache.o \
+ obj/src/sighook.o \
+ obj/src/stat.o \
+ obj/src/thread.o \
+ obj/src/trie.o \
+ obj/src/typo.o \
+ obj/src/xregex.o \
+ obj/src/xspawn.o \
+ obj/src/xtime.o \
+ obj/gen/version.o
+
+# Unit test objects
+UNIT_OBJS := \
+ obj/tests/alloc.o \
+ obj/tests/bfstd.o \
+ obj/tests/bit.o \
+ obj/tests/ioq.o \
+ obj/tests/main.o \
+ obj/tests/sighook.o \
+ obj/tests/trie.o \
+ obj/tests/xspawn.o \
+ obj/tests/xtime.o
+
+# All object files
+OBJS := \
+ obj/src/main.o \
+ obj/tests/mksock.o \
+ obj/tests/xspawnee.o \
+ obj/tests/xtouch.o \
+ ${LIBBFS} \
+ ${UNIT_OBJS}
diff --git a/config/libacl.c b/build/use/libacl.c
index de1fe50..de1fe50 100644
--- a/config/libacl.c
+++ b/build/use/libacl.c
diff --git a/config/libcap.c b/build/use/libcap.c
index 58e832c..58e832c 100644
--- a/config/libcap.c
+++ b/build/use/libcap.c
diff --git a/config/libselinux.c b/build/use/libselinux.c
index bca409d..bca409d 100644
--- a/config/libselinux.c
+++ b/build/use/libselinux.c
diff --git a/config/liburing.c b/build/use/liburing.c
index bea499a..bea499a 100644
--- a/config/liburing.c
+++ b/build/use/liburing.c
diff --git a/config/oniguruma.c b/build/use/oniguruma.c
index cb17596..cb17596 100644
--- a/config/oniguruma.c
+++ b/build/use/oniguruma.c
diff --git a/config/config.mk b/config/config.mk
deleted file mode 100644
index 5750b49..0000000
--- a/config/config.mk
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-# Makefile fragment that implements `make config`
-
-include config/prelude.mk
-include config/exports.mk
-
-# All configuration steps
-config: ${CONFIG} ${GEN}/config.h
-.PHONY: config
-
-# Makefile fragments generated by `make config`
-MKS := \
- ${GEN}/vars.mk \
- ${GEN}/flags.mk \
- ${GEN}/deps.mk \
- ${GEN}/pkgs.mk
-
-# The main configuration file, which includes the others
-${CONFIG}: ${MKS}
- ${MSG} "[ GEN] ${TGT}"
- @printf '# %s\n' "${TGT}" >$@
- @printf 'include $${GEN}/%s\n' ${MKS:${GEN}/%=%} >>$@
- ${VCAT} ${CONFIG}
-.PHONY: ${CONFIG}
-
-# Saves the configurable variables
-${GEN}/vars.mk::
- @${MKDIR} ${@D}
- ${MSG} "[ GEN] ${TGT}"
- @printf '# %s\n' "${TGT}" >$@
- @printf 'PREFIX := %s\n' "$$XPREFIX" >>$@
- @printf 'MANDIR := %s\n' "$$XMANDIR" >>$@
- @printf 'OS := %s\n' "$${OS:-$$(uname)}" >>$@
- @printf 'CC := %s\n' "$$XCC" >>$@
- @printf 'INSTALL := %s\n' "$$XINSTALL" >>$@
- @printf 'MKDIR := %s\n' "$$XMKDIR" >>$@
- @printf 'PKG_CONFIG := %s\n' "$$XPKG_CONFIG" >>$@
- @printf 'RM := %s\n' "$$XRM" >>$@
- @printf 'PKGS :=\n' >>$@
- ${VCAT} $@
-
-# Sets the build flags. This depends on vars.mk and uses a recursive make so
-# that the default flags can depend on variables like ${OS}.
-${GEN}/flags.mk: ${GEN}/vars.mk
- @+${MAKE} -sf config/flags.mk
-.PHONY: ${GEN}/flags.mk
-
-# Check for dependency generation support
-${GEN}/deps.mk: ${GEN}/flags.mk
- @+${MAKE} -sf config/deps.mk
-.PHONY: ${GEN}/deps.mk
-
-# External dependencies
-PKG_MKS := \
- ${GEN}/libacl.mk \
- ${GEN}/libcap.mk \
- ${GEN}/libselinux.mk \
- ${GEN}/liburing.mk \
- ${GEN}/oniguruma.mk
-
-# Auto-detect dependencies and their build flags
-${GEN}/pkgs.mk: ${PKG_MKS}
- @printf '# %s\n' "${TGT}" >$@
- @printf 'include $${GEN}/%s\n' ${.ALLSRC:${GEN}/%=%} >>$@
- @+${MAKE} -sf config/pkgs.mk
-.PHONY: ${GEN}/pkgs.mk
-
-# Auto-detect dependencies
-${PKG_MKS}: ${GEN}/flags.mk
- @+${MAKE} -sf config/pkg.mk TARGET=$@
-.PHONY: ${PKG_MKS}
-
-# Compile-time feature detection
-${GEN}/config.h: ${CONFIG}
- @+${MAKE} -sf config/header.mk $@
-.PHONY: ${GEN}/config.h
diff --git a/config/deps.mk b/config/deps.mk
deleted file mode 100644
index 2201f06..0000000
--- a/config/deps.mk
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-# Makefile that generates gen/deps.mk
-
-include config/prelude.mk
-include ${GEN}/vars.mk
-include ${GEN}/flags.mk
-include config/exports.mk
-
-${GEN}/deps.mk::
- ${MSG} "[ GEN] ${TGT}"
- printf '# %s\n' "${TGT}" >$@
- if config/cc.sh -MD -MP -MF /dev/null config/empty.c; then \
- printf 'DEPFLAGS = -MD -MP\n'; \
- fi >>$@ 2>$@.log
- ${VCAT} $@
- @printf -- '-include %s\n' ${OBJS:.o=.d} >>$@
diff --git a/config/header.mk b/config/header.mk
deleted file mode 100644
index 86a4dc5..0000000
--- a/config/header.mk
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-# Makefile that generates gen/config.h
-
-include config/prelude.mk
-include ${GEN}/config.mk
-include config/exports.mk
-
-# All header fragments we generate
-HEADERS := \
- ${GEN}/acl-is-trivial-np.h \
- ${GEN}/aligned-alloc.h \
- ${GEN}/confstr.h \
- ${GEN}/fdclosedir.h \
- ${GEN}/getdents.h \
- ${GEN}/getdents64.h \
- ${GEN}/getdents64-syscall.h \
- ${GEN}/getprogname.h \
- ${GEN}/getprogname-gnu.h \
- ${GEN}/pipe2.h \
- ${GEN}/posix-spawn-addfchdir.h \
- ${GEN}/posix-spawn-addfchdir-np.h \
- ${GEN}/st-acmtim.h \
- ${GEN}/st-acmtimespec.h \
- ${GEN}/st-birthtim.h \
- ${GEN}/st-birthtimespec.h \
- ${GEN}/st-flags.h \
- ${GEN}/statx.h \
- ${GEN}/statx-syscall.h \
- ${GEN}/strerror-l.h \
- ${GEN}/strerror-r-gnu.h \
- ${GEN}/strerror-r-posix.h \
- ${GEN}/tm-gmtoff.h \
- ${GEN}/uselocale.h
-
-${GEN}/config.h: ${HEADERS}
- ${MSG} "[ GEN] ${TGT}"
- printf '// %s\n' "${TGT}" >$@
- printf '#ifndef BFS_CONFIG_H\n' >>$@
- printf '#define BFS_CONFIG_H\n' >>$@
- cat ${.ALLSRC} >>$@
- printf '#endif // BFS_CONFIG_H\n' >>$@
- cat ${.ALLSRC:%=%.log} >$@.log
- ${RM} ${.ALLSRC} ${.ALLSRC:%=%.log}
- ${VCAT} $@
-.PHONY: ${GEN}/config.h
-
-# The C source file to attempt to compile
-CSRC = ${@:${GEN}/%.h=config/%.c}
-
-${HEADERS}::
- config/cc-define.sh ${CSRC} >$@ 2>$@.log
- if ! [ "${IS_V}" ]; then \
- if grep -q 'true$$' $@; then \
- printf '[ CC ] %-${MSG_WIDTH}s ✔\n' ${CSRC}; \
- else \
- printf '[ CC ] %-${MSG_WIDTH}s ✘\n' ${CSRC}; \
- fi; \
- fi
diff --git a/config/pkg.mk b/config/pkg.mk
deleted file mode 100644
index fafe562..0000000
--- a/config/pkg.mk
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-# Makefile that generates gen/lib*.mk
-
-include config/prelude.mk
-include ${GEN}/vars.mk
-include ${GEN}/flags.mk
-include config/exports.mk
-
-# Like ${TGT} but for ${TARGET}, not $@
-SHORT = ${TARGET:${BUILDDIR}/%=%}
-
-default::
- @printf '# %s\n' "${SHORT}" >${TARGET}
- config/pkg.sh ${TARGET:${GEN}/%.mk=%} >>${TARGET} 2>${TARGET}.log
- @if [ "${IS_V}" ]; then \
- cat ${TARGET}; \
- elif grep -q PKGS ${TARGET}; then \
- printf '[ GEN] %-${MSG_WIDTH}s ✔\n' ${SHORT}; \
- else \
- printf '[ GEN] %-${MSG_WIDTH}s ✘\n' ${SHORT}; \
- fi
diff --git a/config/pkg.sh b/config/pkg.sh
deleted file mode 100755
index 4ebea64..0000000
--- a/config/pkg.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/sh
-
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-# pkg-config wrapper that outputs a makefile fragment
-
-set -eu
-
-NAME=$(printf '%s' "$1" | tr 'a-z' 'A-Z')
-eval "XUSE=\"\${USE_$NAME:-}\""
-
-if [ "$XUSE" ]; then
- USE="$XUSE"
-elif config/pkgconf.sh "$1"; then
- USE=y
-else
- USE=n
-fi
-
-if [ "$USE" = y ]; then
- printf 'PKGS += %s\n' "$1"
- printf 'CPPFLAGS += -DBFS_USE_%s=1\n' "$NAME"
-else
- printf 'CPPFLAGS += -DBFS_USE_%s=0\n' "$NAME"
-fi
diff --git a/config/pkgs.mk b/config/pkgs.mk
deleted file mode 100644
index 3a18289..0000000
--- a/config/pkgs.mk
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-# Makefile that generates gen/pkgs.mk
-
-include config/prelude.mk
-include ${GEN}/vars.mk
-include ${GEN}/flags.mk
-include ${GEN}/pkgs.mk
-include config/exports.mk
-
-${GEN}/pkgs.mk::
- ${MSG} "[ GEN] ${TGT}"
- printf 'CFLAGS += %s\n' "$$(config/pkgconf.sh --cflags ${PKGS})" >>$@ 2>>$@.log
- printf 'LDFLAGS += %s\n' "$$(config/pkgconf.sh --ldflags ${PKGS})" >>$@ 2>>$@.log
- printf 'LDLIBS := %s $${LDLIBS}\n' "$$(config/pkgconf.sh --ldlibs ${PKGS})" >>$@ 2>>$@.log
- ${VCAT} $@
diff --git a/config/prelude.mk b/config/prelude.mk
deleted file mode 100644
index b9bc61b..0000000
--- a/config/prelude.mk
+++ /dev/null
@@ -1,170 +0,0 @@
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-# Common makefile utilities. Compatible with both GNU make and most BSD makes.
-
-# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
-.OBJDIR: .
-
-# We don't use any suffix rules
-.SUFFIXES:
-
-# GNU make has $^ for the full list of targets, while BSD make has $> and the
-# long-form ${.ALLSRC}. We could write $^ $> to get them both, but that would
-# break if one of them implemented support for the other. So instead, bring
-# BSD's ${.ALLSRC} to GNU.
-.ALLSRC ?= $^
-
-# For out-of-tree builds, e.g.
-#
-# $ make config BUILDDIR=/path/to/build/dir
-# $ make BUILDDIR=/path/to/build/dir
-BUILDDIR ?= .
-
-# Shorthand for build subdirectories
-BIN := ${BUILDDIR}/bin
-GEN := ${BUILDDIR}/gen
-OBJ := ${BUILDDIR}/obj
-
-# GNU make strips a leading ./ from target names, so do the same for BSD make
-BIN := ${BIN:./%=%}
-GEN := ${GEN:./%=%}
-OBJ := ${OBJ:./%=%}
-
-# The configuration file generated by `make config`
-CONFIG := ${GEN}/config.mk
-
-# Installation paths
-DESTDIR ?=
-PREFIX ?= /usr
-MANDIR ?= ${PREFIX}/share/man
-
-# GNU make supports `export VAR`, but BSD make requires `export VAR=value`.
-# Sadly, GNU make gives a recursion error on `export VAR=${VAR}`.
-_BUILDDIR := ${BUILDDIR}
-export BUILDDIR=${_BUILDDIR}
-
-# Configurable executables; can be overridden with
-#
-# $ make config CC=clang
-CC ?= cc
-INSTALL ?= install
-MKDIR ?= mkdir -p
-PKG_CONFIG ?= pkg-config
-RM ?= rm -f
-
-# GNU and BSD make have incompatible syntax for conditionals, but we can do a
-# lot with just nested variable expansion. We use "y" as the canonical
-# truthy value, and "" (the empty string) as the canonical falsey value.
-#
-# To normalize a boolean, use ${TRUTHY,${VAR}}, which expands like this:
-#
-# VAR=y ${TRUTHY,${VAR}} => ${TRUTHY,y} => y
-# VAR=1 ${TRUTHY,${VAR}} => ${TRUTHY,1} => y
-# VAR=n ${TRUTHY,${VAR}} => ${TRUTHY,n} => [empty]
-# VAR=other ${TRUTHY,${VAR}} => ${TRUTHY,other} => [empty]
-# VAR= ${TRUTHY,${VAR}} => ${TRUTHY,} => [emtpy]
-#
-# Inspired by https://github.com/wahern/autoguess
-TRUTHY,y := y
-TRUTHY,1 := y
-
-# Boolean operators are also implemented with nested expansion
-NOT,y :=
-NOT, := y
-
-# Support up to 5 arguments
-AND,y := y
-AND,y,y := y
-AND,y,y,y := y
-AND,y,y,y,y := y
-AND,y,y,y,y,y := y
-
-# NOR can be defined without combinatorial explosion.
-# OR is just ${NOT,${NOR,...}}
-NOR, := y
-NOR,, := y
-NOR,,, := y
-NOR,,,, := y
-NOR,,,,, := y
-
-# Normalize ${V} to either "y" or ""
-IS_V := ${TRUTHY,${V}}
-
-# Suppress output unless V=1
-Q, := @
-Q := ${Q,${IS_V}}
-
-# The current target, with ${BUILDDIR} stripped for shorter messages
-TGT = ${@:${BUILDDIR}/%=%}
-
-# Show full commands with `make V=1`, otherwise short summaries
-MSG = @msg() { \
- MSG="$$1"; \
- shift; \
- test "${IS_V}" || printf '%s\n' "$$MSG"; \
- test "$${1:-}" || return 0; \
- test "${IS_V}" && printf '%s\n' "$$*"; \
- "$$@"; \
- }; \
- msg
-
-# Maximum width of a short message, to align the [X]
-MSG_WIDTH := 33
-
-# cat a file if V=1
-VCAT,y := @cat
-VCAT, := @:
-VCAT := ${VCAT,${IS_V}}
-
-# List all object files here, as they're needed by both `make config` and `make`
-
-# All object files except the entry point
-LIBBFS := \
- ${OBJ}/src/alloc.o \
- ${OBJ}/src/bar.o \
- ${OBJ}/src/bfstd.o \
- ${OBJ}/src/bftw.o \
- ${OBJ}/src/color.o \
- ${OBJ}/src/ctx.o \
- ${OBJ}/src/diag.o \
- ${OBJ}/src/dir.o \
- ${OBJ}/src/dstring.o \
- ${OBJ}/src/eval.o \
- ${OBJ}/src/exec.o \
- ${OBJ}/src/expr.o \
- ${OBJ}/src/fsade.o \
- ${OBJ}/src/ioq.o \
- ${OBJ}/src/mtab.o \
- ${OBJ}/src/opt.o \
- ${OBJ}/src/parse.o \
- ${OBJ}/src/printf.o \
- ${OBJ}/src/pwcache.o \
- ${OBJ}/src/stat.o \
- ${OBJ}/src/thread.o \
- ${OBJ}/src/trie.o \
- ${OBJ}/src/typo.o \
- ${OBJ}/src/xregex.o \
- ${OBJ}/src/xspawn.o \
- ${OBJ}/src/xtime.o \
- ${OBJ}/gen/version.o
-
-# Unit test objects
-UNIT_OBJS := \
- ${OBJ}/tests/alloc.o \
- ${OBJ}/tests/bfstd.o \
- ${OBJ}/tests/bit.o \
- ${OBJ}/tests/ioq.o \
- ${OBJ}/tests/main.o \
- ${OBJ}/tests/trie.o \
- ${OBJ}/tests/xspawn.o \
- ${OBJ}/tests/xtime.o
-
-# All object files
-OBJS := \
- ${OBJ}/src/main.o \
- ${OBJ}/tests/mksock.o \
- ${OBJ}/tests/xspawnee.o \
- ${OBJ}/tests/xtouch.o \
- ${LIBBFS} \
- ${UNIT_OBJS}
diff --git a/configure b/configure
new file mode 100755
index 0000000..d42dec3
--- /dev/null
+++ b/configure
@@ -0,0 +1,137 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# bfs build configuration script
+
+set -eu
+
+# Default to `make`
+MAKE="${MAKE:-make}"
+
+# Pass -j$(nproc) unless MAKEFLAGS is set
+if [ "${MAKEFLAGS+y}" ]; then
+ j=""
+else
+ j="-j$({ nproc || sysctl -n hw.ncpu || getconf _NPROCESSORS_ONLN || echo 1; } 2>/dev/null)"
+fi
+
+for arg; do
+ case "$arg" in
+ -h|--help)
+ cat <<EOF
+Usage:
+
+ \$ $0 [--enable-*|--disable-*] [CC=...] [CFLAGS=...] [...]
+ \$ $MAKE $j
+
+Variables set in the environment or on the command line will be picked up:
+
+ MAKE
+ The make implementation to use
+ CC
+ The C compiler to use
+
+ CPPFLAGS="-I... -D..."
+ CFLAGS="-W... -f..."
+ LDFLAGS="-L... -Wl,..."
+ Preprocessor/compiler/linker flags
+
+ LDLIBS="-l... -l..."
+ Dynamic libraries to link
+
+ EXTRA_{CPPFLAGS,CFLAGS,LDFLAGS,LDLIBS}
+ Adds to the default flags, instead of replacing them
+
+The default flags result in a plain debug build. Other build profiles include:
+
+ --enable-release
+ Enable optimizations, disable assertions
+ --enable-{asan,lsan,msan,tsan,ubsan}
+ Enable sanitizers
+ --enable-gcov
+ Enable code coverage instrumentation
+
+External dependencies are auto-detected by default, but you can --enable or
+--disable them manually:
+
+ --enable-libacl --disable-libacl
+ --enable-libcap --disable-libcap
+ --enable-libselinux --disable-libselinux
+ --enable-liburing --disable-liburing
+ --enable-oniguruma --disable-oniguruma
+
+Packaging:
+
+ --prefix=/path
+ Set the installation prefix (default: /usr)
+
+This script is a thin wrapper around a makefile-based configuration system.
+Any other arguments will be passed directly to the $MAKE invocation, e.g.
+
+ \$ $0 $j V=1
+EOF
+ exit 0
+ ;;
+
+ --enable-*|--disable-*)
+ case "$arg" in
+ --enable-*) yn=y ;;
+ --disable-*) yn=n ;;
+ esac
+
+ name="${arg#--*able-}"
+ NAME=$(printf '%s' "$name" | tr 'a-z-' 'A-Z_')
+ case "$name" in
+ libacl|libcap|libselinux|liburing|oniguruma)
+ shift
+ set -- "$@" "USE_$NAME=$yn"
+ ;;
+ release|asan|lsan|msan|tsan|ubsan|lint|gcov)
+ shift
+ set -- "$@" "$NAME=$yn"
+ ;;
+ *)
+ printf 'error: Unrecognized option "%s"\n\n' "$arg" >&2
+ printf 'Run %s --help for more information.\n' "$0" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+
+ --prefix=*)
+ shift
+ set -- "$@" "PREFIX=${arg#*=}"
+ ;;
+
+ MAKE=*)
+ MAKE="${arg#*=}"
+ shift
+ ;;
+
+ # make flag (-j2) or variable (CC=clang)
+ -*|*=*)
+ continue
+ ;;
+
+ *)
+ printf 'error: Unrecognized option "%s"\n\n' "$arg" >&2
+ printf 'Run %s --help for more information.\n' "$0" >&2
+ exit 1
+ ;;
+ esac
+done
+
+# Get the relative path to the source tree based on how the script was run
+DIR=$(dirname -- "$0")
+
+# Set up symbolic links for out-of-tree builds
+for f in Makefile build completions docs src tests; do
+ test -e "$f" || ln -s "$DIR/$f" "$f"
+done
+
+# Set MAKEFLAGS to -j$(nproc) if it's unset
+export MAKEFLAGS="${MAKEFLAGS-$j}"
+
+$MAKE -rf build/config.mk "$@"
diff --git a/docs/BUILDING.md b/docs/BUILDING.md
index 4ed139c..cb33c51 100644
--- a/docs/BUILDING.md
+++ b/docs/BUILDING.md
@@ -1,110 +1,157 @@
Building `bfs`
==============
-Compiling
----------
-
-`bfs` uses [GNU Make](https://www.gnu.org/software/make/) as its build system.
A simple invocation of
- $ make config
+ $ ./configure
$ make
should build `bfs` successfully.
-As usual with `make`, you can run a [parallel build](https://www.gnu.org/software/make/manual/html_node/Parallel.html) with `-j`.
-For example, to use all your cores, run `make -j$(nproc)`.
-### Targets
-| Command | Description |
-|------------------|---------------------------------------------------------------|
-| `make config` | Configures the build system |
-| `make` | Builds just the `bfs` binary |
-| `make all` | Builds everything, including the tests (but doesn't run them) |
-| `make check` | Builds everything, and runs the tests |
-| `make install` | Installs `bfs` (with man page, shell completions, etc.) |
-| `make uninstall` | Uninstalls `bfs` |
-| `make clean` | Delete the build products |
-| `make distclean` | Delete all generated files, including the build configuration |
+Configuration
+-------------
+
+```console
+$ ./configure --help
+Usage:
+
+ $ ./configure [--enable-*|--disable-*] [CC=...] [CFLAGS=...] [...]
+ $ make
+
+...
+```
+
+### Variables
+
+Variables set in the environment or on the command line will be picked up:
+These variables specify binaries to run during the configuration and build process:
+
+<pre>
+<b>MAKE</b>=<i>make</i>
+ <a href="https://en.wikipedia.org/wiki/Make_(software)">make</a> implementation
+<b>CC</b>=<i>cc</i>
+ C compiler
+<b>INSTALL</b>=<i>install</i>
+ Copy files during <i>make install</i>
+<b>MKDIR</b>="<i>mkdir -p</i>"
+ Create directories
+<b>PKG_CONFIG</b>=<i>pkg-config</i>
+ Detect external libraries and required build flags
+<b>RM</b>="<i>rm -f</i>"
+ Delete files
+</pre>
+
+These flags will be used by the build process:
+
+<pre>
+<b>CPPFLAGS</b>="<i>-I... -D...</i>"
+<b>CFLAGS</b>="<i>-W... -f...</i>"
+<b>LDFLAGS</b>="<i>-L... -Wl,...</i>"
+ Preprocessor/compiler/linker flags
+
+<b>LDLIBS</b>="<i>-l... -l...</i>"
+ Dynamic libraries to link
+
+<b>EXTRA_</b>{<b>CPPFLAGS</b>,<b>CFLAGS</b>,<b>LDFLAGS</b>,<b>LDLIBS</b>}="<i>...</i>"
+ Adds to the default flags, instead of replacing them
+</pre>
### Build profiles
-The configuration system provides a few shorthand flags for handy configurations:
-
-| Command | Description |
-|-------------------------|-------------------------------------------------------------|
-| `make config RELEASE=y` | Build `bfs` with optimizations, LTO, and without assertions |
-| `make config ASAN=y` | Enable [AddressSanitizer] |
-| `make config LSAN=y` | Enable [LeakSanitizer] |
-| `make config MSAN=y` | Enable [MemorySanitizer] |
-| `make config TSAN=y` | Enable [ThreadSanitizer] |
-| `make config UBSAN=y` | Enable [UndefinedBehaviorSanitizer] |
-| `make config GCOV=y` | Enable [code coverage] |
-
-[AddressSanitizer]: https://github.com/google/sanitizers/wiki/AddressSanitizer
-[LeakSanitizer]: https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer#stand-alone-mode
-[MemorySanitizer]: https://github.com/google/sanitizers/wiki/MemorySanitizer
-[ThreadSanitizer]: https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
-[UndefinedBehaviorSanitizer]: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
-[code coverage]: https://gcc.gnu.org/onlinedocs/gcc/Gcov.html
-
-You can combine multiple profiles (e.g. `make config ASAN=y UBSAN=y`), but not all of them will work together.
-
-### Flags
-
-Other flags can be specified on the `make config` command line or in the environment.
-Here are some of the common ones; check the [`Makefile`](/Makefile) for more.
-
-| Flag | Description |
-|-------------------------------------|----------------------------------------------------|
-| `CC` | The C compiler to use, e.g. `make config CC=clang` |
-| `CFLAGS`<br>`EXTRA_CFLAGS` | Override/add to the default compiler flags |
-| `LDFLAGS`<br>`EXTRA_LDFLAGS` | Override/add to the linker flags |
-| `USE_LIBACL`<br>`USE_LIBCAP`<br>... | Enable/disable [optional dependencies] |
-| `TEST_FLAGS` | `tests.sh` flags for `make check` |
-| `BUILDDIR` | The build output directory (default: `.`) |
-| `DESTDIR` | The root directory for `make install` |
-| `PREFIX` | The installation prefix (default: `/usr`) |
-| `MANDIR` | The man page installation directory |
-
-[optional dependencies]: #dependencies
+The default flags result in a plain debug build.
+Other build profiles can be enabled:
+
+<pre>
+--enable-release
+ Enable optimizations, disable assertions
+
+--enable-<a href="https://github.com/google/sanitizers/wiki/AddressSanitizer">asan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer#stand-alone-mode">lsan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/MemorySanitizer">msan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual">tsan</a>
+--enable-<a href="https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html">ubsan</a>
+ Enable sanitizers
+
+--enable-<a href="https://gcc.gnu.org/onlinedocs/gcc/gcov/introduction-to-gcov.html">gcov</a>
+ Enable code coverage instrumentation
+</pre>
+
+You can combine multiple profiles (e.g. `./configure --enable-asan --enable-ubsan`), but not all of them will work together.
### Dependencies
`bfs` depends on some system libraries for some of its features.
-These dependencies are optional, and can be turned off in `make config` if necessary by setting the appropriate variable to `n` (e.g. `make config USE_ONIGURUMA=n`).
+External dependencies are auto-detected by default, but you can `--enable` or `--disable` them manually:
-| Dependency | Platforms | `make config` flag |
-|--------------|------------|--------------------|
-| [libacl] | Linux only | `USE_LIBACL` |
-| [libcap] | Linux only | `USE_LIBCAP` |
-| [liburing] | Linux only | `USE_LIBURING` |
-| [libselinux] | Linux only | `USE_LIBSELINUX` |
-| [Oniguruma] | All | `USE_ONIGURUMA` |
+<pre>
+--enable-<a href="https://savannah.nongnu.org/projects/acl">libacl</a> --disable-libacl
+--enable-<a href="https://sites.google.com/site/fullycapable/">libcap</a> --disable-libcap
+--enable-<a href="https://github.com/SELinuxProject/selinux">libselinux</a> --disable-libselinux
+--enable-<a href="https://github.com/axboe/liburing">liburing</a> --disable-liburing
+--enable-<a href="https://github.com/kkos/oniguruma">oniguruma</a> --disable-oniguruma
+</pre>
-[libacl]: https://savannah.nongnu.org/projects/acl
-[libcap]: https://sites.google.com/site/fullycapable/
-[libselinux]: https://github.com/SELinuxProject/selinux
-[liburing]: https://github.com/axboe/liburing
-[Oniguruma]: https://github.com/kkos/oniguruma
+[`pkg-config`] is used, if available, to detect these libraries and any additional build flags they may require.
+If this is undesireable, disable it by setting `PKG_CONFIG` to the empty string (`./configure PKG_CONFIG=""`).
-### Dependency tracking
+[`pkg-config`]: https://www.freedesktop.org/wiki/Software/pkg-config/
-The build system automatically tracks header dependencies with the `-M` family of compiler options (see `DEPFLAGS` in the [`Makefile`](/Makefile)).
-So if you edit a header file, `make` will rebuild the necessary object files ensuring they don't go out of sync.
+### Out-of-tree builds
-We also add a dependency on the current configuration, so you can change configurations and rebuild without having to `make clean`.
+You can set up an out-of-tree build by running the `configure` script from another directory, for example:
-We go one step further than most build systems by tracking the flags that were used for the previous compilation.
-That means you can change configurations without having to `make clean`.
-For example,
-
- $ make config
- $ make
- $ make config RELEASE=y
+ $ mkdir out
+ $ cd out
+ $ ../configure
$ make
-will build the project in debug mode and then rebuild it in release mode.
+
+Building
+--------
+
+### Targets
+
+The [`Makefile`](/Makefile) supports several different build targets:
+
+<pre>
+make
+ The default target; builds just the <i>bfs</i> binary
+make <b>all</b>
+ Builds everything, including the tests (but doesn't run them)
+
+make <b>check</b>
+ Builds everything, and runs all tests
+make <b>unit-tests</b>
+ Builds and runs the unit tests
+make <b>integration-tests</b>
+ Builds and runs the integration tests
+make <b>distcheck</b>
+ Builds and runs the tests in multiple different configurations
+
+make <b>install</b>
+ Installs bfs globally
+make <b>uninstall</b>
+ Uninstalls bfs
+
+make <b>clean</b>
+ Deletes all built files
+make <b>distclean</b>
+ Also deletes files generated by ./configure
+</pre>
+
+
+Troubleshooting
+---------------
+
+If the build fails or behaves unexpectedly, start by enabling verbose mode:
+
+ $ ./configure V=1
+ $ make V=1
+
+This will print the generated configuration and the exact commands that are executed.
+
+You can also check the file `gen/config.log`, which contains any errors from commands run during the configuration phase.
Testing
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 672c2b4..62b6480 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,36 @@
3.*
===
+3.2
+---
+
+**May 2, 2024**
+
+### New features
+
+- New `-limit N` action that quits immediately after `N` results
+
+- Implemented `-context` (from GNU find) for matching SELinux contexts ([#27](https://github.com/tavianator/bfs/issues/27))
+
+- Implemented `-printf %Z` for printing SELinux contexts
+
+### Changes
+
+- The build system has been rewritten, and there is now a configure step:
+
+ $ ./configure
+ $ make
+
+ See `./configure --help` or [docs/BUILDING.md](/docs/BUILDING.md) for more details.
+
+- Improved platform support
+ - Implemented `-acl` on Solaris/Illumos
+ - Implemented `-xattr` on DragonFly BSD
+
+### Bug fixes
+
+- Fixed some rarely-used code paths that clean up after allocation failures
+
3.1.3
-----
diff --git a/src/atomic.h b/src/atomic.h
index f1a6bea..360de20 100644
--- a/src/atomic.h
+++ b/src/atomic.h
@@ -8,6 +8,8 @@
#ifndef BFS_ATOMIC_H
#define BFS_ATOMIC_H
+#include "prelude.h"
+#include "sanity.h"
#include <stdatomic.h>
/**
@@ -82,4 +84,35 @@
#define fetch_and(obj, arg, order) \
atomic_fetch_and_explicit(obj, arg, memory_order_##order)
+/**
+ * Shorthand for atomic_thread_fence().
+ */
+#if SANITIZE_THREAD
+// TSan doesn't support fences: https://github.com/google/sanitizers/issues/1415
+# define thread_fence(obj, order) \
+ fetch_add(obj, 0, order)
+#else
+# define thread_fence(obj, order) \
+ atomic_thread_fence(memory_order_##order)
+#endif
+
+/**
+ * Shorthand for atomic_signal_fence().
+ */
+#define signal_fence(order) \
+ atomic_signal_fence(memory_order_##order)
+
+/**
+ * A hint to the CPU to relax while it spins.
+ */
+#if __has_builtin(__builtin_ia32_pause)
+# define spin_loop() __builtin_ia32_pause()
+#elif __has_builtin(__builtin_arm_yield)
+# define spin_loop() __builtin_arm_yield()
+#elif __has_builtin(__builtin_riscv_pause)
+# define spin_loop() __builtin_riscv_pause()
+#else
+# define spin_loop() ((void)0)
+#endif
+
#endif // BFS_ATOMIC_H
diff --git a/src/bar.c b/src/bar.c
index 184d9a0..b928373 100644
--- a/src/bar.c
+++ b/src/bar.c
@@ -3,10 +3,12 @@
#include "prelude.h"
#include "bar.h"
+#include "alloc.h"
#include "atomic.h"
#include "bfstd.h"
#include "bit.h"
#include "dstring.h"
+#include "sighook.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
@@ -19,11 +21,9 @@ struct bfs_bar {
int fd;
atomic unsigned int width;
atomic unsigned int height;
-};
-/** The global status bar instance. */
-static struct bfs_bar the_bar = {
- .fd = -1,
+ struct sighook *exit_hook;
+ struct sighook *winch_hook;
};
/** Get the terminal size, if possible. */
@@ -43,10 +43,14 @@ static int bfs_bar_getsize(struct bfs_bar *bar) {
#endif
}
-/** Async Signal Safe puts(). */
-static int ass_puts(int fd, const char *str) {
- size_t len = strlen(str);
- return xwrite(fd, str, len) == len ? 0 : -1;
+/** Write a string to the status bar (async-signal-safe). */
+static int bfs_bar_write(struct bfs_bar *bar, const char *str, size_t len) {
+ return xwrite(bar->fd, str, len) == len ? 0 : -1;
+}
+
+/** Write a string to the status bar (async-signal-safe). */
+static int bfs_bar_puts(struct bfs_bar *bar, const char *str) {
+ return bfs_bar_write(bar, str, strlen(str));
}
/** Number of decimal digits needed for terminal sizes. */
@@ -70,39 +74,40 @@ static char *ass_itoa(char *str, unsigned int n) {
/** Update the size of the scrollable region. */
static int bfs_bar_resize(struct bfs_bar *bar) {
- char esc_seq[12 + ITOA_DIGITS] =
+ static const char PREFIX[] =
+ "\033D" // IND: Line feed, possibly scrolling
+ "\033[1A" // CUU: Move cursor up 1 row
"\0337" // DECSC: Save cursor
"\033[;"; // DECSTBM: Set scrollable region
+ static const char SUFFIX[] =
+ "r" // (end of DECSTBM)
+ "\0338" // DECRC: Restore the cursor
+ "\033[J"; // ED: Erase display from cursor to end
+
+ char esc_seq[sizeof(PREFIX) + ITOA_DIGITS + sizeof(SUFFIX)];
// DECSTBM takes the height as the second argument
- unsigned int height = load(&bar->height, relaxed);
- char *ptr = esc_seq + strlen(esc_seq);
- ptr = ass_itoa(ptr, height - 1);
+ unsigned int height = load(&bar->height, relaxed) - 1;
- strcpy(ptr,
- "r" // DECSTBM
- "\0338" // DECRC: Restore the cursor
- "\033[J" // ED: Erase display from cursor to end
- );
+ char *cur = stpcpy(esc_seq, PREFIX);
+ cur = ass_itoa(cur, height);
+ cur = stpcpy(cur, SUFFIX);
- return ass_puts(bar->fd, esc_seq);
+ return bfs_bar_write(bar, esc_seq, cur - esc_seq);
}
#ifdef SIGWINCH
/** SIGWINCH handler. */
-static void sighand_winch(int sig) {
- int error = errno;
-
- bfs_bar_getsize(&the_bar);
- bfs_bar_resize(&the_bar);
-
- errno = error;
+static void bfs_bar_sigwinch(int sig, siginfo_t *info, void *arg) {
+ struct bfs_bar *bar = arg;
+ bfs_bar_getsize(bar);
+ bfs_bar_resize(bar);
}
#endif
/** Reset the scrollable region and hide the bar. */
static int bfs_bar_reset(struct bfs_bar *bar) {
- return ass_puts(bar->fd,
+ return bfs_bar_puts(bar,
"\0337" // DECSC: Save cursor
"\033[r" // DECSTBM: Reset scrollable region
"\0338" // DECRC: Restore cursor
@@ -111,19 +116,9 @@ static int bfs_bar_reset(struct bfs_bar *bar) {
}
/** Signal handler for process-terminating signals. */
-static void sighand_reset(int sig) {
- bfs_bar_reset(&the_bar);
- raise(sig);
-}
-
-/** Register sighand_reset() for a signal. */
-static void reset_before_death_by(int sig) {
- struct sigaction sa = {
- .sa_handler = sighand_reset,
- .sa_flags = SA_RESETHAND,
- };
- sigemptyset(&sa.sa_mask);
- sigaction(sig, &sa, NULL);
+static void bfs_bar_sigexit(int sig, siginfo_t *info, void *arg) {
+ struct bfs_bar *bar = arg;
+ bfs_bar_reset(bar);
}
/** printf() to the status bar with a single write(). */
@@ -138,15 +133,15 @@ static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) {
return -1;
}
- int ret = ass_puts(bar->fd, str);
+ int ret = bfs_bar_write(bar, str, dstrlen(str));
dstrfree(str);
return ret;
}
struct bfs_bar *bfs_bar_show(void) {
- if (the_bar.fd >= 0) {
- errno = EBUSY;
- goto fail;
+ struct bfs_bar *bar = ALLOC(struct bfs_bar);
+ if (!bar) {
+ return NULL;
}
char term[L_ctermid];
@@ -156,46 +151,36 @@ struct bfs_bar *bfs_bar_show(void) {
goto fail;
}
- the_bar.fd = open(term, O_RDWR | O_CLOEXEC);
- if (the_bar.fd < 0) {
+ bar->fd = open(term, O_RDWR | O_CLOEXEC);
+ if (bar->fd < 0) {
goto fail;
}
- if (bfs_bar_getsize(&the_bar) != 0) {
+ if (bfs_bar_getsize(bar) != 0) {
goto fail_close;
}
- reset_before_death_by(SIGABRT);
- reset_before_death_by(SIGINT);
- reset_before_death_by(SIGPIPE);
- reset_before_death_by(SIGQUIT);
- reset_before_death_by(SIGTERM);
+ bar->exit_hook = atsigexit(bfs_bar_sigexit, bar);
+ if (!bar->exit_hook) {
+ goto fail_close;
+ }
#ifdef SIGWINCH
- struct sigaction sa = {
- .sa_handler = sighand_winch,
- .sa_flags = SA_RESTART,
- };
- sigemptyset(&sa.sa_mask);
- sigaction(SIGWINCH, &sa, NULL);
+ bar->winch_hook = sighook(SIGWINCH, bfs_bar_sigwinch, bar, 0);
+ if (!bar->winch_hook) {
+ goto fail_hook;
+ }
#endif
- unsigned int height = load(&the_bar.height, relaxed);
- bfs_bar_printf(&the_bar,
- "\n" // Make space for the bar
- "\0337" // DECSC: Save cursor
- "\033[;%ur" // DECSTBM: Set scrollable region
- "\0338" // DECRC: Restore cursor
- "\033[1A", // CUU: Move cursor up 1 row
- height - 1
- );
-
- return &the_bar;
+ bfs_bar_resize(bar);
+ return bar;
+fail_hook:
+ sigunhook(bar->exit_hook);
fail_close:
- close_quietly(the_bar.fd);
- the_bar.fd = -1;
+ close_quietly(bar->fd);
fail:
+ free(bar);
return NULL;
}
@@ -223,17 +208,11 @@ void bfs_bar_hide(struct bfs_bar *bar) {
return;
}
- signal(SIGABRT, SIG_DFL);
- signal(SIGINT, SIG_DFL);
- signal(SIGPIPE, SIG_DFL);
- signal(SIGQUIT, SIG_DFL);
- signal(SIGTERM, SIG_DFL);
-#ifdef SIGWINCH
- signal(SIGWINCH, SIG_DFL);
-#endif
+ sigunhook(bar->winch_hook);
+ sigunhook(bar->exit_hook);
bfs_bar_reset(bar);
xclose(bar->fd);
- bar->fd = -1;
+ free(bar);
}
diff --git a/src/bfstd.c b/src/bfstd.c
index 1144380..7680f17 100644
--- a/src/bfstd.c
+++ b/src/bfstd.c
@@ -252,40 +252,6 @@ int ynprompt(void) {
return ret;
}
-/** Get the single character describing the given file type. */
-static char type_char(mode_t mode) {
- switch (mode & S_IFMT) {
- case S_IFREG:
- return '-';
- case S_IFBLK:
- return 'b';
- case S_IFCHR:
- return 'c';
- case S_IFDIR:
- return 'd';
- case S_IFLNK:
- return 'l';
- case S_IFIFO:
- return 'p';
- case S_IFSOCK:
- return 's';
-#ifdef S_IFDOOR
- case S_IFDOOR:
- return 'D';
-#endif
-#ifdef S_IFPORT
- case S_IFPORT:
- return 'P';
-#endif
-#ifdef S_IFWHT
- case S_IFWHT:
- return 'w';
-#endif
- }
-
- return '?';
-}
-
void *xmemdup(const void *src, size_t size) {
void *ret = malloc(size);
if (ret) {
@@ -356,6 +322,40 @@ const char *xstrerror(int errnum) {
return ret;
}
+/** Get the single character describing the given file type. */
+static char type_char(mode_t mode) {
+ switch (mode & S_IFMT) {
+ case S_IFREG:
+ return '-';
+ case S_IFBLK:
+ return 'b';
+ case S_IFCHR:
+ return 'c';
+ case S_IFDIR:
+ return 'd';
+ case S_IFLNK:
+ return 'l';
+ case S_IFIFO:
+ return 'p';
+ case S_IFSOCK:
+ return 's';
+#ifdef S_IFDOOR
+ case S_IFDOOR:
+ return 'D';
+#endif
+#ifdef S_IFPORT
+ case S_IFPORT:
+ return 'P';
+#endif
+#ifdef S_IFWHT
+ case S_IFWHT:
+ return 'w';
+#endif
+ }
+
+ return '?';
+}
+
void xstrmode(mode_t mode, char str[11]) {
strcpy(str, "----------");
@@ -637,8 +637,14 @@ error:
return NULL;
}
+#if BFS_HAS_STRTOFFLAGS
+# define BFS_STRTOFFLAGS strtofflags
+#elif BFS_HAS_STRING_TO_FLAGS
+# define BFS_STRTOFFLAGS string_to_flags
+#endif
+
int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) {
-#if BSD && !__GNU__
+#ifdef BFS_STRTOFFLAGS
char *str_arg = (char *)*str;
#if __OpenBSD__
@@ -649,11 +655,7 @@ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *
bfs_fflags_t set_arg = 0;
bfs_fflags_t clear_arg = 0;
-#if __NetBSD__
- int ret = string_to_flags(&str_arg, &set_arg, &clear_arg);
-#else
- int ret = strtofflags(&str_arg, &set_arg, &clear_arg);
-#endif
+ int ret = BFS_STRTOFFLAGS(&str_arg, &set_arg, &clear_arg);
*str = str_arg;
*set = set_arg;
@@ -663,12 +665,27 @@ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *
errno = EINVAL;
}
return ret;
-#else // !BSD
+#else // !BFS_STRTOFFLAGS
errno = ENOTSUP;
return -1;
#endif
}
+long xsysconf(int name) {
+#if __FreeBSD__ && SANITIZE_MEMORY
+ // Work around https://github.com/llvm/llvm-project/issues/88163
+ __msan_scoped_disable_interceptor_checks();
+#endif
+
+ long ret = sysconf(name);
+
+#if __FreeBSD__ && SANITIZE_MEMORY
+ __msan_scoped_enable_interceptor_checks();
+#endif
+
+ return ret;
+}
+
size_t asciilen(const char *str) {
return asciinlen(str, strlen(str));
}
diff --git a/src/bfstd.h b/src/bfstd.h
index f91e380..d06bbd9 100644
--- a/src/bfstd.h
+++ b/src/bfstd.h
@@ -410,6 +410,11 @@ char *xconfstr(int name);
*/
int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear);
+/**
+ * Wrapper for sysconf() that works around an MSan bug.
+ */
+long xsysconf(int name);
+
#include <wchar.h>
/**
diff --git a/src/bftw.c b/src/bftw.c
index c4d3c17..5322181 100644
--- a/src/bftw.c
+++ b/src/bftw.c
@@ -1281,7 +1281,7 @@ static int bftw_pin_parent(struct bftw_state *state, struct bftw_file *file) {
int fd = parent->fd;
if (fd < 0) {
- bfs_static_assert(AT_FDCWD != -1);
+ bfs_static_assert((int)AT_FDCWD != -1);
return -1;
}
@@ -1298,7 +1298,7 @@ static int bftw_ioq_opendir(struct bftw_state *state, struct bftw_file *file) {
}
int dfd = bftw_pin_parent(state, file);
- if (dfd < 0 && dfd != AT_FDCWD) {
+ if (dfd < 0 && dfd != (int)AT_FDCWD) {
goto fail;
}
@@ -1450,7 +1450,7 @@ static int bftw_ioq_stat(struct bftw_state *state, struct bftw_file *file) {
}
int dfd = bftw_pin_parent(state, file);
- if (dfd < 0 && dfd != AT_FDCWD) {
+ if (dfd < 0 && dfd != (int)AT_FDCWD) {
goto fail;
}
diff --git a/src/bftw.h b/src/bftw.h
index 2805361..8656ca7 100644
--- a/src/bftw.h
+++ b/src/bftw.h
@@ -54,7 +54,7 @@ struct BFTW {
/** The file type. */
enum bfs_type type;
- /** The errno that occurred, if type == BFTW_ERROR. */
+ /** The errno that occurred, if type == BFS_ERROR. */
int error;
/** A parent file descriptor for the *at() family of calls. */
@@ -104,7 +104,7 @@ const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat
* @param flags
* flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags.
* @return
- * The type of the file, or BFTW_ERROR if an error occurred.
+ * The type of the file, or BFS_ERROR if an error occurred.
*/
enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags);
diff --git a/src/color.c b/src/color.c
index f004bf2..a0e553f 100644
--- a/src/color.c
+++ b/src/color.c
@@ -161,8 +161,13 @@ static struct esc_seq **get_esc(const struct colors *colors, const char *name) {
return leaf ? leaf->value : NULL;
}
+/** Append an escape sequence to a string. */
+static int cat_esc(dchar **dstr, const struct esc_seq *seq) {
+ return dstrxcat(dstr, seq->seq, seq->len);
+}
+
/** Set a named escape sequence. */
-static int set_esc(struct colors *colors, const char *name, char *value) {
+static int set_esc(struct colors *colors, const char *name, dchar *value) {
struct esc_seq **field = get_esc(colors, name);
if (!field) {
return 0;
@@ -587,8 +592,8 @@ static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) {
// All-zero values should be treated like NULL, to fall
// back on any other relevant coloring for that file
- char *esc = value;
- if (strspn(value, "0") == strlen(value)
+ dchar *esc = value;
+ if (strspn(value, "0") == dstrlen(value)
&& strcmp(key, "rs") != 0
&& strcmp(key, "lc") != 0
&& strcmp(key, "rc") != 0
@@ -693,6 +698,20 @@ struct colors *parse_colors(void) {
colors->link->len = 0;
}
+ // Pre-compute the reset escape sequence
+ if (!colors->endcode) {
+ dchar *ec = dstralloc(0);
+ if (!ec
+ || cat_esc(&ec, colors->leftcode) != 0
+ || cat_esc(&ec, colors->reset) != 0
+ || cat_esc(&ec, colors->rightcode) != 0
+ || set_esc(colors, "ec", ec) != 0) {
+ dstrfree(ec);
+ goto fail;
+ }
+ dstrfree(ec);
+ }
+
return colors;
fail:
@@ -727,10 +746,11 @@ CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) {
}
cfile->file = file;
+ cfile->fd = fileno(file);
cfile->need_reset = false;
cfile->close = close;
- if (isatty(fileno(file))) {
+ if (isatty(cfile->fd)) {
cfile->colors = colors;
} else {
cfile->colors = NULL;
@@ -874,7 +894,7 @@ error:
/** Print an escape sequence chunk. */
static int print_esc_chunk(CFILE *cfile, const struct esc_seq *esc) {
- return dstrxcat(&cfile->buffer, esc->seq, esc->len);
+ return cat_esc(&cfile->buffer, esc);
}
/** Print an ANSI escape sequence. */
@@ -908,12 +928,7 @@ static int print_reset(CFILE *cfile) {
}
cfile->need_reset = false;
- const struct colors *colors = cfile->colors;
- if (colors->endcode) {
- return print_esc_chunk(cfile, colors->endcode);
- } else {
- return print_esc(cfile, colors->reset);
- }
+ return print_esc_chunk(cfile, cfile->colors->endcode);
}
/** Print a shell-escaped string. */
@@ -961,7 +976,7 @@ static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf,
} else {
// We're in print_link_target(), so resolve relative to the link's parent directory
at_fd = ftwbuf->at_fd;
- if (at_fd == AT_FDCWD && path[0] != '/') {
+ if (at_fd == (int)AT_FDCWD && path[0] != '/') {
at_path = dstrndup(ftwbuf->path, ftwbuf->nameoff);
if (at_path && dstrncat(&at_path, path, max) != 0) {
ret = -1;
@@ -1390,3 +1405,14 @@ int cfprintf(CFILE *cfile, const char *format, ...) {
va_end(args);
return ret;
}
+
+int cfreset(CFILE *cfile) {
+ const struct colors *colors = cfile->colors;
+ if (!colors) {
+ return 0;
+ }
+
+ const struct esc_seq *esc = colors->endcode;
+ size_t ret = xwrite(cfile->fd, esc->seq, esc->len);
+ return ret == esc->len ? 0 : -1;
+}
diff --git a/src/color.h b/src/color.h
index 3278cd6..3550888 100644
--- a/src/color.h
+++ b/src/color.h
@@ -42,6 +42,8 @@ typedef struct CFILE {
const struct colors *colors;
/** A buffer for colored formatting. */
dchar *buffer;
+ /** Cached file descriptor number. */
+ int fd;
/** Whether the next ${rs} is actually necessary. */
bool need_reset;
/** Whether to close the underlying stream. */
@@ -108,4 +110,9 @@ int cfprintf(CFILE *cfile, const char *format, ...);
attr(printf(2, 0))
int cvfprintf(CFILE *cfile, const char *format, va_list args);
+/**
+ * Reset the TTY state when terminating abnormally (async-signal-safe).
+ */
+int cfreset(CFILE *cfile);
+
#endif // BFS_COLOR_H
diff --git a/src/ctx.c b/src/ctx.c
index f5b28c7..11531df 100644
--- a/src/ctx.c
+++ b/src/ctx.c
@@ -3,24 +3,27 @@
#include "ctx.h"
#include "alloc.h"
+#include "bfstd.h"
#include "color.h"
#include "diag.h"
#include "expr.h"
#include "list.h"
#include "mtab.h"
#include "pwcache.h"
+#include "sighook.h"
#include "stat.h"
#include "trie.h"
#include "xtime.h"
#include <errno.h>
#include <limits.h>
+#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/** Get the initial value for ctx->threads (-j). */
static int bfs_nproc(void) {
- long nproc = sysconf(_SC_NPROCESSORS_ONLN);
+ long nproc = xsysconf(_SC_NPROCESSORS_ONLN);
if (nproc < 1) {
nproc = 1;
@@ -98,13 +101,20 @@ struct bfs_ctx_file {
CFILE *cfile;
/** The path to the file (for diagnostics). */
const char *path;
+ /** Signal hook to send a reset escape sequence. */
+ struct sighook *hook;
/** Remembers I/O errors, to propagate them to the exit status. */
int error;
};
+/** Call cfreset() on a tracked file. */
+static void cfreset_hook(int sig, siginfo_t *info, void *arg) {
+ cfreset(arg);
+}
+
CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) {
struct bfs_stat sb;
- if (bfs_stat(fileno(cfile->file), NULL, 0, &sb) != 0) {
+ if (bfs_stat(cfile->fd, NULL, 0, &sb) != 0) {
return NULL;
}
@@ -124,19 +134,31 @@ CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) {
leaf->value = ctx_file = ALLOC(struct bfs_ctx_file);
if (!ctx_file) {
- trie_remove(&ctx->files, leaf);
- return NULL;
+ goto fail;
}
ctx_file->cfile = cfile;
ctx_file->path = path;
ctx_file->error = 0;
+ ctx_file->hook = NULL;
+
+ if (cfile->colors) {
+ ctx_file->hook = atsigexit(cfreset_hook, cfile);
+ if (!ctx_file->hook) {
+ goto fail;
+ }
+ }
if (cfile != ctx->cout && cfile != ctx->cerr) {
++ctx->nfiles;
}
return cfile;
+
+fail:
+ trie_remove(&ctx->files, leaf);
+ free(ctx_file);
+ return NULL;
}
void bfs_ctx_flush(const struct bfs_ctx *ctx) {
@@ -156,7 +178,7 @@ void bfs_ctx_flush(const struct bfs_ctx *ctx) {
const char *path = ctx_file->path;
if (path) {
- bfs_error(ctx, "'%s': %m.\n", path);
+ bfs_error(ctx, "%pq: %m.\n", path);
} else if (cfile == ctx->cout) {
bfs_error(ctx, "(standard output): %m.\n");
}
@@ -188,30 +210,49 @@ static int bfs_ctx_fflush(CFILE *cfile) {
static int bfs_ctx_fclose(struct bfs_ctx *ctx, struct bfs_ctx_file *ctx_file) {
CFILE *cfile = ctx_file->cfile;
- if (cfile == ctx->cout) {
- // Will be checked later
- return 0;
- } else if (cfile == ctx->cerr) {
- // Writes to stderr are allowed to fail silently, unless the same file was used by
- // -fprint, -fls, etc.
- if (ctx_file->path) {
- return bfs_ctx_fflush(cfile);
- } else {
- return 0;
- }
- }
-
+ // Writes to stderr are allowed to fail silently, unless the same file
+ // was used by -fprint, -fls, etc.
+ bool silent = cfile == ctx->cerr && !ctx_file->path;
int ret = 0, error = 0;
- if (ferror(cfile->file)) {
+
+ if (ctx_file->error) {
+ // An error was previously reported during bfs_ctx_flush()
ret = -1;
- error = EIO;
+ error = ctx_file->error;
}
- if (cfclose(cfile) != 0) {
+
+ // Flush the file just before we remove the hook, to maximize the chance
+ // we leave the TTY in a good state
+ if (bfs_ctx_fflush(cfile) != 0) {
ret = -1;
error = errno;
}
- errno = error;
+ if (ctx_file->hook) {
+ sigunhook(ctx_file->hook);
+ }
+
+ // Close the CFILE, except for stdio streams, which are closed later
+ if (cfile != ctx->cout && cfile != ctx->cerr) {
+ if (cfclose(cfile) != 0) {
+ ret = -1;
+ error = errno;
+ }
+ }
+
+ if (silent) {
+ ret = 0;
+ }
+
+ if (ret != 0 && ctx->cerr) {
+ if (ctx_file->path) {
+ bfs_error(ctx, "%pq: %s.\n", ctx_file->path, xstrerror(error));
+ } else if (cfile == ctx->cout) {
+ bfs_error(ctx, "(standard output): %s.\n", xstrerror(error));
+ }
+ }
+
+ free(ctx_file);
return ret;
}
@@ -229,33 +270,14 @@ int bfs_ctx_free(struct bfs_ctx *ctx) {
for_trie (leaf, &ctx->files) {
struct bfs_ctx_file *ctx_file = leaf->value;
-
- if (ctx_file->error) {
- // An error was previously reported during bfs_ctx_flush()
- ret = -1;
- }
-
if (bfs_ctx_fclose(ctx, ctx_file) != 0) {
- if (cerr) {
- bfs_error(ctx, "'%s': %m.\n", ctx_file->path);
- }
ret = -1;
}
-
- free(ctx_file);
}
trie_destroy(&ctx->files);
- if (cout && bfs_ctx_fflush(cout) != 0) {
- if (cerr) {
- bfs_error(ctx, "(standard output): %m.\n");
- }
- ret = -1;
- }
-
cfclose(cout);
cfclose(cerr);
-
free_colors(ctx->colors);
for_slist (struct bfs_expr, expr, &ctx->expr_list, freelist) {
diff --git a/src/diag.c b/src/diag.c
index deb6f26..bb744f6 100644
--- a/src/diag.c
+++ b/src/diag.c
@@ -38,6 +38,10 @@ noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...) {
abort();
}
+const char *bfs_errstr(void) {
+ return xstrerror(errno);
+}
+
const char *debug_flag_name(enum debug_flags flag) {
switch (flag) {
case DEBUG_COST:
diff --git a/src/diag.h b/src/diag.h
index 2b13609..f2498b5 100644
--- a/src/diag.h
+++ b/src/diag.h
@@ -55,6 +55,20 @@ void bfs_diagf(const struct bfs_loc *loc, const char *format, ...);
#define bfs_diag(...) bfs_diagf(bfs_location(), __VA_ARGS__)
/**
+ * Get the last error message.
+ */
+const char *bfs_errstr(void);
+
+/**
+ * Print a diagnostic message including the last error.
+ */
+#define bfs_ediag(...) \
+ bfs_ediag_("" __VA_ARGS__, bfs_errstr())
+
+#define bfs_ediag_(format, ...) \
+ bfs_diag(sizeof(format) > 1 ? format ": %s" : "%s", __VA_ARGS__)
+
+/**
* Print a message to standard error and abort.
*/
attr(cold, printf(2, 3))
@@ -63,15 +77,27 @@ noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...);
/**
* Unconditional abort with a message.
*/
-#define bfs_abort(...) bfs_abortf(bfs_location(), __VA_ARGS__)
+#define bfs_abort(...) \
+ bfs_abortf(bfs_location(), __VA_ARGS__)
+
+/**
+ * Abort with a message including the last error.
+ */
+#define bfs_eabort(...) \
+ bfs_eabort_("" __VA_ARGS__, bfs_errstr())
+
+#define bfs_eabort_(format, ...) \
+ bfs_abort(sizeof(format) > 1 ? format ": %s" : "%s", __VA_ARGS__)
/**
* Abort in debug builds; no-op in release builds.
*/
#ifdef NDEBUG
# define bfs_bug(...) ((void)0)
+# define bfs_ebug(...) ((void)0)
#else
# define bfs_bug bfs_abort
+# define bfs_ebug bfs_eabort
#endif
/**
@@ -88,12 +114,27 @@ noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...);
str, __VA_ARGS__))
/**
+ * Unconditional assert, including the last error.
+ */
+#define bfs_everify(...) \
+ bfs_everify_(#__VA_ARGS__, __VA_ARGS__, "", bfs_errstr())
+
+#define bfs_everify_(str, cond, format, ...) \
+ ((cond) ? (void)0 : bfs_abort( \
+ sizeof(format) > 1 \
+ ? "%.0s" format "%s: %s" \
+ : "Assertion failed: `%s`: %s", \
+ str, __VA_ARGS__))
+
+/**
* Assert in debug builds; no-op in release builds.
*/
#ifdef NDEBUG
# define bfs_assert(...) ((void)0)
+# define bfs_eassert(...) ((void)0)
#else
# define bfs_assert bfs_verify
+# define bfs_eassert bfs_everify
#endif
struct bfs_ctx;
diff --git a/src/dir.c b/src/dir.c
index cfbbca4..fadf1c0 100644
--- a/src/dir.c
+++ b/src/dir.c
@@ -25,7 +25,13 @@
static ssize_t bfs_getdents(int fd, void *buf, size_t size) {
sanitize_uninit(buf, size);
-#if BFS_HAS_GETDENTS
+#if BFS_HAS_POSIX_GETDENTS
+ int flags = 0;
+# ifdef DT_FORCE_TYPE
+ flags |= DT_FORCE_TYPE;
+# endif
+ ssize_t ret = posix_getdents(fd, buf, size, flags);
+#elif BFS_HAS_GETDENTS
ssize_t ret = getdents(fd, buf, size);
#elif BFS_HAS_GETDENTS64
ssize_t ret = getdents64(fd, buf, size);
@@ -44,11 +50,13 @@ static ssize_t bfs_getdents(int fd, void *buf, size_t size) {
#endif // BFS_USE_GETDENTS
-#if BFS_USE_GETDENTS && !BFS_HAS_GETDENTS
/** Directory entry type for bfs_getdents() */
-typedef struct dirent64 sys_dirent;
-#else
+#if !BFS_USE_GETDENTS || BFS_HAS_GETDENTS
typedef struct dirent sys_dirent;
+#elif BFS_HAS_POSIX_GETDENTS
+typedef struct posix_dent sys_dirent;
+#else
+typedef struct dirent64 sys_dirent;
#endif
enum bfs_type bfs_mode_to_type(mode_t mode) {
diff --git a/src/dir.h b/src/dir.h
index bc6c4ed..bbba071 100644
--- a/src/dir.h
+++ b/src/dir.h
@@ -8,14 +8,19 @@
#ifndef BFS_DIR_H
#define BFS_DIR_H
+#include "prelude.h"
#include <sys/types.h>
/**
* Whether the implementation uses the getdents() syscall directly, rather than
* libc's readdir().
*/
-#if !defined(BFS_USE_GETDENTS) && (__linux__ || __FreeBSD__)
-# define BFS_USE_GETDENTS (BFS_HAS_GETDENTS || BFS_HAS_GETDENTS64 | BFS_HAS_GETDENTS64_SYSCALL)
+#ifndef BFS_USE_GETDENTS
+# if BFS_HAS_POSIX_GETDENTS
+# define BFS_USE_GETDENTS true
+# elif __linux__ || __FreeBSD__
+# define BFS_USE_GETDENTS (BFS_HAS_GETDENTS || BFS_HAS_GETDENTS64 | BFS_HAS_GETDENTS64_SYSCALL)
+# endif
#endif
/**
diff --git a/src/dstring.c b/src/dstring.c
index 913dda8..86ab646 100644
--- a/src/dstring.c
+++ b/src/dstring.c
@@ -174,7 +174,7 @@ int dstrxcpy(dchar **dest, const char *src, size_t len) {
return 0;
}
-char *dstrprintf(const char *format, ...) {
+dchar *dstrprintf(const char *format, ...) {
va_list args;
va_start(args, format);
@@ -184,7 +184,7 @@ char *dstrprintf(const char *format, ...) {
return str;
}
-char *dstrvprintf(const char *format, va_list args) {
+dchar *dstrvprintf(const char *format, va_list args) {
// Guess a capacity to try to avoid reallocating
dchar *str = dstralloc(2 * strlen(format));
if (!str) {
@@ -245,6 +245,7 @@ int dstrvcatf(dchar **str, const char *format, va_list args) {
return 0;
fail:
+ va_end(copy);
*tail = '\0';
return -1;
}
diff --git a/src/dstring.h b/src/dstring.h
index 9ea7eb9..14e1d3e 100644
--- a/src/dstring.h
+++ b/src/dstring.h
@@ -244,7 +244,7 @@ int dstrxcpy(dchar **dest, const char *str, size_t len);
* The created string, or NULL on failure.
*/
attr(printf(1, 2))
-char *dstrprintf(const char *format, ...);
+dchar *dstrprintf(const char *format, ...);
/**
* Create a dynamic string from a format string and a va_list.
@@ -257,7 +257,7 @@ char *dstrprintf(const char *format, ...);
* The created string, or NULL on failure.
*/
attr(printf(1, 0))
-char *dstrvprintf(const char *format, va_list args);
+dchar *dstrvprintf(const char *format, va_list args);
/**
* Format some text onto the end of a dynamic string.
diff --git a/src/eval.c b/src/eval.c
index b103912..4fcda60 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -445,38 +445,42 @@ bool eval_depth(const struct bfs_expr *expr, struct bfs_eval *state) {
* -empty test.
*/
bool eval_empty(const struct bfs_expr *expr, struct bfs_eval *state) {
- bool ret = false;
const struct BFTW *ftwbuf = state->ftwbuf;
+ const struct bfs_stat *statbuf;
+ struct bfs_dir *dir;
+
+ switch (ftwbuf->type) {
+ case BFS_REG:
+ statbuf = eval_stat(state);
+ return statbuf && statbuf->size == 0;
- if (ftwbuf->type == BFS_DIR) {
- struct bfs_dir *dir = bfs_allocdir();
+ case BFS_DIR:
+ dir = bfs_allocdir();
if (!dir) {
- eval_report_error(state);
- return ret;
+ goto error;
}
if (bfs_opendir(dir, ftwbuf->at_fd, ftwbuf->at_path, 0) != 0) {
- eval_report_error(state);
- return ret;
+ goto error;
}
int did_read = bfs_readdir(dir, NULL);
+ bfs_closedir(dir);
+
if (did_read < 0) {
- eval_report_error(state);
- } else {
- ret = !did_read;
+ goto error;
}
- bfs_closedir(dir);
free(dir);
- } else if (ftwbuf->type == BFS_REG) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (statbuf) {
- ret = statbuf->size == 0;
- }
- }
+ return did_read == 0;
+ error:
+ eval_report_error(state);
+ free(dir);
+ return false;
- return ret;
+ default:
+ return false;
+ }
}
/**
@@ -1266,7 +1270,7 @@ static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, enu
bfs_debug_prefix(ctx, DEBUG_STAT);
fprintf(stderr, "bfs_stat(");
- if (ftwbuf->at_fd == AT_FDCWD) {
+ if (ftwbuf->at_fd == (int)AT_FDCWD) {
fprintf(stderr, "AT_FDCWD");
} else {
size_t baselen = strlen(ftwbuf->path) - strlen(ftwbuf->at_path);
diff --git a/src/exec.c b/src/exec.c
index e782d49..cd73d6c 100644
--- a/src/exec.c
+++ b/src/exec.c
@@ -56,7 +56,7 @@ static size_t bfs_exec_arg_size(const char *arg) {
/** Determine the maximum argv size. */
static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) {
- long arg_max = sysconf(_SC_ARG_MAX);
+ long arg_max = xsysconf(_SC_ARG_MAX);
bfs_exec_debug(execbuf, "ARG_MAX: %ld according to sysconf()\n", arg_max);
if (arg_max < 0) {
arg_max = BFS_EXEC_ARG_MAX;
@@ -82,7 +82,7 @@ static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) {
// Assume arguments are counted with the granularity of a single page,
// so allow a one page cushion to account for rounding up
- long page_size = sysconf(_SC_PAGESIZE);
+ long page_size = xsysconf(_SC_PAGESIZE);
if (page_size < 4096) {
page_size = 4096;
}
@@ -268,7 +268,7 @@ static int bfs_exec_openwd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf)
bfs_assert(execbuf->wd_fd < 0);
bfs_assert(!execbuf->wd_path);
- if (ftwbuf->at_fd != AT_FDCWD) {
+ if (ftwbuf->at_fd != (int)AT_FDCWD) {
// Rely on at_fd being the immediate parent
bfs_assert(xbaseoff(ftwbuf->at_path) == 0);
diff --git a/src/fsade.c b/src/fsade.c
index 51b629f..7310141 100644
--- a/src/fsade.c
+++ b/src/fsade.c
@@ -41,7 +41,7 @@ static const char *fake_at(const struct BFTW *ftwbuf) {
static atomic int proc_works = -1;
dchar *path = NULL;
- if (ftwbuf->at_fd == AT_FDCWD || load(&proc_works, relaxed) == 0) {
+ if (ftwbuf->at_fd == (int)AT_FDCWD || load(&proc_works, relaxed) == 0) {
goto fail;
}
@@ -121,11 +121,23 @@ static bool is_absence_error(int error) {
#if BFS_CAN_CHECK_ACL
+#if BFS_HAS_ACL_GET_FILE
+
/** Unified interface for incompatible acl_get_entry() implementations. */
static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) {
-#if __DragonFly__ && !defined(ACL_FIRST_ENTRY) && !defined(ACL_NEXT_ENTRY)
-# define ACL_FIRST_ENTRY 0
-# define ACL_NEXT_ENTRY 1
+#if BFS_HAS_ACL_GET_ENTRY
+ int ret = acl_get_entry(acl, which, entry);
+# if __APPLE__
+ // POSIX.1e specifies a return value of 1 for success, but macOS returns 0 instead
+ return !ret;
+# else
+ return ret;
+# endif
+#elif __DragonFly__
+# if !defined(ACL_FIRST_ENTRY) && !defined(ACL_NEXT_ENTRY)
+# define ACL_FIRST_ENTRY 0
+# define ACL_NEXT_ENTRY 1
+# endif
switch (which) {
case ACL_FIRST_ENTRY:
@@ -142,24 +154,22 @@ static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) {
acl_entry_t last = &acl->acl_entry[acl->acl_cnt];
return *entry == last;
#else
- int ret = acl_get_entry(acl, which, entry);
-# if __APPLE__
- // POSIX.1e specifies a return value of 1 for success, but macOS returns 0 instead
- return !ret;
-# else
- return ret;
-# endif
+ errno = ENOTSUP;
+ return -1;
#endif
}
/** Unified interface for acl_get_tag_type(). */
attr(maybe_unused)
static int bfs_acl_tag_type(acl_entry_t entry, acl_tag_t *tag) {
-#if __DragonFly__
+#if BFS_HAS_ACL_GET_TAG_TYPE
+ return acl_get_tag_type(entry, tag);
+#elif __DragonFly__
*tag = entry->ae_tag;
return 0;
#else
- return acl_get_tag_type(entry, tag);
+ errno = ENOTSUP;
+ return -1;
#endif
}
@@ -218,29 +228,35 @@ static int bfs_check_acl_type(acl_t acl, acl_type_t type) {
#endif
}
+#endif // BFS_HAS_ACL_GET_FILE
+
int bfs_check_acl(const struct BFTW *ftwbuf) {
+ if (ftwbuf->type == BFS_LNK) {
+ return 0;
+ }
+
+ const char *path = fake_at(ftwbuf);
+
+#if BFS_HAS_ACL_TRIVIAL
+ int ret = acl_trivial(path);
+ int error = errno;
+#elif BFS_HAS_ACL_GET_FILE
static const acl_type_t acl_types[] = {
-#if __APPLE__
+# if __APPLE__
// macOS gives EINVAL for either of the two standard ACL types,
// supporting only ACL_TYPE_EXTENDED
ACL_TYPE_EXTENDED,
-#else
+# else
// The two standard POSIX.1e ACL types
ACL_TYPE_ACCESS,
ACL_TYPE_DEFAULT,
-#endif
+# endif
-#ifdef ACL_TYPE_NFS4
+# ifdef ACL_TYPE_NFS4
ACL_TYPE_NFS4,
-#endif
+# endif
};
- if (ftwbuf->type == BFS_LNK) {
- return 0;
- }
-
- const char *path = fake_at(ftwbuf);
-
int ret = -1, error = 0;
for (size_t i = 0; i < countof(acl_types) && ret <= 0; ++i) {
acl_type_t type = acl_types[i];
@@ -264,6 +280,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf) {
error = errno;
acl_free(acl);
}
+#endif
free_fake_at(ftwbuf, path);
errno = error;
@@ -327,17 +344,62 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf) {
#if BFS_CAN_CHECK_XATTRS
+#if BFS_USE_SYS_EXTATTR_H
+
+/** Wrapper for extattr_list_{file,link}. */
+static ssize_t bfs_extattr_list(const char *path, enum bfs_type type, int namespace) {
+ if (type == BFS_LNK) {
+#if BFS_HAS_EXTATTR_LIST_LINK
+ return extattr_list_link(path, namespace, NULL, 0);
+#elif BFS_HAS_EXTATTR_GET_LINK
+ return extattr_get_link(path, namespace, "", NULL, 0);
+#else
+ return 0;
+#endif
+ }
+
+#if BFS_HAS_EXTATTR_LIST_FILE
+ return extattr_list_file(path, namespace, NULL, 0);
+#elif BFS_HAS_EXTATTR_GET_FILE
+ // From man extattr(2):
+ //
+ // In earlier versions of this API, passing an empty string for the
+ // attribute name to extattr_get_file() would return the list of attributes
+ // defined for the target object. This interface has been deprecated in
+ // preference to using the explicit list API, and should not be used.
+ return extattr_get_file(path, namespace, "", NULL, 0);
+#else
+ return 0;
+#endif
+}
+
+/** Wrapper for extattr_get_{file,link}. */
+static ssize_t bfs_extattr_get(const char *path, enum bfs_type type, int namespace, const char *name) {
+ if (type == BFS_LNK) {
+#if BFS_HAS_EXTATTR_GET_LINK
+ return extattr_get_link(path, namespace, name, NULL, 0);
+#else
+ return 0;
+#endif
+ }
+
+#if BFS_HAS_EXTATTR_GET_FILE
+ return extattr_get_file(path, namespace, name, NULL, 0);
+#else
+ return 0;
+#endif
+}
+
+#endif // BFS_USE_SYS_EXTATTR_H
+
int bfs_check_xattrs(const struct BFTW *ftwbuf) {
const char *path = fake_at(ftwbuf);
ssize_t len;
#if BFS_USE_SYS_EXTATTR_H
- ssize_t (*extattr_list)(const char *, int, void *, size_t) =
- ftwbuf->type == BFS_LNK ? extattr_list_link : extattr_list_file;
-
- len = extattr_list(path, EXTATTR_NAMESPACE_SYSTEM, NULL, 0);
+ len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM);
if (len <= 0) {
- len = extattr_list(path, EXTATTR_NAMESPACE_USER, NULL, 0);
+ len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_USER);
}
#elif __APPLE__
int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0;
@@ -371,12 +433,9 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) {
ssize_t len;
#if BFS_USE_SYS_EXTATTR_H
- ssize_t (*extattr_get)(const char *, int, const char *, void *, size_t) =
- ftwbuf->type == BFS_LNK ? extattr_get_link : extattr_get_file;
-
- len = extattr_get(path, EXTATTR_NAMESPACE_SYSTEM, name, NULL, 0);
+ len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM, name);
if (len < 0) {
- len = extattr_get(path, EXTATTR_NAMESPACE_USER, name, NULL, 0);
+ len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_USER, name);
}
#elif __APPLE__
int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0;
diff --git a/src/fsade.h b/src/fsade.h
index 6300852..eefef9f 100644
--- a/src/fsade.h
+++ b/src/fsade.h
@@ -11,14 +11,9 @@
#include "prelude.h"
-#define BFS_CAN_CHECK_ACL BFS_USE_SYS_ACL_H
+#define BFS_CAN_CHECK_ACL (BFS_HAS_ACL_GET_FILE || BFS_HAS_ACL_TRIVIAL)
-#if !defined(BFS_CAN_CHECK_CAPABILITIES) && BFS_USE_SYS_CAPABILITY_H
-# include <sys/capability.h>
-# ifdef CAP_CHOWN
-# define BFS_CAN_CHECK_CAPABILITIES true
-# endif
-#endif
+#define BFS_CAN_CHECK_CAPABILITIES BFS_USE_LIBCAP
#define BFS_CAN_CHECK_CONTEXT BFS_USE_LIBSELINUX
diff --git a/src/ioq.c b/src/ioq.c
index 189bdac..43a1b35 100644
--- a/src/ioq.c
+++ b/src/ioq.c
@@ -918,17 +918,14 @@ static void ioq_thread_join(struct ioq_thread *thread) {
}
struct ioq *ioq_create(size_t depth, size_t nthreads) {
- struct ioq *ioq = ALLOC_FLEX(struct ioq, threads, nthreads);
+ struct ioq *ioq = ZALLOC_FLEX(struct ioq, threads, nthreads);
if (!ioq) {
goto fail;
}
ioq->depth = depth;
- ioq->size = 0;
- ioq->cancel = false;
ARENA_INIT(&ioq->ents, struct ioq_ent);
-
#if BFS_USE_LIBURING && BFS_USE_STATX
ARENA_INIT(&ioq->xbufs, struct statx);
#endif
diff --git a/src/main.c b/src/main.c
index 9d8b206..5dd88e4 100644
--- a/src/main.c
+++ b/src/main.c
@@ -36,6 +36,7 @@
* - mtab.[ch] (parses the system's mount table)
* - pwcache.[ch] (a cache for the user/group tables)
* - sanity.h (sanitizer interfaces)
+ * - sighook.[ch] (signal hooks)
* - stat.[ch] (wraps stat(), or statx() on Linux)
* - thread.h (multi-threading)
* - trie.[ch] (a trie set/map implementation)
diff --git a/src/mtab.c b/src/mtab.c
index 7905d14..0377fea 100644
--- a/src/mtab.c
+++ b/src/mtab.c
@@ -13,11 +13,11 @@
#include <string.h>
#include <sys/types.h>
-#if !defined(BFS_USE_MNTENT) && BFS_USE_MNTENT_H
+#if !defined(BFS_USE_MNTENT) && BFS_HAS_GETMNTENT_1
# define BFS_USE_MNTENT true
-#elif !defined(BFS_USE_MNTINFO) && BSD
+#elif !defined(BFS_USE_MNTINFO) && BFS_HAS_GETMNTINFO
# define BFS_USE_MNTINFO true
-#elif !defined(BFS_USE_MNTTAB) && __SVR4
+#elif !defined(BFS_USE_MNTTAB) && BFS_HAS_GETMNTENT_2
# define BFS_USE_MNTTAB true
#endif
@@ -27,7 +27,6 @@
# include <stdio.h>
#elif BFS_USE_MNTINFO
# include <sys/mount.h>
-# include <sys/ucred.h>
#elif BFS_USE_MNTTAB
# include <stdio.h>
# include <sys/mnttab.h>
@@ -148,7 +147,7 @@ struct bfs_mtab *bfs_mtab_parse(void) {
bfs_statfs *mntbuf;
int size = getmntinfo(&mntbuf, MNT_WAIT);
- if (size < 0) {
+ if (size <= 0) {
error = errno;
goto fail;
}
diff --git a/src/opt.c b/src/opt.c
index ffc795b..883d598 100644
--- a/src/opt.c
+++ b/src/opt.c
@@ -947,8 +947,12 @@ static struct bfs_expr *visit_shallow(struct bfs_opt *opt, struct bfs_expr *expr
expr = general(opt, expr, visitor);
}
+ if (!expr) {
+ return NULL;
+ }
+
visit_fn *specific = look_up_visitor(expr, visitor->table);
- if (expr && specific) {
+ if (specific) {
expr = specific(opt, expr, visitor);
}
diff --git a/src/prelude.h b/src/prelude.h
index a23b167..72f88b0 100644
--- a/src/prelude.h
+++ b/src/prelude.h
@@ -49,12 +49,6 @@ extern const char bfs_version[];
#if __has_include(<paths.h>)
# define BFS_HAS_PATHS_H true
#endif
-#if __has_include(<sys/acl.h>)
-# define BFS_HAS_SYS_ACL_H true
-#endif
-#if __has_include(<sys/capability.h>)
-# define BFS_HAS_SYS_CAPABILITY_H true
-#endif
#if __has_include(<sys/extattr.h>)
# define BFS_HAS_SYS_EXTATTR_H true
#endif
@@ -81,8 +75,6 @@ extern const char bfs_version[];
#define BFS_HAS_MNTENT_H __GLIBC__
#define BFS_HAS_PATHS_H true
-#define BFS_HAS_SYS_ACL_H true
-#define BFS_HAS_SYS_CAPABILITY_H __linux__
#define BFS_HAS_SYS_EXTATTR_H __FreeBSD__
#define BFS_HAS_SYS_MKDEV_H false
#define BFS_HAS_SYS_PARAM_H true
@@ -99,14 +91,8 @@ extern const char bfs_version[];
#ifndef BFS_USE_PATHS_H
# define BFS_USE_PATHS_H BFS_HAS_PATHS_H
#endif
-#ifndef BFS_USE_SYS_ACL_H
-# define BFS_USE_SYS_ACL_H (BFS_HAS_SYS_ACL_H && !__illumos__ && (!__linux__ || BFS_USE_LIBACL))
-#endif
-#ifndef BFS_USE_SYS_CAPABILITY_H
-# define BFS_USE_SYS_CAPABILITY_H (BFS_HAS_SYS_CAPABILITY_H && !__FreeBSD__ && (!__linux__ || BFS_USE_LIBCAP))
-#endif
#ifndef BFS_USE_SYS_EXTATTR_H
-# define BFS_USE_SYS_EXTATTR_H (BFS_HAS_SYS_EXTATTR_H && !__DragonFly__)
+# define BFS_USE_SYS_EXTATTR_H BFS_HAS_SYS_EXTATTR_H
#endif
#ifndef BFS_USE_SYS_MKDEV_H
# define BFS_USE_SYS_MKDEV_H BFS_HAS_SYS_MKDEV_H
@@ -141,21 +127,6 @@ extern const char bfs_version[];
# define __has_attribute(attr) false
#endif
-// Platform detection
-
-// Get the definition of BSD if available
-#if BFS_USE_SYS_PARAM_H
-# include <sys/param.h>
-#endif
-
-#ifndef __GLIBC_PREREQ
-# define __GLIBC_PREREQ(maj, min) false
-#endif
-
-#ifndef __NetBSD_Prereq__
-# define __NetBSD_Prereq__(maj, min, patch) false
-#endif
-
// Fundamental utilities
/**
@@ -182,14 +153,25 @@ extern const char bfs_version[];
#endif
/**
+ * Polyfill max_align_t if we don't already have it.
+ */
+#if !BFS_HAS_MAX_ALIGN_T
+typedef union {
+# ifdef __BIGGEST_ALIGNMENT__
+ alignas(__BIGGEST_ALIGNMENT__) char c;
+# else
+ long double ld;
+ long long ll;
+ void *ptr;
+# endif
+} max_align_t;
+#endif
+
+/**
* Alignment specifier that avoids false sharing.
*/
#define cache_align alignas(FALSE_SHARING_SIZE)
-#if __COSMOPOLITAN__
-typedef long double max_align_t;
-#endif
-
// Wrappers for attributes
/**
@@ -247,6 +229,15 @@ typedef long double max_align_t;
#endif
/**
+ * Annotates functions that potentially modify and return format strings.
+ */
+#if __has_attribute(format_arg)
+# define attr_format_arg(arg) __attribute__((format_arg(arg)))
+#else
+# define attr_format_arg(args)
+#endif
+
+/**
* Annotates allocator-like functions.
*/
#if __has_attribute(malloc)
diff --git a/src/printf.c b/src/printf.c
index 4df399b..f8428f7 100644
--- a/src/printf.c
+++ b/src/printf.c
@@ -88,22 +88,20 @@ static bool should_color(CFILE *cfile, const struct bfs_fmt *fmt) {
bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf)); \
(void)ret
-/**
- * Common entry point for fprintf() with a dynamic format string.
- */
-static int dyn_fprintf(FILE *file, const struct bfs_fmt *fmt, ...) {
- va_list args;
- va_start(args, fmt);
-
-#if __GNUC__
-# pragma GCC diagnostic push
-# pragma GCC diagnostic ignored "-Wformat-nonliteral"
-#endif
- int ret = vfprintf(file, fmt->str, args);
-#if __GNUC__
-# pragma GCC diagnostic pop
-#endif
+/** Return a dynamic format string. */
+attr(format_arg(2))
+static const char *dyn_fmt(const char *str, const char *fake) {
+ bfs_assert(strcmp(str + strlen(str) - strlen(fake) + 1, fake + 1) == 0,
+ "Mismatched format specifiers: '%s' vs. '%s'", str, fake);
+ return str;
+}
+/** Wrapper for fprintf(). */
+attr(printf(3, 4))
+static int bfs_fprintf(CFILE *cfile, const struct bfs_fmt *fmt, const char *fake, ...) {
+ va_list args;
+ va_start(args, fake);
+ int ret = vfprintf(cfile->file, dyn_fmt(fmt->str, fake), args);
va_end(args);
return ret;
}
@@ -139,7 +137,7 @@ static int bfs_printf_ctime(CFILE *cfile, const struct bfs_fmt *fmt, const struc
(long)ts->tv_nsec,
1900 + tm.tm_year);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %A, %B/%W, %C, %T: strftime() */
@@ -214,7 +212,7 @@ static int bfs_printf_strftime(CFILE *cfile, const struct bfs_fmt *fmt, const st
bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf));
(void)ret;
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %b: blocks */
@@ -226,12 +224,12 @@ static int bfs_printf_b(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 511) / 512;
BFS_PRINTF_BUF(buf, "%ju", blocks);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %d: depth */
static int bfs_printf_d(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
- return dyn_fprintf(cfile->file, fmt, (intmax_t)ftwbuf->depth);
+ return bfs_fprintf(cfile, fmt, "%jd", (intmax_t)ftwbuf->depth);
}
/** %D: device */
@@ -242,7 +240,7 @@ static int bfs_printf_D(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->dev);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %f: file name */
@@ -250,7 +248,7 @@ static int bfs_printf_f(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
if (should_color(cfile, fmt)) {
return cfprintf(cfile, "%pF", ftwbuf);
} else {
- return dyn_fprintf(cfile->file, fmt, ftwbuf->path + ftwbuf->nameoff);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + ftwbuf->nameoff);
}
}
@@ -266,7 +264,7 @@ static int bfs_printf_F(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return -1;
}
- return dyn_fprintf(cfile->file, fmt, type);
+ return bfs_fprintf(cfile, fmt, "%s", type);
}
/** %G: gid */
@@ -277,7 +275,7 @@ static int bfs_printf_G(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->gid);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %g: group name */
@@ -293,7 +291,7 @@ static int bfs_printf_g(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return bfs_printf_G(cfile, fmt, ftwbuf);
}
- return dyn_fprintf(cfile->file, fmt, grp->gr_name);
+ return bfs_fprintf(cfile, fmt, "%s", grp->gr_name);
}
/** %h: leading directories */
@@ -322,7 +320,7 @@ static int bfs_printf_h(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
if (should_color(cfile, fmt)) {
ret = cfprintf(cfile, "${di}%pQ${rs}", buf);
} else {
- ret = dyn_fprintf(cfile->file, fmt, buf);
+ ret = bfs_fprintf(cfile, fmt, "%s", buf);
}
free(copy);
@@ -338,7 +336,7 @@ static int bfs_printf_H(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return cfprintf(cfile, "${di}%pQ${rs}", ftwbuf->root);
}
} else {
- return dyn_fprintf(cfile->file, fmt, ftwbuf->root);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->root);
}
}
@@ -350,7 +348,7 @@ static int bfs_printf_i(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->ino);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %k: 1K blocks */
@@ -362,7 +360,7 @@ static int bfs_printf_k(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 1023) / 1024;
BFS_PRINTF_BUF(buf, "%ju", blocks);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %l: link target */
@@ -384,7 +382,7 @@ static int bfs_printf_l(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
}
- int ret = dyn_fprintf(cfile->file, fmt, target);
+ int ret = bfs_fprintf(cfile, fmt, "%s", target);
free(buf);
return ret;
}
@@ -396,7 +394,7 @@ static int bfs_printf_m(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return -1;
}
- return dyn_fprintf(cfile->file, fmt, (unsigned int)(statbuf->mode & 07777));
+ return bfs_fprintf(cfile, fmt, "%o", (unsigned int)(statbuf->mode & 07777));
}
/** %M: symbolic mode */
@@ -408,7 +406,7 @@ static int bfs_printf_M(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
char buf[11];
xstrmode(statbuf->mode, buf);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %n: link count */
@@ -419,7 +417,7 @@ static int bfs_printf_n(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->nlink);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %p: full path */
@@ -427,7 +425,7 @@ static int bfs_printf_p(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
if (should_color(cfile, fmt)) {
return cfprintf(cfile, "%pP", ftwbuf);
} else {
- return dyn_fprintf(cfile->file, fmt, ftwbuf->path);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path);
}
}
@@ -448,7 +446,7 @@ static int bfs_printf_P(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
copybuf.nameoff -= offset;
return cfprintf(cfile, "%pP", &copybuf);
} else {
- return dyn_fprintf(cfile->file, fmt, ftwbuf->path + offset);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + offset);
}
}
@@ -460,7 +458,7 @@ static int bfs_printf_s(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->size);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %S: sparseness */
@@ -476,7 +474,7 @@ static int bfs_printf_S(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
} else {
sparsity = (double)BFS_STAT_BLKSIZE * statbuf->blocks / statbuf->size;
}
- return dyn_fprintf(cfile->file, fmt, sparsity);
+ return bfs_fprintf(cfile, fmt, "%g", sparsity);
}
/** %U: uid */
@@ -487,7 +485,7 @@ static int bfs_printf_U(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->uid);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %u: user name */
@@ -503,7 +501,7 @@ static int bfs_printf_u(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return bfs_printf_U(cfile, fmt, ftwbuf);
}
- return dyn_fprintf(cfile->file, fmt, pwd->pw_name);
+ return bfs_fprintf(cfile, fmt, "%s", pwd->pw_name);
}
static const char *bfs_printf_type(enum bfs_type type) {
@@ -520,10 +518,14 @@ static const char *bfs_printf_type(enum bfs_type type) {
return "p";
case BFS_LNK:
return "l";
+ case BFS_PORT:
+ return "P";
case BFS_REG:
return "f";
case BFS_SOCK:
return "s";
+ case BFS_WHT:
+ return "w";
default:
return "U";
}
@@ -532,39 +534,29 @@ static const char *bfs_printf_type(enum bfs_type type) {
/** %y: type */
static int bfs_printf_y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const char *type = bfs_printf_type(ftwbuf->type);
- return dyn_fprintf(cfile->file, fmt, type);
+ return bfs_fprintf(cfile, fmt, "%s", type);
}
/** %Y: target type */
static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
- int error = 0;
-
- if (ftwbuf->type != BFS_LNK) {
- return bfs_printf_y(cfile, fmt, ftwbuf);
- }
+ enum bfs_type type = bftw_type(ftwbuf, BFS_STAT_FOLLOW);
+ const char *str;
- const char *type = "U";
-
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, BFS_STAT_FOLLOW);
- if (statbuf) {
- type = bfs_printf_type(bfs_mode_to_type(statbuf->mode));
- } else {
- switch (errno) {
- case ELOOP:
- type = "L";
- break;
- case ENOENT:
- case ENOTDIR:
- type = "N";
- break;
- default:
- type = "?";
+ int error = 0;
+ if (type == BFS_ERROR) {
+ if (errno_is_like(ELOOP)) {
+ str = "L";
+ } else if (errno_is_like(ENOENT)) {
+ str = "N";
+ } else {
+ str = "?";
error = errno;
- break;
}
+ } else {
+ str = bfs_printf_type(type);
}
- int ret = dyn_fprintf(cfile->file, fmt, type);
+ int ret = bfs_fprintf(cfile, fmt, "%s", str);
if (error != 0) {
ret = -1;
errno = error;
@@ -580,7 +572,7 @@ static int bfs_printf_Z(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return -1;
}
- int ret = dyn_fprintf(cfile->file, fmt, con);
+ int ret = bfs_fprintf(cfile, fmt, "%s", con);
bfs_freecon(con);
return ret;
}
diff --git a/src/sighook.c b/src/sighook.c
new file mode 100644
index 0000000..ff5b96f
--- /dev/null
+++ b/src/sighook.c
@@ -0,0 +1,600 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Dynamic (un)registration of signal handlers.
+ *
+ * Because signal handlers can interrupt any thread at an arbitrary point, they
+ * must be lock-free or risk deadlock. Therefore, we implement the global table
+ * of signal "hooks" with a simple read-copy-update (RCU) scheme. Readers get a
+ * reference-counted pointer (struct arc) to the table in a lock-free way, and
+ * release the reference count when finished.
+ *
+ * Updates are managed by struct rcu, which has two slots: one active and one
+ * inactive. Readers acquire a reference to the active slot. A single writer
+ * can safely update it by initializing the inactive slot, atomically swapping
+ * the slots, and waiting for the reference count of the newly inactive slot to
+ * drop to zero. Once it does, the old pointer can be safely freed.
+ */
+
+#include "prelude.h"
+#include "sighook.h"
+#include "alloc.h"
+#include "atomic.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "thread.h"
+#include <errno.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#if _POSIX_SEMAPHORES > 0
+# include <semaphore.h>
+#endif
+
+/**
+ * An atomically reference-counted pointer.
+ */
+struct arc {
+ /** The current reference count (0 means empty). */
+ atomic size_t refs;
+ /** The reference itself. */
+ void *ptr;
+
+#if _POSIX_SEMAPHORES > 0
+ /** A semaphore for arc_wake(). */
+ sem_t sem;
+ /** sem_init() result. */
+ int sem_status;
+#endif
+};
+
+/** Initialize an arc. */
+static void arc_init(struct arc *arc) {
+ atomic_init(&arc->refs, 0);
+ arc->ptr = NULL;
+
+#if _POSIX_SEMAPHORES > 0
+ arc->sem_status = sem_init(&arc->sem, false, 0);
+#endif
+}
+
+/** Get the current refcount. */
+static size_t arc_refs(const struct arc *arc) {
+ return load(&arc->refs, relaxed);
+}
+
+/** Set the pointer in an empty arc. */
+static void arc_set(struct arc *arc, void *ptr) {
+ bfs_assert(arc_refs(arc) == 0);
+ bfs_assert(ptr);
+
+ arc->ptr = ptr;
+ store(&arc->refs, 1, release);
+}
+
+/** Acquire a reference. */
+static void *arc_get(struct arc *arc) {
+ size_t refs = arc_refs(arc);
+ do {
+ if (refs < 1) {
+ return NULL;
+ }
+ } while (!compare_exchange_weak(&arc->refs, &refs, refs + 1, acquire, relaxed));
+
+ return arc->ptr;
+}
+
+/** Release a reference. */
+static void arc_put(struct arc *arc) {
+ size_t refs = fetch_sub(&arc->refs, 1, release);
+
+ if (refs == 1) {
+#if _POSIX_SEMAPHORES > 0
+ if (arc->sem_status == 0 && sem_post(&arc->sem) != 0) {
+ abort();
+ }
+#endif
+ }
+}
+
+/** Wait on the semaphore. */
+static int arc_sem_wait(struct arc *arc) {
+#if _POSIX_SEMAPHORES > 0
+ if (arc->sem_status == 0) {
+ while (sem_wait(&arc->sem) != 0) {
+ bfs_everify(errno == EINTR, "sem_wait()");
+ }
+ return 0;
+ }
+#endif
+
+ return -1;
+}
+
+/** Wait for all references to be released. */
+static void *arc_wait(struct arc *arc) {
+ size_t refs = fetch_sub(&arc->refs, 1, relaxed);
+ bfs_assert(refs > 0);
+
+ --refs;
+ while (refs > 0) {
+ if (arc_sem_wait(arc) == 0) {
+ bfs_assert(arc_refs(arc) == 0);
+ // sem_wait() provides enough ordering, so we can skip the fence
+ goto done;
+ }
+
+ // Some platforms (like macOS) don't support unnamed semaphores,
+ // but we can always busy-wait
+ spin_loop();
+ refs = arc_refs(arc);
+ }
+
+ thread_fence(&arc->refs, acquire);
+
+done:;
+ void *ptr = arc->ptr;
+ arc->ptr = NULL;
+ return ptr;
+}
+
+/** Destroy an arc. */
+static void arc_destroy(struct arc *arc) {
+ bfs_assert(arc_refs(arc) <= 1);
+
+#if _POSIX_SEMAPHORES > 0
+ if (arc->sem_status == 0) {
+ bfs_everify(sem_destroy(&arc->sem) == 0, "sem_destroy()");
+ }
+#endif
+}
+
+/**
+ * A simple read-copy-update memory reclamation scheme.
+ */
+struct rcu {
+ /** The currently active slot. */
+ atomic size_t active;
+ /** The two slots. */
+ struct arc slots[2];
+};
+
+/** Sentinel value for RCU, since arc uses NULL already. */
+static void *RCU_NULL = &RCU_NULL;
+
+/** Initialize an RCU block. */
+static void rcu_init(struct rcu *rcu) {
+ atomic_init(&rcu->active, 0);
+ arc_init(&rcu->slots[0]);
+ arc_init(&rcu->slots[1]);
+ arc_set(&rcu->slots[0], RCU_NULL);
+}
+
+/** Get the active slot. */
+static struct arc *rcu_active(struct rcu *rcu) {
+ size_t i = load(&rcu->active, relaxed);
+ return &rcu->slots[i];
+}
+
+/** Read an RCU-protected pointer. */
+static void *rcu_read(struct rcu *rcu, struct arc **slot) {
+ while (true) {
+ *slot = rcu_active(rcu);
+ void *ptr = arc_get(*slot);
+ if (ptr == RCU_NULL) {
+ return NULL;
+ } else if (ptr) {
+ return ptr;
+ }
+ // Otherwise, the other slot became active; retry
+ }
+}
+
+/** Get the RCU-protected pointer without acquiring a reference. */
+static void *rcu_peek(struct rcu *rcu) {
+ struct arc *arc = rcu_active(rcu);
+ void *ptr = arc->ptr;
+ if (ptr == RCU_NULL) {
+ return NULL;
+ } else {
+ return ptr;
+ }
+}
+
+/** Update an RCU-protected pointer, and return the old one. */
+static void *rcu_update(struct rcu *rcu, void *ptr) {
+ size_t i = load(&rcu->active, relaxed);
+ struct arc *prev = &rcu->slots[i];
+
+ size_t j = i ^ 1;
+ struct arc *next = &rcu->slots[j];
+
+ arc_set(next, ptr ? ptr : RCU_NULL);
+ store(&rcu->active, j, relaxed);
+ return arc_wait(prev);
+}
+
+struct sighook {
+ int sig;
+ sighook_fn *fn;
+ void *arg;
+ enum sigflags flags;
+};
+
+/**
+ * A table of signal hooks.
+ */
+struct sigtable {
+ /** The number of filled slots. */
+ size_t filled;
+ /** The length of the array. */
+ size_t size;
+ /** An array of signal hooks. */
+ struct arc hooks[];
+};
+
+/** Add a hook to a table. */
+static int sigtable_add(struct sigtable *table, struct sighook *hook) {
+ if (!table || table->filled == table->size) {
+ return -1;
+ }
+
+ for (size_t i = 0; i < table->size; ++i) {
+ struct arc *arc = &table->hooks[i];
+ if (arc_refs(arc) == 0) {
+ arc_set(arc, hook);
+ ++table->filled;
+ return 0;
+ }
+ }
+
+ return -1;
+}
+
+/** Delete a hook from a table. */
+static int sigtable_del(struct sigtable *table, struct sighook *hook) {
+ for (size_t i = 0; i < table->size; ++i) {
+ struct arc *arc = &table->hooks[i];
+ if (arc->ptr == hook) {
+ arc_wait(arc);
+ --table->filled;
+ return 0;
+ }
+ }
+
+ return -1;
+}
+
+/** Create a bigger copy of a signal table. */
+static struct sigtable *sigtable_grow(struct sigtable *prev) {
+ size_t old_size = prev ? prev->size : 0;
+ size_t new_size = old_size ? 2 * old_size : 1;
+ struct sigtable *table = ALLOC_FLEX(struct sigtable, hooks, new_size);
+ if (!table) {
+ return NULL;
+ }
+
+ table->filled = 0;
+ table->size = new_size;
+ for (size_t i = 0; i < new_size; ++i) {
+ arc_init(&table->hooks[i]);
+ }
+
+ for (size_t i = 0; i < old_size; ++i) {
+ struct sighook *hook = prev->hooks[i].ptr;
+ if (hook) {
+ bfs_verify(sigtable_add(table, hook) == 0);
+ }
+ }
+
+ return table;
+}
+
+/** Free a signal table. */
+static void sigtable_free(struct sigtable *table) {
+ if (!table) {
+ return;
+ }
+
+ for (size_t i = 0; i < table->size; ++i) {
+ struct arc *arc = &table->hooks[i];
+ arc_destroy(arc);
+ }
+ free(table);
+}
+
+/** Add a hook to a signal table, growing it if necessary. */
+static int rcu_sigtable_add(struct rcu *rcu, struct sighook *hook) {
+ struct sigtable *prev = rcu_peek(rcu);
+ if (sigtable_add(prev, hook) == 0) {
+ return 0;
+ }
+
+ struct sigtable *next = sigtable_grow(prev);
+ if (!next) {
+ return -1;
+ }
+
+ bfs_verify(sigtable_add(next, hook) == 0);
+ rcu_update(rcu, next);
+ sigtable_free(prev);
+ return 0;
+}
+
+/** The global table of signal hooks. */
+static struct rcu rcu_sighooks;
+/** The global table of atsigexit() hooks. */
+static struct rcu rcu_exithooks;
+
+/** Mutex for initialization and RCU writer exclusion. */
+static pthread_mutex_t sigmutex = PTHREAD_MUTEX_INITIALIZER;
+
+/** Check if a signal was generated by userspace. */
+static bool is_user_generated(const siginfo_t *info) {
+ // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03
+ //
+ // If si_code is SI_USER or SI_QUEUE, or any value less than or
+ // equal to 0, then the signal was generated by a process ...
+ int code = info->si_code;
+ return code == SI_USER || code == SI_QUEUE || code <= 0;
+}
+
+/** Check if a signal is caused by a fault. */
+static bool is_fault(const siginfo_t *info) {
+ int sig = info->si_signo;
+ if (sig == SIGBUS || sig == SIGFPE || sig == SIGILL || sig == SIGSEGV) {
+ return !is_user_generated(info);
+ } else {
+ return false;
+ }
+}
+
+// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html
+static const int FATAL_SIGNALS[] = {
+ SIGABRT,
+ SIGALRM,
+ SIGBUS,
+ SIGFPE,
+ SIGHUP,
+ SIGILL,
+ SIGINT,
+ SIGPIPE,
+ SIGQUIT,
+ SIGSEGV,
+ SIGTERM,
+ SIGUSR1,
+ SIGUSR2,
+#ifdef SIGPOLL
+ SIGPOLL,
+#endif
+#ifdef SIGPROF
+ SIGPROF,
+#endif
+#ifdef SIGSYS
+ SIGSYS,
+#endif
+ SIGTRAP,
+#ifdef SIGVTALRM
+ SIGVTALRM,
+#endif
+ SIGXCPU,
+ SIGXFSZ,
+};
+
+/** Check if a signal's default action is to terminate the process. */
+static bool is_fatal(int sig) {
+ for (size_t i = 0; i < countof(FATAL_SIGNALS); ++i) {
+ if (sig == FATAL_SIGNALS[i]) {
+ return true;
+ }
+ }
+
+#ifdef SIGRTMIN
+ // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03
+ //
+ // The default actions for the realtime signals in the range
+ // SIGRTMIN to SIGRTMAX shall be to terminate the process
+ // abnormally.
+ if (sig >= SIGRTMIN && sig <= SIGRTMAX) {
+ return true;
+ }
+#endif
+
+ return false;
+}
+
+/** Reraise a fatal signal. */
+static noreturn void reraise(int sig) {
+ // Restore the default signal action
+ if (signal(sig, SIG_DFL) == SIG_ERR) {
+ goto fail;
+ }
+
+ // Unblock the signal, since we didn't set SA_NODEFER
+ sigset_t mask;
+ if (sigemptyset(&mask) != 0
+ || sigaddset(&mask, sig) != 0
+ || pthread_sigmask(SIG_UNBLOCK, &mask, NULL) != 0) {
+ goto fail;
+ }
+
+ raise(sig);
+fail:
+ abort();
+}
+
+/** Find any matching hooks and run them. */
+static enum sigflags run_hooks(struct rcu *rcu, int sig, siginfo_t *info) {
+ enum sigflags ret = 0;
+ struct arc *slot;
+ struct sigtable *table = rcu_read(rcu, &slot);
+ if (!table) {
+ goto done;
+ }
+
+ for (size_t i = 0; i < table->size; ++i) {
+ struct arc *arc = &table->hooks[i];
+ struct sighook *hook = arc_get(arc);
+ if (!hook) {
+ continue;
+ }
+
+ if (hook->sig == sig || hook->sig == 0) {
+ hook->fn(sig, info, hook->arg);
+ ret |= hook->flags;
+ }
+ arc_put(arc);
+ }
+
+done:
+ arc_put(slot);
+ return ret;
+}
+
+/** Dispatches a signal to the registered handlers. */
+static void sigdispatch(int sig, siginfo_t *info, void *context) {
+ // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03
+ //
+ // The behavior of a process is undefined after it returns normally
+ // from a signal-catching function for a SIGBUS, SIGFPE, SIGILL, or
+ // SIGSEGV signal that was not generated by kill(), sigqueue(), or
+ // raise().
+ if (is_fault(info)) {
+ reraise(sig);
+ }
+
+ // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03
+ //
+ // After returning from a signal-catching function, the value of
+ // errno is unspecified if the signal-catching function or any
+ // function it called assigned a value to errno and the signal-
+ // catching function did not save and restore the original value of
+ // errno.
+ int error = errno;
+
+ // Run the normal hooks
+ enum sigflags flags = run_hooks(&rcu_sighooks, sig, info);
+
+ // Run the atsigexit() hooks, if we're exiting
+ if (!(flags & SH_CONTINUE) && is_fatal(sig)) {
+ run_hooks(&rcu_exithooks, sig, info);
+ reraise(sig);
+ }
+
+ errno = error;
+}
+
+/** Make sure our signal handler is installed for a given signal. */
+static int siginit(int sig) {
+ static struct sigaction action = {
+ .sa_sigaction = sigdispatch,
+ .sa_flags = SA_RESTART | SA_SIGINFO,
+ };
+
+ static sigset_t signals;
+ static bool initialized = false;
+
+ if (!initialized) {
+ if (sigemptyset(&signals) != 0
+ || sigemptyset(&action.sa_mask) != 0) {
+ return -1;
+ }
+ rcu_init(&rcu_sighooks);
+ rcu_init(&rcu_exithooks);
+ initialized = true;
+ }
+
+ int installed = sigismember(&signals, sig);
+ if (installed < 0) {
+ return -1;
+ } else if (installed) {
+ return 0;
+ }
+
+ if (sigaction(sig, &action, NULL) != 0) {
+ return -1;
+ }
+
+ if (sigaddset(&signals, sig) != 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Shared sighook()/atsigexit() implementation. */
+static struct sighook *sighook_impl(struct rcu *rcu, int sig, sighook_fn *fn, void *arg, enum sigflags flags) {
+ struct sighook *hook = ALLOC(struct sighook);
+ if (!hook) {
+ return NULL;
+ }
+
+ hook->sig = sig;
+ hook->fn = fn;
+ hook->arg = arg;
+ hook->flags = flags;
+
+ if (rcu_sigtable_add(rcu, hook) != 0) {
+ free(hook);
+ return NULL;
+ }
+
+ return hook;
+}
+
+struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags) {
+ mutex_lock(&sigmutex);
+
+ struct sighook *ret = NULL;
+ if (siginit(sig) != 0) {
+ goto done;
+ }
+
+ ret = sighook_impl(&rcu_sighooks, sig, fn, arg, flags);
+done:
+ mutex_unlock(&sigmutex);
+ return ret;
+}
+
+struct sighook *atsigexit(sighook_fn *fn, void *arg) {
+ mutex_lock(&sigmutex);
+
+ struct sighook *ret = NULL;
+
+ for (size_t i = 0; i < countof(FATAL_SIGNALS); ++i) {
+ if (siginit(FATAL_SIGNALS[i]) != 0) {
+ goto done;
+ }
+ }
+
+#ifdef SIGRTMIN
+ for (int i = SIGRTMIN; i <= SIGRTMAX; ++i) {
+ if (siginit(i) != 0) {
+ goto done;
+ }
+ }
+#endif
+
+ ret = sighook_impl(&rcu_exithooks, 0, fn, arg, 0);
+done:
+ mutex_unlock(&sigmutex);
+ return ret;
+}
+
+void sigunhook(struct sighook *hook) {
+ mutex_lock(&sigmutex);
+
+ struct rcu *rcu = hook->sig ? &rcu_sighooks : &rcu_exithooks;
+ struct sigtable *table = rcu_peek(rcu);
+ bfs_verify(sigtable_del(table, hook) == 0);
+
+ if (table->filled == 0) {
+ rcu_update(rcu, NULL);
+ sigtable_free(table);
+ }
+
+ mutex_unlock(&sigmutex);
+ free(hook);
+}
diff --git a/src/sighook.h b/src/sighook.h
new file mode 100644
index 0000000..74d18c0
--- /dev/null
+++ b/src/sighook.h
@@ -0,0 +1,73 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Signal hooks.
+ */
+
+#ifndef BFS_SIGHOOK_H
+#define BFS_SIGHOOK_H
+
+#include <signal.h>
+
+/**
+ * A dynamic signal hook.
+ */
+struct sighook;
+
+/**
+ * Signal hook flags.
+ */
+enum sigflags {
+ /** Suppress the default action for this signal. */
+ SH_CONTINUE = 1 << 0,
+};
+
+/**
+ * A signal hook callback. Hooks are executed from a signal handler, so must
+ * only call async-signal-safe functions.
+ *
+ * @param sig
+ * The signal number.
+ * @param info
+ * Additional information about the signal.
+ * @param arg
+ * An arbitrary pointer passed to the hook.
+ */
+typedef void sighook_fn(int sig, siginfo_t *info, void *arg);
+
+/**
+ * Install a hook for a signal.
+ *
+ * @param sig
+ * The signal to hook.
+ * @param fn
+ * The function to call.
+ * @param arg
+ * An argument passed to the function.
+ * @param flags
+ * Flags for the new hook.
+ * @return
+ * The installed hook, or NULL on failure.
+ */
+struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags);
+
+/**
+ * On a best-effort basis, invoke the given hook just before the program is
+ * abnormally terminated by a signal.
+ *
+ * @param fn
+ * The function to call.
+ * @param arg
+ * An argument passed to the function.
+ * @return
+ * The installed hook, or NULL on failure.
+ */
+struct sighook *atsigexit(sighook_fn *fn, void *arg);
+
+/**
+ * Remove a signal hook.
+ */
+void sigunhook(struct sighook *hook);
+
+#endif // BFS_SIGHOOK_H
diff --git a/src/stat.c b/src/stat.c
index f5cf3fe..2044564 100644
--- a/src/stat.c
+++ b/src/stat.c
@@ -62,7 +62,7 @@ int bfs_fstatat_flags(enum bfs_stat_flags flags) {
ret |= AT_SYMLINK_NOFOLLOW;
}
-#if defined(AT_NO_AUTOMOUNT) && (!__GNU__ || __GLIBC_PREREQ(2, 35))
+#ifdef AT_NO_AUTOMOUNT
ret |= AT_NO_AUTOMOUNT;
#endif
@@ -116,6 +116,9 @@ void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src) {
#if BFS_HAS_ST_BIRTHTIM
dest->btime = src->st_birthtim;
dest->mask |= BFS_STAT_BTIME;
+#elif BFS_HAS___ST_BIRTHTIM
+ dest->btime = src->__st_birthtim;
+ dest->mask |= BFS_STAT_BTIME;
#elif BFS_HAS_ST_BIRTHTIMESPEC
dest->btime = src->st_birthtimespec;
dest->mask |= BFS_STAT_BTIME;
@@ -297,27 +300,21 @@ int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct b
return bfs_stat_tryfollow(at_fd, at_path, at_flags, flags, buf);
}
- // Check __GNU__ to work around https://lists.gnu.org/archive/html/bug-hurd/2021-12/msg00001.html
-#if defined(AT_EMPTY_PATH) && !__GNU__
- static atomic bool has_at_ep = true;
- if (load(&has_at_ep, relaxed)) {
- at_flags |= AT_EMPTY_PATH;
- int ret = bfs_stat_explicit(at_fd, "", at_flags, buf);
- if (ret != 0 && errno == EINVAL) {
- store(&has_at_ep, false, relaxed);
- } else {
- return ret;
- }
- }
-#endif
-
- struct stat statbuf;
- if (fstat(at_fd, &statbuf) == 0) {
- bfs_stat_convert(buf, &statbuf);
- return 0;
- } else {
+#if BFS_USE_STATX
+ // If we have statx(), use it with AT_EMPTY_PATH for its extra features
+ at_flags |= AT_EMPTY_PATH;
+ return bfs_stat_explicit(at_fd, "", at_flags, buf);
+#else
+ // Otherwise, just use fstat() rather than fstatat(at_fd, ""), to save
+ // the kernel the trouble of copying in the empty string
+ struct stat sb;
+ if (fstat(at_fd, &sb) != 0) {
return -1;
}
+
+ bfs_stat_convert(buf, &sb);
+ return 0;
+#endif
}
const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field) {
diff --git a/src/xspawn.c b/src/xspawn.c
index 0b0cea4..33e5a4a 100644
--- a/src/xspawn.c
+++ b/src/xspawn.c
@@ -5,9 +5,11 @@
#include "xspawn.h"
#include "alloc.h"
#include "bfstd.h"
+#include "diag.h"
#include "list.h"
#include <errno.h>
#include <fcntl.h>
+#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
@@ -590,30 +592,49 @@ static pid_t bfs_fork_spawn(struct bfs_resolver *res, const struct bfs_spawn *ct
return -1;
}
+ // Block signals before fork() so handlers don't run in the child
+ sigset_t new_mask;
+ if (sigfillset(&new_mask) != 0) {
+ goto fail;
+ }
+ sigset_t old_mask;
+ errno = pthread_sigmask(SIG_BLOCK, &new_mask, &old_mask);
+ if (errno != 0) {
+ goto fail;
+ }
+
pid_t pid = fork();
- if (pid < 0) {
- close_quietly(pipefd[1]);
- close_quietly(pipefd[0]);
- return -1;
- } else if (pid == 0) {
+ if (pid == 0) {
// Child
bfs_spawn_exec(res, ctx, argv, envp, pipefd);
}
- // Parent
+ // Restore the original signal mask
+ int ret = pthread_sigmask(SIG_SETMASK, &old_mask, NULL);
+ bfs_everify(ret == 0, "pthread_sigmask()");
+
+ if (pid < 0) {
+ // fork() failed
+ goto fail;
+ }
+
xclose(pipefd[1]);
int error;
ssize_t nbytes = xread(pipefd[0], &error, sizeof(error));
xclose(pipefd[0]);
if (nbytes == sizeof(error)) {
- int wstatus;
- xwaitpid(pid, &wstatus, 0);
+ xwaitpid(pid, NULL, 0);
errno = error;
return -1;
}
return pid;
+
+fail:
+ close_quietly(pipefd[1]);
+ close_quietly(pipefd[0]);
+ return -1;
}
/** Call the right bfs_spawn() implementation. */
diff --git a/src/xtime.c b/src/xtime.c
index 91ed915..2808455 100644
--- a/src/xtime.c
+++ b/src/xtime.c
@@ -5,6 +5,7 @@
#include "xtime.h"
#include "bfstd.h"
#include "diag.h"
+#include "sanity.h"
#include <errno.h>
#include <limits.h>
#include <sys/time.h>
@@ -12,14 +13,14 @@
#include <unistd.h>
int xmktime(struct tm *tm, time_t *timep) {
- *timep = mktime(tm);
+ time_t time = mktime(tm);
- if (*timep == -1) {
+ if (time == -1) {
int error = errno;
struct tm tmp;
- if (!localtime_r(timep, &tmp)) {
- bfs_bug("localtime_r(-1): %s", xstrerror(errno));
+ if (!localtime_r(&time, &tmp)) {
+ bfs_ebug("localtime_r(-1)");
return -1;
}
@@ -30,9 +31,38 @@ int xmktime(struct tm *tm, time_t *timep) {
}
}
+ *timep = time;
+ return 0;
+}
+
+// FreeBSD is missing an interceptor
+#if BFS_HAS_TIMEGM && !(__FreeBSD__ && SANITIZE_MEMORY)
+
+int xtimegm(struct tm *tm, time_t *timep) {
+ time_t time = timegm(tm);
+
+ if (time == -1) {
+ int error = errno;
+
+ struct tm tmp;
+ if (!gmtime_r(&time, &tmp)) {
+ bfs_ebug("gmtime_r(-1)");
+ return -1;
+ }
+
+ if (tm->tm_year != tmp.tm_year || tm->tm_yday != tmp.tm_yday
+ || tm->tm_hour != tmp.tm_hour || tm->tm_min != tmp.tm_min || tm->tm_sec != tmp.tm_sec) {
+ errno = error;
+ return -1;
+ }
+ }
+
+ *timep = time;
return 0;
}
+#else
+
static int safe_add(int *value, int delta) {
if (*value >= 0) {
if (delta > INT_MAX - *value) {
@@ -147,6 +177,8 @@ overflow:
return -1;
}
+#endif // !BFS_HAS_TIMEGM
+
/** Parse a decimal digit. */
static int xgetdigit(char c) {
int ret = c - '0';
diff --git a/tests/bfstd.c b/tests/bfstd.c
index 07b68b0..f0f61ce 100644
--- a/tests/bfstd.c
+++ b/tests/bfstd.c
@@ -15,12 +15,12 @@ static bool check_base_dir(const char *path, const char *dir, const char *base)
bool ret = true;
char *xdir = xdirname(path);
- bfs_verify(xdir, "xdirname(): %s", xstrerror(errno));
+ bfs_everify(xdir, "xdirname()");
ret &= bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir);
free(xdir);
char *xbase = xbasename(path);
- bfs_verify(xbase, "xbasename(): %s", xstrerror(errno));
+ bfs_everify(xbase, "xbasename()");
ret &= bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base);
free(xbase);
diff --git a/tests/gnu/empty.out b/tests/common/empty.out
index a0f4b76..a0f4b76 100644
--- a/tests/gnu/empty.out
+++ b/tests/common/empty.out
diff --git a/tests/gnu/empty.sh b/tests/common/empty.sh
index 95ee988..95ee988 100644
--- a/tests/gnu/empty.sh
+++ b/tests/common/empty.sh
diff --git a/tests/common/empty_error.out b/tests/common/empty_error.out
new file mode 100644
index 0000000..da45e23
--- /dev/null
+++ b/tests/common/empty_error.out
@@ -0,0 +1,3 @@
+./bar
+./baz
+./qux
diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh
new file mode 100644
index 0000000..7c8049c
--- /dev/null
+++ b/tests/common/empty_error.sh
@@ -0,0 +1,7 @@
+cd "$TEST"
+
+"$XTOUCH" -p foo/ bar/ baz qux
+chmod -r foo baz
+defer chmod +r foo baz
+
+! bfs_diff . -empty
diff --git a/tests/gnu/empty_special.out b/tests/common/empty_special.out
index fa35478..fa35478 100644
--- a/tests/gnu/empty_special.out
+++ b/tests/common/empty_special.out
diff --git a/tests/gnu/empty_special.sh b/tests/common/empty_special.sh
index 31e9d2e..31e9d2e 100644
--- a/tests/gnu/empty_special.sh
+++ b/tests/common/empty_special.sh
diff --git a/tests/ioq.c b/tests/ioq.c
index ef5ee3b..99c98a2 100644
--- a/tests/ioq.c
+++ b/tests/ioq.c
@@ -40,15 +40,15 @@ static void check_ioq_push_block(void) {
const size_t depth = 2;
struct ioq *ioq = ioq_create(depth, 1);
- bfs_verify(ioq, "ioq_create(): %s", xstrerror(errno));
+ bfs_everify(ioq, "ioq_create()");
// Push enough operations to fill the queue
for (size_t i = 0; i < depth; ++i) {
struct bfs_dir *dir = bfs_allocdir();
- bfs_verify(dir, "bfs_allocdir(): %s", xstrerror(errno));
+ bfs_everify(dir, "bfs_allocdir()");
int ret = ioq_opendir(ioq, dir, AT_FDCWD, ".", 0, NULL);
- bfs_verify(ret == 0, "ioq_opendir(): %s", xstrerror(errno));
+ bfs_everify(ret == 0, "ioq_opendir()");
}
bfs_verify(ioq_capacity(ioq) == 0);
diff --git a/tests/main.c b/tests/main.c
index 429772b..aef0583 100644
--- a/tests/main.c
+++ b/tests/main.c
@@ -9,7 +9,6 @@
#include "tests.h"
#include "bfstd.h"
#include "color.h"
-#include <errno.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
@@ -90,10 +89,6 @@ static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) {
}
}
-const char *bfs_errstr(void) {
- return xstrerror(errno);
-}
-
int main(int argc, char *argv[]) {
// Try to set a UTF-8 locale
if (!setlocale(LC_ALL, "C.UTF-8")) {
@@ -116,6 +111,7 @@ int main(int argc, char *argv[]) {
run_test(&ctx, "bfstd", check_bfstd);
run_test(&ctx, "bit", check_bit);
run_test(&ctx, "ioq", check_ioq);
+ run_test(&ctx, "sighook", check_sighook);
run_test(&ctx, "trie", check_trie);
run_test(&ctx, "xspawn", check_xspawn);
run_test(&ctx, "xtime", check_xtime);
diff --git a/tests/sighook.c b/tests/sighook.c
new file mode 100644
index 0000000..c94526e
--- /dev/null
+++ b/tests/sighook.c
@@ -0,0 +1,97 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "sighook.h"
+#include "atomic.h"
+#include "thread.h"
+#include <pthread.h>
+#include <signal.h>
+#include <sys/time.h>
+
+static atomic size_t count = 0;
+
+/** SIGALRM handler. */
+static void alrm_hook(int sig, siginfo_t *info, void *arg) {
+ fetch_add(&count, 1, relaxed);
+}
+
+/** Swap out an old hook for a new hook. */
+static int swap_hooks(struct sighook **hook) {
+ struct sighook *next = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE);
+ if (!bfs_echeck(next, "sighook(SIGALRM)")) {
+ return -1;
+ }
+
+ sigunhook(*hook);
+ *hook = next;
+ return 0;
+}
+
+/** Background thread that rapidly (un)registers signal hooks. */
+static void *hook_thread(void *ptr) {
+ struct sighook *hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE);
+ if (!bfs_echeck(hook, "sighook(SIGALRM)")) {
+ return NULL;
+ }
+
+ while (load(&count, relaxed) < 1000) {
+ if (swap_hooks(&hook) != 0) {
+ sigunhook(hook);
+ return NULL;
+ }
+ }
+
+ sigunhook(hook);
+ return &count;
+}
+
+bool check_sighook(void) {
+ bool ret = true;
+
+ struct sighook *hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE);
+ ret &= bfs_echeck(hook, "sighook(SIGALRM)");
+ if (!ret) {
+ goto done;
+ }
+
+ struct itimerval ival = {
+ .it_value = {
+ .tv_usec = 100,
+ },
+ .it_interval = {
+ .tv_usec = 100,
+ },
+ };
+ ret &= bfs_echeck(setitimer(ITIMER_REAL, &ival, NULL) == 0);
+ if (!ret) {
+ goto unhook;
+ }
+
+ pthread_t thread;
+ ret &= bfs_echeck(thread_create(&thread, NULL, hook_thread, NULL) == 0);
+ if (!ret) {
+ goto untime;
+ }
+
+ while (ret && load(&count, relaxed) < 1000) {
+ ret &= swap_hooks(&hook) == 0;
+ }
+
+ void *ptr;
+ thread_join(thread, &ptr);
+ ret &= bfs_check(ptr);
+
+untime:
+ ival.it_value.tv_usec = 0;
+ ret &= bfs_echeck(setitimer(ITIMER_REAL, &ival, NULL) == 0);
+ if (!ret) {
+ goto unhook;
+ }
+
+unhook:
+ sigunhook(hook);
+done:
+ return ret;
+}
diff --git a/tests/tests.h b/tests/tests.h
index 9078938..19b7f5e 100644
--- a/tests/tests.h
+++ b/tests/tests.h
@@ -26,6 +26,9 @@ bool check_bit(void);
/** I/O queue tests. */
bool check_ioq(void);
+/** Signal hook tests. */
+bool check_sighook(void);
+
/** Trie tests. */
bool check_trie(void);
@@ -54,20 +57,17 @@ static inline bool bfs_check(bool ret) {
: "Check failed: `%s`%s", \
str, __VA_ARGS__), false))
-/** Get a string description of the last error. */
-const char *bfs_errstr(void);
-
/**
* Check a condition, logging the current error string on failure.
*/
-#define bfs_pcheck(...) \
- bfs_pcheck_(#__VA_ARGS__, __VA_ARGS__, "", "")
+#define bfs_echeck(...) \
+ bfs_echeck_(#__VA_ARGS__, __VA_ARGS__, "", bfs_errstr())
-#define bfs_pcheck_(str, cond, format, ...) \
+#define bfs_echeck_(str, cond, format, ...) \
((cond) ? true : (bfs_diag( \
sizeof(format) > 1 \
- ? "%.0s" format "%s%s: %s" \
- : "Check failed: `%s`%s: %s", \
- str, __VA_ARGS__, bfs_errstr()), false))
+ ? "%.0s" format "%s: %s" \
+ : "Check failed: `%s`: %s", \
+ str, __VA_ARGS__), false))
#endif // BFS_TESTS_H
diff --git a/tests/util.sh b/tests/util.sh
index 7dba9fb..3969db5 100644
--- a/tests/util.sh
+++ b/tests/util.sh
@@ -12,12 +12,9 @@ _realpath() (
)
# Globals
-TESTS=$(_realpath "$TESTS")
-if [ "${BUILDDIR-}" ]; then
- BIN=$(_realpath "$BUILDDIR/bin")
-else
- BIN=$(_realpath "$TESTS/../bin")
-fi
+ROOT=$(_realpath "$(dirname -- "$TESTS")")
+TESTS="$ROOT/tests"
+BIN="$ROOT/bin"
MKSOCK="$BIN/tests/mksock"
XTOUCH="$BIN/tests/xtouch"
UNAME=$(uname)
diff --git a/tests/xspawn.c b/tests/xspawn.c
index 7362aa5..b1d6dc1 100644
--- a/tests/xspawn.c
+++ b/tests/xspawn.c
@@ -54,7 +54,7 @@ static bool check_use_path(bool use_posix) {
bool ret = true;
struct bfs_spawn spawn;
- ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0);
+ ret &= bfs_echeck(bfs_spawn_init(&spawn) == 0);
if (!ret) {
goto out;
}
@@ -64,34 +64,27 @@ static bool check_use_path(bool use_posix) {
spawn.flags &= ~BFS_SPAWN_USE_POSIX;
}
- const char *builddir = getenv("BUILDDIR");
- dchar *bin = dstrprintf("%s/bin", builddir ? builddir : ".");
- ret &= bfs_pcheck(bin, "dstrprintf()");
+ ret &= bfs_echeck(bfs_spawn_addopen(&spawn, 10, "bin", O_RDONLY | O_DIRECTORY, 0) == 0);
+ ret &= bfs_echeck(bfs_spawn_adddup2(&spawn, 10, 11) == 0);
+ ret &= bfs_echeck(bfs_spawn_addclose(&spawn, 10) == 0);
+ ret &= bfs_echeck(bfs_spawn_addfchdir(&spawn, 11) == 0);
+ ret &= bfs_echeck(bfs_spawn_addclose(&spawn, 11) == 0);
if (!ret) {
goto destroy;
}
- ret &= bfs_pcheck(bfs_spawn_addopen(&spawn, 10, bin, O_RDONLY | O_DIRECTORY, 0) == 0);
- ret &= bfs_pcheck(bfs_spawn_adddup2(&spawn, 10, 11) == 0);
- ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 10) == 0);
- ret &= bfs_pcheck(bfs_spawn_addfchdir(&spawn, 11) == 0);
- ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 11) == 0);
- if (!ret) {
- goto bin;
- }
-
// Check that $PATH is resolved in the parent's environment
char **envp;
- ret &= bfs_pcheck(envp = envdup());
+ ret &= bfs_echeck(envp = envdup());
if (!ret) {
- goto bin;
+ goto destroy;
}
// Check that $PATH is resolved after the file actions
char *old_path = getenv("PATH");
dchar *new_path = NULL;
if (old_path) {
- ret &= bfs_pcheck(old_path = strdup(old_path));
+ ret &= bfs_echeck(old_path = strdup(old_path));
if (!ret) {
goto env;
}
@@ -104,20 +97,20 @@ static bool check_use_path(bool use_posix) {
goto path;
}
- ret &= bfs_pcheck(setenv("PATH", new_path, true) == 0);
+ ret &= bfs_echeck(setenv("PATH", new_path, true) == 0);
if (!ret) {
goto path;
}
char *argv[] = {"xspawnee", old_path, NULL};
pid_t pid = bfs_spawn("xspawnee", &spawn, argv, envp);
- ret &= bfs_pcheck(pid >= 0, "bfs_spawn()");
+ ret &= bfs_echeck(pid >= 0, "bfs_spawn()");
if (!ret) {
goto unset;
}
int wstatus;
- ret &= bfs_pcheck(xwaitpid(pid, &wstatus, 0) == pid)
+ ret &= bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid)
&& bfs_check(WIFEXITED(wstatus));
if (ret) {
int wexit = WEXITSTATUS(wstatus);
@@ -126,9 +119,9 @@ static bool check_use_path(bool use_posix) {
unset:
if (old_path) {
- ret &= bfs_pcheck(setenv("PATH", old_path, true) == 0);
+ ret &= bfs_echeck(setenv("PATH", old_path, true) == 0);
} else {
- ret &= bfs_pcheck(unsetenv("PATH") == 0);
+ ret &= bfs_echeck(unsetenv("PATH") == 0);
}
path:
dstrfree(new_path);
@@ -138,10 +131,8 @@ env:
free(*var);
}
free(envp);
-bin:
- dstrfree(bin);
destroy:
- ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0);
+ ret &= bfs_echeck(bfs_spawn_destroy(&spawn) == 0);
out:
return ret;
}
@@ -151,7 +142,7 @@ static bool check_enoent(bool use_posix) {
bool ret = true;
struct bfs_spawn spawn;
- ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0);
+ ret &= bfs_echeck(bfs_spawn_init(&spawn) == 0);
if (!ret) {
goto out;
}
@@ -163,9 +154,9 @@ static bool check_enoent(bool use_posix) {
char *argv[] = {"eW6f5RM9Qi", NULL};
pid_t pid = bfs_spawn("eW6f5RM9Qi", &spawn, argv, NULL);
- ret &= bfs_pcheck(pid < 0 && errno == ENOENT, "bfs_spawn()");
+ ret &= bfs_echeck(pid < 0 && errno == ENOENT, "bfs_spawn()");
- ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0);
+ ret &= bfs_echeck(bfs_spawn_destroy(&spawn) == 0);
out:
return ret;
}
@@ -175,18 +166,18 @@ static bool check_resolve(void) {
char *exe;
exe = bfs_spawn_resolve("sh");
- ret &= bfs_pcheck(exe, "bfs_spawn_resolve('sh')");
+ ret &= bfs_echeck(exe, "bfs_spawn_resolve('sh')");
free(exe);
exe = bfs_spawn_resolve("/bin/sh");
- ret &= bfs_pcheck(exe && strcmp(exe, "/bin/sh") == 0);
+ ret &= bfs_echeck(exe && strcmp(exe, "/bin/sh") == 0);
free(exe);
exe = bfs_spawn_resolve("bin/tests/xspawnee");
- ret &= bfs_pcheck(exe && strcmp(exe, "bin/tests/xspawnee") == 0);
+ ret &= bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0);
free(exe);
- ret &= bfs_pcheck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT);
+ ret &= bfs_echeck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT);
return ret;
}
diff --git a/tests/xtime.c b/tests/xtime.c
index a7c63d2..1907e26 100644
--- a/tests/xtime.c
+++ b/tests/xtime.c
@@ -29,9 +29,9 @@ static bool check_one_xgetdate(const char *str, int error, time_t expected) {
int ret = xgetdate(str, &ts);
if (error) {
- return bfs_pcheck(ret == -1 && errno == error, "xgetdate('%s')", str);
+ return bfs_echeck(ret == -1 && errno == error, "xgetdate('%s')", str);
} else {
- return bfs_pcheck(ret == 0, "xgetdate('%s')", str)
+ return bfs_echeck(ret == 0, "xgetdate('%s')", str)
&& bfs_check(ts.tv_sec == expected && ts.tv_nsec == 0,
"xgetdate('%s'): %jd.%09jd != %jd",
str, (intmax_t)ts.tv_sec, (intmax_t)ts.tv_nsec, (intmax_t)expected);
@@ -82,12 +82,12 @@ static bool check_xgetdate(void) {
static bool check_one_xmktime(time_t expected) {
struct tm tm;
if (!localtime_r(&expected, &tm)) {
- bfs_diag("localtime_r(%jd): %s", (intmax_t)expected, xstrerror(errno));
+ bfs_ediag("localtime_r(%jd)", (intmax_t)expected);
return false;
}
time_t actual;
- return bfs_pcheck(xmktime(&tm, &actual) == 0, "xmktime(" TM_FORMAT ")", TM_PRINTF(tm))
+ return bfs_echeck(xmktime(&tm, &actual) == 0, "xmktime(" TM_FORMAT ")", TM_PRINTF(tm))
&& bfs_check(actual == expected, "xmktime(" TM_FORMAT "): %jd != %jd", TM_PRINTF(tm), (intmax_t)actual, (intmax_t)expected);
}
@@ -137,6 +137,7 @@ static bool check_one_xtimegm(const struct tm *tm) {
return ret;
}
+#if !BFS_HAS_TIMEGM
/** Check an overflowing xtimegm() call. */
static bool check_xtimegm_overflow(const struct tm *tm) {
struct tm copy = *tm;
@@ -154,6 +155,7 @@ static bool check_xtimegm_overflow(const struct tm *tm) {
return ret;
}
+#endif
/** xtimegm() tests. */
static bool check_xtimegm(void) {
@@ -173,11 +175,13 @@ static bool check_xtimegm(void) {
ret &= check_one_xtimegm(&tm);
}
+#if !BFS_HAS_TIMEGM
// Check integer overflow cases
- check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX });
- check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX });
- check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX });
- check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX });
+ ret &= check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX });
+ ret &= check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX });
+ ret &= check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX });
+ ret &= check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX });
+#endif
return ret;
}
diff --git a/tests/xtouch.c b/tests/xtouch.c
index cd41842..427e3e0 100644
--- a/tests/xtouch.c
+++ b/tests/xtouch.c
@@ -120,7 +120,7 @@ static int at_flags(const struct args *args) {
/** Touch one path. */
static int xtouch(const struct args *args, const char *path) {
int dfd = open_parent(args, &path);
- if (dfd < 0 && dfd != AT_FDCWD) {
+ if (dfd < 0 && dfd != (int)AT_FDCWD) {
return -1;
}