[GUIDE]: How to tweak the kernel configuration to optimize it for a specific device / [sub-]target without breaking the kernel (full build script included)

This guide is intended for those who are building a custom firmware image for a particular [sub-]target or device wish to tweak the openwrt linux kernel to optimize it for that specific [sub-]target or device. Tweaking the kernel configuration in openwrt is hard to do right...due to the way the buildsystem is setup it is easy to tweak things and then either

  1. The kernel wont build at all due to missing dependencies, or
  2. The kernel builds but is broken and the resulting firmware image is unstable (to some degree) due to using a broken kernel

This is particularly frustrating since its often not immediately obvious why this is happening if you dont have a thorough understanding of how this part of the build process actually works. This guide analyses the buildsystem's kernel configuration process, describing why these outcomes often occur when you tweak the kernel configuration and offers a couple of ways to avoid this problem and produce custom kernels that actually work correctly.

Note: to clarify, this is not a guide about what tweaks you should make to optimize the kernel for a given device. It is a guide on how to work with the openwrt buuildsystem to allow you to tweak the kernel as you would like and not have it break the kernel. The buildsystem kernel configuration is currently optimized for setting up kernels on a variety of targets/subtargets/devices using layered config templates. If you want to optimize the kernel for a wide range of devices use the build system as-is. If you only care about a specific device or [sub-]target and wont be using this buildroot to build for anything other than that, then read on, since make kernel_menuconfig probably doesnt work how you think it does...


TL;DR

To build firmware with a custom kernel, dont just run

./scripts/feeds update -a
./scripts/feeds install -a
make menuconfig kernel_menuconfig prepare
make

At the bare minimum, run

./scripts/feeds update -a
./scripts/feeds install -a
make menuconfig kernel_menuconfig target/linux/clean prepare
#                                 ## ^^ADD THIS^^ ##
make

Ideally, go a step further and follow "Solution #2" near the bottom of this guide (a full build script is provided at the bottom of this guide)

Note: make prepare builds the build tools / toolchain and builds the kernel, but doesn't build target openwrt packages (including kmods).

Note: its not a bad idea to change make menuconfig to make menuconfig download check and to add -j$(nproc) V=sc to the make commands. Building will utilize multiple CPUs and will be a good bit faster this way.


How make kernel_menuconfig works

The kernel configuration uses a single kernel .config file in the kernrl build_dir, but generates this .config from multiple sources. It does this because the buildsystem is optimized to build firmware for a wide range of targets/devices, and this makes it possible to automate the kernel's configuration over a wide range of targets/devices by using a series of progressively more "device-specific" configuration options. Assuming the router runs linux, it pulls config info from a few sources:

  1. The "generic" kernel config for linux-based routers (found in target/linux/generic/)
  2. The kernel config for that target (found in target/linux/$TARGET/)
  3. The kernel config for that subtarget (found in target/linux/$TARGET/$SUBTARGET/)
  4. Dynamicly-generated config options based on what you chose when running make menuconfig (Im not sure exactly the process of how these get added, but they do somehow)

Note: these aren't full expanded .config's, but rather just enable / disable a few specific options.

The configs dictated by these 4 sources get layered together, combining them into a single config template (or a "sparse .config), and then something like make defconfig is run on this combined sparse .config, expanding it to the full .config that is actually used in the kernel build.


The Problem

The problem is that when you run make kernel_menuconfig you dont see the full .config...rather it only pulls the config from one of these sources (you can choose which using make kernel_menuconfig CONFIG_TARGET=<...>). Furthermore, regardless of which CONFIG_TARGET you use (or dont use), these dont seem to include the dynamically generated config options based on your make menuconfig selections). You then tweak it with the kernel_menuconfig, which expands it to a full .config when you save it.

Furthermore, if the .config file exists in the kernel's build_dir build root (at something like build_dir/target_$GCC_TARGET/linux-$TARGET/linux-$KERNEL_VER/.config) then that is what gets used to build the kernel. Which means that if you run

make menuconfig kernel_menuconfig prepare

