From 0ad6b8b533add251f433fea93f479c1950799027 Mon Sep 17 00:00:00 2001 From: Cheng Lingfei <1599101385@qq.com> Date: Wed, 20 May 2026 14:12:59 +0800 Subject: [PATCH] lkl: fix incremental build via tools/lkl `make -C tools/lkl` had two related problems: 1. Correctness: modifying any kernel source file did not trigger a rebuild. tools/lkl/Makefile's lib/lkl.o rule depended only on bin/stat and $(DOT_CONFIG), so the recursive kernel make was never invoked when only kernel sources changed. Host libraries got relinked over a stale lib/lkl.o, masking the failure. 2. Performance: even with no source changes, several pipeline stages unconditionally bumped output mtimes, causing the whole chain (objcopy -> install -> all ~200 LKL headers -> every host .o -> liblkl.a -> every .so) to fire on every make. The patch consists of following modifications: tools/lkl/Makefile: add FORCE to lib/lkl.o so the recursive make is always entered; kbuild's own incremental check then decides what to rebuild from source timestamps. arch/lkl/include/uapi/asm/Kbuild: declare syscall_defs.h as generated-y. Without this, scripts/Makefile.asm-generic treats the file as a stale wrapper and REMOVEs it on every build, forcing arch/lkl/Makefile to re-extract it via objcopy. arch/lkl/Makefile: rewrite the lkl.o and syscall_defs.h rules to objcopy to a tmp file and cmp before replacing $@. vmlinux is PHONY in the top-level Makefile so its recipe always runs; if_changed cannot be used here (newer-prereqs filters out PHONY prereqs and would silently skip real vmlinux content changes), so an in-recipe cmp preserves mtime on no-op rebuilds while still updating on real changes. Pass -p to the install cp so the preserved mtime propagates to tools/lkl/lib/lkl.o. arch/lkl/scripts/headers_install.py: stage the two-pass install. The first pass writes scripts/headers_install.sh output to dst.raw. The second pass (update_header) reads dst.raw, produces the final lkl_-prefixed content, and only writes dst when the content differs from the existing destination. After applying this patch, editing any kernel source correctly propagates to the final libraries, and a no-op `make` in tools/lkl performs no host-side work -- only the two unavoidable OBJCOPY invocations against the PHONY vmlinux target. A no-op make would run as follows: ``` make -C tools/lkl MMU=1 -j128 make: Entering directory '/path/to/linux/tools/lkl' CALL scripts/checksyscalls.sh OBJCOPY lkl.o OBJCOPY arch/lkl/include/generated/uapi/asm/syscall_defs.h make -C ../.. ARCH=lkl install INSTALL_PATH=/path/to/linux/tools/lkl/ INSTALL linux/tools/lkl//lib/lkl.o make: Leaving directory '/path/to/linux/tools/lkl' ``` Signed-off-by: Cheng Lingfei <1599101385@qq.com> --- arch/lkl/Makefile | 28 +++++++++++++++++++++++----- arch/lkl/include/uapi/asm/Kbuild | 1 + arch/lkl/scripts/headers_install.py | 24 ++++++++++++++++++------ tools/lkl/Makefile | 6 +++++- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/arch/lkl/Makefile b/arch/lkl/Makefile index f615869a2814c3..003ca5a7555659 100644 --- a/arch/lkl/Makefile +++ b/arch/lkl/Makefile @@ -82,18 +82,36 @@ archprepare: arch/lkl/include/generated/uapi/asm/config.h all: lkl.o arch/lkl/include/generated/uapi/asm/syscall_defs.h +# The recipe runs on every build because 'vmlinux' is PHONY in the top-level +# Makefile, so we cannot use $(if_changed) (newer-prereqs filters out PHONY, +# making it miss real vmlinux content changes). Instead, write to a tmp file +# and cmp before replacing, so $@'s mtime is preserved on no-op rebuilds and +# downstream consumers (e.g. tools/lkl/liblkl.a) are not relinked needlessly. +quiet_cmd_link_lkl = OBJCOPY $@ + cmd_link_lkl = $(OBJCOPY) $(if $(KEEP_EH_FRAMES),,-R .eh_frame) \ + -R .syscall_defs \ + $(foreach sym,$(LKL_ENTRY_POINTS),-G$(prefix)$(sym)) \ + --prefix-symbols=$(prefix) $< $@.new && \ + { if cmp -s $@.new $@; then rm -f $@.new; \ + else mv -f $@.new $@; fi; } + lkl.o: vmlinux - $(OBJCOPY) $(if $(KEEP_EH_FRAMES),,-R .eh_frame) -R .syscall_defs $(foreach sym,$(LKL_ENTRY_POINTS),-G$(prefix)$(sym)) --prefix-symbols=$(prefix) vmlinux lkl.o + $(call cmd,link_lkl) + +quiet_cmd_gen_syscall_defs = OBJCOPY $@ + cmd_gen_syscall_defs = $(OBJCOPY) -j .syscall_defs -O binary \ + --set-section-flags .syscall_defs=alloc $< $@.raw && \ + sed 's/\x0//g' $@.raw > $@.new && rm -f $@.raw && \ + { if cmp -s $@.new $@; then rm -f $@.new; \ + else mv -f $@.new $@; fi; } arch/lkl/include/generated/uapi/asm/syscall_defs.h: vmlinux - $(OBJCOPY) -j .syscall_defs -O binary --set-section-flags .syscall_defs=alloc $< $@ - $(Q) export tmpfile=$(shell mktemp); \ - sed 's/\x0//g' $@ > $$tmpfile; mv $$tmpfile $@ ; rm -f $$tmpfile + $(call cmd,gen_syscall_defs) install: scripts_unifdef @echo " INSTALL $(INSTALL_PATH)/lib/lkl.o" @mkdir -p $(INSTALL_PATH)/lib/ - @cp lkl.o $(INSTALL_PATH)/lib/ + @cp -p lkl.o $(INSTALL_PATH)/lib/ @$(srctree)/arch/lkl/scripts/headers_install.py \ $(subst -j,-j$(NPROC),$(findstring -j,$(MAKEFLAGS))) \ $(INSTALL_PATH)/include diff --git a/arch/lkl/include/uapi/asm/Kbuild b/arch/lkl/include/uapi/asm/Kbuild index c9ed295242b2d5..b3c1b6b6d6deaf 100644 --- a/arch/lkl/include/uapi/asm/Kbuild +++ b/arch/lkl/include/uapi/asm/Kbuild @@ -3,6 +3,7 @@ generic-y += kvm_para.h generated-y += config.h +generated-y += syscall_defs.h # no header-y since we need special user headers handling # see arch/lkl/script/headers.py diff --git a/arch/lkl/scripts/headers_install.py b/arch/lkl/scripts/headers_install.py index 43fe5ac5829d78..4b895e35e129ff 100755 --- a/arch/lkl/scripts/headers_install.py +++ b/arch/lkl/scripts/headers_install.py @@ -128,10 +128,16 @@ def install_headers(self): os.makedirs(out_dir) except: pass - print(" INSTALL\t%s" % (out_dir + "/" + os.path.basename(h))) - os.system(self.srctree+"/scripts/headers_install.sh %s %s" % (self.relpath2abspath(h), - out_dir + "/" + os.path.basename(h))) - new_headers.add(out_dir + "/" + os.path.basename(h)) + # Install to a .raw tmp first. update_header() will read the raw + # content, produce the final prefixed version, and only overwrite + # the final destination when content differs - preserving mtime + # on no-op rebuilds so downstream host objects are not recompiled. + raw = out_dir + "/" + os.path.basename(h) + ".raw" + ret = os.system(self.srctree+"/scripts/headers_install.sh %s %s" % + (self.relpath2abspath(h), raw)) + if ret != 0: + sys.exit(1) + new_headers.add(raw) self.headers = new_headers @@ -161,7 +167,9 @@ def find_all_symbols(self): self.defines.add("__NR_stime") def update_header(self, h): - print(" REPLACE\t%s" % h) + # h is a .raw tmp file produced by install_headers(); the final + # destination is h without the .raw suffix. + dst = h[:-len(".raw")] content = open(h).read() for i in self.includes: search_str = r"(#[ \t]*include[ \t]*[<\"][ \t]*)" + i + r"([ \t]*[>\"])" @@ -184,7 +192,11 @@ def update_header(self, h): search_str = r"(\W?union\s+)" + s + r"(\W)" replace_str = "\\1" + self.lkl_prefix(s) + "\\2" content = re.sub(search_str, replace_str, content, flags = re.MULTILINE) - open(h, 'w').write(content) + existing = open(dst).read() if os.path.exists(dst) else None + if content != existing: + print(" INSTALL\t%s" % dst) + open(dst, 'w').write(content) + os.unlink(h) def update_headers(self): p = multiprocessing.Pool(args.jobs) diff --git a/tools/lkl/Makefile b/tools/lkl/Makefile index d3d91841014ea9..7708058ab141f8 100644 --- a/tools/lkl/Makefile +++ b/tools/lkl/Makefile @@ -75,7 +75,11 @@ $(DOT_CONFIG): $(OUTPUT)/kernel.config $(Q)$(MAKE) -C ../.. ARCH=lkl $(KOPT) syncconfig # rule to build lkl.o -$(OUTPUT)lib/lkl.o: bin/stat $(DOT_CONFIG) +# FORCE is required so the recursive kernel build is always invoked; kbuild +# inside the sub-make handles incremental compilation based on source-file +# timestamps. Without FORCE, changes to kernel sources are silently ignored +# because lib/lkl.o has no dependency on them. +$(OUTPUT)lib/lkl.o: bin/stat $(DOT_CONFIG) FORCE # this workaround is for arm32 linker (ld.gold) $(Q)export PATH="$(srctree)/tools/lkl/bin/:${PATH}" ;\ $(MAKE) -C ../.. ARCH=lkl $(KOPT)