diff --git a/examples/ulib/Makefile b/examples/ulib/Makefile
index 81885cf8726..a11c5dad9be 100644
--- a/examples/ulib/Makefile
+++ b/examples/ulib/Makefile
@@ -25,72 +25,22 @@
 #   PLATFORM_LIBS  - Platform-specific libraries
 #   LIB_STATIC_LDS - Linker script for static library
 
-# For standalone builds, provide default values
-EXAMPLE_DIR ?= .
-OUTDIR ?= .
-CC ?= gcc
-SDL_CONFIG ?= sdl2-config
-PLATFORM_LIBS ?= $(shell $(SDL_CONFIG) --libs)
-LIB_STATIC_LDS ?= static.lds
-# The main Makefile passes in Q=@ for quiet output
-Q ?=
+# Include configuration and helper functions
+include config.mk
 
-# Common compiler flags for programs using system headers first
-SYSTEM_CFLAGS := -I$(UBOOT_BUILD)/include -idirafter$(srctree)/include \
-		-include $(srctree)/include/linux/compiler_attributes.h
+# Programs to build
+progs := demo
 
-# Common compiler flags for programs using U-Boot headers first (like U-Boot
-# internal build)
-UBOOT_CFLAGS := -nostdinc -isystem $(shell $(CC) -print-file-name=include) \
-	-I$(UBOOT_BUILD)/include \
-	-I$(srctree)/include \
-	-I$(srctree)/arch/sandbox/include \
-	-include $(UBOOT_BUILD)/include/config.h \
-	-include $(srctree)/include/linux/kconfig.h \
-	-I$(srctree)/dts/upstream/include \
-	-D__KERNEL__ -DCONFIG_SYS_TEXT_BASE=0
+# Program definitions - list of object files for each program
+demo_objs := demo.o demo_helper.o
 
-SHARED_LDFLAGS := -L$(UBOOT_BUILD) -lu-boot -Wl,-rpath,$(UBOOT_BUILD)
+# Objects that need system headers (default is U-Boot headers)
+sys-objs := demo_helper.o
 
