commit 6ff61d3c14d3eacb93b08ccc06f0dc8261fd892f
parent 6b2e04f0f93f4b32a33f92ffb37e565f412154e6
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 3 Jun 2026 09:09:52 -0700
build: give libkit.a members unique, path-flattened names
ar stores each archive member under its basename only, so the ~33 sources
that share a basename (the eight link.c, src/{abi,arch,obj,os}/registry.c,
obj/wasm vs arch/wasm emit.c, ...) collided as members in libkit.a. Harmless
for symbol-resolved linking, but it broke ar t/x, name-based member ops, and
linker-map attribution.
Fold each libkit/lang/vendor object basename to its full sub-path
(src/arch/aa64/native.c -> build/lib/arch/aa64/arch_aa64_native.o), keeping
the mirrored build/ subdirs. Member names are now unique by construction.
Since the basename no longer matches the source, the src/lang/vendor compile
rules become generated explicit rules (flatobjs + a per-group target-specific
RULE_CFLAGS so comma-bearing flags like -fsanitize=a,b never pass through
$(call)). driver/ objects are not archived, so they keep simple patterns.
relmap_md.awk now strips the dist_ dir-prefix when sub-splitting dist, since
the dist objects are flattened too.
Diffstat:
3 files changed, 70 insertions(+), 33 deletions(-)
diff --git a/Makefile b/Makefile
@@ -57,53 +57,69 @@ $(BIN): $(DRIVER_OBJS) $(LIB_AR) $(BUILD_CONFIG)
# Compile rules, grouped by source tree: src/ (libkit) -> lang/ (frontends) ->
# vendor/ -> driver/. Each tree gets its own include scope; see mk/flags.mk for
# the *_CFLAGS definitions and the rationale behind each tree's include set.
+#
+# The libkit (src/, vendor/, lang/) objects use path-flattened basenames so the
+# `ar` member names in libkit.a are unique (see `flatobjs` in mk/lib_srcs.mk);
+# the basename no longer matches the source, so these are generated explicit
+# rules rather than `%`-pattern rules. The per-group compile flags are attached
+# as a target-specific RULE_CFLAGS, not threaded through $(call) — the flag
+# strings can contain commas (e.g. -fsanitize=address,undefined). The driver/
+# objects are NOT archived (they link directly), so they keep simple patterns.
# ---------------------------------------------------------------------------
-# --- src/: libkit core (C + asm), freestanding, hidden visibility -----------
-$(BUILD_DIR)/lib/%.o: src/%.c Makefile $(BUILD_CONFIG)
- @mkdir -p $(dir $@)
- $(CC) $(LIB_CFLAGS) $(DEPFLAGS) -c $< -o $@
+# $(1)=source $(2)=tree root to strip $(3)=dest subdir under BUILD_DIR
+define libkit_obj_rule
+$(call flatobjs,$(1),$(2),$(3)): $(1) Makefile $$(BUILD_CONFIG)
+ @mkdir -p $$(@D)
+ $$(CC) $$(RULE_CFLAGS) $$(DEPFLAGS) -c $$< -o $$@
+endef
-$(BUILD_DIR)/lib/%.o: src/%.S Makefile $(BUILD_CONFIG)
- @mkdir -p $(dir $@)
- $(CC) $(LIB_CFLAGS) $(DEPFLAGS) -c $< -o $@
+# --- src/: libkit core (C + asm), freestanding, hidden visibility -----------
+$(foreach s,$(LIB_SRCS_C_GENERAL) $(LIB_ASMS),$(eval $(call libkit_obj_rule,$(s),src,lib)))
+$(call flatobjs,$(LIB_SRCS_C_GENERAL) $(LIB_ASMS),src,lib): RULE_CFLAGS = $(LIB_CFLAGS)
# lang_registry.c is the one libkit source that crosses into lang/*; it
# uses -Ilang so the frontend headers can be reached as "c/c.h" etc.
-$(BUILD_DIR)/lib/api/lang_registry.o: src/api/lang_registry.c Makefile $(BUILD_CONFIG)
- @mkdir -p $(dir $@)
- $(CC) $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Ilang -Isrc $(DEPFLAGS) -c $< -o $@
+ifneq ($(LIB_SRCS_LANGREG),)
+$(foreach s,$(LIB_SRCS_LANGREG),$(eval $(call libkit_obj_rule,$(s),src,lib)))
+$(call flatobjs,$(LIB_SRCS_LANGREG),src,lib): RULE_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Ilang -Isrc
+endif
# --- lang/: frontends, each with its own include set ------------------------
-$(BUILD_DIR)/lang/cpp/%.o: lang/cpp/%.c Makefile $(BUILD_CONFIG)
- @mkdir -p $(dir $@)
- $(CC) $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Ilang/cpp $(DEPFLAGS) -c $< -o $@
+ifeq ($(KIT_LANG_CPP_ENABLED),1)
+$(foreach s,$(LANG_CPP_SRCS),$(eval $(call libkit_obj_rule,$(s),lang,lang)))
+$(call flatobjs,$(LANG_CPP_SRCS),lang,lang): RULE_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Ilang/cpp
+endif
# The C frontend includes the lexer and preprocessor headers (pp/pp.h,
# lex/lex.h) which now live under lang/cpp/, and cpp_support.h is the
# shared substrate. So lang/c objects build with -Ilang/cpp -Ilang/c.
-$(BUILD_DIR)/lang/c/%.o: lang/c/%.c Makefile $(BUILD_CONFIG)
- @mkdir -p $(dir $@)
- $(CC) $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Ilang/cpp -Ilang/c $(DEPFLAGS) -c $< -o $@
+ifeq ($(KIT_LANG_C_ENABLED),1)
+$(foreach s,$(LANG_C_SRCS),$(eval $(call libkit_obj_rule,$(s),lang,lang)))
+$(call flatobjs,$(LANG_C_SRCS),lang,lang): RULE_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Ilang/cpp -Ilang/c
+endif
# The Wasm frontend is the one lang/* tree that reaches internal headers: it
# includes "wasm/wasm.h" to share src/wasm/ (the module/encode/decode library)
# with the Wasm backend, so it gets -Isrc. lang/c and lang/cpp deliberately do
# not — they see only the public include/ surface.
-$(BUILD_DIR)/lang/wasm/%.o: lang/wasm/%.c Makefile $(BUILD_CONFIG)
- @mkdir -p $(dir $@)
- $(CC) $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Isrc -Ilang/wasm $(DEPFLAGS) -c $< -o $@
+ifeq ($(KIT_LANG_WASM_ENABLED),1)
+$(foreach s,$(LANG_WASM_SRCS),$(eval $(call libkit_obj_rule,$(s),lang,lang)))
+$(call flatobjs,$(LANG_WASM_SRCS),lang,lang): RULE_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Isrc -Ilang/wasm
+endif
-$(BUILD_DIR)/lang/toy/%.o: lang/toy/%.c Makefile $(BUILD_CONFIG)
- @mkdir -p $(dir $@)
- $(CC) $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Ilang/toy $(DEPFLAGS) -c $< -o $@
+ifeq ($(KIT_LANG_TOY_ENABLED),1)
+$(foreach s,$(LANG_TOY_SRCS),$(eval $(call libkit_obj_rule,$(s),lang,lang)))
+$(call flatobjs,$(LANG_TOY_SRCS),lang,lang): RULE_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Ilang/toy
+endif
# --- vendor/: third-party sources compiled into libkit ----------------------
# Vendored third-party sources (monocypher) compiled into libkit. Same
# freestanding flags as the rest of the library; symbols stay hidden.
-$(BUILD_DIR)/vendor/%.o: vendor/%.c Makefile $(BUILD_CONFIG)
- @mkdir -p $(dir $@)
- $(CC) $(LIB_CFLAGS) $(DEPFLAGS) -c $< -o $@
+ifneq ($(LIB_SRCS_VENDOR),)
+$(foreach s,$(LIB_SRCS_VENDOR),$(eval $(call libkit_obj_rule,$(s),vendor,vendor)))
+$(call flatobjs,$(LIB_SRCS_VENDOR),vendor,vendor): RULE_CFLAGS = $(LIB_CFLAGS)
+endif
# --- driver/: hosted CLI; env/ is the OS/libc adapter sub-tree --------------
$(BUILD_DIR)/driver/%.o: driver/%.c Makefile $(BUILD_CONFIG)
diff --git a/mk/lib_srcs.mk b/mk/lib_srcs.mk
@@ -6,6 +6,17 @@
# `#if` gates in the corresponding sources so the build drops exactly what the
# compile drops. The matching frontend `#if`s live in src/api/lang_registry.c.
+# Flattened, archive-unique object path. libkit.a is built with `ar`, which
+# stores each member under its basename only — so two sources sharing a
+# basename (the eight link.c, or src/{abi,arch,obj,os}/registry.c, …) would
+# collide as members even though their build/ paths differ. We keep the
+# mirrored build/ subdirs but fold the full sub-path into the basename, making
+# every member name unique. An immediate-parent prefix is not enough on its
+# own: src/obj/wasm/emit.c and src/arch/wasm/emit.c would both be wasm_emit.o.
+# $(1)=sources $(2)=tree root to strip $(3)=dest subdir under BUILD_DIR
+# e.g. src/arch/aa64/native.c -> $(BUILD_DIR)/lib/arch/aa64/arch_aa64_native.o
+flatobjs = $(foreach s,$(1),$(BUILD_DIR)/$(3)/$(dir $(patsubst $(2)/%,%,$(s)))$(subst /,_,$(basename $(patsubst $(2)/%,%,$(s)))).o)
+
# When a native feature is disabled, drop its per-arch TU from all three
# native backends and, where one exists, compile a stub in its place.
# $(1) = TU basename (e.g. disasm.c) $(2) = stub source, or empty
@@ -235,16 +246,16 @@ LANG_TOY_SRCS = $(wildcard lang/toy/*.c)
LANG_OBJS =
ifeq ($(KIT_LANG_CPP_ENABLED),1)
-LANG_OBJS += $(patsubst lang/cpp/%.c,$(BUILD_DIR)/lang/cpp/%.o,$(LANG_CPP_SRCS))
+LANG_OBJS += $(call flatobjs,$(LANG_CPP_SRCS),lang,lang)
endif
ifeq ($(KIT_LANG_C_ENABLED),1)
-LANG_OBJS += $(patsubst lang/c/%.c,$(BUILD_DIR)/lang/c/%.o,$(LANG_C_SRCS))
+LANG_OBJS += $(call flatobjs,$(LANG_C_SRCS),lang,lang)
endif
ifeq ($(KIT_LANG_WASM_ENABLED),1)
-LANG_OBJS += $(patsubst lang/wasm/%.c,$(BUILD_DIR)/lang/wasm/%.o,$(LANG_WASM_SRCS))
+LANG_OBJS += $(call flatobjs,$(LANG_WASM_SRCS),lang,lang)
endif
ifeq ($(KIT_LANG_TOY_ENABLED),1)
-LANG_OBJS += $(patsubst lang/toy/%.c,$(BUILD_DIR)/lang/toy/%.o,$(LANG_TOY_SRCS))
+LANG_OBJS += $(call flatobjs,$(LANG_TOY_SRCS),lang,lang)
endif
LIB_ASMS =
@@ -252,8 +263,16 @@ ifeq ($(KIT_JIT_ENABLED),1)
LIB_ASMS += $(LIB_SRCS_JIT_ASM)
endif
-LIB_OBJS = $(patsubst src/%.c,$(BUILD_DIR)/lib/%.o,$(filter src/%.c,$(LIB_SRCS))) \
- $(patsubst vendor/%.c,$(BUILD_DIR)/vendor/%.o,$(filter vendor/%.c,$(LIB_SRCS))) \
+# Compile-rule source groups (each shares one compile-flag profile; the rules
+# that consume these live in the Makefile). lang_registry.c is split out: it is
+# a libkit TU but compiles with frontend includes (-Ilang), not the library set.
+LIB_SRCS_LANGREG := $(filter src/api/lang_registry.c,$(LIB_SRCS))
+LIB_SRCS_C_GENERAL := $(filter-out $(LIB_SRCS_LANGREG),$(filter src/%.c,$(LIB_SRCS)))
+LIB_SRCS_VENDOR := $(filter vendor/%.c,$(LIB_SRCS))
+
+LIB_OBJS = $(call flatobjs,$(LIB_SRCS_C_GENERAL),src,lib) \
+ $(call flatobjs,$(LIB_SRCS_LANGREG),src,lib) \
+ $(call flatobjs,$(LIB_SRCS_VENDOR),vendor,vendor) \
$(LANG_OBJS) \
- $(patsubst src/%.S,$(BUILD_DIR)/lib/%.o,$(LIB_ASMS))
+ $(call flatobjs,$(LIB_ASMS),src,lib)
LIB_DEPS = $(LIB_OBJS:.o=.d)
diff --git a/scripts/relmap_md.awk b/scripts/relmap_md.awk
@@ -38,7 +38,9 @@ function classify(p, b) {
if (p ~ /\/lib\/link\//) { label["link"]="linker (`src/link`)"; return "link" }
if (p ~ /\/lib\/dist\//) {
- b = stem(p)
+ # object basenames are path-flattened (src/dist/manifest.c -> dist_manifest.o,
+ # see flatobjs in mk/lib_srcs.mk); strip the dir prefix to recover the file.
+ b = stem(p); sub(/^dist_/, "", b)
if (b ~ /^(manifest|kpkg|tar|trust|dist)$/) { label["dist_pkg"]="packaging (manifest/kpkg/tar/trust)"; return "dist_pkg" }
if (b ~ /^(deflate|lz4|lz4frame)$/) { label["dist_cmp"]="vendored compression (deflate/lz4)"; return "dist_cmp" }
if (b ~ /^(blob|tree|cas)$/) { label["dist_cas"]="CAS store (blob/tree/cas)"; return "dist_cas" }