diff options
| author | Ethan Werner <mail@ethanwerner.com> | 2026-10-10 20:55:33 +0000 |
|---|---|---|
| committer | Ethan Werner <mail@ethanwerner.com> | 2026-10-10 20:56:50 +0000 |
| commit | f8423ebea413605b42de23f5dd06c5479d3fa1dd (patch) | |
| tree | 2612a5eb026c59e7aae01f10a1cf62f2ee43042c /Makefile | |
Diffstat (limited to 'Makefile')
| -rw-r--r-- | Makefile | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..04edb8c --- /dev/null +++ b/Makefile @@ -0,0 +1,250 @@ +# llsh — host-toolchain build with locally built static library archives. +# +# Supported target: x86_64 Linux. The bundled source archives are compiled +# with the host GCC toolchain and linked against the system glibc/static C++ +# runtime. No musl toolchain or musl shim is used. + +SHELL ?= /bin/sh + +ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +VENDOR := $(ROOT)/vendor +VEXT := $(VENDOR)/src/_extract +BUILD := $(VENDOR)/build +ZLIB_D := $(BUILD)/zlib +OPENSSL_D := $(BUILD)/openssl +CURL_D := $(BUILD)/curl +CJSON_D := $(BUILD)/cjson +NLOHMANN_D := $(BUILD)/nlohmann +SYSRT := $(BUILD)/system-runtime +SYSRT_LINKS := $(SYSRT)/linker-scripts +GCC_TRIPLE := $(shell $(CXX) -dumpmachine) +GCC_MAJOR := $(shell $(CXX) -dumpversion | cut -d. -f1) +SYS_GCC_LIBDIR := $(SYSRT)/usr/lib/gcc/$(GCC_TRIPLE)/$(GCC_MAJOR) + +CC ?= gcc +CXX ?= g++ +AR ?= ar +RANLIB ?= ranlib +STATIC ?= 1 + +CFLAGS ?= -D_GNU_SOURCE -std=gnu11 -Os -g -Wall -Wextra \ + -Wno-unused-parameter -ffunction-sections -fdata-sections +CXXFLAGS ?= -D_GNU_SOURCE -std=gnu++20 -Os -g -Wall -Wextra \ + -Wno-unused-parameter -Wno-write-strings \ + -Wno-missing-field-initializers \ + -ffunction-sections -fdata-sections + +CPPFLAGS += -I. -Isrc -Iinclude \ + -I$(ZLIB_D)/include \ + -I$(OPENSSL_D)/include \ + -I$(CURL_D)/include \ + -I$(CJSON_D)/include \ + -I$(NLOHMANN_D)/include + +VLIBS := \ + $(CURL_D)/lib/libcurl.a \ + $(OPENSSL_D)/lib64/libssl.a \ + $(OPENSSL_D)/lib64/libcrypto.a \ + $(ZLIB_D)/lib/libz.a \ + $(CJSON_D)/lib/libcjson.a + +LIBS := -lm -ldl -pthread +ifeq ($(STATIC),1) +LINK_MODE := -static +RUNTIME_TARGET := static-runtime +STATIC_LIB_DIRS := -L$(SYSRT_LINKS) -L$(SYSRT)/usr/lib64 \ + -L$(SYS_GCC_LIBDIR) +else +LINK_MODE := +RUNTIME_TARGET := +STATIC_LIB_DIRS := +endif + +BIN := llsh +MAIN := src/main.cpp +MAIN_OBJ := src/main.o +LIB_SRC_CPP := $(wildcard src/*.cpp) +LIB_SRC_C := src/escape.c +LIB_SRC := $(LIB_SRC_CPP) $(LIB_SRC_C) +LIB_OBJ := $(LIB_SRC_CPP:.cpp=.o) $(LIB_SRC_C:.c=.o) +LIB_OBJ_NO_MAIN := $(filter-out $(MAIN_OBJ),$(LIB_OBJ)) + +TEST_SRC_CPP := $(wildcard tests/test_*.cpp) +TEST_SRC_C := tests/test_frontmatter.c tests/test_dotagents.c \ + tests/test_quine.c tests/test_clio.c +TEST_SRC := $(TEST_SRC_CPP) $(TEST_SRC_C) +TEST_BIN := $(TEST_SRC_CPP:.cpp=) $(TEST_SRC_C:.c=) +LINKED_TESTS := $(TEST_SRC_CPP:.cpp=) + +HEADERS := $(wildcard src/*.h) $(wildcard include/*.h) + +ASAN_CC ?= clang +ASAN_CXX ?= clang++ +ASAN_FLAGS := -fsanitize=address,undefined -fno-omit-frame-pointer +ASANDIR := build-asan +LIB_SRC_NO_MAIN := $(filter-out $(MAIN),$(LIB_SRC)) + +.PHONY: all test valgrind asan clean vendor vendor-deps vendor-clean static-runtime + +all: $(BIN) $(TEST_BIN) + +# --------------------------------------------------------------- dependencies +# Builds the bundled upstream sources with the host compiler. These are +# ordinary glibc-targeting static archives; the final link uses host glibc. +vendor-deps: + @set -e; \ + mkdir -p "$(VEXT)"; \ + for t in $(VENDOR)/src/*.tar.gz; do \ + b=$$(basename $$t .tar.gz); \ + [ -d "$(VEXT)/$$b" ] || tar xzf "$$t" -C "$(VEXT)"; \ + done; \ + if [ ! -f "$(NLOHMANN_D)/include/nlohmann/json.hpp" ]; then \ + mkdir -p "$(NLOHMANN_D)"; \ + cp -r "$(VEXT)/nlohmann-json-3.12.0/include" "$(NLOHMANN_D)/"; \ + fi; \ + if [ ! -f "$(ZLIB_D)/lib/libz.a" ]; then \ + cd "$(VEXT)/zlib-1.3.1"; \ + make distclean >/dev/null 2>&1 || true; \ + CC="$(CC)" CFLAGS="-Os -ffunction-sections -fdata-sections" \ + ./configure --prefix="$(ZLIB_D)" --static >/dev/null; \ + make -j4 >/dev/null; make install >/dev/null; \ + fi; \ + if [ ! -f "$(OPENSSL_D)/lib64/libcrypto.a" ]; then \ + cd "$(VEXT)/openssl-3.3.2"; \ + CC="$(CC)" CFLAGS="-Os -fPIC" AR="$(AR)" RANLIB="$(RANLIB)" \ + ./Configure linux-x86_64 no-shared no-tests no-dso \ + --prefix="$(OPENSSL_D)" >/dev/null; \ + make -j4 >/dev/null; make install_sw >/dev/null; \ + fi; \ + if [ ! -f "$(CJSON_D)/lib/libcjson.a" ]; then \ + mkdir -p "$(CJSON_D)/include/cjson" "$(CJSON_D)/lib"; \ + "$(CC)" -Os -ffunction-sections -fdata-sections -fPIC \ + -c "$(VEXT)/cJSON-1.7.19/cJSON.c" -o "$(CJSON_D)/lib/cJSON.o"; \ + "$(AR)" rcs "$(CJSON_D)/lib/libcjson.a" "$(CJSON_D)/lib/cJSON.o"; \ + "$(RANLIB)" "$(CJSON_D)/lib/libcjson.a"; \ + cp "$(VEXT)/cJSON-1.7.19/cJSON.h" "$(CJSON_D)/include/cjson/"; \ + fi; \ + if [ ! -f "$(CURL_D)/lib/libcurl.a" ]; then \ + cd "$(VEXT)/curl-8.10.1"; \ + CC="$(CC)" CFLAGS="-Os -ffunction-sections -fdata-sections" \ + LDFLAGS="-Wl,--gc-sections" \ + ./configure --host=x86_64-linux --prefix="$(CURL_D)" \ + --disable-shared --enable-static \ + --with-ssl="$(OPENSSL_D)" --with-zlib="$(ZLIB_D)" \ + --disable-ftp --disable-rtsp --disable-dict --disable-telnet \ + --disable-imap --disable-pop3 --disable-smtp --disable-ldap \ + --without-proxy --without-metalink --without-libpsl \ + --without-fish --without-gssapi --without-brotli --without-zstd \ + --without-libssh2 --without-nghttp2 --without-ngtcp2 \ + --without-libidn2 --without-libgsasl >/dev/null; \ + make -j4 >/dev/null; make install >/dev/null; \ + fi + +# Fedora does not install the static system C/C++ runtimes by default. Fetch +# the matching distro packages into the ignored build tree (no system install +# or sudo required) and link against those package archives. +static-runtime: + @set -e; \ + mkdir -p "$(SYSRT)"; \ + if [ ! -f "$(SYSRT)/usr/lib64/libc.a" ] || \ + [ ! -f "$(SYS_GCC_LIBDIR)/libstdc++.a" ]; then \ + glibc_ver=$$(rpm -q --qf '%{VERSION}-%{RELEASE}.%{ARCH}' glibc); \ + gcc_ver=$$(rpm -q --qf '%{VERSION}-%{RELEASE}.%{ARCH}' libstdc++); \ + dnf download --destdir="$(SYSRT)" \ + "glibc-static-$$glibc_ver" "libstdc++-static-$$gcc_ver" >/dev/null; \ + for r in "$(SYSRT)"/*.rpm; do \ + rpm2cpio "$$r" | (cd "$(SYSRT)" && cpio -idm --quiet 2>/dev/null); \ + done; \ + fi; \ + mkdir -p "$(SYSRT_LINKS)"; \ + sed "s#/usr/lib64/#$(SYSRT)/usr/lib64/#g" \ + "$(SYSRT)/usr/lib64/libm.a" > "$(SYSRT_LINKS)/libm.a"; \ + test -f "$(SYSRT)/usr/lib64/libc.a"; \ + test -f "$(SYS_GCC_LIBDIR)/libstdc++.a" + +vendor: vendor-deps + +# ------------------------------------------------------------------- app +$(BIN): $(MAIN_OBJ) $(LIB_OBJ_NO_MAIN) | vendor-deps $(RUNTIME_TARGET) + $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LINK_MODE) $(STATIC_LIB_DIRS) -o $@ $^ \ + $(VLIBS) $(LIBS) -Wl,--gc-sections -s -Wl,--build-id=none + +src/%.o: src/%.cpp $(HEADERS) | vendor-deps + $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@ + +src/%.o: src/%.c $(HEADERS) | vendor-deps + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ + +tests/%.o: tests/%.cpp $(HEADERS) | vendor-deps + $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@ + +tests/%.o: tests/%.c $(HEADERS) | vendor-deps + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ + +# Header-only portability tests. +tests/test_frontmatter: tests/test_frontmatter.o | vendor-deps $(RUNTIME_TARGET) + $(CC) $(CFLAGS) -o $@ $< $(STATIC_LIB_DIRS) $(LINK_MODE) + +tests/test_dotagents: tests/test_dotagents.o | vendor-deps $(RUNTIME_TARGET) + $(CC) $(CFLAGS) -o $@ $< $(STATIC_LIB_DIRS) $(LINK_MODE) + +tests/test_clio: tests/test_clio.o | vendor-deps $(RUNTIME_TARGET) + $(CC) $(CFLAGS) -o $@ $< $(STATIC_LIB_DIRS) $(LINK_MODE) + +tests/test_quine: tests/test_quine.c | vendor-deps $(RUNTIME_TARGET) + $(CC) $(CFLAGS) -o $@ $< $(STATIC_LIB_DIRS) $(LINK_MODE) + +$(LINKED_TESTS): %: %.o $(LIB_OBJ_NO_MAIN) | vendor-deps $(RUNTIME_TARGET) + $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LINK_MODE) $(STATIC_LIB_DIRS) -o $@ \ + $< $(LIB_OBJ_NO_MAIN) $(VLIBS) $(LIBS) -Wl,--gc-sections -s + +test: all + @set -e; for t in $(TEST_BIN); do echo "== $$t"; ./$$t; done; \ + echo "ALL TESTS PASSED" + +valgrind: all + @set -e; \ + for t in $(TEST_BIN); do \ + echo "== $$t"; \ + valgrind --quiet --leak-check=full --error-exitcode=9 \ + --suppressions=valgrind.supp ./$$t >/dev/null; \ + done; \ + echo "VALGRIND CLEAN" + +# The sanitizer executable uses dynamic host runtimes by design; application +# and dependency code are instrumented independently in a scratch directory. +asan: vendor-deps + @rm -rf $(ASANDIR); mkdir -p $(ASANDIR) + @set -e; \ + libs=""; \ + for f in $(LIB_SRC_NO_MAIN); do \ + b=$$(basename $$f); b=$${b%.c}; b=$${b%.cpp}; \ + case $$f in \ + *.cpp) $(ASAN_CXX) $(CXXFLAGS) $(ASAN_FLAGS) $(CPPFLAGS) -c $$f -o $(ASANDIR)/$$b.o ;; \ + *) $(ASAN_CC) $(CFLAGS) $(ASAN_FLAGS) $(CPPFLAGS) -c $$f -o $(ASANDIR)/$$b.o ;; \ + esac; \ + libs="$$libs $(ASANDIR)/$$b.o"; \ + done; \ + for f in $(TEST_SRC); do \ + b=$$(basename $$f); b=$${b%.c}; b=$${b%.cpp}; \ + case $$f in \ + *.cpp) $(ASAN_CXX) $(CXXFLAGS) $(ASAN_FLAGS) $(CPPFLAGS) -c $$f -o $(ASANDIR)/$$b.test.o ;; \ + *) $(ASAN_CC) $(CFLAGS) $(ASAN_FLAGS) $(CPPFLAGS) -c $$f -o $(ASANDIR)/$$b.test.o ;; \ + esac; \ + case $$b in \ + test_clio) $(ASAN_CXX) $(ASAN_FLAGS) -o $(ASANDIR)/$$b.test $(ASANDIR)/$$b.test.o $(LIBS) ;; \ + *) $(ASAN_CXX) $(ASAN_FLAGS) -o $(ASANDIR)/$$b.test $(ASANDIR)/$$b.test.o $$libs $(VLIBS) $(LIBS) ;; \ + esac; \ + echo "== $$b"; \ + ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ + UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ + $(ASANDIR)/$$b.test >/dev/null; \ + done; \ + echo "ASAN CLEAN" + +clean: + rm -f $(LIB_OBJ) $(TEST_BIN) tests/*.o $(BIN) vg.log + rm -rf $(ASANDIR) + +vendor-clean: + rm -rf $(BUILD) $(VEXT) |