-STATIC_LDFLAGS := -Wl,-T,$(LIB_STATIC_LDS) \
-	-Wl,--whole-archive $(UBOOT_BUILD)/libu-boot.a -Wl,--no-whole-archive \
-	-lpthread -ldl $(PLATFORM_LIBS) -Wl,-z,noexecstack
-
-# Program definitions - can be single file or multi-object
-DEMO_SRC := $(EXAMPLE_DIR)/demo.c
-demo-objs := demo.o demo_helper.o
-DEMO_BINS := $(OUTDIR)/demo $(OUTDIR)/demo_static
-
-ALL_BINS := $(DEMO_BINS)
-
-# Default target builds both programs
-all: $(ALL_BINS)
-
-# Create the output directory if it doesn't exist
-$(OUTDIR):
-	@mkdir -p $@
-
-# Pattern rule for building object files with system headers first (default)
-$(OUTDIR)/%.o: $(EXAMPLE_DIR)/%.c | $(OUTDIR)
-	$(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -c -o $@ $<
-
-# Pattern rule for building object files with U-Boot headers first
-$(OUTDIR)/%_uboot.o: $(EXAMPLE_DIR)/%.c | $(OUTDIR)
-	$(CC) $(CFLAGS) $(UBOOT_CFLAGS) -c -o $@ $<
-
-# The U-Boot library must be built before we can link against it
-# Order-only prerequisites ensure libraries exist before linking
-$(ALL_BINS): | $(UBOOT_BUILD)/libu-boot.a $(UBOOT_BUILD)/libu-boot.so $(OUTDIR)
-
-# Build demo (dynamically linked with libu-boot.so)
-$(OUTDIR)/demo: $(if $(demo-objs),$(addprefix $(OUTDIR)/,$(demo-objs)),$(DEMO_SRC))
-	$(if $(demo-objs),$(CC) $(CFLAGS) -o $@ $^ $(SHARED_LDFLAGS),$(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -o $@ $< $(SHARED_LDFLAGS))
-
-# Build demo_static (statically linked with libu-boot.a)
-$(OUTDIR)/demo_static: $(if $(demo-objs),$(addprefix $(OUTDIR)/,$(demo-objs)),$(DEMO_SRC))
-	$(if $(demo-objs),$(CC) $(CFLAGS) -o $@ $^ $(STATIC_LDFLAGS),$(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -o $@ $< $(STATIC_LDFLAGS))
+# Include build rules (must come after variable definitions)
+include rules.mk
 
 clean:
-	$(Q)rm -f $(DEMO_BINS)
+	$(Q)rm -f $(all_bins) $(OUTDIR)/*.o
 
 .PHONY: all clean
diff --git a/examples/ulib/config.mk b/examples/ulib/config.mk
new file mode 100644
index 00000000000..993f0923b4f
--- /dev/null
+++ b/examples/ulib/config.mk
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Configuration and helpers for U-Boot library examples
+#
+# Copyright 2025 Canonical Ltd.
+# Written by Simon Glass <simon.glass@canonical.com>
+
+# For standalone builds, provide default values
+EXAMPLE_DIR ?= .
+OUTDIR ?= .
+CC ?= gcc
+SDL_CONFIG ?= sdl2-config
+PLATFORM_LIBS ?= $(shell $(SDL_CONFIG) --libs)
+LIB_STATIC_LDS ?= static.lds
+# The main Makefile passes in Q=@ for quiet output
+Q ?=
+
+# Common compiler flags for programs using system headers
+SYSTEM_CFLAGS := -I$(UBOOT_BUILD)/include \
+	-idirafter$(srctree)/include \
+	-include $(srctree)/include/linux/compiler_attributes.h
+
+# Common compiler flags for programs using U-Boot headers (these match the
+# U-Boot internal build)
+UBOOT_CFLAGS := -nostdinc \
+	-isystem $(shell $(CC) -print-file-name=include) \
+	-I$(UBOOT_BUILD)/include \
+	-I$(srctree)/include \
+	-I$(srctree)/arch/sandbox/include \
+	-include $(UBOOT_BUILD)/include/config.h \
+	-include $(srctree)/include/linux/kconfig.h \
+	-I$(srctree)/dts/upstream/include \
+	"-DMBEDTLS_CONFIG_FILE=\"mbedtls_def_config.h\"" \
+	-I$(srctree)/lib/mbedtls \
+	-I$(srctree)/lib/mbedtls/port \
+	-I$(srctree)/lib/mbedtls/external/mbedtls \
+	-I$(srctree)/lib/mbedtls/external/mbedtls/include \
+	-Wno-builtin-declaration-mismatch \
+	-D__KERNEL__ -DCONFIG_SYS_TEXT_BASE=0
+
+# Linking flags
+SHARED_LDFLAGS := -L$(UBOOT_BUILD) -lu-boot -Wl,-rpath,$(UBOOT_BUILD)
+
+STATIC_LDFLAGS := -Wl,-T,$(LIB_STATIC_LDS) \
+	-Wl,--whole-archive $(UBOOT_BUILD)/libu-boot.a \
+	-Wl,--no-whole-archive \
+	-lpthread -ldl $(PLATFORM_LIBS) -Wl,-z,noexecstack
diff --git a/examples/ulib/rules.mk b/examples/ulib/rules.mk
new file mode 100644
index 00000000000..aba6c63d8b6
--- /dev/null
+++ b/examples/ulib/rules.mk
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Build rules for U-Boot library examples
+#
+# Copyright 2025 Canonical Ltd.
+# Written by Simon Glass <simon.glass@canonical.com>
+
+# Generate normal and statically linked binary names from progs variable
+all_bins := $(foreach prog,$(progs),$(OUTDIR)/$(prog) \
+	$(OUTDIR)/$(prog)_static)
+
+# Default target builds both programs
+all: $(all_bins)
+
+# System headers rule for objects that need system APIs
+$(foreach obj,$(sys-objs),$(eval $(OUTDIR)/$(obj): \
+	$(EXAMPLE_DIR)/$(obj:.o=.c) | $(OUTDIR) ; \
+	$$(CC) $$(CFLAGS) $$(SYSTEM_CFLAGS) -c -o $$@ $$<))
+
+# Automatic build rules for all programs
+$(foreach prog,$(progs),$(eval $(OUTDIR)/$(prog): \
+	$$(addprefix $(OUTDIR)/,$$($(prog)_objs)) ; \
+	$$(CC) $$(CFLAGS) -o $$@ $$^ $$(SHARED_LDFLAGS)))
+$(foreach prog,$(progs),$(eval $(OUTDIR)/$(prog)_static: \
+	$$(addprefix $(OUTDIR)/,$$($(prog)_objs)) ; \
+	$$(CC) $$(CFLAGS) -o $$@ $$^ $$(STATIC_LDFLAGS)))
+
+# Create the output directory if it doesn't exist
+$(OUTDIR):
+	@mkdir -p $@
+
+# Default rule: compile with U-Boot headers
+$(OUTDIR)/%.o: $(EXAMPLE_DIR)/%.c | $(OUTDIR)
+	$(CC) $(CFLAGS) $(UBOOT_CFLAGS) -c -o $@ $<
+
+# The U-Boot library must be built before we can link against it
+# Order-only prerequisites ensure libraries exist before linking
+$(all_bins): | $(UBOOT_BUILD)/libu-boot.a $(UBOOT_BUILD)/libu-boot.so \
+	$(OUTDIR)
\ No newline at end of file
