Skip to content
Merged
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ Once installed, the tool is invoked as:
```sh
qcom-ptool gen_partition -i platforms/<soc>/<variant>/partitions.conf -o partitions.xml
qcom-ptool gen_contents -p partitions.xml -t contents.xml.in -o contents.xml
qcom-ptool gen_udev_rules -o 55-qcom-raw-partitions-noblkid.rules
qcom-ptool ptool -x partitions.xml
qcom-ptool msp -r rawprogram0.xml -d /dev/sdX -p patch0.xml
```

By default, the generator scans all `platforms/*/*/partitions.conf` files.
Repeatable `-i` options can select specific layouts. It skips known filesystem
partition names and emits exact `PARTNAME` rules for the others. Unknown names
retain normal blkid probing. The rules use
`UDEV_DISABLE_PERSISTENT_STORAGE_BLKID_FLAG` on systemd v252 and newer.

Run `qcom-ptool <subcommand> -h` to see the options accepted by each
subcommand.

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ include = ["qcom_ptool*"]

[tool.setuptools.package-data]
"qcom_ptool.schema" = ["*.json"]
"qcom_ptool.data" = ["*.in", "*.list"]

[tool.ruff]
target-version = "py38"
Expand Down
1 change: 1 addition & 0 deletions qcom_ptool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
SUBCOMMANDS = {
"gen_partition": "qcom_ptool.gen_partition",
"gen_contents": "qcom_ptool.gen_contents",
"gen_udev_rules": "qcom_ptool.gen_udev_rules",
"ptool": "qcom_ptool.ptool",
"msp": "qcom_ptool.msp",
}
Expand Down
35 changes: 35 additions & 0 deletions qcom_ptool/data/55-qcom-raw-partitions-noblkid.rules.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause

# Skip filesystem probing for known Qualcomm raw GPT partitions.
#
# Match by GPT partition name instead of kernel disk name. Disk enumeration is
# not stable across all boards, and a disk can contain both raw firmware and
# mountable filesystem partitions.
#
# 60-persistent-storage.rules creates partition metadata links after the blkid
# import. Since this rule skips that import for raw partitions, emit those
# links here from kernel-provided PARTNAME/PARTUUID properties.
ACTION=="remove", GOTO="qcom_raw_noblkid_end"
SUBSYSTEM!="block", GOTO="qcom_raw_noblkid_end"
ENV{DEVTYPE}!="partition", GOTO="qcom_raw_noblkid_end"
ENV{PARTNAME}=="", GOTO="qcom_raw_links"
Comment thread
lumag marked this conversation as resolved.

# Raw partition name patterns
@QCOM_RAW_PARTITION_RULES@
GOTO="qcom_raw_noblkid_end"

LABEL="qcom_raw_noblkid"
# Supported since systemd v252. Older versions probe as usual.
ENV{UDEV_DISABLE_PERSISTENT_STORAGE_BLKID_FLAG}="1"
Comment thread
lumag marked this conversation as resolved.

LABEL="qcom_raw_links"
# persistent partition links
ENV{ID_PATH}!="?*", IMPORT{parent}="ID_PATH"
ENV{PARTUUID}=="?*", SYMLINK+="disk/by-partuuid/$env{PARTUUID}"
ENV{PARTNAME}=="?*", OPTIONS+="string_escape=replace", SYMLINK+="disk/by-partlabel/$env{PARTNAME}"
ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partnum/%n"
ENV{ID_PATH}=="?*", ENV{PARTUUID}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partuuid/$env{PARTUUID}"
ENV{ID_PATH}=="?*", ENV{PARTNAME}=="?*", OPTIONS+="string_escape=replace", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partlabel/$env{PARTNAME}"

LABEL="qcom_raw_noblkid_end"
2 changes: 2 additions & 0 deletions qcom_ptool/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause
11 changes: 11 additions & 0 deletions qcom_ptool/data/filesystem-partition-names.list
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause

