Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/tools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
run: sudo apt-get update &&
sudo apt-get -y install ninja-build
automake autoconf libtool gettext libasound2-dev
pkg-config
pkg-config python3-construct python3-pytest

# Ubuntu 24.04 ships autoconf 2.71, but current alsa-lib/alsa-utils
# require >= 2.72. Build a newer autoconf from the GNU release
Expand Down Expand Up @@ -84,3 +84,10 @@ jobs:
run: CMAKE_BUILD_TYPE=Release ./sof/scripts/build-tools.sh ||
VERBOSE=1 NO_PROCESSORS=1 USE_XARGS=no
CMAKE_BUILD_TYPE=Release ./sof/scripts/build-tools.sh

# The build above already validated every topology v2 binary
- name: topology validator tests
env:
LD_LIBRARY_PATH: ${{ github.workspace }}/tools/lib
ALSATPLG: ${{ github.workspace }}/tools/bin/alsatplg
run: python3 -m pytest -v sof/tools/topology/validator/tests
36 changes: 36 additions & 0 deletions .github/workflows/tplg-validator-schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: Topology validator kernel schema

# Checks that tools/topology/validator/schema/kernel_tokens.py still
# matches the SOF driver token tables in the kernel. When this fails,
# rerun schema/gen_kernel_tokens.py against a kernel tree and review the
# validator rules against the kernel changes.

# yamllint disable-line rule:truthy
on:
schedule:
- cron: '0 3 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
kernel-schema:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
filter: 'tree:0'
path: sof

- name: Fetch SOF driver sources
run: |
git clone --depth 1 --filter=blob:none --sparse \
--branch topic/sof-dev https://github.com/thesofproject/linux.git linux
git -C linux sparse-checkout set --no-cone \
/include/uapi/sound/ /sound/soc/sof/ /sound/soc/soc-topology.c

- name: Check kernel token schema
run: python3 sof/tools/topology/validator/schema/gen_kernel_tokens.py
--kernel linux --check
33 changes: 33 additions & 0 deletions tools/topology/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ else()
message("-- alsatplg version: " ${ALSATPLG_VERSION_NUMBER})
endif()

# Check every topology2 binary like the Linux SOF driver would when
# loading it, see validator/README.md. Errors fail the build, warnings
# are only counted. Needs the python3 construct module.
option(TPLG_VALIDATE "Validate topology2 binaries after building them" ON)
set(TPLG_VALIDATOR "${CMAKE_CURRENT_SOURCE_DIR}/validator/tplg_validate.py")
set(TPLG_VALIDATE_WAIVERS "${CMAKE_CURRENT_SOURCE_DIR}/validator/known-issues.txt")
set(TPLG_VALIDATE_ENABLED FALSE)
if(TPLG_VALIDATE)
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
execute_process(COMMAND ${Python3_EXECUTABLE} -c "import construct"
RESULT_VARIABLE construct_status OUTPUT_QUIET ERROR_QUIET)
endif()
if(Python3_Interpreter_FOUND AND construct_status EQUAL 0)
set(TPLG_VALIDATE_ENABLED TRUE)
message("-- topology validation: enabled")
else()
message(WARNING "topology validation skipped: python3 with the construct "
"module not found (install python3-construct or set -DTPLG_VALIDATE=OFF)")
endif()
endif()