It builds the generic kernel with your modifications, but without the TARGET / SUBTARGET configs and without the dynamic configs generated from your make menuconfig picks. Which, in particular, if you did a lot of tweaking during your make menuconfig, tends to produce the sort of errors described at the top of the post that are so easy to run into.


Solution #1 - quick and easy, but imperfect

THE SOLUTION

Its important to note that running make kernel_menuconfig doesnt just save the .config file in the kernel; build_dir, but also updates the config templates in the target/linux/<...> directory. Which leads to an easy (though imperfect) solution: remove the kernel .config after you run kernel_menuconfig but before you build the kernel. i.e., run

make menuconfig kernel_menuconfig target/linux/clean prepare
  • the menuconfig sets up the build and configures the "dynamically added kernel configuration based on menuconfig selections" kernel configuration templates
  • the kernel_menuconfig generates the .config in the build_dir and updates the target/linux/*config templates,
  • the target/linux/clean removes the kernel build_dir (and the generated .config), and
  • the prepare builds the kernel, using the proper config layering with the modified config templates that were updated by kernel_menuconfig

WHY IT IS "IMPERFECT"

This mostly fixes the problems that you describe, but leaves the possibility that things can get screwed up when updating the config template overrides some other essential config option. This might be due to a couple of things:

Problem #1: Your kernel_menuconfig choices.

You change something essential that is set in one of the other config templates or in the dynamically generated configs added based on your make menuconfig choices (meaning that you dont see it in the make kernel_menuconfig menu), overriding it.

Problem #2: The automatic update of the kernel config templates

When kernel_menuconfig exits and updates the config templates, it replaces them with a partially-expanded config, sort of like what running ./scripts/diffconfig produces for the standard )(non-kernel) config. i.e., not just the exact config options you selected / deselected, but also any config options that (due to the automatic kconfig dependency rules) were unlocked based on your config change. These automatic dependencies can, for example, add "default" config options for a given item. These unlocked configs (that you didnt explicitly choose) are more often than not are wayyy deep in submenus in the kernel_menuconfig.

So, if you re-enable some config that is enabled by a config template but that isnt shown in the kernel_menuconfig, all the unlocked config options will be assigned default values, which will get saved in the config template and can override other (essential) configs pulled in from one of the other config template sources. Furthermore, because part of the complete config is missing, diffconfig can expand things in such a way that just running make kernel_menuconfig and immediately exiting (with or without saving ) can/will update the config template in a way that overrides some of the other configs templates, and ultimately gives a different final kernel .config configuration and a different (and possibly broken) compiled kernel than if you didnt run make kernel_menuconfig at all and just started with make prepare.


Solution #2: a better (but more involved) way to configure the kernel

So, the idea here is to (before running make kernel_menuconfig) first run make prepare and get the unmodified but complete kernel .configfrom the kernel's build directory and incorporate that info into the $TARGET config template, so that when you run make kernel_menuconfig it shows the full (properly layered and including dynamic modifications from make menuconfig choices) kernel configuration.

Unfortunately, we cant just replace the $TARGET config template with the full .config, since that would break the automatic dependency system that is used in the kernel config (automatic dependencies wont override used-selected config options, and using the full .config makes it think everything is user selected). We want/need this dependency system working to produce working kernels all the time except when it would override an essential config option listed in one of the config template sources.

The code for the solution is below, but it more-or-less involves

  1. building an unmodified kernel (make prepare) and saving the full unmodified .config and then remove it from the kernel build dir
  2. getting the partial unmodified .config (make kernel_menuconfig--> save (without making changes) to .config --> exit), then saving that .config and then remove it from the kernel build dir
  3. running a diff between the full and partial unmodified .configs, and adding all the stuff that is present in the full .config but missing from the partial .config into the $TARGET config template
  4. run make_kernel menuconfig again (which should now basically be using the diffconfig version of the full kernal .config configuration) and actually tweak the kernel as desired, then save it to .config
  5. (optional "sanity check") save this .config somewhere, remove it from the kernel build_dir, then re-run make kernel_menuconfig and save to .config without changing anything. This .config should be identical to the .config you just saved outside of the kernel build directory, meaning that everything in the $TARGET config template includes (and isnt getting overwritten by) everything that the other config template sources would have done to the .config.
  6. run make prepare to build the kernel, then make to finish building the firmware.

FULL BUILD SCRIPT FOR ROBUSTLY BUILDING FIRMWARE WITH A CUSTOM KERNEL

#!/usr/bin/env bash

# clone openwrt repo and cd to openwrt buildroot
git clone https://github.com/openwrt/openwrt.git
cd openwrt

# setup package feeds
./scripts/feeds update -a
./scripts/feeds install -a
./scripts/feeds install -a

# OPTIONAL: copy a generic .config for your device to serve as a "starting point". save it is `.config`

# configure openwrt settings
make menuconfig

# downloiad/check sources, then build unmodified kernel 
make -j$(nproc) V=sc download check prepare

# save important paths in variables
target_board="$(grep -F CONFIG_TARGET_BOARD <.config | sed -E 's/^[^"]*"//;s/".*$//')"
target_kconfig="$(ls target/linux/${target_board}/config-*)"
builddir_kconfig="$(ls build_dir/target*/linux-${target_board}*/linux-*/.config)"