# Partition names known to contain filesystems. Raw rules are generated for
# every other partition name found in the supplied in-tree layouts.
efi
logfs
persist
rootfs
userdata
vm-data
126 changes: 126 additions & 0 deletions qcom_ptool/gen_udev_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause

"""Generate udev rules for Qualcomm raw partitions."""

from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

from qcom_ptool.loaders import load as load_spec

DATA_DIR = Path(__file__).with_name("data")
FILESYSTEM_NAMES_FILE = DATA_DIR / "filesystem-partition-names.list"
TEMPLATE_FILE = DATA_DIR / "55-qcom-raw-partitions-noblkid.rules.in"
RULES_PLACEHOLDER = "@QCOM_RAW_PARTITION_RULES@"
NAME_RE = re.compile(r"[A-Za-z0-9_.+-]+")
DEFAULT_LAYOUT_GLOB = "platforms/*/*/partitions.conf"


def load_filesystem_names() -> set[str]:
"""Load names of partitions that may contain filesystems."""
names: set[str] = set()
for line_number, line in enumerate(
FILESYSTEM_NAMES_FILE.read_text(encoding="utf-8").splitlines(), start=1
):
name = line.partition("#")[0].strip()
if not name:
continue
if NAME_RE.fullmatch(name) is None:
raise ValueError(
f"{FILESYSTEM_NAMES_FILE}:{line_number}: invalid name: {name}"
)
if name in names:
raise ValueError(
f"{FILESYSTEM_NAMES_FILE}:{line_number}: duplicate name: {name}"
)
names.add(name)

if not names:
raise ValueError(f"filesystem name list is empty: {FILESYSTEM_NAMES_FILE}")
return names


def load_partition_names(inputs: list[Path]) -> set[str]:
"""Load and merge partition names from all supplied layouts."""
names: set[str] = set()
for path in inputs:
spec = load_spec(str(path))
for partitions in spec["partitions"].values():
for partition in partitions:
name = partition["label"]
if name and name != "last_parti":
if NAME_RE.fullmatch(name) is None:
raise ValueError(f"{path}: invalid partition name: {name}")
names.add(name)
return names


def load_raw_partition_names(inputs: list[Path]) -> list[str]:
"""Return known raw partition names from the supplied layouts."""
return sorted(load_partition_names(inputs) - load_filesystem_names())


def discover_inputs(inputs: list[Path] | None) -> list[Path]:
"""Use explicit layouts or discover all layouts in the current repo."""
if inputs:
return inputs

discovered = sorted(Path.cwd().glob(DEFAULT_LAYOUT_GLOB))
if not discovered:
raise ValueError(
"no partition layouts found; run from the qcom-ptool repository "
"or provide one or more -i/--input paths"
)
return discovered


def render_rules(names: list[str]) -> str:
"""Render the udev rules template for the supplied partition names."""
rules = "\n".join(
f'ENV{{PARTNAME}}=="{name}", GOTO="qcom_raw_noblkid"'
for name in names
)
template = TEMPLATE_FILE.read_text(encoding="utf-8")
if template.count(RULES_PLACEHOLDER) != 1:
raise ValueError("rules template must contain exactly one placeholder")
return template.replace(RULES_PLACEHOLDER, rules)


def generate_rules(inputs: list[Path] | None = None) -> str:
"""Render rules for raw partitions in all supplied layouts."""
return render_rules(load_raw_partition_names(discover_inputs(inputs)))


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"-i",
"--input",
action="append",
type=Path,
help="partition layout to scan; may be repeated (defaults to platforms/*/*/partitions.conf)",
)
parser.add_argument("-o", "--output", required=True, type=Path)
return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
try:
content = generate_rules(args.input)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(content, encoding="utf-8")
except (OSError, ValueError) as error:
print(f"error: {error}", file=sys.stderr)
return 2

