Writing a Plugin¶
This guide walks through creating a sawmill plugin from scratch using the plugin generator.
Prerequisites¶
Python 3.10+
sawmill installed (
pip install sawmill)
Generate a scaffold¶
The plugin generator is a local tool in the sawmill repository. Install it first:
$ cd tools/sawmill-plugin-generator
$ pip install -e .
Then generate a plugin project:
$ sawmill-new-plugin --name quartus
This creates a sawmill-plugin-quartus/ directory with:
sawmill-plugin-quartus/
├── pyproject.toml # Package metadata and entry point
├── README.md
├── Makefile # install / test / lint shortcuts
├── src/
│ └── sawmill_plugin_quartus/
│ ├── __init__.py # Plugin class export
│ ├── plugin.py # Hook implementations (fill in TODOs)
│ └── patterns.py # Regex patterns for log parsing
├── tests/
│ ├── conftest.py # Shared fixtures
│ ├── test_detection.py # can_handle() tests
│ ├── test_parsing.py # load_and_parse() tests
│ ├── test_severity.py # Severity level contract tests
│ └── test_plugin_contract.py # Hook interface compliance
└── examples/
└── sample.log # Example log file for development
The hookspec contract¶
Plugins inherit from SawmillPlugin and decorate methods with
@hookimpl:
from sawmill.plugin import SawmillPlugin, hookimpl
class QuartusPlugin(SawmillPlugin):
name = "quartus"
version = "0.1.0"
description = "Sawmill plugin for Intel Quartus logs"
Six hooks are available. One is required; the rest are optional.
can_handle(path) -> float (optional)¶
Return a confidence score from 0.0 to 1.0. Examine the filename, extension, or first few lines of the file:
@hookimpl
def can_handle(self, path):
if path.suffix == ".log" and "quartus" in path.stem.lower():
return 0.9
# Read first lines for tool signature
try:
with open(path) as f:
header = f.read(1024)
if "Quartus Prime" in header:
return 0.8
except OSError:
pass
return 0.0
load_and_parse(path) -> list[Message] (optional)¶
Open the file, parse every line, group multi-line messages, and return
a flat list of Message objects:
from sawmill.models.message import Message, FileRef
@hookimpl
def load_and_parse(self, path):
messages = []
with open(path, errors="replace") as f:
for line_num, line in enumerate(f, start=1):
severity, msg_id, content = self._parse_line(line)
if severity:
messages.append(Message(
start_line=line_num,
end_line=line_num,
raw_text=line.rstrip(),
content=content,
severity=severity,
message_id=msg_id,
))
return messages
get_severity_levels() -> list[dict] (required)¶
Define the tool’s severity levels. Levels must be consecutive integers starting at 0 (lowest severity):
@hookimpl
def get_severity_levels(self):
return [
{"id": "error", "name": "Error", "level": 2, "style": "red bold"},
{"id": "warning", "name": "Warning", "level": 1, "style": "yellow"},
{"id": "info", "name": "Info", "level": 0, "style": "cyan"},
]
get_filters() -> list[dict] (optional)¶
Provide pre-defined filter categories:
from sawmill.models.filter_def import FilterDefinition
@hookimpl
def get_filters(self):
return [
FilterDefinition(
id="timing",
name="Timing",
pattern=r"Timing Analyzer",
source="plugin:quartus",
description="Timing analysis messages",
),
]
extract_file_reference(content) -> FileRef | None (optional)¶
Extract source file references from message content:
import re
from sawmill.models.message import FileRef
@hookimpl
def extract_file_reference(self, content):
m = re.search(r'File: (.+?) Line: (\d+)', content)
if m:
return FileRef(path=m.group(1), line=int(m.group(2)))
return None
get_grouping_fields() -> list[dict] (optional)¶
Declare custom grouping dimensions. The base class provides defaults (severity, id, file, category). Override to add tool-specific fields:
@hookimpl
def get_grouping_fields(self):
return [
{"id": "severity", "name": "Severity", "field_type": "builtin",
"description": "Group by message severity level"},
{"id": "id", "name": "Message ID", "field_type": "builtin",
"description": "Group by tool-specific message ID"},
{"id": "file", "name": "Source File", "field_type": "file_ref",
"description": "Group by source file path"},
{"id": "entity", "name": "Design Entity", "field_type": "metadata",
"description": "Group by design entity name"},
]
Metadata fields use Message.metadata[field_id] — populate this
dict in load_and_parse().
The Message model¶
Each Message has these fields:
start_line/end_lineLine numbers in the source log (1-indexed). For multi-line messages,
end_line > start_line.raw_textThe complete original text (all lines concatenated).
contentCleaned/extracted message content (without severity prefix, etc.).
severitySeverity level ID (lowercase string matching one of your
get_severity_levels()IDs).message_idTool-specific ID (e.g.
"Synth 8-6157"). Used for waiver matching, suppression, and grouping.categoryOptional category for grouping (e.g.
"timing").file_refOptional
FileRef(path, line)extracted from the message.metadatadict[str, str]for plugin-specific data used by custom grouping fields.
Filling in hooks¶
The typical workflow is:
Study your tool’s log format. Identify the severity prefix pattern, message ID format, and multi-line grouping rules.
Write regex patterns in
patterns.py.Implement
can_handle()to detect the tool’s log files.Implement
load_and_parse()to parse lines into messages.Implement
get_severity_levels()with the correct level mapping.Optionally add
get_filters()for common categories.
Testing¶
The generated scaffold includes test files with fixtures and basic structure. Run tests with:
$ make test
The test_plugin_contract.py file tests that your plugin conforms to
the hookspec interface (severity levels are consecutive ints from 0,
required hooks are implemented, etc.).
Installing¶
Install the plugin in development mode:
$ make install # runs: pip install -e .
Verify it appears in sawmill:
$ sawmill --list-plugins
$ sawmill --show-info --plugin quartus
Entry point registration¶
The generated pyproject.toml includes the entry point declaration:
[project.entry-points."sawmill.plugins"]
quartus = "sawmill_plugin_quartus:QuartusPlugin"
This is how sawmill discovers the plugin at runtime. After changing
entry points, reinstall the package (pip install -e .).