Skip to content

Commit a87f93e

Browse files
committed
Support validating sof-bin against Debian (.deb) and RPM (.rpm) packages
1 parent ecd76dd commit a87f93e

3 files changed

Lines changed: 636 additions & 115 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ sudo ./validate_sof_install.py --version v2.14.1 --platform tgl --fix
122122
# Validate an offline rootfs (e.g. PXE NFS mount)
123123
./validate_sof_install.py --version v2.12 --platform tgl --target-root /srv/nfs/spider-rootfs
124124

125+
# Validate Debian (.deb) package against SOF v2.14.1
126+
./validate_sof_install.py --deb firmware-sof-signed_2.14.1_all.deb --version v2.14.1
127+
128+
# Validate RPM (.rpm) package against SOF v2.14.1
129+
./validate_sof_install.py --rpm alsa-sof-firmware-2.14.1.rpm --version v2.14.1
130+
131+
# Auto-identify SOF release versions inside a package archive
132+
./validate_sof_install.py --pkg firmware-sof-signed.deb --identify
133+
125134
# Generate a standalone md5sum manifest
126135
./validate_sof_install.py --version v2.14.1 --generate-md5 /tmp/v2.14.1-md5.txt
127136
```

‎tests/test_validate_sof_install.py‎

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,5 +475,234 @@ def test_format_fix_record_transitions(self):
475475
self.assertTrue(any("Manifest: Host (missing) -> Expected [ver v2.14.1, built 2025-12-18]" in l for l in lines_missing))
476476

477477

478+
class TestPackageValidation(unittest.TestCase):
479+
"""Test Debian (.deb) and RPM (.rpm) package extraction, inspection, and validation."""
480+
481+
def setUp(self):
482+
self.temp_dir = tempfile.TemporaryDirectory()
483+
self.base = Path(self.temp_dir.name)
484+
485+
def tearDown(self):
486+
self.temp_dir.cleanup()
487+
488+
def _build_test_deb(self, data_files: Dict[str, bytes], control_fields: Dict[str, str], out_path: Path) -> Path:
489+
"""Create a standard Debian .deb package (ar archive containing debian-binary, control.tar.gz, data.tar.gz)."""
490+
import io
491+
import tarfile
492+
493+
# 1. debian-binary
494+
deb_bin = b"2.0\n"
495+
496+
# 2. control.tar.gz
497+
ctrl_buf = io.BytesIO()
498+
with tarfile.open(fileobj=ctrl_buf, mode="w:gz") as tar:
499+
ctrl_text = "".join(f"{k}: {v}\n" for k, v in control_fields.items()).encode("utf-8")
500+
ti = tarfile.TarInfo(name="control")
501+
ti.size = len(ctrl_text)
502+
ti.mode = 0o644
503+
tar.addfile(ti, io.BytesIO(ctrl_text))
504+
ctrl_data = ctrl_buf.getvalue()
505+
506+
# 3. data.tar.gz
507+
data_buf = io.BytesIO()
508+
with tarfile.open(fileobj=data_buf, mode="w:gz") as tar:
509+
for rel_path, content in data_files.items():
510+
ti = tarfile.TarInfo(name=rel_path)
511+
ti.size = len(content)
512+
ti.mode = 0o644
513+
tar.addfile(ti, io.BytesIO(content))
514+
data_bytes = data_buf.getvalue()
515+
516+
def make_ar_member(name: str, payload: bytes) -> bytes:
517+
hdr = f"{name:<16}{1700000000:<12}{0:<6}{0:<6}{100644:<8}{len(payload):<10}\x60\n".encode("ascii")
518+
pad = b"\n" if len(payload) % 2 == 1 else b""
519+
return hdr + payload + pad
520+
521+
ar_data = b"!<arch>\n"
522+
ar_data += make_ar_member("debian-binary", deb_bin)
523+
ar_data += make_ar_member("control.tar.gz", ctrl_data)
524+
ar_data += make_ar_member("data.tar.gz", data_bytes)
525+
526+
out_path.parent.mkdir(parents=True, exist_ok=True)
527+
out_path.write_bytes(ar_data)
528+
return out_path
529+
530+
def test_extract_deb_package(self):
531+
from validate_sof_install import extract_deb
532+
533+
deb_path = self.base / "firmware-sof-signed_2.14.1_all.deb"
534+
self._build_test_deb(
535+
data_files={
536+
"lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri": b"TEST_TGL_FW_DATA",
537+
"lib/firmware/intel/sof-ipc4-tplg/sof-tgl-nocodec.tplg": b"TEST_TPLG_DATA",
538+
},
539+
control_fields={
540+
"Package": "firmware-sof-signed",
541+
"Version": "2.14.1-1",
542+
"Architecture": "all",
543+
"Description": "Sound Open Firmware binary release",
544+
},
545+
out_path=deb_path,
546+
)
547+
548+
dest_dir = self.base / "extracted_deb"
549+
dest_dir.mkdir()
550+
meta = extract_deb(deb_path, dest_dir)
551+
552+
self.assertEqual(meta.get("package"), "firmware-sof-signed")
553+
self.assertEqual(meta.get("version"), "2.14.1-1")
554+
self.assertEqual(meta.get("architecture"), "all")
555+
556+
extracted_files = [str(p.relative_to(dest_dir)) for p in dest_dir.rglob("*") if p.is_file()]
557+
self.assertIn("lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri", extracted_files)
558+
self.assertIn("lib/firmware/intel/sof-ipc4-tplg/sof-tgl-nocodec.tplg", extracted_files)
559+
560+
def test_collect_packages(self):
561+
from validate_sof_install import collect_packages
562+
563+
deb1 = self.base / "pkg1.deb"
564+
deb2 = self.base / "pkg2.deb"
565+
deb1.write_bytes(b"dummy1")
566+
deb2.write_bytes(b"dummy2")
567+
568+
# Comma-separated list
569+
res = collect_packages([f"{deb1},{deb2}"])
570+
self.assertEqual(res, [deb1.resolve(), deb2.resolve()])
571+
572+
# Directory containing packages
573+
pkg_dir = self.base / "pkgs"
574+
pkg_dir.mkdir()
575+
(pkg_dir / "a.deb").write_bytes(b"a")
576+
(pkg_dir / "b.rpm").write_bytes(b"b")
577+
578+
dir_res = collect_packages([str(pkg_dir)])
579+
self.assertEqual(len(dir_res), 2)
580+
581+
# Missing file error
582+
with self.assertRaises(FileNotFoundError):
583+
collect_packages([str(self.base / "nonexistent.deb")])
584+
585+
def test_package_target_validation_and_identify(self):
586+
from validate_sof_install import PackageTarget, SofBinRepo, SofValidator, Status
587+
588+
# 1. Create mock sof-bin repo with v2.99
589+
repo_dir = self.base / "sof-bin"
590+
vdir = repo_dir / "v2.99.x"
591+
fw_dir = vdir / "sof-ipc4-v2.99" / "tgl" / "community"
592+
tplg_dir = vdir / "sof-ipc4-tplg-v2.99"
593+
fw_dir.mkdir(parents=True)
594+
tplg_dir.mkdir(parents=True)
595+
596+
fw_content = b"IDENTICAL_FW_DATA_FOR_VALIDATION"
597+
tplg_content = b"IDENTICAL_TPLG_DATA_FOR_VALIDATION"
598+
599+
(fw_dir / "sof-tgl.ri").write_bytes(fw_content)
600+
(tplg_dir / "sof-tgl-hdmi.tplg").write_bytes(tplg_content)
601+
602+
repo = SofBinRepo(repo_dir)
603+
604+
# 2. Build test deb matching repo contents
605+
deb_path = self.base / "firmware-sof_2.99_all.deb"
606+
self._build_test_deb(
607+
data_files={
608+
"lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri": fw_content,
609+
"lib/firmware/intel/sof-ipc4-tplg/sof-tgl-hdmi.tplg": tplg_content,
610+
},
611+
control_fields={"Package": "firmware-sof", "Version": "2.99-1", "Architecture": "all"},
612+
out_path=deb_path,
613+
)
614+
615+
# 3. Validate PackageTarget against v2.99
616+
target = PackageTarget([deb_path])
617+
try:
618+
validator = SofValidator(
619+
repo=repo,
620+
target=target,
621+
platform_filter={"tgl"},
622+
)
623+
624+
records = validator.validate_installation("v2.99")
625+
self.assertGreater(len(records), 0)
626+
self.assertTrue(all(r.status in (Status.MATCH, Status.SYMLINK_MATCH) for r in records))
627+
628+
# 4. Identify mode on PackageTarget
629+
identified = validator.identify_installed()
630+
self.assertEqual(len(identified), 2)
631+
self.assertTrue(all(item["installed_version"] == "v2.99" for item in identified))
632+
finally:
633+
target.cleanup()
634+
635+
def test_rpm_package_target(self):
636+
import shutil
637+
import subprocess
638+
from validate_sof_install import PackageTarget, SofBinRepo, SofValidator, Status
639+
640+
if not shutil.which("rpmbuild") or not shutil.which("rpm2cpio"):
641+
self.skipTest("rpmbuild or rpm2cpio not installed")
642+
643+
# 1. Create mock repo
644+
repo_dir = self.base / "sof-bin-rpm"
645+
vdir = repo_dir / "v2.99.x"
646+
fw_dir = vdir / "sof-ipc4-v2.99" / "tgl" / "community"
647+
fw_dir.mkdir(parents=True)
648+
fw_content = b"RPM_TGL_FW_TEST_PAYLOAD"
649+
(fw_dir / "sof-tgl.ri").write_bytes(fw_content)
650+
repo = SofBinRepo(repo_dir)
651+
652+
# 2. Build minimal test RPM
653+
topdir = self.base / "rpmbuild_test"
654+
for sub in ["BUILD", "RPMS", "SOURCES", "SPECS", "SRPMS"]:
655+
(topdir / sub).mkdir(parents=True)
656+
657+
payload_dir = self.base / "payload"
658+
(payload_dir / "lib/firmware/intel/sof-ipc4/tgl/community").mkdir(parents=True)
659+
(payload_dir / "lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri").write_bytes(fw_content)
660+
661+
spec = f"""
662+
Name: test-sof-firmware
663+
Version: 2.99
664+
Release: 1
665+
Summary: Test SOF Firmware Package
666+
License: BSD
667+
BuildArch: noarch
668+
669+
%description
670+
Test package
671+
672+
%install
673+
mkdir -p %{{buildroot}}/lib/firmware/intel/sof-ipc4/tgl/community
674+
cp {payload_dir}/lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri %{{buildroot}}/lib/firmware/intel/sof-ipc4/tgl/community/
675+
676+
%files
677+
/lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri
678+
"""
679+
spec_file = self.base / "test.spec"
680+
spec_file.write_text(spec)
681+
682+
subprocess.run(
683+
["rpmbuild", "-bb", "--define", f"_topdir {topdir}", str(spec_file)],
684+
check=True,
685+
capture_output=True,
686+
)
687+
688+
rpm_files = list((topdir / "RPMS").rglob("*.rpm"))
689+
self.assertTrue(len(rpm_files) > 0)
690+
rpm_file = rpm_files[0]
691+
692+
# 3. Validate with PackageTarget
693+
target = PackageTarget([rpm_file])
694+
try:
695+
validator = SofValidator(repo=repo, target=target, platform_filter={"tgl"})
696+
records = validator.validate_installation("v2.99")
697+
self.assertEqual(len(records), 1)
698+
self.assertEqual(records[0].status, Status.MATCH)
699+
700+
identified = validator.identify_installed()
701+
self.assertEqual(len(identified), 1)
702+
self.assertEqual(identified[0]["installed_version"], "v2.99")
703+
finally:
704+
target.cleanup()
705+
706+
478707
if __name__ == "__main__":
479708
unittest.main()

0 commit comments

Comments
 (0)