# 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)