summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-x.github/diag.sh16
-rw-r--r--.github/workflows/ci.yml57
-rw-r--r--.github/workflows/codecov.yml2
-rw-r--r--.github/workflows/codeql.yml2
-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)8
-rw-r--r--build/flags.mk115
-rw-r--r--build/has/--st-birthtim.c9
-rw-r--r--build/has/acl-get-entry.c (renamed from config/acl-get-entry.c)3
-rw-r--r--build/has/acl-get-file.c (renamed from config/acl-get-file.c)3
-rw-r--r--build/has/acl-get-tag-type.c (renamed from config/acl-get-tag-type.c)3
-rw-r--r--build/has/acl-is-trivial-np.c (renamed from config/acl-is-trivial-np.c)0
-rw-r--r--build/has/acl-trivial.c (renamed from config/acl-trivial.c)0
-rw-r--r--build/has/aligned-alloc.c (renamed from config/aligned-alloc.c)0
-rw-r--r--build/has/confstr.c (renamed from config/confstr.c)0
-rw-r--r--build/has/extattr-get-file.c (renamed from config/extattr-get-file.c)0
-rw-r--r--build/has/extattr-get-link.c (renamed from config/extattr-get-link.c)0
-rw-r--r--build/has/extattr-list-file.c (renamed from config/extattr-list-file.c)0
-rw-r--r--build/has/extattr-list-link.c (renamed from config/extattr-list-link.c)0
-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/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.c (renamed from config/max-align-t.c)3
-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.mk73
-rwxr-xr-xbuild/msg-if.sh31
-rwxr-xr-xbuild/msg.sh62
-rwxr-xr-xbuild/pkgconf.sh (renamed from config/pkgconf.sh)10
-rw-r--r--build/pkgs.mk33
-rw-r--r--build/prelude.mk124
-rw-r--r--build/with/libacl.c (renamed from config/libacl.c)0
-rw-r--r--build/with/libcap.c (renamed from config/libcap.c)0
-rw-r--r--build/with/libselinux.c (renamed from config/libselinux.c)0
-rw-r--r--build/with/liburing.c (renamed from config/liburing.c)0
-rw-r--r--build/with/oniguruma.c (renamed from config/oniguruma.c)0
-rw-r--r--config/config.mk78
-rw-r--r--config/deps.mk18
-rw-r--r--config/flags.mk135
-rw-r--r--config/header.mk69
-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-xconfigure198
-rw-r--r--docs/BUILDING.md211
-rw-r--r--docs/CHANGELOG.md30
-rw-r--r--src/atomic.h33
-rw-r--r--src/bar.c142
-rw-r--r--src/bfstd.c45
-rw-r--r--src/bfstd.h20
-rw-r--r--src/bftw.c8
-rw-r--r--src/color.c59
-rw-r--r--src/color.h8
-rw-r--r--src/ctx.c105
-rw-r--r--src/ctx.h2
-rw-r--r--src/diag.c14
-rw-r--r--src/diag.h39
-rw-r--r--src/dir.c16
-rw-r--r--src/dir.h9
-rw-r--r--src/dstring.c4
-rw-r--r--src/dstring.h4
-rw-r--r--src/eval.c161
-rw-r--r--src/exec.c6
-rw-r--r--src/expr.h6
-rw-r--r--src/fsade.c2
-rw-r--r--src/fsade.h4
-rw-r--r--src/ioq.c26
-rw-r--r--src/main.c1
-rw-r--r--src/mtab.c9
-rw-r--r--src/opt.c56
-rw-r--r--src/parse.c20
-rw-r--r--src/prelude.h33
-rw-r--r--src/printf.c2
-rw-r--r--src/sighook.c604
-rw-r--r--src/sighook.h73
-rw-r--r--src/stat.c37
-rw-r--r--src/trie.c14
-rw-r--r--src/xregex.c14
-rw-r--r--src/xspawn.c37
-rw-r--r--src/xtime.c40
-rw-r--r--tests/bfs/printf_invalid_flag.sh1
-rw-r--r--tests/bfstd.c4
-rw-r--r--tests/color.sh116
-rw-r--r--tests/gnu/printf_flags.sh2
-rw-r--r--tests/ioq.c6
-rw-r--r--tests/main.c6
-rw-r--r--tests/run.sh90
-rw-r--r--tests/sighook.c97
-rw-r--r--tests/tests.h19
-rw-r--r--tests/util.sh18
-rw-r--r--tests/xspawn.c53
-rw-r--r--tests/xtime.c20
-rw-r--r--tests/xtouch.c2
123 files changed, 2772 insertions, 1187 deletions
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 3890bb0..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
@@ -73,7 +78,6 @@ jobs:
with:
operating_system: freebsd
version: "14.0"
- sync_files: runner-to-vm
run: |
sudo pkg install -y \
@@ -83,7 +87,12 @@ jobs:
pkgconf \
tcl-wrapper
sudo mount -t fdescfs none /dev/fd
- make -j$(nproc) distcheck
+ .github/diag.sh make -j$(nproc) distcheck
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: freebsd-config.log
+ path: distcheck-*/gen/config.log
openbsd:
name: OpenBSD
@@ -98,7 +107,6 @@ jobs:
with:
operating_system: openbsd
version: "7.5"
- sync_files: runner-to-vm
run: |
sudo pkg_add \
@@ -107,8 +115,13 @@ jobs:
gmake \
oniguruma
jobs=$(sysctl -n hw.ncpu)
- gmake -j$jobs config
- gmake -j$jobs check TEST_FLAGS="--sudo --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
@@ -123,7 +136,6 @@ jobs:
with:
operating_system: netbsd
version: "10.0"
- sync_files: runner-to-vm
run: |
PATH="/sbin:/usr/sbin:$PATH"
@@ -133,8 +145,13 @@ jobs:
pkgconf \
tcl-expect
jobs=$(sysctl -n hw.ncpu)
- make -j$jobs config
- 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
@@ -149,7 +166,6 @@ jobs:
with:
release: "6.4.0"
usesh: true
- copyback: false
prepare: |
pkg install -y \
@@ -165,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
@@ -181,7 +202,6 @@ jobs:
with:
release: "r151048"
usesh: true
- copyback: false
prepare: |
pkg install \
@@ -198,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 2245f40..6aaace6 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -25,7 +25,7 @@ jobs:
- name: Generate coverage
run: |
- make config GCOV=y
+ ./configure --enable-gcov
make -j$(nproc) check TEST_FLAGS="--sudo"
gcov -abcfpu obj/*/*.o
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index c21fda5..a0b8fe3 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -41,7 +41,7 @@ jobs:
- name: Configure
run: |
- make -j$(nproc) config
+ ./configure
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
diff --git a/Makefile b/Makefile
index 981c322..f33a604 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..d382f5d
--- /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..913a1aa 100644
--- a/config/exports.mk
+++ b/build/exports.mk
@@ -12,9 +12,9 @@ export XMKDIR=${MKDIR}
export XPKG_CONFIG=${PKG_CONFIG}
export XRM=${RM}
-export XCPPFLAGS=${CPPFLAGS}
-export XCFLAGS=${CFLAGS}
-export XLDFLAGS=${LDFLAGS}
-export XLDLIBS=${LDLIBS}
+export XCPPFLAGS=${_CPPFLAGS}
+export XCFLAGS=${_CFLAGS}
+export XLDFLAGS=${_LDFLAGS}
+export XLDLIBS=${_LDLIBS}
export XNOLIBS=${NOLIBS}
diff --git a/build/flags.mk b/build/flags.mk
new file mode 100644
index 0000000..c50b077
--- /dev/null
+++ b/build/flags.mk
@@ -0,0 +1,115 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/flags.mk
+
+include build/prelude.mk
+include gen/vars.mk
+
+# Internal flags
+_CPPFLAGS := \
+ -Isrc \
+ -Igen \
+ -D__EXTENSIONS__ \
+ -D_ATFILE_SOURCE \
+ -D_BSD_SOURCE \
+ -D_DARWIN_C_SOURCE \
+ -D_DEFAULT_SOURCE \
+ -D_GNU_SOURCE \
+ -D_POSIX_PTHREAD_SEMANTICS \
+ -D_FILE_OFFSET_BITS=64 \
+ -D_TIME_BITS=64
+
+_CFLAGS := -std=c17 -pthread
+_LDFLAGS :=
+_LDLIBS :=
+
+# Platform-specific system libraries
+LDLIBS,DragonFly := -lposix1e
+LDLIBS,Linux := -lrt
+LDLIBS,NetBSD := -lutil
+LDLIBS,SunOS := -lsec -lsocket -lnsl
+_LDLIBS += ${LDLIBS,${OS}}
+
+# Build profiles
+_ASAN := ${TRUTHY,${ASAN}}
+_LSAN := ${TRUTHY,${LSAN}}
+_MSAN := ${TRUTHY,${MSAN}}
+_TSAN := ${TRUTHY,${TSAN}}
+_UBSAN := ${TRUTHY,${UBSAN}}
+_GCOV := ${TRUTHY,${GCOV}}
+_LINT := ${TRUTHY,${LINT}}
+_RELEASE := ${TRUTHY,${RELEASE}}
+
+# https://github.com/google/sanitizers/issues/342
+TSAN_CPPFLAGS,y := -DBFS_USE_TARGET_CLONES=0
+_CPPFLAGS += ${TSAN_CPPFLAGS,${_TSAN}}
+
+ASAN_CFLAGS,y := -fsanitize=address
+LSAN_CFLAGS,y := -fsanitize=leak
+MSAN_CFLAGS,y := -fsanitize=memory -fsanitize-memory-track-origins
+TSAN_CFLAGS,y := -fsanitize=thread
+UBSAN_CFLAGS,y := -fsanitize=undefined
+
+_CFLAGS += ${ASAN_CFLAGS,${_ASAN}}
+_CFLAGS += ${LSAN_CFLAGS,${_LSAN}}
+_CFLAGS += ${MSAN_CFLAGS,${_MSAN}}
+_CFLAGS += ${TSAN_CFLAGS,${_TSAN}}
+_CFLAGS += ${UBSAN_CFLAGS,${_UBSAN}}
+
+SAN_CFLAGS,y := -fno-sanitize-recover=all
+INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_UBSAN}}
+SAN := ${NOT,${INSANE}}
+_CFLAGS += ${SAN_CFLAGS,${SAN}}
+
+# MSAN and TSAN both need all code to be instrumented
+YESLIBS := ${NOT,${_MSAN}${_TSAN}}
+NOLIBS ?= ${NOT,${YESLIBS}}
+
+# gcov only intercepts fork()/exec() with -std=gnu*
+GCOV_CFLAGS,y := -std=gnu17 --coverage
+_CFLAGS += ${GCOV_CFLAGS,${_GCOV}}
+
+LINT_CPPFLAGS,y := -D_FORTIFY_SOURCE=3 -DBFS_LINT
+LINT_CFLAGS,y := -Werror -O2
+
+_CPPFLAGS += ${LINT_CPPFLAGS,${_LINT}}
+_CFLAGS += ${LINT_CFLAGS,${_LINT}}
+
+RELEASE_CPPFLAGS,y := -DNDEBUG
+RELEASE_CFLAGS,y := -O3 -flto=auto
+
+_CPPFLAGS += ${RELEASE_CPPFLAGS,${_RELEASE}}
+_CFLAGS += ${RELEASE_CFLAGS,${_RELEASE}}
+
+# Configurable flags
+CFLAGS ?= \
+ -g \
+ -Wall \
+ -Wformat=2 \
+ -Werror=implicit \
+ -Wimplicit-fallthrough \
+ -Wmissing-declarations \
+ -Wshadow \
+ -Wsign-compare \
+ -Wstrict-prototypes
+
+# Add the configurable flags last so they can override ours
+_CPPFLAGS += ${CPPFLAGS} ${EXTRA_CPPFLAGS}
+_CFLAGS += ${CFLAGS} ${EXTRA_CFLAGS}
+_LDFLAGS += ${LDFLAGS} ${EXTRA_LDFLAGS}
+# (except LDLIBS, as earlier libs override later ones)
+_LDLIBS := ${LDLIBS} ${EXTRA_LDLIBS} ${_LDLIBS}
+
+include build/exports.mk
+
+gen/flags.mk::
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf '_CPPFLAGS := %s\n' "$$XCPPFLAGS" >>$@
+ @printf '_CFLAGS := %s\n' "$$XCFLAGS" >>$@
+ @printf '_LDFLAGS := %s\n' "$$XLDFLAGS" >>$@
+ @printf '_LDLIBS := %s\n' "$$XLDLIBS" >>$@
+ @printf 'NOLIBS := %s\n' "$$XNOLIBS" >>$@
+ @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/config/acl-get-entry.c b/build/has/acl-get-entry.c
index 3cce771..1e7f473 100644
--- a/config/acl-get-entry.c
+++ b/build/has/acl-get-entry.c
@@ -1,3 +1,6 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
#include <sys/types.h>
#include <sys/acl.h>
diff --git a/config/acl-get-file.c b/build/has/acl-get-file.c
index 89fbf23..0b76ee2 100644
--- a/config/acl-get-file.c
+++ b/build/has/acl-get-file.c
@@ -1,3 +1,6 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
#include <stddef.h>
#include <sys/types.h>
#include <sys/acl.h>
diff --git a/config/acl-get-tag-type.c b/build/has/acl-get-tag-type.c
index 2901956..67b7d37 100644
--- a/config/acl-get-tag-type.c
+++ b/build/has/acl-get-tag-type.c
@@ -1,3 +1,6 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
#include <string.h>
#include <sys/types.h>
#include <sys/acl.h>
diff --git a/config/acl-is-trivial-np.c b/build/has/acl-is-trivial-np.c
index 9ca9fc7..9ca9fc7 100644
--- a/config/acl-is-trivial-np.c
+++ b/build/has/acl-is-trivial-np.c
diff --git a/config/acl-trivial.c b/build/has/acl-trivial.c
index 7efc838..7efc838 100644
--- a/config/acl-trivial.c
+++ b/build/has/acl-trivial.c
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/config/confstr.c b/build/has/confstr.c
index 58280b4..58280b4 100644
--- a/config/confstr.c
+++ b/build/has/confstr.c
diff --git a/config/extattr-get-file.c b/build/has/extattr-get-file.c
index ac9cf96..ac9cf96 100644
--- a/config/extattr-get-file.c
+++ b/build/has/extattr-get-file.c
diff --git a/config/extattr-get-link.c b/build/has/extattr-get-link.c
index c35be5b..c35be5b 100644
--- a/config/extattr-get-link.c
+++ b/build/has/extattr-get-link.c
diff --git a/config/extattr-list-file.c b/build/has/extattr-list-file.c
index e68a8bb..e68a8bb 100644
--- a/config/extattr-list-file.c
+++ b/build/has/extattr-list-file.c
diff --git a/config/extattr-list-link.c b/build/has/extattr-list-link.c
index 49f0ec2..49f0ec2 100644
--- a/config/extattr-list-link.c
+++ b/build/has/extattr-list-link.c
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/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/config/max-align-t.c b/build/has/strtofflags.c
index 96165ce..73ecbcb 100644
--- a/config/max-align-t.c
+++ b/build/has/strtofflags.c
@@ -2,7 +2,8 @@
// SPDX-License-Identifier: 0BSD
#include <stddef.h>
+#include <unistd.h>
int main(void) {
- return _Alignof(max_align_t);
+ 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..09454c5
--- /dev/null
+++ b/build/header.mk
@@ -0,0 +1,73 @@
+# 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/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/with/%.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..afb478c
--- /dev/null
+++ b/build/msg-if.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Print a success/failure indicator from a makefile:
+#
+# $ ./configure
+# [ CC ] with/liburing.c ✘
+# [ CC ] with/oniguruma.c ✔
+
+set -eu
+
+MSG="$1"
+shift
+
+if [ -z "${NO_COLOR:-}" ] && [ -t 1 ]; then
+ Y='\033[1;32m✔\033[0m'
+ N='\033[1;31m✘\033[0m'
+else
+ Y='✔'
+ N='✘'
+fi
+
+if "$@"; then
+ YN="$Y"
+else
+ YN="$N"
+fi
+
+build/msg.sh "$(printf "%-37s $YN" "$MSG")"
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..244c95d 100755
--- a/config/pkgconf.sh
+++ b/build/pkgconf.sh
@@ -26,10 +26,18 @@ esac
if [ -z "$MODE" ]; then
# Check whether the libraries exist at all
for LIB; do
+ # Check ${WITH_$LIB}
+ WITH_LIB="WITH_$(printf '%s' "$LIB" | tr 'a-z-' 'A-Z_')"
+ eval "WITH=\"\${$WITH_LIB:-}\""
+ case "$WITH" 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/with/$LIB.c $LDLIBS || exit 1
done
fi
diff --git a/build/pkgs.mk b/build/pkgs.mk
new file mode 100644
index 0000000..5de9ac2
--- /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/with/%.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/with/foo.h to foo
+PKG = ${@:gen/with/%.h=%}
+
+${HEADERS}::
+ @${MKDIR} ${@D}
+ @build/define-if.sh with/${PKG} build/pkgconf.sh ${PKG} >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] with/${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/with/libacl.c
index de1fe50..de1fe50 100644
--- a/config/libacl.c
+++ b/build/with/libacl.c
diff --git a/config/libcap.c b/build/with/libcap.c
index 58e832c..58e832c 100644
--- a/config/libcap.c
+++ b/build/with/libcap.c
diff --git a/config/libselinux.c b/build/with/libselinux.c
index bca409d..bca409d 100644
--- a/config/libselinux.c
+++ b/build/with/libselinux.c
diff --git a/config/liburing.c b/build/with/liburing.c
index bea499a..bea499a 100644
--- a/config/liburing.c
+++ b/build/with/liburing.c
diff --git a/config/oniguruma.c b/build/with/oniguruma.c
index cb17596..cb17596 100644
--- a/config/oniguruma.c
+++ b/build/with/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/flags.mk b/config/flags.mk
deleted file mode 100644
index ee13f06..0000000
--- a/config/flags.mk
+++ /dev/null
@@ -1,135 +0,0 @@
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-# Makefile that generates gen/flags.mk
-
-include config/prelude.mk
-include ${GEN}/vars.mk
-
-# Configurable flags
-CPPFLAGS ?=
-CFLAGS ?= \
- -g \
- -Wall \
- -Wformat=2 \
- -Werror=implicit \
- -Wimplicit-fallthrough \
- -Wmissing-declarations \
- -Wshadow \
- -Wsign-compare \
- -Wstrict-prototypes
-LDFLAGS ?=
-LDLIBS ?=
-
-export XCPPFLAGS=${CPPFLAGS}
-export XCFLAGS=${CFLAGS}
-export XLDFLAGS=${LDFLAGS}
-export XLDLIBS=${LDLIBS}
-
-# Immutable flags
-export BFS_CPPFLAGS= \
- -Isrc \
- -I${GEN} \
- -D__EXTENSIONS__ \
- -D_ATFILE_SOURCE \
- -D_BSD_SOURCE \
- -D_DARWIN_C_SOURCE \
- -D_DEFAULT_SOURCE \
- -D_GNU_SOURCE \
- -D_POSIX_PTHREAD_SEMANTICS \
- -D_FILE_OFFSET_BITS=64 \
- -D_TIME_BITS=64
-export BFS_CFLAGS= -std=c17 -pthread
-
-# Platform-specific system libraries
-LDLIBS,DragonFly := -lposix1e
-LDLIBS,Linux := -lrt
-LDLIBS,NetBSD := -lutil
-LDLIBS,SunOS := -lsec -lsocket -lnsl
-export BFS_LDLIBS=${LDLIBS,${OS}}
-
-# Build profiles
-_ASAN := ${TRUTHY,${ASAN}}
-_LSAN := ${TRUTHY,${LSAN}}
-_MSAN := ${TRUTHY,${MSAN}}
-_TSAN := ${TRUTHY,${TSAN}}
-_UBSAN := ${TRUTHY,${UBSAN}}
-_GCOV := ${TRUTHY,${GCOV}}
-_LINT := ${TRUTHY,${LINT}}
-_RELEASE := ${TRUTHY,${RELEASE}}
-
-# https://github.com/google/sanitizers/issues/342
-TSAN_CPPFLAGS,y := -DBFS_USE_TARGET_CLONES=0
-export TSAN_CPPFLAGS=${TSAN_CPPFLAGS,${_TSAN}}
-
-ASAN_CFLAGS,y := -fsanitize=address
-LSAN_CFLAGS,y := -fsanitize=leak
-MSAN_CFLAGS,y := -fsanitize=memory -fsanitize-memory-track-origins
-TSAN_CFLAGS,y := -fsanitize=thread
-UBSAN_CFLAGS.y := -fsanitize=undefined
-
-export ASAN_CFLAGS=${ASAN_CFLAGS,${_ASAN}}
-export LSAN_CFLAGS=${LSAN_CFLAGS,${_LSAN}}
-export MSAN_CFLAGS=${MSAN_CFLAGS,${_MSAN}}
-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}}
-export SAN_CFLAGS=${SAN_CFLAGS,${SAN}}
-
-# MSAN and TSAN both need all code to be instrumented
-NOLIBS ?= ${NOT,${NOR,${_MSAN},${_TSAN}}}
-export XNOLIBS=${NOLIBS}
-
-# gcov only intercepts fork()/exec() with -std=gnu*
-GCOV_CFLAGS,y := -std=gnu17 --coverage
-export GCOV_CFLAGS=${GCOV_CFLAGS,${_GCOV}}
-
-LINT_CPPFLAGS,y := -D_FORTIFY_SOURCE=3 -DBFS_LINT
-LINT_CFLAGS,y := -Werror -O2
-
-export LINT_CPPFLAGS=${LINT_CPPFLAGS,${_LINT}}
-export LINT_CFLAGS=${LINT_CFLAGS,${_LINT}}
-
-RELEASE_CPPFLAGS,y := -DNDEBUG
-RELEASE_CFLAGS,y := -O3 -flto=auto
-
-export RELEASE_CPPFLAGS=${RELEASE_CPPFLAGS,${_RELEASE}}
-export RELEASE_CFLAGS=${RELEASE_CFLAGS,${_RELEASE}}
-
-# Set a variable
-SETVAR = printf '%s := %s\n' >>$@
-
-# Append to a variable, if non-empty
-APPEND = append() { test -z "$$2" || printf '%s += %s\n' "$$1" "$$2" >>$@; }; append
-
-${GEN}/flags.mk::
- ${MSG} "[ GEN] ${TGT}"
- printf '# %s\n' "${TGT}" >$@
- ${SETVAR} CPPFLAGS "$$BFS_CPPFLAGS"
- ${APPEND} CPPFLAGS "$$TSAN_CPPFLAGS"
- ${APPEND} CPPFLAGS "$$LINT_CPPFLAGS"
- ${APPEND} CPPFLAGS "$$RELEASE_CPPFLAGS"
- ${APPEND} CPPFLAGS "$$XCPPFLAGS"
- ${APPEND} CPPFLAGS "$$EXTRA_CPPFLAGS"
- ${SETVAR} CFLAGS "$$BFS_CFLAGS"
- ${APPEND} CFLAGS "$$ASAN_CFLAGS"
- ${APPEND} CFLAGS "$$LSAN_CFLAGS"
- ${APPEND} CFLAGS "$$MSAN_CFLAGS"
- ${APPEND} CFLAGS "$$TSAN_CFLAGS"
- ${APPEND} CFLAGS "$$UBSAN_CFLAGS"
- ${APPEND} CFLAGS "$$SAN_CFLAGS"
- ${APPEND} CFLAGS "$$GCOV_CFLAGS"
- ${APPEND} CFLAGS "$$LINT_CFLAGS"
- ${APPEND} CFLAGS "$$RELEASE_CFLAGS"
- ${APPEND} CFLAGS "$$XCFLAGS"
- ${APPEND} CFLAGS "$$EXTRA_CFLAGS"
- ${SETVAR} LDFLAGS "$$XLDFLAGS"
- ${SETVAR} LDLIBS "$$XLDLIBS"
- ${APPEND} LDLIBS "$$EXTRA_LDLIBS"
- ${APPEND} LDLIBS "$$BFS_LDLIBS"
- ${SETVAR} NOLIBS "$$XNOLIBS"
- test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@
- ${VCAT} $@
diff --git a/config/header.mk b/config/header.mk
deleted file mode 100644
index 7fd2f78..0000000
--- a/config/header.mk
+++ /dev/null
@@ -1,69 +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-get-entry.h \
- ${GEN}/acl-get-file.h \
- ${GEN}/acl-get-tag-type.h \
- ${GEN}/acl-is-trivial-np.h \
- ${GEN}/acl-trivial.h \
- ${GEN}/aligned-alloc.h \
- ${GEN}/confstr.h \
- ${GEN}/extattr-get-file.h \
- ${GEN}/extattr-get-link.h \
- ${GEN}/extattr-list-file.h \
- ${GEN}/extattr-list-link.h \
- ${GEN}/fdclosedir.h \
- ${GEN}/getdents.h \
- ${GEN}/getdents64.h \
- ${GEN}/getdents64-syscall.h \
- ${GEN}/getprogname.h \
- ${GEN}/getprogname-gnu.h \
- ${GEN}/max-align-t.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..60d7a92
--- /dev/null
+++ b/configure
@@ -0,0 +1,198 @@
+#!/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
+
+# Convert kebab-case to UPPER_CASE
+toupper() {
+ printf '%s' "$1" | tr 'a-z-' 'A-Z_'
+}
+
+# Report an argument parsing error
+invalid() {
+ printf 'error: Unrecognized option "%s"\n\n' "$1" >&2
+ printf 'Run %s --help for more information.\n' "$0" >&2
+ exit 1
+}
+
+for arg; do
+ # --[(enable|disable|with|without)-]$name[=$value]
+ value="${arg#*=}"
+ name="${arg%%=*}"
+ name="${name#--}"
+ case "$arg" in
+ --enable-*|--disable-*|--with-*|--without-*)
+ name="${name#*-}"
+ ;;
+ esac
+ NAME=$(printf '%s' "$name" | tr 'a-z-' 'A-Z_')
+
+ # y/n modality
+ case "$arg" in
+ --enable-*|--with-*)
+ case "$arg" in
+ *=y|*=yes) yn=y ;;
+ *=n|*=no) yn=n ;;
+ *=*) invalid "$arg" ;;
+ *) yn=y ;;
+ esac
+ ;;
+ --disable-*|--without-*)
+ case "$arg" in
+ *=*) invalid "arg" ;;
+ *) yn=n ;;
+ esac
+ ;;
+ esac
+
+ # Fix up --enable-lib* to --with-lib*
+ case "$arg" in
+ --enable-*|--disable-*)
+ case "$name" in
+ libacl|libcap|libselinux|liburing|oniguruma)
+ old="$arg"
+ case "$arg" in
+ --enable-*) arg="--with-${arg#--*-}" ;;
+ --disable-*) arg="--without-${arg#--*-}" ;;
+ esac
+ printf 'warning: Treating "%s" like "%s"\n' "$old" "$arg" >&2
+ ;;
+ esac
+ ;;
+ esac
+
+ case "$arg" in
+ -h|--help)
+ cat <<EOF
+Usage:
+
+ \$ $0 [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...]
+ \$ $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 build --with or
+--without them explicitly:
+
+ --with-libacl --without-libacl
+ --with-libcap --without-libcap
+ --with-libselinux --without-libselinux
+ --with-liburing --without-liburing
+ --with-oniguruma --without-oniguruma
+
+Packaging:
+
+ --prefix=/path
+ Set the installation prefix (default: /usr)
+ --mandir=/path
+ Set the man page directory (default: \$PREFIX/share/man)
+
+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 "$name" in
+ release|asan|lsan|msan|tsan|ubsan|lint|gcov)
+ shift
+ set -- "$@" "$NAME=$yn"
+ ;;
+ *)
+ invalid "$arg"
+ ;;
+ esac
+ ;;
+
+ --with-*|--without-*)
+ case "$name" in
+ libacl|libcap|libselinux|liburing|oniguruma)
+ shift
+ set -- "$@" "WITH_$NAME=$yn"
+ ;;
+ *)
+ invalid "$arg"
+ ;;
+ esac
+ ;;
+
+ --prefix=*|--mandir=*)
+ shift
+ set -- "$@" "$NAME=$value"
+ ;;
+
+ --infodir=*|--build=*|--host=*|--target=*)
+ shift
+ printf 'warning: Ignoring option "%s"\n' "$arg" >&2
+ ;;
+
+ MAKE=*)
+ shift
+ MAKE="$value"
+ ;;
+
+ # make flag (-j2) or variable (CC=clang)
+ -*|*=*)
+ continue
+ ;;
+
+ *)
+ invalid "$arg"
+ ;;
+ 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..025dadf 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-*] [--with-*|--without-*] [CC=...] [...]
+ $ 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 build `--with` or `--without` them explicitly:
-| 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>
+--with-<a href="https://savannah.nongnu.org/projects/acl">libacl</a> --without-libacl
+--with-<a href="https://sites.google.com/site/fullycapable/">libcap</a> --without-libcap
+--with-<a href="https://github.com/SELinuxProject/selinux">libselinux</a> --without-libselinux
+--with-<a href="https://github.com/axboe/liburing">liburing</a> --without-liburing
+--with-<a href="https://github.com/kkos/oniguruma">oniguruma</a> --without-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..be77694 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,64 +133,47 @@ 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];
- ctermid(term);
- if (strlen(term) == 0) {
- errno = ENOTTY;
+ bar->fd = open_cterm(O_RDWR | O_CLOEXEC);
+ if (bar->fd < 0) {
goto fail;
}
- the_bar.fd = open(term, O_RDWR | O_CLOEXEC);
- if (the_bar.fd < 0) {
- goto fail;
+ if (bfs_bar_getsize(bar) != 0) {
+ goto fail_close;
}
- if (bfs_bar_getsize(&the_bar) != 0) {
+ bar->exit_hook = atsigexit(bfs_bar_sigexit, bar);
+ if (!bar->exit_hook) {
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);
-
#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 +201,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 f8ce871..44eda7c 100644
--- a/src/bfstd.c
+++ b/src/bfstd.c
@@ -184,6 +184,16 @@ char *xgetdelim(FILE *file, char delim) {
}
}
+int open_cterm(int flags) {
+ char path[L_ctermid];
+ if (ctermid(path) == NULL || strlen(path) == 0) {
+ errno = ENOTTY;
+ return -1;
+ }
+
+ return open(path, flags);
+}
+
const char *xgetprogname(void) {
const char *cmd = NULL;
#if BFS_HAS_GETPROGNAME
@@ -322,6 +332,10 @@ const char *xstrerror(int errnum) {
return ret;
}
+const char *errstr(void) {
+ return xstrerror(errno);
+}
+
/** Get the single character describing the given file type. */
static char type_char(mode_t mode) {
switch (mode & S_IFMT) {
@@ -637,8 +651,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 +669,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 +679,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..f5d8622 100644
--- a/src/bfstd.h
+++ b/src/bfstd.h
@@ -158,6 +158,16 @@ FILE *xfopen(const char *path, int flags);
*/
char *xgetdelim(FILE *file, char delim);
+/**
+ * Open the controlling terminal.
+ *
+ * @param flags
+ * The open() flags.
+ * @return
+ * An open file descriptor, or -1 on failure.
+ */
+int open_cterm(int flags);
+
// #include <stdlib.h>
/**
@@ -248,6 +258,11 @@ char *xstpencpy(char *dest, char *end, const char *src, size_t n);
const char *xstrerror(int errnum);
/**
+ * Shorthand for xstrerror(errno).
+ */
+const char *errstr(void);
+
+/**
* Format a mode like ls -l (e.g. -rw-r--r--).
*
* @param mode
@@ -410,6 +425,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..c80ac74 100644
--- a/src/bftw.c
+++ b/src/bftw.c
@@ -913,7 +913,7 @@ static int bftw_state_init(struct bftw_state *state, const struct bftw_args *arg
size_t qdepth = 4096;
size_t nthreads = args->nthreads;
-#if BFS_USE_LIBURING
+#if BFS_WITH_LIBURING
// io_uring uses one fd per ring, ioq uses one ring per thread
if (nthreads >= nopenfd - 1) {
nthreads = nopenfd - 2;
@@ -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/color.c b/src/color.c
index f004bf2..81f28bb 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;
@@ -1156,7 +1171,7 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i
}
int count = 0;
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
if (dstrcat(&cfile->buffer, " ") != 0) {
return -1;
}
@@ -1182,7 +1197,6 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i
attr(printf(2, 0))
static int cvbuff(CFILE *cfile, const char *format, va_list args) {
const struct colors *colors = cfile->colors;
- int error = errno;
// Color specifier (e.g. ${blu}) state
struct esc_seq **esc;
@@ -1240,12 +1254,6 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
}
break;
- case 'm':
- if (dstrcat(&cfile->buffer, xstrerror(error)) != 0) {
- return -1;
- }
- break;
-
case 'p':
switch (*++i) {
case 'q':
@@ -1390,3 +1398,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..8582eb3 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. */
@@ -85,7 +87,6 @@ int cfclose(CFILE *cfile);
* %g: A double
* %s: A string
* %zu: A size_t
- * %m: strerror(errno)
* %pq: A shell-escaped string, like bash's printf %q
* %pQ: A TTY-escaped string.
* %pF: A colored file name, from a const struct BFTW * argument
@@ -108,4 +109,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 aa73b35..0f619c2 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;
@@ -53,6 +56,7 @@ struct bfs_ctx *bfs_ctx_new(void) {
goto fail;
}
ctx->cur_nofile = ctx->orig_nofile;
+ ctx->raise_nofile = true;
ctx->users = bfs_users_new();
if (!ctx->users) {
@@ -98,13 +102,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 +135,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,9 +179,9 @@ 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: %s.\n", path, errstr());
} else if (cfile == ctx->cout) {
- bfs_error(ctx, "(standard output): %m.\n");
+ bfs_error(ctx, "(standard output): %s.\n", errstr());
}
}
@@ -188,30 +211,47 @@ 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;
+ 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 +269,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, "%pq: %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/ctx.h b/src/ctx.h
index fc3020c..b28a63c 100644
--- a/src/ctx.h
+++ b/src/ctx.h
@@ -102,6 +102,8 @@ struct bfs_ctx {
struct rlimit orig_nofile;
/** The current RLIMIT_NOFILE limits. */
struct rlimit cur_nofile;
+ /** Whether the fd limit should be raised. */
+ bool raise_nofile;
/** The current time. */
struct timespec now;
diff --git a/src/diag.c b/src/diag.c
index deb6f26..ccafd98 100644
--- a/src/diag.c
+++ b/src/diag.c
@@ -64,7 +64,7 @@ const char *debug_flag_name(enum debug_flags flag) {
}
void bfs_perror(const struct bfs_ctx *ctx, const char *str) {
- bfs_error(ctx, "%s: %m.\n", str);
+ bfs_error(ctx, "%s: %s.\n", str, errstr());
}
void bfs_error(const struct bfs_ctx *ctx, const char *format, ...) {
@@ -91,19 +91,12 @@ bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *for
}
void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args) {
- int error = errno;
-
bfs_error_prefix(ctx);
-
- errno = error;
cvfprintf(ctx->cerr, format, args);
}
bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args) {
- int error = errno;
-
if (bfs_warning_prefix(ctx)) {
- errno = error;
cvfprintf(ctx->cerr, format, args);
return true;
} else {
@@ -112,10 +105,7 @@ bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args) {
}
bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args) {
- int error = errno;
-
if (bfs_debug_prefix(ctx, flag)) {
- errno = error;
cvfprintf(ctx->cerr, format, args);
return true;
} else {
@@ -169,7 +159,7 @@ static bool highlight_expr_recursive(const struct bfs_ctx *ctx, const struct bfs
}
}
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
ret |= highlight_expr_recursive(ctx, child, args);
}
diff --git a/src/diag.h b/src/diag.h
index 2b13609..da22f3a 100644
--- a/src/diag.h
+++ b/src/diag.h
@@ -9,6 +9,7 @@
#define BFS_DIAG_H
#include "prelude.h"
+#include "bfstd.h"
#include <stdarg.h>
/**
@@ -55,6 +56,15 @@ void bfs_diagf(const struct bfs_loc *loc, const char *format, ...);
#define bfs_diag(...) bfs_diagf(bfs_location(), __VA_ARGS__)
/**
+ * Print a diagnostic message including the last error.
+ */
+#define bfs_ediag(...) \
+ bfs_ediag_("" __VA_ARGS__, 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 +73,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__, 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 +110,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__, "", 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 b5bf3d3..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) {
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 49028b7..8490bd4 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -7,6 +7,7 @@
#include "prelude.h"
#include "eval.h"
+#include "atomic.h"
#include "bar.h"
#include "bfstd.h"
#include "bftw.h"
@@ -22,6 +23,7 @@
#include "printf.h"
#include "pwcache.h"
#include "sanity.h"
+#include "sighook.h"
#include "stat.h"
#include "trie.h"
#include "xregex.h"
@@ -38,6 +40,7 @@
#include <strings.h>
#include <sys/resource.h>
#include <sys/types.h>
+#include <termios.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
@@ -63,7 +66,6 @@ static void eval_error(struct bfs_eval *state, const char *format, ...) {
// By POSIX, any errors should be accompanied by a non-zero exit status
*state->ret = EXIT_FAILURE;
- int error = errno;
const struct bfs_ctx *ctx = state->ctx;
CFILE *cerr = ctx->cerr;
@@ -71,7 +73,6 @@ static void eval_error(struct bfs_eval *state, const char *format, ...) {
va_list args;
va_start(args, format);
- errno = error;
cvfprintf(cerr, format, args);
va_end(args);
}
@@ -90,7 +91,7 @@ static bool eval_should_ignore(const struct bfs_eval *state, int error) {
*/
static void eval_report_error(struct bfs_eval *state) {
if (!eval_should_ignore(state, errno)) {
- eval_error(state, "%m.\n");
+ eval_error(state, "%s.\n", errstr());
}
}
@@ -99,9 +100,9 @@ static void eval_report_error(struct bfs_eval *state) {
*/
static void eval_io_error(const struct bfs_expr *expr, struct bfs_eval *state) {
if (expr->path) {
- eval_error(state, "'%s': %m.\n", expr->path);
+ eval_error(state, "'%s': %s.\n", expr->path, errstr());
} else {
- eval_error(state, "(standard output): %m.\n");
+ eval_error(state, "(standard output): %s.\n", errstr());
}
// Don't report the error again in bfs_ctx_free()
@@ -228,7 +229,7 @@ bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state) {
static const struct timespec *eval_stat_time(const struct bfs_stat *statbuf, enum bfs_stat_field field, struct bfs_eval *state) {
const struct timespec *ret = bfs_stat_time(statbuf, field);
if (!ret) {
- eval_error(state, "Couldn't get file %s: %m.\n", bfs_stat_field_name(field));
+ eval_error(state, "Couldn't get file %s: %s.\n", bfs_stat_field_name(field), errstr());
}
return ret;
}
@@ -398,13 +399,13 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c
if (expr->eval_fn == eval_exec) {
if (bfs_exec_finish(expr->exec) != 0) {
if (errno != 0) {
- bfs_error(ctx, "%s %s: %m.\n", expr->argv[0], expr->argv[1]);
+ bfs_error(ctx, "%s %s: %s.\n", expr->argv[0], expr->argv[1], errstr());
}
ret = -1;
}
}
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
if (eval_exec_finish(child, ctx) != 0) {
ret = -1;
}
@@ -419,7 +420,7 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c
bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state) {
bool ret = bfs_exec(expr->exec, state->ftwbuf) == 0;
if (errno != 0) {
- eval_error(state, "%s %s: %m.\n", expr->argv[0], expr->argv[1]);
+ eval_error(state, "%s %s: %s.\n", expr->argv[0], expr->argv[1], errstr());
}
return ret;
}
@@ -902,7 +903,7 @@ bool eval_regex(const struct bfs_expr *expr, struct bfs_eval *state) {
eval_error(state, "%s.\n", str);
free(str);
} else {
- eval_error(state, "bfs_regerror(): %m.\n");
+ eval_error(state, "bfs_regerror(): %s.\n", errstr());
}
}
@@ -1020,7 +1021,7 @@ static int eval_gettime(struct bfs_eval *state, struct timespec *ts) {
#ifdef BFS_CLOCK
int ret = clock_gettime(BFS_CLOCK, ts);
if (ret != 0) {
- bfs_warning(state->ctx, "%pP: clock_gettime(): %m.\n", state->ftwbuf);
+ bfs_warning(state->ctx, "%pP: clock_gettime(): %s.\n", state->ftwbuf, errstr());
}
return ret;
#else
@@ -1091,7 +1092,7 @@ bool eval_not(const struct bfs_expr *expr, struct bfs_eval *state) {
* Evaluate a conjunction.
*/
bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state) {
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
if (!eval_expr(child, state) || state->quit) {
return false;
}
@@ -1104,7 +1105,7 @@ bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state) {
* Evaluate a disjunction.
*/
bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state) {
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
if (eval_expr(child, state) || state->quit) {
return true;
}
@@ -1119,7 +1120,7 @@ bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state) {
bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state) {
bool ret uninit(false);
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
ret = eval_expr(child, state);
if (state->quit) {
break;
@@ -1270,7 +1271,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);
@@ -1375,6 +1376,18 @@ struct callback_args {
struct bfs_bar *bar;
/** The time of the last status update. */
struct timespec last_status;
+ /** SIGINFO hook. */
+ struct sighook *info_hook;
+ /** Number of times SIGINFO was caught (even: hide; odd: show). */
+ atomic size_t info_count;
+
+#ifdef NOKERNINFO
+ /** atsigexit() hook. */
+ struct sighook *exit_hook;
+ /** Whether to unset NOKERNINFO later. */
+ bool clear_nokerninfo;
+#endif
+
/** The number of files visited so far. */
size_t count;
@@ -1401,6 +1414,20 @@ static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) {
state.ret = &args->ret;
state.quit = false;
+ // Check whether SIGINFO was delivered and show/hide the bar
+ bool status = load(&args->info_count, relaxed) % 2;
+ if (status && !args->bar) {
+ args->bar = bfs_bar_show();
+ if (!args->bar) {
+ // Don't keep trying
+ fetch_sub(&args->info_count, 1, relaxed);
+ bfs_warning(ctx, "Couldn't show status bar: %s.\n", errstr());
+ }
+ } else if (!status && args->bar) {
+ bfs_bar_hide(args->bar);
+ args->bar = NULL;
+ }
+
if (args->bar) {
eval_status(&state, args->bar, &args->last_status, args->count);
}
@@ -1464,10 +1491,104 @@ done:
return state.action;
}
+/** Show/hide the bar in response to SIGINFO. */
+static void eval_siginfo(int sig, siginfo_t *info, void *ptr) {
+ struct callback_args *args = ptr;
+ fetch_add(&args->info_count, 1, relaxed);
+}
+
+#ifdef NOKERNINFO
+
+/** Reset NOKERNINFO. */
+static void eval_clear_nokerninfo(void) {
+ int tty = open_cterm(O_RDWR | O_CLOEXEC);
+ if (tty < 0) {
+ return;
+ }
+
+ // Re-read the tty attributes, in case they changed due to -exec stty
+ struct termios tc;
+ if (tcgetattr(tty, &tc) != 0) {
+ goto done;
+ }
+
+ if (tc.c_lflag & NOKERNINFO) {
+ tc.c_lflag &= ~NOKERNINFO;
+ tcsetattr(tty, TCSANOW, &tc);
+ }
+
+done:
+ xclose(tty);
+}
+
+/** atsigexit() hook. */
+static void eval_sigexit(int sig, siginfo_t *info, void *ptr) {
+ struct callback_args *args = ptr;
+ if (args->clear_nokerninfo) {
+ eval_clear_nokerninfo();
+ }
+}
+
+#endif // NOKERNINFO
+
+/** Install the SIGINFO hook. */
+static void eval_hook_siginfo(struct callback_args *args) {
+#ifdef SIGINFO
+ int sig = SIGINFO;
+#else
+ int sig = SIGUSR1;
+#endif
+ args->info_hook = sighook(sig, eval_siginfo, args, SH_CONTINUE);
+ if (!args->info_hook) {
+ return;
+ }
+
+#ifdef NOKERNINFO
+ // Disable the kernel's own SIGINFO message
+ int tty = open_cterm(O_RDWR | O_CLOEXEC);
+ if (tty < 0) {
+ return;
+ }
+
+ struct termios tc;
+ if (tcgetattr(tty, &tc) != 0) {
+ goto done;
+ }
+
+ if (tc.c_lflag & NOKERNINFO) {
+ goto done;
+ }
+
+ tc.c_lflag |= NOKERNINFO;
+ if (tcsetattr(tty, TCSANOW, &tc) == 0) {
+ args->clear_nokerninfo = true;
+ args->exit_hook = atsigexit(eval_sigexit, args);
+ }
+
+done:
+ xclose(tty);
+#endif
+}
+
+/** Uninstall the SIGINFO hook. */
+static void eval_unhook_siginfo(struct callback_args *args) {
+ sigunhook(args->info_hook);
+
+#ifdef NOKERNINFO
+ if (args->clear_nokerninfo) {
+ eval_clear_nokerninfo();
+ }
+ sigunhook(args->exit_hook);
+#endif
+}
+
/** Raise RLIMIT_NOFILE if possible, and return the new limit. */
static int raise_fdlimit(struct bfs_ctx *ctx) {
rlim_t cur = ctx->orig_nofile.rlim_cur;
rlim_t max = ctx->orig_nofile.rlim_max;
+ if (!ctx->raise_nofile) {
+ max = cur;
+ }
rlim_t target = 64 << 10;
if (rlim_cmp(target, max) > 0) {
@@ -1596,7 +1717,7 @@ static bool eval_must_buffer(const struct bfs_expr *expr) {
return true;
}
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
if (eval_must_buffer(child)) {
return true;
}
@@ -1618,10 +1739,13 @@ int bfs_eval(struct bfs_ctx *ctx) {
if (ctx->status) {
args.bar = bfs_bar_show();
- if (!args.bar) {
- bfs_warning(ctx, "Couldn't show status bar: %m.\n\n");
+ if (args.bar) {
+ atomic_init(&args.info_count, 1);
+ } else {
+ bfs_warning(ctx, "Couldn't show status bar: %s.\n\n", errstr());
}
}
+ eval_hook_siginfo(&args);
struct trie seen;
if (ctx->unique) {
@@ -1690,6 +1814,7 @@ int bfs_eval(struct bfs_ctx *ctx) {
trie_destroy(&seen);
}
+ eval_unhook_siginfo(&args);
bfs_bar_hide(args.bar);
return args.ret;
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/expr.h b/src/expr.h
index 7bcace7..60b298d 100644
--- a/src/expr.h
+++ b/src/expr.h
@@ -244,4 +244,10 @@ bool bfs_expr_cmp(const struct bfs_expr *expr, long long n);
*/
void bfs_expr_clear(struct bfs_expr *expr);
+/**
+ * Iterate over the children of an expression.
+ */
+#define for_expr(child, expr) \
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next)
+
#endif // BFS_EXPR_H
diff --git a/src/fsade.c b/src/fsade.c
index d56fb07..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;
}
diff --git a/src/fsade.h b/src/fsade.h
index eefef9f..4465017 100644
--- a/src/fsade.h
+++ b/src/fsade.h
@@ -13,9 +13,9 @@
#define BFS_CAN_CHECK_ACL (BFS_HAS_ACL_GET_FILE || BFS_HAS_ACL_TRIVIAL)
-#define BFS_CAN_CHECK_CAPABILITIES BFS_USE_LIBCAP
+#define BFS_CAN_CHECK_CAPABILITIES BFS_WITH_LIBCAP
-#define BFS_CAN_CHECK_CONTEXT BFS_USE_LIBSELINUX
+#define BFS_CAN_CHECK_CONTEXT BFS_WITH_LIBSELINUX
#define BFS_CAN_CHECK_XATTRS (BFS_USE_SYS_EXTATTR_H || BFS_USE_SYS_XATTR_H)
diff --git a/src/ioq.c b/src/ioq.c
index 43a1b35..4057a5e 100644
--- a/src/ioq.c
+++ b/src/ioq.c
@@ -135,7 +135,7 @@
#include <stdlib.h>
#include <sys/stat.h>
-#if BFS_USE_LIBURING
+#if BFS_WITH_LIBURING
# include <liburing.h>
#endif
@@ -459,7 +459,7 @@ static void ioq_batch_push(struct ioqq *ioqq, struct ioq_batch *batch, struct io
/** Sentinel stop command. */
static struct ioq_ent IOQ_STOP;
-#if BFS_USE_LIBURING
+#if BFS_WITH_LIBURING
/**
* Supported io_uring operations.
*/
@@ -477,7 +477,7 @@ struct ioq_thread {
/** Pointer back to the I/O queue. */
struct ioq *parent;
-#if BFS_USE_LIBURING
+#if BFS_WITH_LIBURING
/** io_uring instance. */
struct io_uring ring;
/** Any error that occurred initializing the ring. */
@@ -497,7 +497,7 @@ struct ioq {
/** ioq_ent arena. */
struct arena ents;
-#if BFS_USE_LIBURING && BFS_USE_STATX
+#if BFS_WITH_LIBURING && BFS_USE_STATX
/** struct statx arena. */
struct arena xbufs;
#endif
@@ -559,7 +559,7 @@ static void ioq_dispatch_sync(struct ioq *ioq, struct ioq_ent *ent) {
ent->result = -ENOSYS;
}
-#if BFS_USE_LIBURING
+#if BFS_WITH_LIBURING
/** io_uring worker state. */
struct ioq_ring_state {
@@ -775,7 +775,7 @@ static void ioq_ring_work(struct ioq_thread *thread) {
}
}
-#endif // BFS_USE_LIBURING
+#endif // BFS_WITH_LIBURING
/** Synchronous syscall loop. */
static void ioq_sync_work(struct ioq_thread *thread) {
@@ -811,7 +811,7 @@ static void ioq_sync_work(struct ioq_thread *thread) {
static void *ioq_work(void *ptr) {
struct ioq_thread *thread = ptr;
-#if BFS_USE_LIBURING
+#if BFS_WITH_LIBURING
if (thread->ring_err == 0) {
ioq_ring_work(thread);
return NULL;
@@ -824,7 +824,7 @@ static void *ioq_work(void *ptr) {
/** Initialize io_uring thread state. */
static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) {
-#if BFS_USE_LIBURING
+#if BFS_WITH_LIBURING
struct ioq_thread *prev = NULL;
if (thread > ioq->threads) {
prev = thread - 1;
@@ -890,7 +890,7 @@ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) {
/** Destroy an io_uring. */
static void ioq_ring_exit(struct ioq_thread *thread) {
-#if BFS_USE_LIBURING
+#if BFS_WITH_LIBURING
if (thread->ring_err == 0) {
io_uring_queue_exit(&thread->ring);
}
@@ -926,7 +926,7 @@ struct ioq *ioq_create(size_t depth, size_t nthreads) {
ioq->depth = depth;
ARENA_INIT(&ioq->ents, struct ioq_ent);
-#if BFS_USE_LIBURING && BFS_USE_STATX
+#if BFS_WITH_LIBURING && BFS_USE_STATX
ARENA_INIT(&ioq->xbufs, struct statx);
#endif
@@ -1036,7 +1036,7 @@ int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags fla
args->flags = flags;
args->buf = buf;
-#if BFS_USE_LIBURING && BFS_USE_STATX
+#if BFS_WITH_LIBURING && BFS_USE_STATX
args->xbuf = arena_alloc(&ioq->xbufs);
if (!args->xbuf) {
ioq_free(ioq, ent);
@@ -1060,7 +1060,7 @@ void ioq_free(struct ioq *ioq, struct ioq_ent *ent) {
bfs_assert(ioq->size > 0);
--ioq->size;
-#if BFS_USE_LIBURING && BFS_USE_STATX
+#if BFS_WITH_LIBURING && BFS_USE_STATX
if (ent->op == IOQ_STAT && ent->stat.xbuf) {
arena_free(&ioq->xbufs, ent->stat.xbuf);
}
@@ -1091,7 +1091,7 @@ void ioq_destroy(struct ioq *ioq) {
ioqq_destroy(ioq->ready);
ioqq_destroy(ioq->pending);
-#if BFS_USE_LIBURING && BFS_USE_STATX
+#if BFS_WITH_LIBURING && BFS_USE_STATX
arena_destroy(&ioq->xbufs);
#endif
arena_destroy(&ioq->ents);
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 883d598..6704a46 100644
--- a/src/opt.c
+++ b/src/opt.c
@@ -1088,7 +1088,7 @@ static struct bfs_expr *annotate_and(struct bfs_opt *opt, struct bfs_expr *expr,
expr->cost = 0.0;
expr->probability = 1.0;
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
expr->pure &= child->pure;
expr->always_true &= child->always_true;
expr->always_false |= child->always_false;
@@ -1107,7 +1107,7 @@ static struct bfs_expr *annotate_or(struct bfs_opt *opt, struct bfs_expr *expr,
expr->cost = 0.0;
float false_prob = 1.0;
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
expr->pure &= child->pure;
expr->always_true |= child->always_true;
expr->always_false &= child->always_false;
@@ -1124,7 +1124,7 @@ static struct bfs_expr *annotate_comma(struct bfs_opt *opt, struct bfs_expr *exp
expr->pure = true;
expr->cost = 0.0;
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
expr->pure &= child->pure;
expr->always_true = child->always_true;
expr->always_false = child->always_false;
@@ -1790,7 +1790,7 @@ static struct bfs_expr *data_flow_leave(struct bfs_opt *opt, struct bfs_expr *ex
if (df_is_bottom(&opt->after_false)) {
if (!expr->pure) {
expr->always_true = true;
- expr->probability = 0.0;
+ expr->probability = 1.0;
} else if (expr->eval_fn != eval_true) {
opt_warning(opt, expr, "This expression is always true.\n\n");
opt_debug(opt, "pure, always true\n");
@@ -1919,7 +1919,7 @@ static struct bfs_expr *simplify_not(struct bfs_opt *opt, struct bfs_expr *expr,
static struct bfs_expr *lift_andor_not(struct bfs_opt *opt, struct bfs_expr *expr) {
// Only lift negations if it would reduce the number of (-not) expressions
size_t added = 0, removed = 0;
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
if (child->eval_fn == eval_not) {
++removed;
} else {
@@ -1968,7 +1968,7 @@ static struct bfs_expr *first_ignorable(struct bfs_opt *opt, struct bfs_expr *ex
}
struct bfs_expr *ret = NULL;
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
if (!child->pure) {
ret = NULL;
} else if (!ret) {
@@ -2176,17 +2176,20 @@ static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) {
return expr;
}
-/** Estimate the odds of an expression calling stat(). */
-static float expr_stat_odds(struct bfs_expr *expr) {
- if (expr->calls_stat) {
+/** An expression predicate. */
+typedef bool expr_pred(const struct bfs_expr *expr);
+
+/** Estimate the odds that a matching expression will be evaluated. */
+static float estimate_odds(const struct bfs_expr *expr, expr_pred *pred) {
+ if (pred(expr)) {
return 1.0;
}
- float nostat_odds = 1.0;
+ float nonmatch_odds = 1.0;
float reached_odds = 1.0;
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
- float child_odds = expr_stat_odds(child);
- nostat_odds *= 1.0 - reached_odds * child_odds;
+ for_expr (child, expr) {
+ float child_odds = estimate_odds(child, pred);
+ nonmatch_odds *= 1.0 - reached_odds * child_odds;
if (expr->eval_fn == eval_and) {
reached_odds *= child->probability;
@@ -2195,7 +2198,12 @@ static float expr_stat_odds(struct bfs_expr *expr) {
}
}
- return 1.0 - nostat_odds;
+ return 1.0 - nonmatch_odds;
+}
+
+/** Whether an expression calls stat(). */
+static bool calls_stat(const struct bfs_expr *expr) {
+ return expr->calls_stat;
}
/** Estimate the odds of calling stat(). */
@@ -2204,15 +2212,20 @@ static float estimate_stat_odds(struct bfs_ctx *ctx) {
return 1.0;
}
- float nostat_odds = 1.0 - expr_stat_odds(ctx->exclude);
+ float nostat_odds = 1.0 - estimate_odds(ctx->exclude, calls_stat);
float reached_odds = 1.0 - ctx->exclude->probability;
- float expr_odds = expr_stat_odds(ctx->expr);
+ float expr_odds = estimate_odds(ctx->expr, calls_stat);
nostat_odds *= 1.0 - reached_odds * expr_odds;
return 1.0 - nostat_odds;
}
+/** Matches -(exec|ok) ... \; */
+static bool single_exec(const struct bfs_expr *expr) {
+ return expr->eval_fn == eval_exec && !(expr->exec->flags & BFS_EXEC_MULTI);
+}
+
int bfs_optimize(struct bfs_ctx *ctx) {
bfs_ctx_dump(ctx, DEBUG_OPT);
@@ -2291,6 +2304,17 @@ int bfs_optimize(struct bfs_ctx *ctx) {
opt_leave(&opt, "eager stat cost: ${ylw}%g${rs}\n", eager_cost);
}
+#ifndef POSIX_SPAWN_SETRLIMIT
+ // If bfs_spawn_setrlimit() would force us to use fork() over
+ // posix_spawn(), the extra cost may outweigh the benefit of a
+ // higher RLIMIT_NOFILE
+ float single_exec_odds = estimate_odds(ctx->expr, single_exec);
+ if (single_exec_odds >= 0.5) {
+ opt_enter(&opt, "single ${blu}-exec${rs} odds: ${ylw}%g${rs}\n", single_exec_odds);
+ ctx->raise_nofile = false;
+ opt_leave(&opt, "not raising RLIMIT_NOFILE\n");
+ }
+#endif
}
opt_leave(&opt, NULL);
diff --git a/src/parse.c b/src/parse.c
index a1155c0..539aa05 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -381,7 +381,7 @@ static int expr_open(struct bfs_parser *parser, struct bfs_expr *expr, const cha
return 0;
fail:
- parse_expr_error(parser, expr, "%m.\n");
+ parse_expr_error(parser, expr, "%s.\n", errstr());
if (cfile) {
cfclose(cfile);
} else if (file) {
@@ -401,7 +401,7 @@ static int stat_arg(const struct bfs_parser *parser, char **arg, struct bfs_stat
int ret = bfs_stat(AT_FDCWD, *arg, flags, sb);
if (ret != 0) {
- parse_argv_error(parser, arg, 1, "%m.\n");
+ parse_argv_error(parser, arg, 1, "%s.\n", errstr());
}
return ret;
}
@@ -1344,7 +1344,7 @@ static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, i
file = xfopen(from, O_RDONLY | O_CLOEXEC);
}
if (!file) {
- parse_expr_error(parser, expr, "%m.\n");
+ parse_expr_error(parser, expr, "%s.\n", errstr());
return NULL;
}
@@ -1509,7 +1509,7 @@ static struct bfs_expr *parse_fstype(struct bfs_parser *parser, int arg1, int ar
}
if (!bfs_ctx_mtab(parser->ctx)) {
- parse_expr_error(parser, expr, "Couldn't parse the mount table: %m.\n");
+ parse_expr_error(parser, expr, "Couldn't parse the mount table: %s.\n", errstr());
return NULL;
}
@@ -1534,7 +1534,7 @@ static struct bfs_expr *parse_group(struct bfs_parser *parser, int arg1, int arg
return NULL;
}
} else if (errno) {
- parse_expr_error(parser, expr, "%m.\n");
+ parse_expr_error(parser, expr, "%s.\n", errstr());
return NULL;
} else {
parse_expr_error(parser, expr, "No such group.\n");
@@ -1577,7 +1577,7 @@ static struct bfs_expr *parse_user(struct bfs_parser *parser, int arg1, int arg2
return NULL;
}
} else if (errno) {
- parse_expr_error(parser, expr, "%m.\n");
+ parse_expr_error(parser, expr, "%s.\n", errstr());
return NULL;
} else {
parse_expr_error(parser, expr, "No such user.\n");
@@ -1737,7 +1737,7 @@ static int parse_reftime(const struct bfs_parser *parser, struct bfs_expr *expr)
if (xgetdate(expr->argv[1], &expr->reftime) == 0) {
return 0;
} else if (errno != EINVAL) {
- parse_expr_error(parser, expr, "%m.\n");
+ parse_expr_error(parser, expr, "%s.\n", errstr());
return -1;
}
@@ -2258,7 +2258,7 @@ static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int
parser->regex_type = BFS_REGEX_POSIX_BASIC;
} else if (strcmp(type, "posix-extended") == 0) {
parser->regex_type = BFS_REGEX_POSIX_EXTENDED;
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
} else if (strcmp(type, "emacs") == 0) {
parser->regex_type = BFS_REGEX_EMACS;
} else if (strcmp(type, "grep") == 0) {
@@ -2280,7 +2280,7 @@ list_types:
cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n");
cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n");
cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n");
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n");
cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n");
#endif
@@ -3458,7 +3458,7 @@ static void dump_expr_multiline(const struct bfs_ctx *ctx, enum debug_flags flag
++rparens;
} else {
cfprintf(ctx->cerr, "(${red}%s${rs}\n", expr->argv[0]);
- for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ for_expr (child, expr) {
int parens = child->next ? 0 : rparens + 1;
dump_expr_multiline(ctx, flag, child, indent + 1, parens);
}
diff --git a/src/prelude.h b/src/prelude.h
index ddeacbd..3521fe8 100644
--- a/src/prelude.h
+++ b/src/prelude.h
@@ -16,7 +16,8 @@
#define C17 201710L
#define C23 202311L
-#include <stddef.h>
+// Get the static_assert() definition as well as __GLIBC__
+#include <assert.h>
#if __STDC_VERSION__ < C23
# include <stdalign.h>
@@ -127,21 +128,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
/**
@@ -168,21 +154,6 @@ 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)
diff --git a/src/printf.c b/src/printf.c
index f8428f7..be09ebd 100644
--- a/src/printf.c
+++ b/src/printf.c
@@ -709,9 +709,9 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
case '#':
case '0':
case '+':
+ case ' ':
must_be_numeric = true;
fallthru;
- case ' ':
case '-':
if (strchr(fmt.str, c)) {
bfs_expr_error(ctx, expr);
diff --git a/src/sighook.c b/src/sighook.c
new file mode 100644
index 0000000..ece8147
--- /dev/null
+++ b/src/sighook.c
@@ -0,0 +1,604 @@
+// 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) {
+ if (!hook) {
+ return;
+ }
+
+ 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/trie.c b/src/trie.c
index 808953e..7504d53 100644
--- a/src/trie.c
+++ b/src/trie.c
@@ -221,7 +221,8 @@ struct trie_leaf *trie_find_str(const struct trie *trie, const char *key) {
return trie_find_mem(trie, key, strlen(key) + 1);
}
-struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length) {
+trie_clones
+static struct trie_leaf *trie_find_mem_impl(const struct trie *trie, const void *key, size_t length) {
struct trie_leaf *rep = trie_representative(trie, key, length);
if (rep && rep->length == length && memcmp(rep->key, key, length) == 0) {
return rep;
@@ -230,7 +231,12 @@ struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t
}
}
-struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) {
+struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length) {
+ return trie_find_mem_impl(trie, key, length);
+}
+
+trie_clones
+static struct trie_leaf *trie_find_postfix_impl(const struct trie *trie, const char *key) {
size_t length = strlen(key);
struct trie_leaf *rep = trie_representative(trie, key, length + 1);
if (rep && rep->length >= length && memcmp(rep->key, key, length) == 0) {
@@ -240,6 +246,10 @@ struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) {
}
}
+struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) {
+ return trie_find_postfix_impl(trie, key);
+}
+
/**
* Find a leaf that may end at the current node.
*/
diff --git a/src/xregex.c b/src/xregex.c
index c2711bc..484f099 100644
--- a/src/xregex.c
+++ b/src/xregex.c
@@ -13,7 +13,7 @@
#include <stdlib.h>
#include <string.h>
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
# include <langinfo.h>
# include <oniguruma.h>
#else
@@ -21,7 +21,7 @@
#endif
struct bfs_regex {
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
unsigned char *pattern;
OnigRegex impl;
int err;
@@ -32,7 +32,7 @@ struct bfs_regex {
#endif
};
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
static int bfs_onig_status;
static OnigEncoding bfs_onig_enc;
@@ -121,7 +121,7 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ
return -1;
}
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
// onig_error_code_to_str() says
//
// don't call this after the pattern argument of onig_new() is freed
@@ -204,7 +204,7 @@ fail:
int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags flags) {
size_t len = strlen(str);
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
const unsigned char *ustr = (const unsigned char *)str;
const unsigned char *end = ustr + len;
@@ -263,7 +263,7 @@ int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags
void bfs_regfree(struct bfs_regex *regex) {
if (regex) {
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
onig_free(regex->impl);
free(regex->pattern);
#else
@@ -278,7 +278,7 @@ char *bfs_regerror(const struct bfs_regex *regex) {
return strdup(xstrerror(ENOMEM));
}
-#if BFS_USE_ONIGURUMA
+#if BFS_WITH_ONIGURUMA
unsigned char *str = malloc(ONIG_MAX_ERROR_MESSAGE_LEN);
if (str) {
onig_error_code_to_str(str, regex->err, &regex->einfo);
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/bfs/printf_invalid_flag.sh b/tests/bfs/printf_invalid_flag.sh
new file mode 100644
index 0000000..70dfe97
--- /dev/null
+++ b/tests/bfs/printf_invalid_flag.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '% p'
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/color.sh b/tests/color.sh
index 805d2b8..4f4312e 100644
--- a/tests/color.sh
+++ b/tests/color.sh
@@ -35,3 +35,119 @@ color() {
"$@" | "$SED" $'s/\e\\[[^m]*m//g'
fi
}
+
+## Status bar
+
+# Show the terminal status bar
+show_bar() {
+ if [ -z "$TTY" ]; then
+ return 1
+ fi
+
+ # Name the pipe deterministically based on the ttyname, so that concurrent
+ # tests.sh runs on the same terminal (e.g. make -jN check) cooperate
+ local pipe
+ pipe=$(printf '%s' "$TTY" | tr '/' '-')
+ pipe="${TMPDIR:-/tmp}/bfs$pipe.bar"
+
+ if mkfifo "$pipe" 2>/dev/null; then
+ # We won the race, create the background process to manage the bar
+ bar_proc "$pipe" &
+ exec {BAR}>"$pipe"
+ elif [ -p "$pipe" ]; then
+ # We lost the race, connect to the existing process.
+ # There is a small TOCTTOU race here but I don't see how to avoid it.
+ exec {BAR}>"$pipe"
+ else
+ return 1
+ fi
+}
+
+# Print to the terminal status bar
+print_bar() {
+ printf 'PRINT:%d:%s\0' $$ "$1" >&$BAR
+}
+
+# Hide the terminal status bar
+hide_bar() {
+ printf 'HIDE:%d:\0' $$ >&$BAR
+ exec {BAR}>&-
+ unset BAR
+}
+
+# The background process that muxes multiple status bars for one TTY
+bar_proc() {
+ # Read from the pipe, write to the TTY
+ exec <"$1" >"$TTY"
+
+ # Delete the pipe when done
+ defer rm "$1"
+ # Reset the scroll region when done
+ defer printf '\e7\e[r\e8\e[J'
+
+ # Workaround for bash 4: checkwinsize is off by default. We can turn it
+ # on, but we also have to explicitly trigger a foreground job to finish
+ # so that it will update the window size before we use $LINES
+ shopt -s checkwinsize
+ (:)
+
+ BAR_HEIGHT=0
+ resize_bar
+ # Adjust the bar when the TTY size changes
+ trap resize_bar WINCH
+
+ # Map from PID to status bar
+ local -A pid2bar
+
+ # Read commands of the form "OP:PID:STRING\0"
+ while IFS=':' read -r -d '' op pid str; do
+ # Map the pid to a bar, creating a new one if necessary
+ if [ -z "${pid2bar[$pid]:-}" ]; then
+ pid2bar["$pid"]=$((BAR_HEIGHT++))
+ resize_bar
+ fi
+ bar="${pid2bar[$pid]}"
+
+ case "$op" in
+ PRINT)
+ printf '\e7\e[%d;0f\e[K%s\e8' $((TTY_HEIGHT - bar)) "$str"
+ ;;
+ HIDE)
+ bar="${pid2bar[$pid]}"
+ # Delete this status bar
+ unset 'pid2bar[$pid]'
+ # Shift all higher status bars down
+ for i in "${!pid2bar[@]}"; do
+ ibar="${pid2bar[$i]}"
+ if ((ibar > bar)); then
+ pid2bar["$i"]=$((ibar - 1))
+ fi
+ done
+ ((BAR_HEIGHT--))
+ resize_bar
+ ;;
+ esac
+ done
+}
+
+# Resize the status bar
+resize_bar() {
+ # Bash gets $LINES from stderr, so if it's redirected use tput instead
+ TTY_HEIGHT="${LINES:-$(tput lines 2>"$TTY")}"
+
+ if ((BAR_HEIGHT == 0)); then
+ return
+ fi
+
+ # Hide the bars temporarily
+ local seq='\e7\e[r\e8\e[J'
+ # Print \eD (IND) N times to ensure N blank lines at the bottom
+ for ((i = 0; i < BAR_HEIGHT; ++i)); do
+ seq="${seq}\\eD"
+ done
+ # Go back up N lines
+ seq="${seq}\\e[${BAR_HEIGHT}A"
+ # Create the new scroll region
+ seq="${seq}\\e7\\e[;$((TTY_HEIGHT - BAR_HEIGHT))r\\e8\\e[J"
+ printf "$seq"
+}
diff --git a/tests/gnu/printf_flags.sh b/tests/gnu/printf_flags.sh
index 2ef37ad..98e8faa 100644
--- a/tests/gnu/printf_flags.sh
+++ b/tests/gnu/printf_flags.sh
@@ -1 +1 @@
-bfs_diff basic -printf '|%- 10.10p| %+03d %#4m\n'
+bfs_diff basic -printf '|%-10.10p| %+03d % #4m\n'
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/run.sh b/tests/run.sh
index ad9c0be..8c1402d 100644
--- a/tests/run.sh
+++ b/tests/run.sh
@@ -5,23 +5,6 @@
## Running test cases
-# Beginning/end of line escape sequences
-BOL=$'\n'
-EOL=$'\n'
-
-# Update $EOL for the terminal size
-update_eol() {
- # Bash gets $COLUMNS from stderr, so if it's redirected use tput instead
- local cols="${COLUMNS-}"
- if [ -z "$cols" ]; then
- cols=$(tput cols 2>/dev/tty)
- fi
-
- # Put the cursor at the last column, then write a space so the next
- # character will wrap
- EOL=$'\e['"${cols}G "
-}
-
# ERR trap for tests
debug_err() {
local ret=$? line func file
@@ -64,19 +47,19 @@ run_test() {
case $ret in
0)
if ((VERBOSE_TESTS)); then
- color printf "${BOL}${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST"
+ color printf "${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST"
fi
;;
$EX_SKIP)
if ((VERBOSE_SKIPPED || VERBOSE_TESTS)); then
- color printf "${BOL}${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST"
+ color printf "${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST"
fi
;;
*)
if ((!VERBOSE_ERRORS)); then
cat "$TMP/$TEST.err" >&2
fi
- color printf "${BOL}${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST"
+ color printf "${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST"
;;
esac
@@ -112,12 +95,21 @@ reap_test() {
# Wait for a background test to finish
wait_test() {
local pid
- wait -n -ppid
- ret=$?
- if [ -z "${pid:-}" ]; then
- debug "${BASH_SOURCE[0]}" $((LINENO - 3)) "${RED}error $ret${RST}" >&$DUPERR
- exit 1
- fi
+
+ while true; do
+ wait -n -ppid
+ ret=$?
+
+ if [ "${pid:-}" ]; then
+ break
+ elif ((ret > 128)); then
+ # Interrupted by signal
+ continue
+ else
+ debug "${BASH_SOURCE[0]}" $((LINENO - 3)) "${RED}error $ret${RST}" >&$DUPERR
+ exit 1
+ fi
+ done
reap_test $ret
}
@@ -164,35 +156,24 @@ comake() {
exec {READY_PIPE}<&${COPROC[0]} {DONE_PIPE}>&${COPROC[1]}
}
-# Run all the tests
-run_tests() {
- if ((VERBOSE_TESTS)); then
- BOL=''
- elif ((COLOR_STDOUT)); then
- # Carriage return + clear line
- BOL=$'\r\e[K'
-
- # Workaround for bash 4: checkwinsize is off by default. We can turn it
- # on, but we also have to explicitly trigger a foreground job to finish
- # so that it will update the window size before we use $COLUMNS
- shopt -s checkwinsize
- (:)
-
- update_eol
- trap update_eol WINCH
+# Print the current test progess
+progress() {
+ if [ "${BAR:-}" ]; then
+ print_bar "$(printf "$@")"
+ elif ((VERBOSE_TESTS)); then
+ color printf "$@"
fi
+}
+# Run all the tests
+run_tests() {
passed=0
failed=0
skipped=0
ran=0
total=${#TEST_CASES[@]}
- if ((COLOR_STDOUT || VERBOSE_TESTS)); then
- TEST_FMT="${BOL}${YLW}[%3d%%]${RST} ${BLD}%s${RST}${EOL}"
- else
- TEST_FMT="."
- fi
+ TEST_FMT="${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n"
if ((${#MAKE[@]})); then
comake
@@ -201,6 +182,10 @@ run_tests() {
# Turn off set -e (but turn it back on in run_test)
set +e
+ if ((COLOR_STDOUT && !VERBOSE_TESTS)); then
+ show_bar
+ fi
+
for TEST in "${TEST_CASES[@]}"; do
wait_ready
if ((STOP && failed > 0)); then
@@ -208,7 +193,7 @@ run_tests() {
fi
percent=$((100 * ran / total))
- color printf "$TEST_FMT" $percent "$TEST"
+ progress "${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n" $percent "$TEST"
mkdir -p "$TMP/$TEST"
OUT="$TMP/$TEST.out"
@@ -221,7 +206,9 @@ run_tests() {
wait_test
done
- printf "${BOL}"
+ if [ "${BAR:-}" ]; then
+ hide_bar
+ fi
if ((passed > 0)); then
color printf "${GRN}[PASS]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $passed $total
@@ -253,7 +240,6 @@ skip() {
if ((VERBOSE_SKIPPED)); then
caller | {
read -r line file
- printf "${BOL}"
debug "$file" $line "" >&$DUPOUT
}
fi
@@ -430,8 +416,8 @@ make_xattrs() {
EX_DIFF=20
# Detect colored diff support
-if diff --color /dev/null /dev/null &>/dev/null; then
- DIFF="diff --color"
+if ((COLOR_STDERR)) && diff --color=always /dev/null /dev/null &>/dev/null; then
+ DIFF="diff --color=always"
else
DIFF="diff"
fi
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..de95a49 100644
--- a/tests/tests.h
+++ b/tests/tests.h
@@ -9,6 +9,7 @@
#define BFS_TESTS_H
#include "prelude.h"
+#include "bfstd.h"
#include "diag.h"
/** Unit test function type. */
@@ -26,6 +27,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 +58,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__, "", 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..76b72b9 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)
@@ -62,6 +59,15 @@ stdenv() {
# Close stdin so bfs doesn't think we're interactive
# dup() the standard fds for logging even when redirected
exec </dev/null {DUPOUT}>&1 {DUPERR}>&2
+
+ # Get the ttyname
+ if [ -t $DUPOUT ]; then
+ TTY=$(tty <&$DUPOUT)
+ elif [ -t $DUPERR ]; then
+ TTY=$(tty <&$DUPERR)
+ else
+ TTY=
+ fi
}
# Drop root priviliges or bail
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;
}