Buildroot and compiler on target
Edited on 8/5/2024
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 patch that we developed in
order to reintroduce having a compiler on target. This patch 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 patch was developed on top of Buildroot
version 2021.08-rc1
.
Patch
gcc-target
package
Here, we 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 patch.
--- /dev/null
+++ b/package/gcc/gcc-target/gcc-target.mk
@@ -0,0 +1,79 @@
+################################################################################
+#
+# 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
+
+GCC_TARGET_GLIBC_LIBS = \
+ 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
+
+# Install standard C libraries (from glibc)
+define GCC_TARGET_INSTALL_LIBS
+ for libpattern in $(GCC_TARGET_GLIBC_LIBS); do \
+ $(call copy_toolchain_lib_root,$$libpattern) ; \
+ done
+ cp -dpf $(STAGING_DIR)/usr/lib/*crt*.o $(TARGET_DIR)/usr/lib/
+ cp -dpf $(STAGING_DIR)/usr/lib/*_nonshared.a $(TARGET_DIR)/usr/lib/
+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))
Global configuration
Now that our gcc-target
package is created, let’s declare it in the global
configuration.
--- a/package/Config.in
+++ b/package/Config.in
@@ -170,6 +170,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 patch, 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 patch is available for download
here and depending on your Buildroot
version, it may be applied using git apply
.