# This use of VERBOSE relies on original CMake behavior.
# From the add_custom_command() manual:
Expand Down Expand Up @@ -94,6 +116,15 @@ macro(add_alsatplg2_command conf_header conf_target input_name output_name inclu
set (defines "")
endif()

if (TPLG_VALIDATE_ENABLED)
set(tplg_validate_command COMMAND ${Python3_EXECUTABLE} ${TPLG_VALIDATOR}
--quiet --remove-on-error
--waivers ${TPLG_VALIDATE_WAIVERS}
${output_name}.tplg)
Comment on lines +120 to +123
else()
set(tplg_validate_command "")
endif()

add_custom_command(
MAIN_DEPENDENCY ${input_name}.conf
# The conf_header file is the real dependency that
Expand All @@ -109,6 +140,8 @@ macro(add_alsatplg2_command conf_header conf_target input_name output_name inclu
# -p to pre-process Topology2.0 conf file
COMMAND ALSA_CONFIG_DIR=${CMAKE_SOURCE_DIR}/topology/topology2 ${ALSATPLG_CMD} \$\${VERBOSE:+-v 1}
-I ${include_path} -D "'${defines}'" -p -c ${output_name}.conf -o ${output_name}.tplg

${tplg_validate_command}
${TPLG_USES_TERMINAL}
)
endmacro()
Expand Down
2 changes: 2 additions & 0 deletions tools/topology/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ To support the evolution of the SOF topology syntax, the configuration files are

- [`topology1/`](topology1/README.md): Contains the legacy (v1) `m4`-based ALSA topology generation scripts and configuration files. Topology v1 heavily relies on `m4` macro preprocessing to generate the final `.conf` files before being compiled by `alsatplg`.
- [`topology2/`](topology2/README.md): Contains the newer (v2) ALSA topology generation files. Topology v2 introduces a more structured, object-oriented syntax natively supported by newer versions of the `alsatplg` compiler (specifically the pre-processor `-p` flag), reducing reliance on external macro languages.
- [`validator/`](validator/README.md): Offline validator that checks topology v2 binaries the way the Linux SOF driver loads them.

## Build Process

Expand All @@ -18,5 +19,6 @@ The topology build process is managed by `CMakeLists.txt` in this root directory
1. **Compiler Detection**: It locates the `alsatplg` tool (usually built in `tools/bin/alsatplg` alongside the firmware) and checks its version.
2. **Feature Validation**: It ensures the `alsatplg` version is at least `1.2.5`. Older versions are known to silently corrupt certain topology structures (e.g., converting `codec_consumer` to `codec_master`). If the tool is too old, topology generation is safely skipped with a warning.
3. **Target Generation**: It provides macros (`add_alsatplg_command` and `add_alsatplg2_command`) used by the subdirectories to invoke the topology compiler on the pre-processed `.conf` files to generate the final `.tplg` binaries.
4. **Validation**: With `TPLG_VALIDATE=ON` (the default) and python3 with the `construct` module available, `add_alsatplg2_command` runs [`validator/tplg_validate.py`](validator/README.md) on every topology v2 binary. Validation errors fail the build; known problems are listed in `validator/known-issues.txt`.

The `topologies` CMake target is the master target that depends on the generation targets inside both `topology1` and `topology2`.
8 changes: 8 additions & 0 deletions tools/topology/topology2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ To build a specific topology target:
make sof-lnl-sdw-cs42l43-l0-cs35l56-l12
```

Each built binary is checked by the [topology validator](../validator/README.md), which repeats
the checks the Linux SOF driver does when loading a topology. A validation error fails the
build of that topology. To check a binary by hand, for example to see warnings too:

```bash
tools/topology/validator/tplg_validate.py build_tools/topology/topology2/production/sof-mtl-rt711-4ch.tplg
```

## Best Practices for Adding New Topology Definitions

### Topology Structure
Expand Down
105 changes: 105 additions & 0 deletions tools/topology/validator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Topology validator (`tools/topology/validator`)

`tplg_validate.py` checks SOF IPC4 topology binaries (`.tplg`) offline, without
target hardware. It parses the binary and repeats what the Linux SOF driver
does when it loads the topology, so that problems show up at build time
instead of on a device. `alsatplg` accepts many topologies that the kernel
rejects; for instance it prints `undefined sink widget` for a dangling route
and still exits successfully.

```bash
# check topologies, print errors and warnings
tplg_validate.py build_tools/topology/topology2/production/sof-mtl-rt711-4ch.tplg

# also print informational findings, check core IDs against a 5 core DSP
tplg_validate.py -v --num-cores 5 *.tplg

# list the rules and the kernel functions they mirror
tplg_validate.py --list-rules

# print the decoded topology with kernel token names
tplg_validate.py --dump foo.tplg
```

Exit status is 1 if any error is found (`--werror`: any warning). Python 3.8+
and the `construct` module (`python3-construct`) are required.

## Build integration

`add_alsatplg2_command` in `tools/topology/CMakeLists.txt` runs the validator
after `alsatplg` for every topology v2 target, unless configured with
`-DTPLG_VALIDATE=OFF` or `construct` is missing. An error fails the target and
deletes the `.tplg`, so the check reruns on the next build.

Known errors are listed in `known-issues.txt` as `<topology glob> <rule>`
pairs and reported as warnings. Do not add entries to silence new errors:
fix the topology. Remove entries when a topology is fixed.

## What is checked

Rules are grouped in layers by how closely they follow kernel code:

| Layer | Mirrors | Examples |
|-------|---------|----------|
| L1 | `sound/soc/soc-topology.c` | headers, element sizes, names, kcontrol handler IDs, TLV types |
| L2 | `sound/soc/sof/topology.c` tuple engine | kernel parse errors, tuple types, audio format blocks vs. counts |
| L3 | `topology.c`, `ipc4-topology.c` setup | routes, pipelines, chain DMA, DAI types, pins, bytes controls, PCM binding |
| L4 | `sof-audio.c` path walk, format selection | PCM formats vs. host copier formats (advisory) |

Every finding names the rule and the kernel function it follows, e.g.

```
foo.tplg: error: G001 route 'mixin.90.1' -> 'mixout.1.1': sink widget 'mixout.1.1' does not exist (kernel: snd_soc_dapm_add_routes, sof_route_load)
```

Severities: `error` means the kernel fails to load the topology, or fails
when the affected PCM is used. `warning` means a likely mistake, or a failure
that depends on runtime state the validator cannot know. `info` lists
observations (`-v`).

Not checked, because the data only exists on the target: firmware module
UUIDs and module types from the firmware manifest, ACPI NHLT blobs, machine
driver BE links, and the DSP's answers to IPC messages.

## Keeping up with the kernel

The kernel driver is the reference. The validator follows it in two ways:

- **Generated token schema.** `schema/kernel_tokens.py` is generated from a
kernel tree by `schema/gen_kernel_tokens.py`. It holds the `SOF_TKN_*` IDs, the
`struct sof_topology_token` tables with token types, the per widget type
token lists of `tplg_ipc4_widget_ops`, the DAI type names and the kcontrol
handler IDs. `tplgparse/kernel_engine.py` emulates the kernel tuple parser
(`sof_parse_token_sets()`, `sof_copy_tuples()`, `sof_update_ipc_object()`)
on top of these tables. New tokens or widget types only need a
regeneration:

```bash
schema/gen_kernel_tokens.py --kernel ~/linux # update kernel_tokens.py
schema/gen_kernel_tokens.py --kernel ~/linux --check # CI: fail on drift
```

- **Hand written rules** in `checks/`. Each rule names the kernel function it
mirrors (`--list-rules`). Changes that add or remove topology checks in
`sound/soc/sof/ipc4-topology.c` or `topology.c` should get a matching rule
update here.

`schema/check_tokens_conf.py` cross-checks `topology2/include/common/tokens.conf`
and the `token_ref` attributes of the topology2 classes against the kernel schema.

## Layout

```
tplg_validate.py command line tool
tplgparse/binary.py ALSA topology binary format (ported from sof-test tplgtool2.py)
tplgparse/model.py typed view, vendor tuples decoded as written
tplgparse/kernel_engine.py kernel tuple engine emulation
schema/ generated kernel token schema and its generator
checks/ rules, one module per layer
tests/ pytest suite; tplgbuild.py builds test topologies
known-issues.txt waived errors
```

Run the tests with `pytest tools/topology/validator/tests`. Set `ALSATPLG`
to select the `alsatplg` binary for the integration tests and `KERNEL_TREE`
to include the schema drift check.
120 changes: 120 additions & 0 deletions tools/topology/validator/checks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# SPDX-License-Identifier: BSD-3-Clause
#
# Copyright(c) 2026 Intel Corporation. All rights reserved.

"""Rule registry and runner.

A rule is a generator function taking a Context and yielding
(where, message) or (where, message, severity) tuples. Each rule names
the kernel function it mirrors so that it can be reviewed against kernel
changes, see `tplg_validate.py --list-rules`.

Layers:
L1 binary structure, generic ASoC loader (soc-topology.c)
L2 vendor tuples as read by the SOF tuple engine (topology.c)
L3 IPC4 load time and pipeline setup rules (topology.c, ipc4-topology.c)
L4 PCM path and audio format simulation (sof-audio.c, ipc4-topology.c)
"""

import importlib
from dataclasses import dataclass, field

from tplgparse import kernel_engine

ERROR, WARNING, INFO = "error", "warning", "info"
SEVERITIES = (ERROR, WARNING, INFO)


@dataclass
class Rule:
id: str
layer: str
severity: str
kernel: str
summary: str
func: object


@dataclass
class Finding:
rule: str
severity: str
where: str
message: str
kernel: str

def format(self) -> str:
return f"{self.severity}: {self.rule} {self.where}: {self.message} (kernel: {self.kernel})"


@dataclass
class Options:
num_cores: int = 0 # 0: unknown, do not check
verbose: bool = False


@dataclass
class Context:
tplg: object
options: Options
_kernel_widgets: dict = field(default_factory=dict)

def kw(self, widget) -> kernel_engine.KernelWidget:
"Kernel view of a widget, computed once."
key = id(widget)
if key not in self._kernel_widgets:
self._kernel_widgets[key] = kernel_engine.widget_ready(widget)
return self._kernel_widgets[key]

def pipeline_widget(self, widget):
"""The scheduler widget sof_complete() binds the widget to.

Widgets are matched by section index, swidget->pipeline_id.
"""
for w in self.tplg.widgets:
if w.type_name == "scheduler" and w.index == widget.index:
return w
return None


RULES = []

MODULES = ("structure", "tokens", "ipc4", "paths")


def rule(rule_id, layer, severity, kernel, summary):
"Register a rule function."
def wrap(func):
RULES.append(Rule(rule_id, layer, severity, kernel, summary, func))
return func
return wrap


def load_rules():
for mod in MODULES:
importlib.import_module(f"checks.{mod}")
return RULES


def run(tplg, options: Options, selected=None) -> list:
"""Run all (or the selected) rules over a topology.

A rule only runs when every finding of the layers below it is clean
enough for its input to make sense: L2+ is skipped if L1 found errors,
since the parsed model may be incomplete.
"""
ctx = Context(tplg, options)
findings = []
l1_errors = False
for r in load_rules():
if selected is not None and r.id not in selected:
continue
if r.layer != "L1" and l1_errors:
continue
for item in r.func(ctx) or ():
where, message = item[0], item[1]
sev = item[2] if len(item) > 2 else r.severity
findings.append(Finding(r.id, sev, where, message, r.kernel))
if r.layer == "L1" and sev == ERROR:
l1_errors = True
return findings
Loading
Loading