Introduction

For a project on which my lab has been working for the past year (project LupBook), we need to build the smallest Linux system possible that embeds a software development toolchain (e.g., compiler, linker, libraries, etc.).

One of the best projects out there that enables building small Linux systems is Buildroot. Unfortunately, while buildroot can generate systems that integrate thousands of different software packages, it stopped supporting having a compiler on target about 10 years ago. Instead, they suggest using other Linux distributions, but which are likely to produce (much) bigger systems.

For our project, having the smallest system possible is of paramount importance because we want to provide an interactive textbook framework that has a minimal memory footprint.

So, in case other people are interested, here is the patchset that we developed in order to reintroduce having a compiler on target. This patchset is strongly inspired on how such support used to exist back in 2012. Feel free to adapt it to your needs. Also, note that this patchset was developed on top of Buildroot version 2021.08-rc1.

The patchset

Patch #1

This patch introduces the gcc-target package (as explained in the buildroot documentation.

First, we create two new configuration variables in package/gcc/Config.in.

--- /dev/null
+++ b/package/gcc/Config.in
@@ -0,0 +1,21 @@
+config BR2_PACKAGE_GCC_TARGET
+	bool "gcc"
+	depends on BR2_TOOLCHAIN_BUILDROOT
+	select BR2_PACKAGE_BINUTILS
+	select BR2_PACKAGE_BINUTILS_TARGET
+	select BR2_PACKAGE_GMP
+	select BR2_PACKAGE_MPFR
+	select BR2_PACKAGE_MPC
+	help
+	  If you want the target system to be able to run
+	  binutils/gcc and compile native code, say Y here.
+
+config BR2_EXTRA_TARGET_GCC_CONFIG_OPTIONS
+	string "Additional target gcc options"
+	default ""
+	depends on BR2_PACKAGE_GCC_TARGET
+	help
+	  Any additional target gcc options you may want to include....
+	  Including, but not limited to --disable-checking etc.
+	  Refer to */configure in your gcc sources.
+

If enabled in the user configuration, the first variable tells buildroot to build a compiler for the target. The second variable allows the user to provide additional options when compiling the compiler.

Next, we create the “hash” file which contains the hashes of the downloaded files for our package. Since we use the same GCC compiler as the internal toolchain built by buildroot, we can reuse their hash file. The file package/gcc/gcc-target/gcc-target.hash is therefore just a symbolic link to the already existing package/gcc/gcc.hash file.

Finally, we create the “mk” file which details how our package should be built and deployed. This is, by far, the most complicated part of the entire patchset.

--- /dev/null
+++ b/package/gcc/gcc-target/gcc-target.mk
@@ -0,0 +1,78 @@
+################################################################################
+#
+# gcc-target
+#
+################################################################################
+
+GCC_TARGET_VERSION = $(GCC_VERSION)
+GCC_TARGET_SITE = $(GCC_SITE)
+GCC_TARGET_SOURCE = $(GCC_SOURCE)
+
+# Use the same archive as gcc-initial and gcc-final
+GCC_TARGET_DL_SUBDIR = gcc
+
+GCC_TARGET_DEPENDENCIES = gmp mpfr mpc
+
+# First, we use HOST_GCC_COMMON_MAKE_OPTS to get a lot of correct flags (such as
+# the arch, abi, float support, etc.) which are based on the config used to
+# build the internal toolchain
+GCC_TARGET_CONF_OPTS = $(HOST_GCC_COMMON_CONF_OPTS)
+# Then, we modify incorrect flags from HOST_GCC_COMMON_CONF_OPTS
+GCC_TARGET_CONF_OPTS += \
+	--with-sysroot=/ \
+	--with-build-sysroot=$(STAGING_DIR) \
+	--disable-__cxa_atexit \
+	--with-gmp=$(STAGING_DIR) \
+	--with-mpc=$(STAGING_DIR) \
+	--with-mpfr=$(STAGING_DIR)
+# Then, we force certain flags that may appear in HOST_GCC_COMMON_CONF_OPTS
+GCC_TARGET_CONF_OPTS += \
+	--disable-libquadmath \
+	--disable-libsanitizer \
+	--disable-plugin \
+	--disable-lto
+# Finally, we add some of our own flags
+GCC_TARGET_CONF_OPTS += \
+	--enable-languages=c \
+	--disable-boostrap \
+	--disable-libgomp \
+	--disable-nls \
+	--disable-libmpx \
+	--disable-gcov \
+	$(EXTRA_TARGET_GCC_CONFIG_OPTIONS)
+
+GCC_TARGET_CONF_ENV = $(HOST_GCC_COMMON_CONF_ENV)
+
+GCC_TARGET_MAKE_OPTS += $(HOST_GCC_COMMON_MAKE_OPTS)
+
+# Install standard C headers (from glibc)
+define GCC_TARGET_INSTALL_HEADERS
+	cp -r $(STAGING_DIR)/usr/include $(TARGET_DIR)/usr
+endef
+GCC_TARGET_POST_INSTALL_TARGET_HOOKS += GCC_TARGET_INSTALL_HEADERS
+
+# Install standard C libraries (from glibc)
+GCC_TARGET_GLIBC_LIBS = \
+	*crt*.o *_nonshared.a \
+	libBrokenLocale.so libanl.so libbfd.so libc.so libcrypt.so libdl.so \
+	libm.so libnss_compat.so libnss_db.so libnss_files.so libnss_hesiod.so \
+	libpthread.so libresolv.so librt.so libthread_db.so libutil.so
+
+define GCC_TARGET_INSTALL_LIBS
+	for libpattern in $(GCC_TARGET_GLIBC_LIBS); do \
+		$(call copy_toolchain_lib_root,$$libpattern) ; \
+	done
+endef
+GCC_TARGET_POST_INSTALL_TARGET_HOOKS += GCC_TARGET_INSTALL_LIBS
+
+# Remove unnecessary files (extra links to gcc binaries, and libgcc which is
+# already in `/lib`)
+define GCC_TARGET_RM_FILES
+	rm -f $(TARGET_DIR)/usr/bin/$(ARCH)-buildroot-linux-gnu-gcc*
+	rm -f $(TARGET_DIR)/usr/lib/libgcc_s*.so*
+	rm -f $(TARGET_DIR)/usr/$(ARCH)-buildroot-linux-gnu/lib/ldscripts/elf32*
+	rm -f $(TARGET_DIR)/usr/$(ARCH)-buildroot-linux-gnu/lib/ldscripts/elf64b*
+endef
+GCC_TARGET_POST_INSTALL_TARGET_HOOKS += GCC_TARGET_RM_FILES
+
+$(eval $(autotools-package))

Patch #2 and #3

Now that our gcc-target package is created, let’s declare it in the global configuration.

--- a/package/Config.in
+++ b/package/Config.in
@@ -171,6 +171,7 @@ menu "Development tools"
 	source "package/flex/Config.in"
 	source "package/gawk/Config.in"
+	source "package/gcc/Config.in"
 	source "package/gettext/Config.in"
 	source "package/gettext-gnu/Config.in"
 	source "package/gettext-tiny/Config.in"

Finally, we need to slightly alter the Makefile that is located in Buildroot’s root directory. Since Buildroot no longer officially supports having a compiler on target, they remove a bunch of files by default that we actually need. The patch here prevents the Makefile from removing these files (i.e., some necessary headers and libraries).

--- a/Makefile
+++ b/Makefile
@@ -738,13 +738,13 @@ target-finalize: $(PACKAGES) $(TARGET_DIR) host-finalize
 	@$(call MESSAGE,"Finalizing target directory")
 	$(call per-package-rsync,$(sort $(PACKAGES)),target,$(TARGET_DIR))
 	$(foreach hook,$(TARGET_FINALIZE_HOOKS),$($(hook))$(sep))
-	rm -rf $(TARGET_DIR)/usr/include $(TARGET_DIR)/usr/share/aclocal \
+	rm -rf $(TARGET_DIR)/usr/share/aclocal \
 		$(TARGET_DIR)/usr/lib/pkgconfig $(TARGET_DIR)/usr/share/pkgconfig \
 		$(TARGET_DIR)/usr/lib/cmake $(TARGET_DIR)/usr/share/cmake \
 		$(TARGET_DIR)/usr/doc
 	find $(TARGET_DIR)/usr/{lib,share}/ -name '*.cmake' -print0 | xargs -0 rm -f
 	find $(TARGET_DIR)/lib/ $(TARGET_DIR)/usr/lib/ $(TARGET_DIR)/usr/libexec/ \
-		\( -name '*.a' -o -name '*.la' -o -name '*.prl' \) -print0 | xargs -0 rm -f
+		\( -name '*.la' -o -name '*.prl' \) -print0 | xargs -0 rm -f
 ifneq ($(BR2_PACKAGE_GDB),y)
 	rm -rf $(TARGET_DIR)/usr/share/gdb
 endif

Conclusion

With this patchset, we are now able to compile programs at runtime:

Welcome to Buildroot
buildroot login: root
# echo -e "#include<stdio.h>\nint main(void){ printf(\"hello world\\\n\"); return 0; }" > /tmp/test.c
# gcc -o /tmp/test /tmp/test.c
# ./tmp/test
hello world

With a minimal Buildroot configuration, we are able to generate a compressed Linux system embedding a compiler that is only 14 MiB in size!!!

$ ll buildroot/output/images/rootfs.cpio.zst
-rw-r--r--  1 joel joel  14M 2021-12-03 15:18 rootfs.cpio.zst

Pretty cool, huh? :D

The patchset is available for download here and depending on your Buildroot version, it may be applied using git apply.