From 4ce21c6798ffcfac38908b2328b6c3b9d160826d Mon Sep 17 00:00:00 2001 From: Anirudh Rayabharam Date: Wed, 8 Apr 2026 10:06:24 +0000 Subject: [PATCH] kernel_source_installer: add local_patches support for PatchModifier Add support for specifying local patch files in PatchModifier via a new 'local_patches' field. This allows users to provide a glob pattern pointing to patch files on the LISA controller machine, which are then copied to the remote node and applied to the kernel source. Previously, patches could only be sourced from a git repository using the 'repo' field. With this change, 'repo' becomes optional and users must specify exactly one of 'repo' or 'local_patches'. The 'local_patches' field accepts glob patterns (e.g., '/path/to/*.patch'). Matching files are expanded on the controller, copied to the node, and applied in sorted order for deterministic behavior. Validation ensures: - 'repo' and 'local_patches' are mutually exclusive - exactly one of the two must be provided - 'local_patches' and 'file_pattern' are mutually exclusive Signed-off-by: Anirudh Rayabharam --- lisa/transformers/kernel_source_installer.py | 56 +++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/lisa/transformers/kernel_source_installer.py b/lisa/transformers/kernel_source_installer.py index 3c6cce7f8f..7a855c5505 100644 --- a/lisa/transformers/kernel_source_installer.py +++ b/lisa/transformers/kernel_source_installer.py @@ -2,7 +2,8 @@ # Licensed under the MIT license. from dataclasses import dataclass, field from datetime import datetime, timezone -from pathlib import PurePath +from glob import glob +from pathlib import Path, PurePath from typing import Any, Dict, List, Optional, Type, cast from dataclasses_json import dataclass_json @@ -70,15 +71,28 @@ class RepoLocationSchema(LocalLocationSchema): @dataclass_json() @dataclass class PatchModifierSchema(BaseModifierSchema): - repo: str = field( - default="", - metadata=field_metadata( - required=True, - ), - ) + repo: str = "" ref: str = "" path: str = "" file_pattern: str = "*.patch" + # Local filesystem glob pattern for patch files (e.g., "/path/to/*.patch"). + # Mutually exclusive with 'repo'. + local_patches: str = "" + + def __post_init__(self, *args: Any, **kwargs: Any) -> None: + if self.repo and self.local_patches: + raise LisaException( + "'repo' and 'local_patches' are mutually exclusive in patch modifier." + ) + if not self.repo and not self.local_patches: + raise LisaException( + "One of 'repo' or 'local_patches' must be specified in patch modifier." + ) + if self.local_patches and self.file_pattern != "*.patch": + raise LisaException( + "'local_patches' and 'file_pattern' are mutually exclusive. " + "Use a glob pattern in 'local_patches' instead." + ) @dataclass_json() @@ -587,13 +601,29 @@ def type_schema(cls) -> Type[schema.TypedSchema]: def modify(self) -> None: runbook: PatchModifierSchema = self.runbook - - code_path = _get_code_path(runbook.path, self._node, "patch") - git = self._node.tools[Git] - code_path = git.clone(url=runbook.repo, cwd=code_path, ref=runbook.ref) - patches_path = code_path / runbook.file_pattern - git.apply(cwd=self._code_path, patches=patches_path) + + if runbook.local_patches: + local_files = sorted( + f for f in glob(runbook.local_patches) if Path(f).is_file() + ) + if not local_files: + raise LisaException( + f"No patch files matched the pattern: {runbook.local_patches}" + ) + remote_patch_dir = _get_code_path(runbook.path, self._node, "patch") + self._node.shell.mkdir(remote_patch_dir, parents=True, exist_ok=True) + for local_file in local_files: + local = Path(local_file) + remote = remote_patch_dir / local.name + self._node.shell.copy(local, remote) + self._log.debug(f"copied and applying patch {local.name} to node") + git.apply(cwd=self._code_path, patches=remote) + else: + code_path = _get_code_path(runbook.path, self._node, "patch") + code_path = git.clone(url=runbook.repo, cwd=code_path, ref=runbook.ref) + patches_path = code_path / runbook.file_pattern + git.apply(cwd=self._code_path, patches=patches_path) def _get_code_path(path: str, node: Node, default_name: str) -> PurePath: