# Makefile for Solo-C. # # Written for the POSIX make utility (IEEE Std 1003.1, XCU make) and a POSIX # shell (/bin/sh), so it works with any conforming make and no GNU-only # features. Deliberately avoided: # # $(shell ...) not POSIX - pkg-config is run by the recipe shell # ?= not POSIX - defaults are plain "=" and are overridden on # the command line, e.g. make CC=clang CFLAGS=-O1 # .PHONY not POSIX - see the FORCE target at the bottom # target-specific variables, ifeq, wildcard, -include, ... all non-POSIX # $(patsubst ...) not POSIX - so the install directory is resolved by the # recipe shell, see INSTALL_DIR below # # Rule syntax used here: recipe lines start with a TAB, macros are written # $(NAME) (POSIX also accepts ${NAME}), and the only automatic macro used is # $@, the name of the target. Prerequisite names are written out: $< is POSIX # for inference rules only, and "$< is the first prerequisite" in a plain rule # is a GNU extension that even BusyBox make lacks. SHELL = /bin/sh # Standard macros. POSIX defaults CC to c99; Solo-C uses the system compiler # driver "cc" instead, which is the compiler a platform actually ships # (gcc, clang, musl-gcc, ...). Every macro here can be overridden from the # command line: make CC=clang, make CFLAGS=-O1, make LDLIBS=-lcurl. CC = cc CFLAGS = -Wall -Wextra -O2 -std=c99 -march=native LDFLAGS = LDLIBS = LINT = lint LINTFLAGS = LINTLIBS = -lc # Link-time discovery of the shared dependencies. POSIX make cannot run # commands while reading a makefile, so the flags are collected by the shell # that runs the recipe instead of by $(shell ...) while parsing. PKG_CONFIG = pkg-config PKGS = libcurl json-c readline # $$(...) survives expansion of the macro and reaches the shell as $(...). PKG_CFLAGS = $$($(PKG_CONFIG) --cflags $(PKGS)) PKG_LIBS = $$($(PKG_CONFIG) --libs $(PKGS)) # Fully static binary. # # The distro libcurl.a cannot be linked statically on Alpine: it pulls in # libpsl, libbrotli, libzstd and libnghttp2, whose static archives hold LTO # bytecode from GCC 14 that the installed GCC 15 cannot read ("bytecode # stream ... generated with LTO version 14.0 instead of the expected 15.1"), # so psl_*/Brotli*/ZSTD* never resolve. -fno-lto does not help; the objects # are bytecode, not C. # # static-deps/libcurl.a is curl built without those four libraries, leaving # only LTO-free static deps. Regenerate with: make static-deps # # c-ares is built from source by the same script, for two reasons: a minimal # system often has no libcares.a at all (Void's c-ares-devel ships the shared # library only), and where an archive does exist it can carry the same LTO # bytecode as the distro libcurl.a. Building it also pins the version. # # Archive order matters: libcurl.a pulls in ares_* symbols, so libcares.a has to # come after it on the link line. CURL_STATIC = static-deps/libcurl.a CARES_STATIC = static-deps/libcares.a STATIC_LIBS = $(CURL_STATIC) $(CARES_STATIC) -lssl -lcrypto -lz -lidn2 -lunistring \ -lreadline -lncursesw -ljson-c -lm -ldl -pthread solo: main.c $(CC) $(CFLAGS) $(PKG_CFLAGS) $(LDFLAGS) -o $@.new main.c $(PKG_LIBS) $(LDLIBS) strip $@.new mv -f $@.new $@ # Writes solo-static, not a file called "static", so $@ is not used here. static: main.c @for f in $(CURL_STATIC) $(CARES_STATIC); do \ test -f $$f || { echo "missing $$f - run: make static-deps"; exit 1; }; \ done $(CC) $(CFLAGS) $(LDFLAGS) -fno-lto -static -o solo-static.new main.c $(STATIC_LIBS) $(LDLIBS) strip solo-static.new mv -f solo-static.new solo-static static-deps: FORCE sh ./mkstaticdeps.sh # Rebuild both archives from scratch, ignoring what is already there. static-deps-force: FORCE FORCE=1 sh ./mkstaticdeps.sh # Install paths. Every one of these can be set on the command line, so the # install target takes any location you name: # # make install # $(DESTDIR)$(BINDIR) = /usr/local/bin # make install PREFIX=$HOME/.local # ~/.local/bin # make install BINDIR=/opt/solo/bin # an explicit directory # make install DIR=/opt/solo/bin # the same, spelled in one macro # make install DIR=~/.local/bin # a leading ~ is expanded, see below # make install DIR=/usr/bin INSTALL="install -C" # make install DESTDIR=/tmp/pkg DIR=/usr/bin # staged, for packaging # # DIR wins over BINDIR when both are given; DESTDIR is prepended either way, so # "make install DESTDIR=/tmp/pkg DIR=/usr/bin" stages into /tmp/pkg/usr/bin. DESTDIR = PREFIX = /usr/local BINDIR = $(PREFIX)/bin DIR = INSTALL = install # The installation directory, as a shell snippet that prints it on stdout. # # POSIX make has no string functions, so "DIR if set, else BINDIR" and the # leading ~ are handled by the recipe shell instead of by the makefile. Both # install and uninstall use this one snippet, so they always agree on the path. # $$(...) survives expansion of the macro and reaches the shell as $(...). INSTALL_DIR = d="$(DIR)"; \ test -n "$$d" || d="$(BINDIR)"; \ case "$$d" in \ '~') d="$$HOME";; \ '~/'*) d="$$HOME/$$(printf '%s' "$$d" | cut -c3-)";; \ esac; \ printf '%s%s\n' "$(DESTDIR)" "$$d" # Install solo, and solo-static too when it has been built. Prints the location # it used, so a script can see where the binaries went. install: solo FORCE @dir=$$($(INSTALL_DIR)); \ echo "installing into $$dir"; \ $(INSTALL) -d "$$dir"; \ $(INSTALL) -m 755 solo "$$dir/solo"; \ if test -f solo-static; then \ $(INSTALL) -m 755 solo-static "$$dir/solo-static"; \ fi # Install the static binary only; fails if it has not been built yet. install-static: static FORCE @dir=$$($(INSTALL_DIR)); \ echo "installing solo-static into $$dir"; \ $(INSTALL) -d "$$dir"; \ $(INSTALL) -m 755 solo-static "$$dir/solo-static" # Prints the installation directory that install would use, and nothing else. installdir: FORCE @dir=$$($(INSTALL_DIR)); echo "$$dir" # Removes the binaries from the location resolved the same way, so # "make install DIR=~/.local/bin" is undone by # "make uninstall DIR=~/.local/bin". uninstall: FORCE @dir=$$($(INSTALL_DIR)); \ echo "removing from $$dir"; \ rm -f "$$dir/solo" "$$dir/solo-static" clean: FORCE rm -f solo solo-static solo.new solo-static.new # Offline regression tests. No API key, no network: every line sent to the agent # starts with / or !, so nothing reaches the model. Pass another binary as $1. test: solo FORCE sh ./test.sh ./solo # POSIX lint target: runs the lint tool named by the LINT macro. lint: main.c FORCE $(LINT) $(LINTFLAGS) $(LINTLIBS) main.c # POSIX make has no .PHONY special target. Any target that depends on FORCE is # always out of date, because FORCE never creates a file, so its recipe always # runs. Keep this target last: the first target in the file is the default goal. FORCE: @: