# puncover: sizing up stack usage and static memory in a firmware build

> puncover is a Python tool that analyses a C or C++ binary for code size, static variables and stack usage, and presents the result as a browsable report with disassembly and call-stack analysis per directory, file or function. It suits embedded developers who need to know where a firmware image spends its flash and RAM, and the stack numbers only appear if you rebuild with the compiler's stack usage flag and point the tool at the build directory.

**HBehrens/puncover** — Analyses C/C++ build output for code size, static variables, and stack usage

- Repository: https://github.com/HBehrens/puncover
- Stars: 591 · Forks: 126
- Language: Python
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/hbehrens-puncover

## A report that drills from directory to function

puncover analyses C/C++ binaries for code size, static variables and stack usages, and creates a report with disassembler and call-stack analysis per directory, file, or function.

That per-level breakdown is the whole design. A linker map file or a size report will tell you that a section is 40 kilobytes, which is not a thing you can act on. A report that says the stack usage of the deepest call chain through one translation unit is 900 bytes, attributed to named functions, is a thing you can act on. The three axes are code size, static variables, which are the RAM that is allocated at link time and lives for the life of the program, and stack usage, which is the transient RAM that grows and shrinks with the call depth.

The stack number is the one embedded developers care about most, because exceeding the stack on a microcontroller is an intermittent failure that looks like a random reset. A tool that tells you the worst-case stack of a call tree, before you flash it, is worth a lot on a board with a few kilobytes of RAM.

The output is a local web application rather than a file. The project is written in Python and the report is served on http://127.0.0.1:5000/ in the browser, and the dependencies confirm how that is built: Flask, Jinja2 and Werkzeug, with configargparse for the command line, including its YAML variant. The pyproject also includes the templates and static directories in both the wheel and the sdist, so the web assets ship with the package.

The repository layout follows the same split. There is a `puncover/` package directory, a `runner.py` at the root, an `examples/` directory with a Pebble example and a PlatformIO example, a `scripts/` directory, a `tests/` directory, and `images/` for the README screenshots.

The project is MIT licensed, the version in pyproject.toml is 0.8.0, and the last push was on 2026-07-25 with releases 0.8.0 from 2026-04-26 and 0.7.1 from 2026-02-23.

## Installing and running it takes one command and a browser

The usage path is short enough to state exactly.

Install with pip:

```bash
pip install puncover
```

Then run it by passing the binary to analyse:

```bash
puncover project.elf
...
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
```

And open the link in your browser to view the analysis.

The alternative is to use `uvx` to run the script without installing it globally:

```bash
uvx puncover project.elf
```

That is the same trade-off as always, with uvx paying for a temporary environment and a resolution on each run in exchange for not touching the global environment. For a tool you run occasionally against a firmware image, that is a reasonable default, and the project uses uv for its own development too.

The command line surface is wider than the README's quick start suggests. The `configargparse` dependency with its YAML extra means the options can come from a configuration file as well as from the command line, and the report export section names `--generate-report`, `--non-interactive`, `--report-type json` and `--report-tag`.

The console entry point is declared in pyproject as `puncover = "puncover.puncover:main"`, so the binary name and the module path are fixed by the packaging rather than by a wrapper script.

The project also requires Python 3.10 or newer, and the classifiers describe it as Development Status 5, Production/Stable, with the audience as Developers and English as the natural language.

## Stack numbers need the compiler flag and the build directory

The section on analysing stack usage contains the one piece of setup that is easy to miss, and the report is noticeably incomplete without it.

In order to evaluate the stack usage of function and call trees, it is necessary to build with the `-fstack-usage` flag. That tells GCC to generate stack usage files, with the .su extension, for each compilation unit. The compiler writes one file per translation unit recording the frame size of each function, and the tool reads them.

Then the second half. For puncover to evaluate these .su files, the `--build_dir` option needs to point to the build folder of the firmware. So the tool needs both the artefacts and the directory they were produced in, because the .su files carry the source path of the function and the tool has to match that back to the source tree in order to attribute the frames to files and directories.

That two-input design is the correct one, since the .su files are a build artefact rather than something you can recover from the ELF alone. The ELF has the code and the symbols; it does not have the per-function frame sizes the compiler computed. So the answer to the question puncover actually answers needs the build, not just the binary.

The consequence for a user is that the stack and call tree portions of the report appear only when both conditions are met, and when they are missing the tool is not broken, the inputs were not there. The code size and static variable figures come from the binary itself, so those are always available.

The embedded context is explicit in the README, which talks about the build folder of the firmware and about monitoring firmware changes in CI, so the intended workflow is a repeatable analysis over a build rather than a one-off inspection.

## JSON reports for CI, and why the report tag exists

The report export section is written for continuous integration, and the mechanism is worth reading closely because the report tag option is the part that is easy to misread.

To monitor firmware changes in CI it can be useful to run puncover and save a JSON report. To generate a report you add the flags `--generate-report --non-interactive --report-type json`, and for a distinguishing entry to save more than one report in one file you also add a report tag, `--report-tag $COMMIT_FEATURE`. The report is saved under this tag as an object entry.

So the output is a single JSON file that accumulates entries, each keyed by a tag, and the tag is meant to be something that identifies the build. Putting a commit or feature name in there is the point, because the file then becomes a history: the same report file accumulates one entry per commit, and you can diff the entries to see how code size and stack usage moved between two revisions.

That is the workflow the tool is designed for, and it is a better use of it than reading a single report. A function that quietly grows by 200 bytes of stack per release is invisible in a report you read once, and obvious in a file where you can compare the tagged entries side by side.

The `--non-interactive` flag is what makes this usable in CI at all, since the default mode starts a web server and waits for a browser. The JSON report mode is the headless path.

The release history shows this is a maintained tool rather than a one-off script. The listed versions are 0.8.0 from 2026-04-26, 0.7.1 from 2026-02-23 and 0.7.0 from 2025-12-03, and the package is on PyPI with a release workflow, a codecov badge and a tox configuration for the test matrix.

## The development setup, and what the examples are for

The contributing section is brief, but the details of the development environment are instructive about how the project is run.

To run the tests locally you need to install the development dependencies, and the project uses `uv` to manage the Python environment:

```bash
uv venv && source .venv/bin/activate
uv sync
```

Then the tests run with tox, and the README notes that this will fail if any of the supported Python versions are not found, with a pointer to `tox.ini`. To target only the current `python` on the path:

```bash
tox -e py
```

That dual tox invocation is the interesting part. The pyproject pins two different tox versions conditionally, `tox==3.27.1` for Python below 3.12 and `tox==4.11.3` for 3.12 and above, because the two tox releases are not compatible with each other's configuration handling. So the project supports running its matrix across several interpreters, and the local quick run is a separate single-interpreter target.

The build system is poetry-core rather than setuptools, and the comment explains why: the poetry build backend enables automatic classifier generation, with a link to an uv issue. The tool.poetry section then lists the classifiers by hand, including the Production/Stable status, and the packaging excludes the examples, images, scripts and tests directories from the wheel and sdist.

The examples directory holds a Pebble example and a PlatformIO example, which tells you the two build systems the author expects people to be using. PlatformIO is a widely used embedded build system, and having a worked configuration for each is what makes the tool usable with a real project rather than only with a toy one., and having a worked configuration for each is what makes the tool usable with a real project rather than only with a toy one.

The dev dependency list is also worth a glance for what it reveals about the test approach: pytest and pytest-cov for the suite, mock for isolation, ruff for linting with a target version of py312 and a line length of 100, and pydantic. The pydantic dependency suggests the report and configuration data are validated through models rather than ad hoc dictionaries.

## Conclusion

Adopt puncover if you are building firmware and need to attribute flash and RAM to individual functions rather than to a total, since the report drills down by directory, file and function. Do not adopt it if you only want a final image size, because it is an analysis tool rather than a size reporter, and check first that your build produced the stack usage files, since without the compiler flag and the build directory the stack and call tree figures are simply absent from the report.

## FAQ

### What does puncover analyse?

It analyses C/C++ binaries for code size, static variables and stack usages, and creates a report with disassembler and call-stack analysis per directory, file or function. The report is served locally in a browser on http://127.0.0.1:5000/.

### How do I install and run puncover?

Install it with `pip install puncover` and run `puncover project.elf`, then open the link in a browser. You can also use `uvx puncover project.elf` to run it without installing anything globally. The package requires Python 3.10 or newer.

### Why does puncover show no stack usage for my project?

Because the stack usage figures come from files the compiler produces, not from the binary. You have to build with the `-fstack-usage` flag so GCC generates .su files for each compilation unit, and pass the `--build_dir` option pointing at the build folder so puncover can read them and match them back to the source files.

### Can puncover produce a report for CI?

Yes. Run it with `--generate-report --non-interactive --report-type json` to write a JSON report, and add `--report-tag $COMMIT_FEATURE` to file the result under a distinguishing entry so more than one report can be saved in one file, which lets you compare size and stack usage between commits.

### How do I run the puncover tests locally?

The project uses uv to manage the environment, so run `uv venv && source .venv/bin/activate` and then `uv sync`, and run the tests with tox. The full matrix will fail if any of the supported Python versions are missing, so to target only the current python on the path use `tox -e py`.

### Is puncover still being maintained?

The repository is not archived and the last push was on 2026-07-25. The listed releases are 0.8.0 from 2026-04-26, 0.7.1 from 2026-02-23 and 0.7.0 from 2025-12-03, the package is published on PyPI, and the licence is MIT.

## Sources

- [HBehrens/puncover on GitHub](https://github.com/HBehrens/puncover)
- [Issues](https://github.com/HBehrens/puncover/issues)
- [License: MIT](https://github.com/HBehrens/puncover/blob/master/LICENSE)
- [README](https://github.com/HBehrens/puncover/blob/master/README.md)
- [Releases](https://github.com/HBehrens/puncover/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hbehrens-puncover