print(f"generated: {args.output}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
115 changes: 115 additions & 0 deletions tests/unit/test_gen_udev_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

from pathlib import Path

import pytest

from qcom_ptool import gen_udev_rules


REPO_ROOT = Path(__file__).resolve().parents[2]
LAYOUT = REPO_ROOT / "platforms" / "qcs6490-rb3gen2" / "ufs" / "partitions.conf"


def test_generate_rules_uses_layout_names() -> None:
rules = gen_udev_rules.generate_rules([LAYOUT])

assert 'ENV{PARTNAME}=="xbl_a"' in rules
assert 'ENV{PARTNAME}=="cdt"' in rules
assert "rootfs" not in rules
assert 'ENV{PARTNAME}=="not-in-layout"' not in rules


def test_generate_rules_merges_layouts_and_deduplicates(
tmp_path: Path,
) -> None:
first = tmp_path / "first.conf"
second = tmp_path / "second.conf"
first.write_text(
"--disk --type=ufs --size=1\n"
"--partition --name=raw_a --size=1KB --type-guid=1\n"
"--partition --name=rootfs --size=1KB --type-guid=2\n",
encoding="utf-8",
)
second.write_text(
"--disk --type=ufs --size=1\n"
"--partition --name=raw_a --size=1KB --type-guid=1\n"
"--partition --name=raw_b --size=1KB --type-guid=2\n",
encoding="utf-8",
)
rules = gen_udev_rules.generate_rules([first, second])

assert rules.count('ENV{PARTNAME}=="raw_a"') == 1
assert 'ENV{PARTNAME}=="raw_b"' in rules
assert 'ENV{PARTNAME}=="rootfs"' not in rules


def test_generate_rules_preserves_persistent_links() -> None:
rules = gen_udev_rules.generate_rules([LAYOUT])

assert 'ENV{PARTNAME}=="", GOTO="qcom_raw_links"' in rules
assert 'ENV{UDEV_DISABLE_PERSISTENT_STORAGE_BLKID_FLAG}="1"' in rules
assert "UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG" not in rules
assert 'SYMLINK+="disk/by-partuuid/$env{PARTUUID}"' in rules
assert 'SYMLINK+="disk/by-partlabel/$env{PARTNAME}"' in rules


def test_load_filesystem_names_rejects_unsafe_name(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
policy = tmp_path / "names.list"
policy.write_text('rootfs\nrootfs", RUN+="/bin/true\n', encoding="utf-8")
monkeypatch.setattr(gen_udev_rules, "FILESYSTEM_NAMES_FILE", policy)

with pytest.raises(ValueError, match="invalid name") as error:
gen_udev_rules.load_filesystem_names()

assert f"{policy}:2:" in str(error.value)


@pytest.mark.parametrize(
("content", "message"),
[
("xbl_a\nxbl_a\n", "duplicate name"),
("# comments only\n", "filesystem name list is empty"),
],
)
def test_load_filesystem_names_rejects_invalid_policy(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
content: str,
message: str,
) -> None:
policy = tmp_path / "names.list"
policy.write_text(content, encoding="utf-8")
monkeypatch.setattr(gen_udev_rules, "FILESYSTEM_NAMES_FILE", policy)

with pytest.raises(ValueError, match=message):
gen_udev_rules.load_filesystem_names()


def test_main_writes_rules(tmp_path: Path) -> None:
output = tmp_path / "rules.d" / "55-qcom.rules"

assert gen_udev_rules.main(["-i", str(LAYOUT), "-o", str(output)]) == 0
assert 'ENV{PARTNAME}=="cdt"' in output.read_text(encoding="utf-8")


def test_main_discovers_repo_layouts(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(REPO_ROOT)
output = tmp_path / "rules"

assert gen_udev_rules.main(["-o", str(output)]) == 0
assert 'ENV{PARTNAME}=="xbl_a"' in output.read_text(encoding="utf-8")


def test_generate_rules_rejects_missing_default_layouts(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)

with pytest.raises(ValueError, match="no partition layouts found"):
gen_udev_rules.generate_rules()
Loading