# copy full default config of the (just built) default kernel
\mv -f "${builddir_kconfig}" .config.kernel

# generate the partial .config (for the kernel that kernel_menuconfig by default shows) in the kernel build dir
# NOTE: ***DONT CHANGE ANYTHING*** in this kernel_menuconfig - immediately save it (to .config) and exit. 
make kernel_menuconfig

# diff and add to target config to get target diffconfig of full default kernel config
diff .config.kernel "${builddir_kconfig}" | grep '<' | sed -E s/'^< '// >> "${target_kconfig}"

# remove incomplete .config out of kernel build dir
rm "${builddir_kconfig}" 

# regenerate .config (which gives the full complete kernel .config) and actually custom configure the kernel this time -- save as .config when done
make kernel_menuconfig

# diff and add to target config to get diffconfig of full custom kernel config,
# then diff that with the target config template (that make kernel_menuconfig 
# *should* have updated) and add anything that is missing to target .config.
# note: diff arguments of inner diff are flipped from previous diff command
echo "$(diff <(diff "${builddir_kconfig}" .config.kernel | grep '<' | sed -E s/'^< '//) "${target_kconfig}" | grep '<' | sed -E s/'^< '//)" >> "${target_kconfig}"

# save full .config and fix up target config. 
# dont change anything in kernel_menuconfig - immediately save it (to .config) and exit
\mv -f "${builddir_kconfig}" .config.kernel
make kernel_menuconfig

# check that re-generating the kernel .config doesnt change anything
# the following command should indicate there are no changes
diff .config.kernel "${builddir_kconfig}"

# make kernel
make -j$(nproc) V=sc prepare

# make the rest of the build
make -j$(nproc) -k V=sc
5 Likes

While I just do some "sed" for existing attributes within kernel config I wonder if there is no benefit in using either env or quilt to make life easier?

I don't know if either of them would track thing properly.

./scripts/env new blah
#make your changes
./scripts/env diff
./scripts/env save
./scripts/env switch blah # for reuse
quilt init
#make your changes
quilt refresh
quilt pop/push # for reuse

Thank you for this script. The only problem I have sometimes. Even if I have the make menuconfig fully sorted out. The last step still fails. Could it mean that I need to be more careful is there are not depencies missing in the make menuconfig. Cause the kernel builds. But also sometimes with the download check prepare something it can't download stuff cause the link to the mirrors are broken.

Sometimes it works but even with this scripts, and it is a really good one cause it will set everything in the kernel_menuconfig. Does it mean that when I change some stuff in the kernel_menuconfig it will get broken?

Sometimes it sets stuff but like net_filter the masqurade options are not in. So I set them in the kernel_menuconfig. But doing the last step. make -j$(nproc) -k V=sc alot of time will fail.

When that happens can I still fix stuff. Cause I feel like you are doing it in order, but when something goes wrong. I need to start over again.

What I do is like I save the menuconfig .config and set it outside the directory. I fully delete the directory and start over again.

But I don't know when I set it in menuconfig if I can still add stuff later on in the kernel_menuconfig.

It still feels very vulnerable to me and easy to break.