In this article, I’ll describe how embedded firmware developers can leverage pre-commit
to automate and enforce code quality checks in their Zephyr RTOS embedded firmware projects.
An earlier revision of this article recommended running clang-format
as a pre-commit hook. While this is still possible, there are some downsides to running clang-format
automatically via pre-commit
(for example, clang-format
formatting does not 100% align with the Zephyr project coding style and needs some manual fixing).
I have updated this article to focus on using pre-commit
to automatically run the Zephyr checkpatch.pl
script as recommended by the Zephyr project coding style. If you’re interested in setting up clang-format
as a pre-commit
hook, check out this article from the Interrupt blog.
What’s the problem?
To prevent back-and-forth discussions over code style and improve code quality, many embedded development teams have adopted a set of “coding style” guidelines. However, if these style guidelines are simply written down in a README or some other project documentation, it’s inevitable that those guidelines will be ignored.
Phillip Johnston from Embedded Artistry wrote an excellent article Creating and Enforcing a Code Formatting Standard with clang-format with this quote from MongoDB that bears repeating here:
A formatting process which is both manual and insufficient is doomed to be abandoned.
Phillip’s article is a fantastic deep-dive on how to use the clang-format
tool to automatically format code to comply with code formatting guidelines encoded in a .clang-format
config file. In a follow up article, he describes how to enforce these rules on a build server in a CI environment to ensure that all changes are checked for formatting before they are merged into the main branch.
He also mentions that it’s possible to enforce formatting checks in a local development environment using git hooks, but points out the following caveat:
[…] the use of
git
hooks requires users to install the hooks on their end. It’s hard to fully enforce that flow, especially since you’re relying on developers to remember that hooks need to be installed.
The reality is that managing git hooks manually is painful and it’s hard to enforce consistency across multiple developers, especially as those hooks change over time.
How do you make sure that everyone on your team is running the same set of pre-commit checks?
A case for using pre-commit
in Zephyr projects
Fortunately, there is a fantastic tool called pre-commit
that can automatically install, manage, and run git commit
hooks, without requiring individual developers to manage these hooks manually. By shipping a single .pre-commit-config.yaml
config with your embedded project, you can make it trivially easy for individual developers to run code quality checks and automatically enforce code style rules in their development environment. And this all happens before the code is checked into git. This can help ensure that code submitted for peer review is consistently formatted and matches the agreed-upon code style for the project, allowing discussions to focus on the actual content of the changes without devolving into code style arguments.
In addition to checking for code style issues such as formatting and linting, pre-commit
can be configured to run other types of checks (linting, line-endings & whitespace, YAML file syntax issues, common code security issues, etc). There is a growing set of custom community-provided hooks in addition to the ones provided out-of-the-box by the developers.
For a great introduction to using pre-commit
for embedded development in general, Noah Pendleton published an article on the Interrupt blog about how they Automatically format and lint code with pre-commit at Memfault.
At this point, you might be thinking “I don’t want to learn a new tool” or “I don’t want to introduce additional dependencies”. However, I would argue that pre-commit
actually simplifies the setup process for new developers vs. a manual approach.
First, if you’re using Zephyr, you’re probably already setting up a python virtual environment. Installing pre-commit
for a Zephyr project is just two commands:
pip install pre-commit
pre-commit install
That’s it! Once pre-commit
is installed, the code quality checks will run automatically whenever a developer runs git commit
in their local development environment. No more having to manually run checks before committing!
Second, if you’re already using west manifest files to manage dependencies for your project’s source code, you will find .pre-commit-config.yaml
to be familiar in concept. Just as west.yml
can be used to lock the specific revision for each source code dependency, the .pre-commit-config.yaml
file is used to lock the specific revision for each code quality check you want to run. Just check this file into the git repo for your project, and all developers on your team will run the exact same set of checks.
What checks should I run?
The Zephyr project Coding Style recommends using a modified version of the Linux kernel checkpatch.pl
tool shipped with Zephyr, along with the provided .checkpatch.conf
file. This is a good place for us to start when setting up pre-commit checks in our Zephyr application.
The remainder of this article is going to describe how to configure pre-commit
to automatically run Zephyr’s checkpatch.pl
before each git commit
. This will flag any code style issues present in our project code before we commit any changes.
If you don’t already have existing code style guidelines required by your team or project, I would recommend simply copying .checkpatch.conf
from the Zephyr project as a starting point. For example, if your project follows the recommended layout in the Zephyr example-application, you can copy this file into your application as follows:
cd example-application-workspace/
cp zephyr/.checkpatch.conf example-application/.checkpatch.conf
You’ll need to change the following line in .checkpatch.conf
to match the location of the typedefsfile
file in your project (in this example, I’m just using the one included in the Zephyr repository):
--typedefsfile=../zephyr/scripts/checkpatch/typedefsfile
If this path is set up incorrectly, you’ll get an error that looks like this:
No additional types will be considered - file '../foo/bar/typedefsfile': No such file or directory
Check these config files into the root of your project’s git repository.
How to configure pre-commit
in a Zephyr project
Now that we’ve got the .checkpatch.conf
config file in our project repo, we can create a pre-commit
config to run these checks. For the remainder of this article, I’m going to assume your project follows the recommended layout in the Zephyr example-application, but it should be simple to adapt this to your project layout.
First, we need to add an empty pre-commit
config file to our project repo:
example-application-workspace/example-application/.pre-commit-config.yaml
pre-commit
hooks are just git repositories and they are specified in the .pre-commit-config.yaml
file as a list of values under the repos
key. Each hook has a:
repo
key that contains the URL of the git repositoryrev
key that contains the specific revision of the hookhooks
key that describes the hooks to run (repos can contain multiple hooks)
Next, we’ll add a hook that runs Zephyr’s checkpatch.pl
(using the .checkpatch.conf
config file we added earlier):
repos:
- repo: https://github.com/cgnd/zephyr-pre-commit-hooks
rev: v1.0.0
hooks:
- id: zephyr-checkpatch-diff
Finally, check in the .pre-commit-config.yaml
file into your project’s git repository.
Now we’re ready to install and run pre-commit.
How to install and run pre-commit
As mentioned earlier, it’s simple to install the pre-commit
tool:
pip install pre-commit
Next, run the following command to install the git hooks:
cd example-application-workspace/example-application/
pre-commit install
NOTE: The zephyr-checkpatch-diff
hook we added requires that the Zephyr environment scripts have been run. Run the following command to set up the Zephyr environment variables (this must be run for each new terminal session):
source example-application-workspace/zephyr/zephyr-env.sh
The zephyr-checkpatch-diff
hook we added will run checks on any staged changes that we’re trying to commit (i.e. it will only check the output of git diff --cached
and does NOT check entire files).
To show how this works, let’s make some changes in the project’s main.c
to violate Zephyr’s coding style:
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(main, CONFIG_APP_LOG_LEVEL);
-int main(void)
-{
- int ret;
+int main(void) {
+ int new;
const struct device *sensor;
printk("Zephyr Example Application %s\n", APP_VERSION_STRING);
Let’s see what happens when we try to commit this change:
❯ git add app/src/main.c
❯ git commit -m "Change variable name"
Run Zephyr's checkpatch.pl...............................................Failed
- hook id: zephyr-checkpatch-diff
- exit code: 1
-:12: ERROR:OPEN_BRACE: open brace '{' following function definitions go on the next line
#12: FILE: app/src/main.c:13:
+int main(void) {
- total: 1 errors, 0 warnings, 11 lines checked
In this example, checkpatch.pl
checks failed because the Zephyr coding style requires that open braces go on the next line after function definitions.
After fixing the error (by moving the open brace to the next line), we can git add
the modified files:
❯ git add app/src/main.c
When we try to commit again, all the checks pass!
❯ git commit -m "Change variable name"
Run Zephyr's checkpatch.pl...............................................Passed
[test 689d9c0] Change variable name
1 file changed, 1 insertion(+), 1 deletion(-)
Tips & Tricks
What to do when clang-format
and checkpatch.pl
disagree
You may run into cases where Zephyr’s .clang-format
and .checkpatch.conf
disagree on code style (in the Zephyr project, clang-format
is not intended to be a one-stop solution to all the code style issues).
For example, here’s how clang-format
will format the following code using Zephyr’s .clang-format
:
static const struct divider_config divider_config = {
.io_channel =
{
DT_IO_CHANNELS_INPUT(VBATT),
},
.power_gpios = GPIO_DT_SPEC_GET_OR(VBATT, power_gpios, {}),
.output_ohm = DT_PROP(VBATT, output_ohms),
.full_ohm = DT_PROP(VBATT, full_ohms),
};
If we try to commit this, checkpatch.pl
will fail because it thinks that the {
should be on the same line as .io_channel =
:
Run Zephyr's checkpatch.pl...............................................Failed
- hook id: zephyr-checkpatch-diff
- exit code: 1
-:15: ERROR:OPEN_BRACE: that open brace { should be on the previous line
#15: FILE: src/battery_monitor/battery.c:87:
+ .io_channel =
+ {
- total: 1 errors, 0 warnings, 24 lines checked
You can resolve this conflict by using special comments to tell clang-format
to skip formatting on certain lines in the file:
static const struct divider_config divider_config = {
/* clang-format off */
.io_channel = {
DT_IO_CHANNELS_INPUT(VBATT),
},
/* clang-format on */
.power_gpios = GPIO_DT_SPEC_GET_OR(VBATT, power_gpios, {}),
.output_ohm = DT_PROP(VBATT, output_ohms),
.full_ohm = DT_PROP(VBATT, full_ohms),
};
Now, if you try to commit this, checkpatch.pl
checks will pass.
How to skip running pre-commit
hooks if necessary
There may be times when pre-commit
is failing but you need to commit anyway.
To skip all pre-commit
hooks, add the --no-verify
switch to the git commit
command:
git commit --no-verify -m "foo"
To skip one or more hooks, you can set the SKIP
environment variable to a comma separated list of hook ids:
SKIP=zephyr-checkpatch-diff git commit -m "foo"
Automatically install pre-commit
hooks when a repo is cloned
You can configure git init
to automatically install pre-commit
hooks when a new repository is cloned (rather than having to run pre-commit install
manually).
Follow the recommended setup instructions at https://pre-commit.com/#pre-commit-init-templatedir
Example Repo
If you would like to check out an example Zephyr application with a pre-commit
config, I’ve added a pre-commit branch to my fork of the Zephyr example-application
repo. Here’s a direct link to the .pre-commit-config.yaml
file:
https://github.com/cgnd/example-application/blob/pre-commit/.pre-commit-config.yaml
Revision History
Revision | Date | Description |
---|---|---|
1 | 2023-07-16 | Initial release |
2 | 2023-08-23 | Remove clang-format hook |
Feedback
If you are using pre-commit
(or something like it) as part of your embedded development workflow, let me know in the comments below! I’d love to learn more about how other embedded teams are using tools like this in their workflow.