Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ jobs:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}

# Run this before any other build step. Later steps build into priv/, and
# a stray write there would then go unnoticed.
- name: Build into another PRIV_DIR, leaving the tree unchanged
run: |
make all PRIV_DIR="$RUNNER_TEMP/priv"
test -z "$(git status --porcelain --ignored -- c_src priv)"

- name: Cache deps & build
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
Expand Down Expand Up @@ -182,6 +189,13 @@ jobs:
- run: mix compile
- run: mix test

- name: Link the shepherd as a static PIE
run: |
make all SHEPHERD_STATIC=1 PRIV_DIR=/tmp/static
readelf -h /tmp/static/shepherd | grep -q 'DYN'
if readelf -l /tmp/static/shepherd | grep -q INTERP; then exit 1; fi
/tmp/static/shepherd 2>&1 | grep -q '^usage: shepherd'

deps-latest:
# Simulates consumer dependency resolution: throw away the lockfile and
# build against the newest versions Hex resolves. Non-blocking by design.
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ net_runner-*.tar
priv/*.so
priv/*.dylib
priv/shepherd
c_src/*.o

# Dialyzer
/priv/plts/
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ remaining findings across shepherd/NIF, lib, tests, docs, and CI).

### Fixed

- **The shepherd is compiled with `-fPIE`.** The flag was passed only when
linking, so a compiler that does not produce position-independent code by
default could not link the shepherd.
- **EINTR is retried** in the NIF `read(2)`/`write(2)` loops and shepherd
I/O; a transient signal can no longer permanently wedge a drain loop.
- **Shepherd diagnostics surface.** A spawn-stage `MSG_ERROR` (cgroup
Expand All @@ -46,6 +49,8 @@ remaining findings across shepherd/NIF, lib, tests, docs, and CI).

### Changed

- **The native build writes no object files.** Each binary is compiled and
linked in one step, so `make PRIV_DIR=…` leaves the source tree unchanged.
- **`NetRunner.Process.Nif` renamed to `NetRunner.Nif`** (internal,
`@moduledoc false`, but referenced in tests/benches).
- **Option-validation convention unified.** Malformed options (unknown keys,
Expand All @@ -70,6 +75,8 @@ remaining findings across shepherd/NIF, lib, tests, docs, and CI).

### Added

- **`SHEPHERD_STATIC=1`** links the shepherd as a static PIE (Linux only), for
a release that runs where the build's libc may be absent.
- **`NetRunner.Process.read_batch/3` and `read_stderr_batch/3`.** Read up to
`max_chunks` pipe chunks in one GenServer round trip. A batch never waits
once it has data (it ends at the first EAGAIN), and an EOF/error after ≥1
Expand Down
56 changes: 32 additions & 24 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# Makefile for NetRunner native code
#
# Builds:
# priv/shepherd - persistent child-process shepherd binary
# priv/net_runner_nif - NIF shared library for async I/O
# Builds these files in PRIV_DIR, which defaults to priv/:
# shepherd - persistent child-process shepherd binary
# net_runner_nif - NIF shared library for async I/O
#
# Only a value on the make command line overrides PRIV_DIR. It is assigned
# with = so that an unrelated PRIV_DIR in the environment cannot move the
# output.

PRIV_DIR = priv
C_SRC_DIR = c_src

# Erlang NIF include paths
ERTS_INCLUDE_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts/erts-~ts/include\", [code:root_dir(), erlang:system_info(version)])." -s init stop)
ERL_INTERFACE_INCLUDE_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts\", [code:lib_dir(erl_interface, include)])." -s init stop)
ERL_INTERFACE_LIB_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts\", [code:lib_dir(erl_interface, lib)])." -s init stop)

# Platform detection
UNAME_S := $(shell uname -s)
Expand Down Expand Up @@ -49,23 +51,36 @@ ifeq ($(UNAME_S),Darwin)
# += so packager-provided CFLAGS are honored, not clobbered.
CFLAGS += $(CFLAGS_BASE) -D_DARWIN_C_SOURCE
NIF_LDFLAGS = -dynamiclib -undefined dynamic_lookup
SHEPHERD_LDFLAGS = -fPIE
NIF_EXT = .so
else
CFLAGS += $(CFLAGS_BASE) -D_GNU_SOURCE
NIF_LDFLAGS = -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack
SHEPHERD_LDFLAGS = -fPIE -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack
SHEPHERD_LDFLAGS = -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack
NIF_EXT = .so
endif

# SHEPHERD_STATIC=1 links the shepherd as a static PIE. The NIF does not need
# this, because it uses the libc that the BEAM loads.
ifeq ($(SHEPHERD_STATIC),1)
ifeq ($(UNAME_S),Darwin)
$(error SHEPHERD_STATIC=1 is Linux-only: macOS has no static libc)
endif
ifeq ($(SANITIZE),1)
$(error SHEPHERD_STATIC=1 cannot be combined with SANITIZE=1: the sanitizer runtimes do not link statically)
endif
SHEPHERD_LDFLAGS += -static-pie
endif

ifeq ($(SANITIZE),1)
NIF_LDFLAGS += $(SAN_FLAGS)
SHEPHERD_LDFLAGS += $(SAN_FLAGS)
endif

# -fvisibility=hidden: only nif_init needs to be exported, and ERL_NIF_INIT
# already marks it default-visible.
NIF_CFLAGS = $(CFLAGS) -I$(ERTS_INCLUDE_DIR) -I$(C_SRC_DIR) -fPIC -fvisibility=hidden
NIF_CFLAGS = $(CFLAGS) -I$(ERTS_INCLUDE_DIR) -fPIC -fvisibility=hidden

SHEPHERD_CFLAGS = $(CFLAGS) -fPIE

# Targets
SHEPHERD = $(PRIV_DIR)/shepherd
Expand All @@ -74,14 +89,11 @@ NIF_LIB = $(PRIV_DIR)/net_runner_nif$(NIF_EXT)
SHEPHERD_SRC = $(C_SRC_DIR)/shepherd.c
NIF_SRC = $(C_SRC_DIR)/net_runner_nif.c

SHEPHERD_OBJ = $(C_SRC_DIR)/shepherd.o
NIF_OBJ = $(C_SRC_DIR)/net_runner_nif.o

HEADERS = $(C_SRC_DIR)/protocol.h $(C_SRC_DIR)/utils.h

.PHONY: all clean asan bench bench-perf bench-claims bench-deadlock bench-spawn bench-exec

all: $(PRIV_DIR) $(SHEPHERD) $(NIF_LIB)
all: $(SHEPHERD) $(NIF_LIB)

# Convenience: force a sanitizer rebuild. Same as SANITIZE=1 make clean all.
asan:
Expand All @@ -91,22 +103,18 @@ asan:
$(PRIV_DIR):
mkdir -p $(PRIV_DIR)

# Shepherd binary
$(SHEPHERD): $(SHEPHERD_OBJ)
$(CC) $(SHEPHERD_LDFLAGS) $(LDFLAGS) -o $@ $<

$(SHEPHERD_OBJ): $(SHEPHERD_SRC) $(HEADERS)
$(CC) $(CFLAGS) -I$(C_SRC_DIR) -c -o $@ $<
$(SHEPHERD) $(NIF_LIB): | $(PRIV_DIR)

# NIF shared library
$(NIF_LIB): $(NIF_OBJ)
$(CC) $(NIF_LDFLAGS) $(LDFLAGS) -o $@ $<
# Each binary has one source file, so each rule compiles and links it in one
# command.
$(SHEPHERD): $(SHEPHERD_SRC) $(HEADERS)
$(CC) $(SHEPHERD_CFLAGS) $(SHEPHERD_LDFLAGS) $(LDFLAGS) -o $@ $<

$(NIF_OBJ): $(NIF_SRC) $(HEADERS)
$(CC) $(NIF_CFLAGS) -c -o $@ $<
$(NIF_LIB): $(NIF_SRC) $(HEADERS)
$(CC) $(NIF_CFLAGS) $(NIF_LDFLAGS) $(LDFLAGS) -o $@ $<

clean:
rm -f $(SHEPHERD) $(NIF_LIB) $(SHEPHERD_OBJ) $(NIF_OBJ)
rm -f $(SHEPHERD) $(NIF_LIB)

# --- Benchmarks (repo-only; see bench/README.md) ---
#
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,25 @@ relying on them.
- `make`
- Linux or macOS

### Building for another target

`make` writes the shepherd and NIF to `priv/`, or to the directory given as
`PRIV_DIR` on its command line, and writes nothing else. A release for another
target, such as one that ships a musl BEAM to glibc hosts, can build into its
own `priv`. The NIF must be built against the ERTS that will load it. Run the
build on a Linux host, because the Makefile selects its platform flags from the
host's `uname`.

```sh
make all \
CC="zig cc -target x86_64-linux-musl" \
ERTS_INCLUDE_DIR=/path/to/target/erts-VERSION/include \
PRIV_DIR=/path/to/release/lib/net_runner-VERSION/priv
```

zig cc already links musl targets statically. With gcc or clang and a static
libc, such as musl's, `SHEPHERD_STATIC=1` links the shepherd as a static PIE.

## License

Apache-2.0 — see [LICENSE](LICENSE).
Loading