summaryrefslogtreecommitdiff
path: root/scripts/build
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-07-10 23:20:03 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-07-10 23:20:03 +0800
commit48ef95c24e9ac3d11200b61af617a01b10b507d1 (patch)
tree57f0e26ab98e795b19839b6eece98ca58a4b702d /scripts/build
parentb9ff206e6bd3f4d67851263a6f2acb5f575cf3b0 (diff)
downloadlume-48ef95c24e9ac3d11200b61af617a01b10b507d1.tar.gz
lume-48ef95c24e9ac3d11200b61af617a01b10b507d1.tar.xz
build: introduce declarative non-recursive build graph
Diffstat (limited to 'scripts/build')
-rw-r--r--scripts/build/Build.mk10
-rwxr-xr-xscripts/build/archive.sh10
-rwxr-xr-xscripts/build/check-kernel-elf.sh47
-rw-r--r--scripts/build/environments/host.mk8
-rw-r--r--scripts/build/environments/kernel.mk28
-rw-r--r--scripts/build/environments/phys.mk12
-rwxr-xr-xscripts/build/generate-asm-offsets.sh25
-rwxr-xr-xscripts/build/generate-autoconf.sh29
-rwxr-xr-xscripts/build/generate-initramfs.sh12
-rwxr-xr-xscripts/build/link-kernel.sh10
-rwxr-xr-xscripts/build/probe-toolchain.sh48
-rwxr-xr-xscripts/build/run-if-changed.sh45
-rw-r--r--scripts/build/targets.mk81
-rw-r--r--scripts/build/tests/host_library.cc8
-rw-r--r--scripts/build/tests/host_test.cc7
-rw-r--r--scripts/build/tests/phys_library.cc5
-rw-r--r--scripts/build/toolchain.mk103
-rwxr-xr-xscripts/build/write-compdb.py26
18 files changed, 514 insertions, 0 deletions
diff --git a/scripts/build/Build.mk b/scripts/build/Build.mk
new file mode 100644
index 0000000..0a15b38
--- /dev/null
+++ b/scripts/build/Build.mk
@@ -0,0 +1,10 @@
+targets += build/host-library-smoke build/host-test-smoke build/phys-library-smoke
+target.build/host-library-smoke.kind := host-library
+target.build/host-library-smoke.sources := scripts/build/tests/host_library.cc
+
+target.build/host-test-smoke.kind := host-test
+target.build/host-test-smoke.sources := scripts/build/tests/host_test.cc
+target.build/host-test-smoke.private-deps := build/host-library-smoke
+
+target.build/phys-library-smoke.kind := phys-library
+target.build/phys-library-smoke.sources := scripts/build/tests/phys_library.cc
diff --git a/scripts/build/archive.sh b/scripts/build/archive.sh
new file mode 100755
index 0000000..79f0bb9
--- /dev/null
+++ b/scripts/build/archive.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+archive=$1
+ar=$2
+shift 2
+rm -f "$archive"
+"$ar" rcs "$archive" "$@"
+
diff --git a/scripts/build/check-kernel-elf.sh b/scripts/build/check-kernel-elf.sh
new file mode 100755
index 0000000..eb92b33
--- /dev/null
+++ b/scripts/build/check-kernel-elf.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+elf=$1
+readelf=$2
+nm=$3
+arch=$4
+
+case "$arch" in
+ riscv) expected_machine='RISC-V' ;;
+ aarch64) expected_machine='AArch64' ;;
+ x86_64) expected_machine='Advanced Micro Devices X86-64' ;;
+ *) echo "ELF check: unsupported architecture: $arch" >&2; exit 2 ;;
+esac
+
+machine=$($readelf -h "$elf" | sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')
+[ "$machine" = "$expected_machine" ] || {
+ echo "ELF check: expected machine '$expected_machine', got '$machine'" >&2
+ exit 1
+}
+
+entry=$($readelf -h "$elf" | sed -n 's/^[[:space:]]*Entry point address:[[:space:]]*//p')
+[ -n "$entry" ] && [ "$entry" != 0x0 ] || {
+ echo "ELF check: kernel has no entry point" >&2
+ exit 1
+}
+
+undefined=$($nm -u "$elf" || true)
+[ -z "$undefined" ] || {
+ echo "ELF check: undefined symbols:" >&2
+ printf '%s\n' "$undefined" >&2
+ exit 1
+}
+
+if $readelf -W -l "$elf" | awk '$1 == "LOAD" && $0 ~ /W/ && $0 ~ /E/ { found=1 } END { exit !found }'; then
+ echo "ELF check: writable and executable LOAD segment" >&2
+ exit 1
+fi
+
+$readelf -S "$elf" | grep -q '[.]rela[.]dyn' || {
+ echo "ELF check: missing relocation section .rela.dyn" >&2
+ exit 1
+}
+
+echo "Kernel ELF OK: $machine, entry $entry"
+
diff --git a/scripts/build/environments/host.mk b/scripts/build/environments/host.mk
new file mode 100644
index 0000000..0911ad3
--- /dev/null
+++ b/scripts/build/environments/host.mk
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+HOSTCC ?= cc
+HOSTCXX ?= c++
+HOSTAR ?= ar
+HOSTCFLAGS ?= -O2 -g -Wall -Wextra
+HOSTCXXFLAGS ?= $(HOSTCFLAGS) -std=c++23
+
diff --git a/scripts/build/environments/kernel.mk b/scripts/build/environments/kernel.mk
new file mode 100644
index 0000000..088cdf0
--- /dev/null
+++ b/scripts/build/environments/kernel.mk
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+WARNINGS ?= -Wall -Wextra -Wno-unused-parameter
+
+ifeq ($(BUILD_VARIANT),release)
+KERNEL_OPT_FLAGS := -O2 -g0 -DNDEBUG
+else
+KERNEL_OPT_FLAGS := -O2 -g
+endif
+
+CFLAGS := $(WARNINGS) $(KERNEL_OPT_FLAGS) \
+ -ffreestanding -nostdlib \
+ -mcmodel=medany -fPIE -MMD -MP \
+ $(TOOLCHAIN_CFLAGS) $(KCPPFLAGS) $(INCLUDES) \
+ -I$(BUILD)/generated
+
+CFLAGS_LIBC := $(CFLAGS) -std=gnu23 -Iinclude/libc-shim \
+ -Wno-parentheses -Wno-sign-compare
+
+CXXFLAGS := $(CFLAGS) $(TOOLCHAIN_CXXFLAGS) -std=c++23 \
+ -fno-exceptions -fno-rtti -fno-use-cxa-atexit \
+ -fno-threadsafe-statics
+
+# Pre-MMU assembly must never use GOT-indirect addressing.
+ASFLAGS := $(filter-out -fPIE,$(CFLAGS)) -fno-pie
+
+LDFLAGS := -nostdlib -shared -Bsymbolic -z notext -z norelro \
+ --no-dynamic-linker
diff --git a/scripts/build/environments/phys.mk b/scripts/build/environments/phys.mk
new file mode 100644
index 0000000..6a11c12
--- /dev/null
+++ b/scripts/build/environments/phys.mk
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Early physical-address execution environment. It deliberately has no heap,
+# exceptions, RTTI, thread-safe statics, or PIE/GOT assumptions.
+PHYS_CFLAGS := $(WARNINGS) -Os -g -ffreestanding -nostdlib \
+ -fno-pie -fno-stack-protector $(TOOLCHAIN_CFLAGS) \
+ $(KCPPFLAGS) $(INCLUDES) -I$(BUILD)/generated
+PHYS_CXXFLAGS := $(PHYS_CFLAGS) $(TOOLCHAIN_CXXFLAGS) -std=c++23 \
+ -fno-exceptions -fno-rtti -fno-use-cxa-atexit \
+ -fno-threadsafe-statics
+PHYS_ASFLAGS := $(PHYS_CFLAGS)
+
diff --git a/scripts/build/generate-asm-offsets.sh b/scripts/build/generate-asm-offsets.sh
new file mode 100755
index 0000000..4e90f94
--- /dev/null
+++ b/scripts/build/generate-asm-offsets.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+output=$1
+assembly=$2
+sed_script=$3
+source=$4
+tmp="$output.tmp.$$"
+{
+ echo '/* AUTO-GENERATED — DO NOT EDIT */'
+ echo "/* Generated from $source */"
+ echo '#ifndef _LUME_GENERATED_ASM_OFFSETS_H'
+ echo '#define _LUME_GENERATED_ASM_OFFSETS_H'
+ echo
+ sed -n -f "$sed_script" "$assembly"
+ echo
+ echo '#endif /* _LUME_GENERATED_ASM_OFFSETS_H */'
+} > "$tmp"
+if [ -e "$output" ] && cmp -s "$tmp" "$output"; then
+ rm -f "$tmp"
+else
+ mv -f "$tmp" "$output"
+fi
+
diff --git a/scripts/build/generate-autoconf.sh b/scripts/build/generate-autoconf.sh
new file mode 100755
index 0000000..493d630
--- /dev/null
+++ b/scripts/build/generate-autoconf.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+output=$1
+shift
+tmp="$output.tmp.$$"
+mkdir -p "$(dirname "$output")"
+{
+ echo '/* AUTO-GENERATED — DO NOT EDIT */'
+ echo '#pragma once'
+ while [ "$#" -gt 0 ]; do
+ name=$1
+ value=$2
+ kind=$3
+ shift 3
+ if [ "$kind" = bool ]; then
+ [ "$value" = y ] && echo "#define $name 1"
+ else
+ echo "#define $name $value"
+ fi
+ done
+} > "$tmp"
+if [ -e "$output" ] && cmp -s "$tmp" "$output"; then
+ rm -f "$tmp"
+else
+ mv -f "$tmp" "$output"
+fi
+
diff --git a/scripts/build/generate-initramfs.sh b/scripts/build/generate-initramfs.sh
new file mode 100755
index 0000000..194e206
--- /dev/null
+++ b/scripts/build/generate-initramfs.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+generator=$1
+wrapper=$2
+output=$3
+uid=$4
+gid=$5
+source=$6
+GEN_INIT_CPIO=$generator "$wrapper" -o "$output" -u "$uid" -g "$gid" "$source"
+
diff --git a/scripts/build/link-kernel.sh b/scripts/build/link-kernel.sh
new file mode 100755
index 0000000..31164e7
--- /dev/null
+++ b/scripts/build/link-kernel.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+output=$1
+size_tool=$2
+shift 2
+"$@"
+"$size_tool" "$output"
+
diff --git a/scripts/build/probe-toolchain.sh b/scripts/build/probe-toolchain.sh
new file mode 100755
index 0000000..2f1f600
--- /dev/null
+++ b/scripts/build/probe-toolchain.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+output=$1
+cc=$2
+cxx=$3
+ld=$4
+family=$5
+target=$6
+gcc_toolchain=$7
+sysroot=$8
+
+set --
+if [ "$family" = clang ]; then
+ set -- "$@" "--target=$target"
+ [ -z "$gcc_toolchain" ] || set -- "$@" "--gcc-toolchain=$gcc_toolchain"
+ [ -z "$sysroot" ] || set -- "$@" "--sysroot=$sysroot"
+fi
+
+probe() {
+ language=$1
+ standard=$2
+ shift 2
+ compiler=$cc
+ [ "$language" = c++ ] && compiler=$cxx
+ printf '' | "$compiler" "$@" "-std=$standard" -x "$language" \
+ -fsyntax-only - >/dev/null 2>&1
+}
+
+tmp="$output.tmp.$$"
+mkdir -p "$(dirname "$output")"
+{
+ echo "cc=$($cc --version | head -n 1)"
+ echo "cxx=$($cxx --version | head -n 1)"
+ echo "ld=$($ld --version | head -n 1)"
+ echo "target=$target"
+ printf 'c23='; if probe c gnu23 "$@"; then echo yes; else echo no; fi
+ printf 'cxx23='; if probe c++ c++23 "$@"; then echo yes; else echo no; fi
+ printf 'fno-pie='; if probe c++ c++23 "$@" -fno-pie; then echo yes; else echo no; fi
+ printf 'mcmodel-medany='; if probe c++ c++23 "$@" -mcmodel=medany; then echo yes; else echo no; fi
+} > "$tmp"
+mv -f "$tmp" "$output"
+
+grep -q '^c23=yes$' "$output"
+grep -q '^cxx23=yes$' "$output"
+grep -q '^fno-pie=yes$' "$output"
+grep -q '^mcmodel-medany=yes$' "$output"
diff --git a/scripts/build/run-if-changed.sh b/scripts/build/run-if-changed.sh
new file mode 100755
index 0000000..6cdc7c4
--- /dev/null
+++ b/scripts/build/run-if-changed.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+
+cmdfile=$1
+output=$2
+description=$3
+shift 3
+
+rebuild=false
+if [ ! -e "$output" ] || [ ! -e "$cmdfile" ]; then
+ rebuild=true
+fi
+
+while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do
+ prerequisite=$1
+ shift
+ if [ "$prerequisite" != FORCE ] && [ -e "$prerequisite" ] && \
+ [ "$prerequisite" -nt "$output" ]; then
+ rebuild=true
+ fi
+done
+
+[ "$#" -gt 0 ] || { echo "run-if-changed: missing --" >&2; exit 2; }
+shift
+
+signature=$(printf '%s\0' "$@" | cksum)
+old_signature=
+if [ -e "$cmdfile" ]; then
+ old_signature=$(cat "$cmdfile")
+fi
+if [ "$signature" != "$old_signature" ]; then
+ rebuild=true
+fi
+
+if [ "$rebuild" = true ]; then
+ printf ' %s\n' "$description"
+ mkdir -p "$(dirname "$output")" "$(dirname "$cmdfile")"
+ "$@"
+ tmp="$cmdfile.tmp.$$"
+ printf '%s\n' "$signature" > "$tmp"
+ mv -f "$tmp" "$cmdfile"
+fi
+
diff --git a/scripts/build/targets.mk b/scripts/build/targets.mk
new file mode 100644
index 0000000..1a66379
--- /dev/null
+++ b/scripts/build/targets.mk
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Richard Qin
+
+# Declarative target registry for the LumeOS build.
+#
+# Build.mk files populate target.<name>.* and append the name to targets. A
+# target name is metadata, not a Make rule, so names may contain '/' and ':'.
+
+target-kinds := \
+ interface-library header-library object-library source-set \
+ static-library whole-archive host-library phys-library host-tool generated \
+ binary-blob kernel-test host-test product
+targets :=
+generated-actions := autoconf asm-offsets linker-script initramfs
+
+empty :=
+space := $(empty) $(empty)
+comma := ,
+
+# Convert a logical target/source name into a path-safe component.
+target-key = $(subst :,_,$(subst /,_,$(1)))
+
+# Recursively collect a target and all of its dependencies. Dependency cycles
+# are rejected separately before this is evaluated for build roots.
+target-deps = $(sort $(target.$(1).deps) $(target.$(1).public-deps) \
+ $(target.$(1).private-deps) $(target.$(1).interface-deps) \
+ $(target.$(1).link-deps) $(target.$(1).tool-deps) \
+ $(target.$(1).generated-deps) $(target.$(1).runtime-deps) \
+ $(target.$(1).order-only-deps))
+target-closure = $(strip \
+ $(if $(filter $(1),$(2)),$(error target dependency cycle: $(2) -> $(1))) \
+ $(1) $(foreach dep,$(call target-deps,$(1)),$(call target-closure,$(dep),$(2) $(1))))
+target-link-deps = $(sort $(target.$(1).deps) $(target.$(1).public-deps) \
+ $(target.$(1).private-deps) $(target.$(1).interface-deps) \
+ $(target.$(1).link-deps))
+target-link-order = $(strip $(1) $(foreach dep,$(call target-link-deps,$(1)),\
+ $(call target-link-order,$(dep))))
+unique-words = $(if $(strip $(1)),$(firstword $(1)) \
+ $(call unique-words,$(filter-out $(firstword $(1)),$(1))))
+
+duplicate-words = $(sort $(foreach word,$(1),\
+ $(if $(word 2,$(filter $(word),$(1))),$(word))))
+all-target-sources = $(foreach target,$(targets),$(target.$(target).sources))
+duplicate-target-sources = $(call duplicate-words,$(all-target-sources))
+
+define validate-target
+$(if $(filter $(target.$(1).kind),$(target-kinds)),,$(error target '$(1)' has invalid kind '$(target.$(1).kind)'))
+$(if $(filter header-library interface-library product generated binary-blob,$(target.$(1).kind)),$(if $(strip $(target.$(1).sources)),$(error target '$(1)' of kind $(target.$(1).kind) declares sources),),)
+$(if $(filter host-tool,$(target.$(1).kind)),$(if $(word 2,$(target.$(1).sources)),$(error host-tool '$(1)' currently requires exactly one source),),)
+$(foreach src,$(target.$(1).sources),$(if $(wildcard $(src)),,$(error target '$(1)' references missing source '$(src)')))
+$(foreach dep,$(call target-deps,$(1)),$(if $(filter $(dep),$(targets)),,$(error target '$(1)' references unknown dependency '$(dep)')))
+$(foreach dep,$(call target-deps,$(1)),$(if $(strip $(target.$(dep).visibility)),\
+ $(if $(filter all $(1),$(target.$(dep).visibility)),,\
+ $(error target '$(1)' cannot depend on '$(dep)': visibility is '$(target.$(dep).visibility)')),))
+$(if $(filter generated,$(target.$(1).kind)),$(if $(strip $(target.$(1).outputs)),,$(error generated target '$(1)' has no outputs)),)
+$(if $(filter generated,$(target.$(1).kind)),$(if $(filter $(target.$(1).action),$(generated-actions)),,$(error generated target '$(1)' has invalid action '$(target.$(1).action)')),)
+endef
+
+define validate-targets
+$(if $(strip $(targets)),,$(error no build targets were registered))
+$(if $(filter-out $(words $(sort $(targets))),$(words $(targets))),$(error duplicate build target name),)
+$(foreach target,$(targets),$(call validate-target,$(target)))
+endef
+
+# Usage requirements are deliberately separate from link inputs. Public
+# include paths and defines propagate through deps; private values apply only
+# while compiling the target itself.
+target-public-closure = $(sort $(1) $(foreach dep,$(target.$(1).public-deps) \
+ $(target.$(1).deps),$(call target-public-closure,$(dep))))
+target-public-includes = $(sort $(foreach target,$(call target-public-closure,$(1)),$(target.$(target).public-includes)))
+target-public-after-includes = $(sort $(foreach target,$(call target-public-closure,$(1)),$(target.$(target).public-after-includes)))
+target-public-defines = $(sort $(foreach target,$(call target-public-closure,$(1)),$(target.$(target).public-defines)))
+
+target-usage-roots = $(sort $(1) $(target.$(1).deps) $(target.$(1).public-deps) \
+ $(target.$(1).private-deps) $(target.$(1).interface-deps))
+target-compile-includes = $(sort $(target.$(1).private-includes) \
+ $(foreach root,$(call target-usage-roots,$(1)),$(call target-public-includes,$(root))))
+target-compile-after-includes = $(sort $(target.$(1).private-after-includes) \
+ $(foreach root,$(call target-usage-roots,$(1)),$(call target-public-after-includes,$(root))))
+target-compile-defines = $(sort $(target.$(1).private-defines) \
+ $(foreach root,$(call target-usage-roots,$(1)),$(call target-public-defines,$(root))))
diff --git a/scripts/build/tests/host_library.cc b/scripts/build/tests/host_library.cc
new file mode 100644
index 0000000..c545815
--- /dev/null
+++ b/scripts/build/tests/host_library.cc
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace lume::build::test {
+
+int HostLibrarySmokeTestSymbol() { return 0; }
+
+} // namespace lume::build::test
+
diff --git a/scripts/build/tests/host_test.cc b/scripts/build/tests/host_test.cc
new file mode 100644
index 0000000..6b13417
--- /dev/null
+++ b/scripts/build/tests/host_test.cc
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace lume::build::test {
+int HostLibrarySmokeTestSymbol();
+}
+
+int main() { return lume::build::test::HostLibrarySmokeTestSymbol(); }
diff --git a/scripts/build/tests/phys_library.cc b/scripts/build/tests/phys_library.cc
new file mode 100644
index 0000000..2cf1596
--- /dev/null
+++ b/scripts/build/tests/phys_library.cc
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+extern "C" unsigned long LumePhysBuildSmoke(unsigned long value) {
+ return value ^ 0x4c554d45UL;
+}
diff --git a/scripts/build/toolchain.mk b/scripts/build/toolchain.mk
new file mode 100644
index 0000000..49d4f0a
--- /dev/null
+++ b/scripts/build/toolchain.mk
@@ -0,0 +1,103 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Richard Qin
+
+# Toolchain selection:
+# make GCC + GNU ld
+# make LLVM=1 Clang + GNU ld
+# make LLVM=1 LD_TYPE=lld Clang + LLD
+
+LLVM ?= 0
+LLVM_VERSION ?= 20
+LD_TYPE ?= bfd
+LLVM_BINUTILS ?= 0
+CXX_STDLIB ?= libstdc++
+CXX_ABI ?= lume
+COMPILER_RUNTIME ?= compiler-default
+
+GCC_TOOLCHAIN ?=
+CROSS_COMPILE ?=
+
+ifeq ($(ARCH),riscv)
+TARGET_TRIPLE := riscv64-unknown-linux-gnu
+ifeq ($(strip $(GCC_TOOLCHAIN)),)
+ifneq ($(wildcard /opt/riscv64-glibc/bin/riscv64-unknown-linux-gnu-g++),)
+GCC_TOOLCHAIN := /opt/riscv64-glibc
+endif
+endif
+ifeq ($(strip $(CROSS_COMPILE)),)
+ifneq ($(strip $(GCC_TOOLCHAIN)),)
+CROSS_COMPILE := $(GCC_TOOLCHAIN)/bin/$(TARGET_TRIPLE)-
+else
+CROSS_COMPILE := $(TARGET_TRIPLE)-
+endif
+endif
+else ifeq ($(ARCH),aarch64)
+TARGET_TRIPLE := aarch64-linux-gnu
+else ifeq ($(ARCH),x86_64)
+TARGET_TRIPLE := x86_64-linux-gnu
+endif
+
+ifeq ($(strip $(CROSS_COMPILE)),)
+CROSS_COMPILE := $(TARGET_TRIPLE)-
+endif
+ifeq ($(strip $(GCC_TOOLCHAIN)),)
+GCC_TOOLCHAIN := $(patsubst %/bin/,%, $(dir $(CROSS_COMPILE)))
+endif
+
+GCC_SYSROOT := $(strip $(shell $(CROSS_COMPILE)g++ -print-sysroot 2>/dev/null))
+
+CLANG_SUFFIX := $(if $(LLVM_VERSION),-$(LLVM_VERSION),)
+CLANG ?= clang$(CLANG_SUFFIX)
+CLANGXX ?= clang++$(CLANG_SUFFIX)
+LLD ?= ld.lld$(CLANG_SUFFIX)
+
+CLANG_TARGET_FLAGS := --target=$(TARGET_TRIPLE)
+ifneq ($(strip $(GCC_TOOLCHAIN)),)
+CLANG_TARGET_FLAGS += --gcc-toolchain=$(GCC_TOOLCHAIN)
+endif
+ifneq ($(strip $(GCC_SYSROOT)),)
+CLANG_TARGET_FLAGS += --sysroot=$(GCC_SYSROOT)
+endif
+
+ifeq ($(LLVM),1)
+CC := $(CLANG)
+CXX := $(CLANGXX)
+COMPILER_FAMILY := clang
+TOOLCHAIN_CFLAGS := $(CLANG_TARGET_FLAGS)
+ifeq ($(CXX_STDLIB),libstdc++)
+TOOLCHAIN_CXXFLAGS := -stdlib=libstdc++
+else
+$(error unsupported CXX_STDLIB '$(CXX_STDLIB)' for the current imported runtime)
+endif
+ifeq ($(LD_TYPE),lld)
+LD := $(LLD)
+LINKER_FAMILY := lld
+else
+ifneq ($(CXX_STDLIB),libstdc++)
+$(error GCC configuration currently requires CXX_STDLIB=libstdc++)
+endif
+LD := $(CROSS_COMPILE)ld
+LINKER_FAMILY := bfd
+endif
+else
+CC := $(CROSS_COMPILE)gcc
+CXX := $(CROSS_COMPILE)g++
+LD := $(CROSS_COMPILE)ld
+COMPILER_FAMILY := gcc
+LINKER_FAMILY := bfd
+endif
+
+ifeq ($(LLVM_BINUTILS),1)
+AR := llvm-ar$(CLANG_SUFFIX)
+NM := llvm-nm$(CLANG_SUFFIX)
+OBJCOPY := llvm-objcopy$(CLANG_SUFFIX)
+OBJDUMP := llvm-objdump$(CLANG_SUFFIX)
+READELF := llvm-readelf$(CLANG_SUFFIX)
+else
+AR := $(CROSS_COMPILE)ar
+NM := $(CROSS_COMPILE)nm
+OBJCOPY := $(CROSS_COMPILE)objcopy
+OBJDUMP := $(CROSS_COMPILE)objdump
+READELF := $(CROSS_COMPILE)readelf
+endif
+SIZE := $(CROSS_COMPILE)size
diff --git a/scripts/build/write-compdb.py b/scripts/build/write-compdb.py
new file mode 100755
index 0000000..9ec5a49
--- /dev/null
+++ b/scripts/build/write-compdb.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import json
+import pathlib
+import sys
+
+source = pathlib.Path(sys.argv[1])
+output = pathlib.Path(sys.argv[2])
+directory = pathlib.Path.cwd()
+entries = []
+for line in source.read_text().splitlines():
+ file_name, object_name, command = line.split("\t", 2)
+ entries.append(
+ {
+ "directory": str(directory),
+ "file": file_name,
+ "output": object_name,
+ "command": command,
+ }
+ )
+output.parent.mkdir(parents=True, exist_ok=True)
+temporary = output.with_suffix(output.suffix + ".tmp")
+temporary.write_text(json.dumps(entries, indent=2) + "\n")
+temporary.replace(output)
+