diff --git a/scripts/dev/upstream_merge/automation_conf.json b/scripts/dev/upstream_merge/automation_conf.json index 58b6a6805..08ddfca63 100644 --- a/scripts/dev/upstream_merge/automation_conf.json +++ b/scripts/dev/upstream_merge/automation_conf.json @@ -12,5 +12,16 @@ "email_to": "", "username": "", "build_args":"", - "ssh_connection":"" + "ssh_connection":"", + + "kernel_build": { + "kernel_src_dir": "", + "arch": "", + "temp_modules_dir": "", + "target_branch": "", + "toolchain_prefix": "", + "ssh_target": "", + "build_host_ip": "", + "work_item_id": "" + } } diff --git a/scripts/dev/upstream_merge/copy_toolchain_from_server.py b/scripts/dev/upstream_merge/copy_toolchain_from_server.py new file mode 100644 index 000000000..e9265b876 --- /dev/null +++ b/scripts/dev/upstream_merge/copy_toolchain_from_server.py @@ -0,0 +1,156 @@ +import os + +# -------------------- +# Nirvana mount base (assumed present) +# -------------------- +NIRVANA_SDK_BASE = ( + "/mnt/nirvana/perforceExports/build/exports/" + "ni/rtos/rtos_toolchain/official/export" +) + + +def _get_latest_major_version(): + if not os.path.isdir(NIRVANA_SDK_BASE): + return None + + versions = [] + for d in os.listdir(NIRVANA_SDK_BASE): + path = os.path.join(NIRVANA_SDK_BASE, d) + if os.path.isdir(path): + try: + versions.append((tuple(map(int, d.split("."))), d)) + except ValueError: + continue + + return sorted(versions)[-1][1] if versions else None + + +def _get_latest_build_version(version_path): + builds = [] + for d in os.listdir(version_path): + path = os.path.join(version_path, d) + if os.path.isdir(path): + try: + builds.append(( + list(map(int, d.replace("d", ".").split("."))), d)) + except ValueError: + continue + + return sorted(builds)[-1][1] if builds else None + + +def copy_toolchain_from_nirvana(build_root): + """ + Copy latest x64 NILRT toolchain from mounted Nirvana to: + /toolchain/NILinuxRT-x64 + """ + + latest_version = _get_latest_major_version() + if not latest_version: + return 1, "No Nirvana toolchain versions found" + + version_path = os.path.join(NIRVANA_SDK_BASE, latest_version) + latest_build = _get_latest_build_version(version_path) + if not latest_build: + return 1, f"No builds found under Nirvana version {latest_version}" + + sdk_src = os.path.join( + version_path, + latest_build, + "toolchain", + "sdk", + "NILinuxRT-x64" + ) + + if not os.path.isdir(sdk_src): + return 1, f"x64 SDK not found at: {sdk_src}" + + toolchain_root = os.path.join(build_root, "toolchain") + toolchain_dst = os.path.join(toolchain_root, "NILinuxRT-x64") + + # Idempotent: skip if already present + if os.path.isdir(toolchain_dst): + return 0, f"Toolchain already present at {toolchain_dst}", toolchain_dst + + os.makedirs(toolchain_root, exist_ok=True) + + ret = os.system(f"cp -r {sdk_src} {toolchain_root}/") + if ret != 0: + return 1, "Failed to copy toolchain from Nirvana" + + return 0, ( + f"Toolchain copied from Nirvana\n" + f"Version: {latest_version}/{latest_build}\n" + f"Location: {toolchain_dst}" + ), toolchain_dst + + +def prepare_toolchain_environment(toolchain_dst): + """ + Run the NILinuxRT SDK installer (non-interactive) if needed, + extracting into the local build toolchain directory, + then source the environment. + """ + + if not os.path.isdir(toolchain_dst): + return 1, f"Toolchain directory not found: {toolchain_dst}" + + env_script = None + for root, dirs, files in os.walk(toolchain_dst): + for f in files: + if f.startswith("environment-setup"): + env_script = os.path.join(root, f) + break + if env_script: + break + + # If SDK is not yet extracted, run installer + if not env_script: + installer = next( + (f for f in os.listdir(toolchain_dst) if f.endswith(".sh")), + None + ) + if not installer: + return 1, "No SDK installer (.sh) found in toolchain directory" + + installer = os.path.join(toolchain_dst, installer) + + os.chmod(installer, 0o755) + + # IMPORTANT: force extraction into toolchain_dst + ret = os.system( + f'bash "{installer}" -y -d "{toolchain_dst}"' + ) + if ret != 0: + return 1, "Failed to run SDK installer" + + # Look again for environment-setup after extraction + for root, dirs, files in os.walk(toolchain_dst): + for f in files: + if f.startswith("environment-setup"): + env_script = os.path.join(root, f) + break + if env_script: + break + + if not env_script: + return 1, "installer ran , env-setup script not found" + + # Source environment into Python process + cmd = f'bash -c "source {env_script} >/dev/null && env"' + with os.popen(cmd) as proc: + for line in proc: + key, _, value = line.strip().partition("=") + os.environ[key] = value + +# CROSS_COMPILE is expected to be exported by the SDK environment-setup script + + if "CROSS_COMPILE" not in os.environ: + return 1, ( + "CROSS_COMPILE not set after sourcing toolchain environment. " + "Ensure the SDK environment-setup script exports it correctly." + ) + + print(f"[TOOLCHAIN] CROSS_COMPILE set to: {os.environ['CROSS_COMPILE']}") + + return 0, "Toolchain environment ready" diff --git a/scripts/dev/upstream_merge/json_config.py b/scripts/dev/upstream_merge/json_config.py index 533fd64f9..58e5be5d3 100644 --- a/scripts/dev/upstream_merge/json_config.py +++ b/scripts/dev/upstream_merge/json_config.py @@ -9,10 +9,9 @@ class JsonConfig: """Handles loading and validating the automation_conf.json configuration file.""" - def __init__(self, automation_conf_path, work_item_id): - with open(automation_conf_path, "r", encoding="utf-8") as file: + def __init__(self, config_path, work_item_id): + with open(config_path, "r", encoding="utf-8") as file: config = json.load(file) - self.work_item_id = work_item_id self.nilrt_branch = config.get("nilrt_branch") self.meta_nilrt_branch = config.get("meta_nilrt_branch") self.conf_file_path = os.getcwd() + f"/{config.get('conf_file_path')}" @@ -27,3 +26,31 @@ def __init__(self, automation_conf_path, work_item_id): self.log_level = config.get("log_level") self.build_args = config.get("build_args", "") self.ssh_connection = config.get("ssh_connection") + # Kernel build configuration (optional section) + kernel_config = config.get("kernel_build", {}) + # Expand environment variables in paths + self.kernel_src_dir = os.path.abspath( + kernel_config.get("kernel_src_dir")) + self.arch = kernel_config.get("arch") + self.temp_modules_dir = kernel_config.get( + "temp_modules_dir") + self.toolchain_prefix = kernel_config.get( + "toolchain_prefix") + self.merge_workdir = kernel_config.get( + "merge_workdir") + self.target_branch = kernel_config.get("target_branch") + self.nilrt_root = kernel_config.get("nilrt_root") + self.required_packages = kernel_config.get("required_packages", []) + self.make_jobs = kernel_config.get("make_jobs", "$(nproc)") + self.kernel_config = kernel_config.get("kernel_config", "defconfig") + self.target_name = kernel_config.get("target_name") + self.build_host_ip = kernel_config.get("build_host_ip") + self.ssh_options = kernel_config.get( + "ssh_options", "-o StrictHostKeyChecking=no") + self.build_host_user = kernel_config.get( + "build_host_user", os.getenv("USER", "builduser")) + self.pr_enabled = config.get("pr_enabled", False) + self.pr_target_branch = config.get( + "pr_target_branch", self.target_branch) + self.work_item_id = work_item_id or config.get("work_item_id", "") + self.ssh_target = kernel_config.get("ssh_target") \ No newline at end of file diff --git a/scripts/dev/upstream_merge/kernel_build_and_test.py b/scripts/dev/upstream_merge/kernel_build_and_test.py new file mode 100644 index 000000000..4c439a545 --- /dev/null +++ b/scripts/dev/upstream_merge/kernel_build_and_test.py @@ -0,0 +1,520 @@ +#!/usr/bin/env python3 +import os +import sys +import re +import argparse +import time +from log_and_email_utils import setup_logging, write_log_and_send_email +from utils import build +from utils.git_commands import ( + git_fetch, + git_merge, + git_tag, + git_status, + git_clone, + git_checkout, +) +from utils.git_repo import GitRepo +from json_config import JsonConfig +from copy_toolchain_from_server import copy_toolchain_from_nirvana +from copy_toolchain_from_server import prepare_toolchain_environment +from rebuild_drivers import install_sshfs_fuse +from rebuild_drivers import mount_kernel_source +from rebuild_drivers import fix_symlinks +from rebuild_drivers import prepare_headers +from rebuild_drivers import dkms_autoinstall +from utils.push_branch_and_create_pr import push_branch_and_create_pr +from utils.build import build_kernel_x86_64 +from utils.build import regenerate_defconfig + +# -------------------- +# Import shared utils +# -------------------- +script_dir = os.path.dirname(os.path.abspath(__file__)) +dev_dir = os.path.dirname(script_dir) +upstream_merge_dir = os.path.join(dev_dir, "upstream_merge") +sys.path.append(upstream_merge_dir) + +REPO_URL = "https://github.com/ni/linux.git" +STABLE_RT_REMOTE = ( + "https://git.kernel.org/pub/scm/linux/kernel/git/rt/linux-stable-rt.git" +) + +config = None + + +# -------------------- +# CLI +# -------------------- +def parse_args(): + parser = argparse.ArgumentParser(description="Stable-RT merge automation") + parser.add_argument( + "-c", + "--config", + default="scripts/dev/upstream_merge/automation_conf.json" + ) + parser.add_argument("--skip-merge", action="store_true") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--work-dir", default=None) + parser.add_argument( + "--skip-push-and-pr", + action="store_true", + help="Skip pushing branch and creating PR" + ) + return parser.parse_args() + + +def wait_for_ssh(ssh_target, ssh_opts="", timeout=120): + print(f"[INFO] Waiting for SSH on {ssh_target}...") + + for _ in range(timeout // 5): + ret = os.system( + f'ssh {ssh_opts} {ssh_target} "echo ok" > /dev/null 2>&1' + ) + if ret == 0: + print("[INFO] SSH is available on target") + return 0 + time.sleep(5) + + return 1 + + +def run_cmd(cmd): + """ + Run a shell command and return (rc, output). + rc = 0 on success, 1 on failure. + """ + stream = os.popen(cmd + " 2>&1") + output = stream.read().strip() + rc = stream.close() + if rc is not None: + return 1, output + return 0, output + + +# -------------------- +# Kernel repo handling +# -------------------- +def clone_kernel_repository(): + print("[INFO] Cloning kernel repository") + + os.makedirs(os.path.dirname(config.kernel_src_dir), exist_ok=True) + git_clone(REPO_URL, config.kernel_src_dir) + +# -------------------- +# RT Merge +# -------------------- + + +def run_rebuild_drivers(config, run_cmd): + print("[INFO] Target is back online, starting rebuild drivers") + + # STEP 0: Ensure sshfs is available + rc = install_sshfs_fuse(config, run_cmd) + if rc != 0: + print("[ERROR] STEP 0 failed") + return 1 + + # STEP 1: Mount kernel source via SSHFS + rc = mount_kernel_source(config, run_cmd) + if rc != 0: + print("[ERROR] STEP 1 failed") + return 1 + + # STEP 2: Fix build/source symlinks + rc = fix_symlinks(config, run_cmd) + if rc != 0: + print("[ERROR] STEP 2 failed") + return 1 + + # STEP 3: Prepare kernel headers + rc = prepare_headers(config, run_cmd) + if rc != 0: + print("[ERROR] STEP 3 failed") + return 1 + + # STEP 4: DKMS rebuild + rc = dkms_autoinstall(config, run_cmd) + if rc != 0: + print("[ERROR] STEP 4 failed") + return 1 + + return 0 + + +def create_kernel_pr(args, config, latest_tag, defconfig_changed): + if args.skip_push_and_pr: + return + + pr_title = f"[{config.work_item_id}] Merge RT {latest_tag}" + + pr_description = ( + f"AB#{config.work_item_id}\n\n" + f"RT tag merged: {latest_tag}\n" + f"Defconfig regenerated: " + f"{'Yes' if defconfig_changed else 'No'}\n" + f"Kernel build: Success\n" + f"Driver rebuild: Success\n" + ) + + git_obj = GitRepo( + local_repo=config.kernel_src_dir, + local_base_branch=config.target_branch, + ) + + status, msg = push_branch_and_create_pr( + git_obj, + branch_name=config.target_branch, + username=config.username, + pr_title=pr_title, + pr_description=pr_description, + ) + + if status != 0: + raise RuntimeError(f"Failed to create PR: {msg}") + + print("[INFO] Branch pushed and PR created successfully") + + +def run_upstream_merge_script(args): + original_cwd = os.getcwd() + print("[INFO] Running upstream RT merge") + + clone_kernel_repository() + + kernel_repo = GitRepo( + local_repo=config.kernel_src_dir, + upstream_repo_url=REPO_URL, + upstream_branch=config.target_branch, + ) + + os.chdir(config.kernel_src_dir) + + git_fetch("origin") + git_checkout(config.target_branch, create=True, force_checkout=True) + + # Ensure stable-rt remote exists using GitRepo abstraction + kernel_repo.add_remote("stable-rt", STABLE_RT_REMOTE) + + # Fetch RT tags + git_fetch("stable-rt", "--tags") + + kernel_version = config.target_branch.split("/")[-1] + _, tags = git_tag(list_pattern=f"v{kernel_version}.*-rt*") + + clean_tags = [ + t + for t in tags.splitlines() + if re.match(rf"^v{kernel_version}\.\d+-rt\d+$", t) + ] + + if not clean_tags: + raise RuntimeError("No stable-RT tags found") + + latest_tag = sorted( + clean_tags, key=lambda t: list(map(int, re.findall(r"\d+", t))) + )[-1] + + print(f"[INFO] Latest RT tag: {latest_tag}") + + result = git_merge( + latest_tag, signoff=True, message=f"Merge latest upstream {latest_tag}" + ) + + # -------------------- + # Failure case + # -------------------- + if result[0] != 0: + _, status_out = git_status() + + merge_details = ( + status_out + if ("both modified" in status_out or "unmerged" in status_out) + else "Merge failed before conflicts were created." + ) + + merge_report = { + kernel_repo: (1, merge_details), + "Build and Test": (1, "Skipped because merge failed"), + } + + os.chdir(original_cwd) + write_log_and_send_email( + email_from=config.email_from, + email_to=config.email_to, + merge_report=merge_report, + email_log_level=0, + skip_push_and_pr=True, + ) + return + + # -------------------- + # Success case + # -------------------- + print("[INFO] RT merge successful") + + # -------------------- + # Copy toolchain from Nirvana + # -------------------- + build_root = os.path.dirname(config.kernel_src_dir) + tc_copy_status, tc_copy_msg, toolchain_dst = copy_toolchain_from_nirvana( + build_root) + if tc_copy_status != 0: + merge_report = { + kernel_repo: ( + 1, + f"RT tag {latest_tag} merged successfully.\n" + f"Branch: {config.target_branch}", + ), + "Build and Test": (1, tc_copy_msg), + } + + os.chdir(original_cwd) + write_log_and_send_email( + email_from=config.email_from, + email_to=config.email_to, + merge_report=merge_report, + email_log_level=0, + skip_push_and_pr=True, + ) + return + # -------------------- + # Prepare toolchain environment + # -------------------- + tc_status, tc_msg = prepare_toolchain_environment(toolchain_dst) + if tc_status != 0: + merge_report = { + kernel_repo: ( + 1, + f"RT tag {latest_tag} merged successfully.\n" + f"Branch: {config.target_branch}", + ), + "Build and Test": (1, tc_msg), + } + + os.chdir(original_cwd) + write_log_and_send_email( + email_from=config.email_from, + email_to=config.email_to, + merge_report=merge_report, + email_log_level=0, + skip_push_and_pr=True, + ) + return + + defconfig_changed = regenerate_defconfig(config.kernel_src_dir) + if defconfig_changed: + print("[US4] Defconfig regeneration commit created") + # -------------------- + # Kernel build (x86_64) + # -------------------- + build_status, build_msg = build_kernel_x86_64(config.kernel_src_dir) + + merge_report = { + kernel_repo: ( + build_status, + f"RT tag {latest_tag} merged successfully.\n" + f"Branch: {config.target_branch}", + ), + "Build and Test": (build_status, build_msg), + } + + # -------------------- + # Install kernel to target + # -------------------- + install_status, install_msg = install_kernel_to_target( + config.kernel_src_dir) + merge_report = { + kernel_repo: ( + install_status, + f"RT tag {latest_tag} merged successfully.\n" + f"Branch: {config.target_branch}", + ), + "Build and Test": (install_status, install_msg), + } + print("[INFO] Kernel install issued reboot request") + + time.sleep(20) + + print("[INFO] Waiting for target to come back after kernel reboot") + if wait_for_ssh(config.ssh_target, timeout=300) != 0: + print("[ERROR] Target did not come back after kernel reboot") + return + # -------------------- + # VERIFY kernel version on target + # -------------------- + ssh_target = config.ssh_target + ssh_opts = getattr(config, "ssh_options", "") + + cmd = f'ssh {ssh_opts} {ssh_target} "uname -r"' + kernel_running = os.popen(cmd).read().strip() + + print(f"[VERIFY] Running kernel on target: {kernel_running}") + + # expected kernel version (from build step) + os.chdir(config.kernel_src_dir) + expected_kernel = os.popen("make -s kernelrelease").read().strip() + + print(f"[VERIFY] Expected kernel: {expected_kernel}") + + if kernel_running != expected_kernel: + print( + f"[ERROR] Kernel version mismatch: expected {expected_kernel}, " + f"got {kernel_running}" + ) + return + + print("[INFO] Kernel version verified successfully") + + rebuild_status = run_rebuild_drivers(config, run_cmd) + if rebuild_status != 0: + return + create_kernel_pr(args, config, latest_tag, defconfig_changed) + + os.chdir(original_cwd) + write_log_and_send_email( + email_from=config.email_from, + email_to=config.email_to, + merge_report=merge_report, + email_log_level=0, + skip_push_and_pr=args.skip_push_and_pr, + ) + return + + +def install_kernel_to_target(kernel_src_dir): + """ + Install x86_64 kernel and modules to an NILRT target . + """ + + ssh_target = config.ssh_target + ssh_opts = getattr(config, "ssh_options", "") + + build_root = os.path.dirname(kernel_src_dir) + + bzimage = os.path.join(kernel_src_dir, "arch", "x86", "boot", "bzImage") + + if not os.path.isfile(bzimage): + return 1, f"Kernel image not found: {bzimage}" + + # Get kernel version from build tree + os.chdir(kernel_src_dir) + kernel_version = os.popen("make -s kernelrelease").read().strip() + + print(f"[INFO] Installing kernel version: {kernel_version}") + + modules_root = os.path.join( + build_root, "tmp-glibc", "modules", "lib", "modules", kernel_version + ) + + if not os.path.isdir(modules_root): + return ( + 1, + f"Kernel modules for {kernel_version} " + f"not found at {modules_root}", + ) + + # Backup existing kernel on target + rc = os.system( + f"ssh {ssh_opts} {ssh_target} " + '"test ! -h /boot/runmode/bzImage && ' + 'mv /boot/runmode/bzImage /boot/runmode/bzImage-$(uname -r) || true"' + ) + if rc != 0: + return 1, "Failed to backup existing kernel on target" + + # Copy kernel image + rc = os.system( + f"scp {ssh_opts} {bzimage} " + f"{ssh_target}:/boot/runmode/bzImage-{kernel_version}" + ) + if rc != 0: + return 1, "FAILED to copy kernel bzImage to target (ABORTING)" + + # VERIFY kernel image exists on target + rc = os.system( + f"ssh {ssh_opts} {ssh_target} " + f'"test -f /boot/runmode/bzImage-{kernel_version}"' + ) + if rc != 0: + return 1, "Kernel bzImage missing on target after copy (ABORTING)" + + # Update boot symlink + rc = os.system( + f"ssh {ssh_opts} {ssh_target} " + f'"ln -sf bzImage-{kernel_version} /boot/runmode/bzImage"' + ) + if rc != 0: + return 1, "FAILED to update bzImage symlink (ABORTING)" + + # Copy kernel modules (versioned directory) + rc = os.system( + f"tar cz -C {modules_root} . | " + f"ssh {ssh_opts} {ssh_target} " + f'"mkdir -p /lib/modules/{kernel_version} && ' + f'tar xz -C /lib/modules/{kernel_version}"' + ) + if rc != 0: + return 1, "FAILED to copy kernel modules to target (ABORTING)" + + # VERIFY modules directory exists on target + rc = os.system( + f"ssh {ssh_opts} {ssh_target} " + f'"test -d /lib/modules/{kernel_version}"' + ) + if rc != 0: + return 1, "Kernel modules missing on target after copy (ABORTING)" + + # CRITICAL: rebuild module dependency indexes + rc = os.system( + f"ssh {ssh_opts} {ssh_target} " + f'"depmod -a {kernel_version}"' + ) + if rc != 0: + return 1, "depmod failed on target (ABORTING reboot)" + + # Set bootdelay for safe mode recovery + rc = os.system( + f"ssh {ssh_opts} {ssh_target} " + f'"fw_setenv bootdelay 5"' + ) + if rc != 0: + return 1, "Failed to set bootdelay for safe mode" + + # Reboot target ONLY after everything succeeded + rc = os.system(f"ssh {ssh_opts} {ssh_target} reboot") + if rc != 0: + return 1, "FAILED to reboot target after install" + + return 0, ( + f"Kernel and modules installed successfully\n" + f"Kernel version: {kernel_version}" + ) + +# -------------------- +# Main +# -------------------- + + +def main(): + global config + + setup_logging() + args = parse_args() + + config = JsonConfig(config_path=args.config, work_item_id=None) + build.config = config + + if args.work_dir: + config.kernel_src_dir = os.path.join( + args.work_dir, "nilrt-kernel-build", "linux" + ) + + print(f"[INFO] Branch: {config.target_branch}, " f"Arch: {config.arch}") + + if not args.skip_merge: + run_upstream_merge_script(args) + + +if __name__ == "__main__": + main() diff --git a/scripts/dev/upstream_merge/rebuild_drivers.py b/scripts/dev/upstream_merge/rebuild_drivers.py new file mode 100644 index 000000000..a820919d2 --- /dev/null +++ b/scripts/dev/upstream_merge/rebuild_drivers.py @@ -0,0 +1,138 @@ +""" +Rebuild NI out-of-tree drivers using DKMS. +This file is intentionally isolated from kernel build/install logic. +""" + +def install_sshfs_fuse(config, run_cmd): + ssh_target = config.ssh_target + + print("[REBUILD][STEP 0] Install sshfs-fuse and load fuse") + + # opkg update (SSH drop expected) + rc, out = run_cmd(f"ssh {ssh_target} 'opkg update || true'") + print(out) + + # opkg install + rc, out = run_cmd( + f"ssh {ssh_target} 'opkg install sshfs-fuse || true'" + ) + print(out) + + # VERIFY sshfs exists + rc, out = run_cmd(f"ssh {ssh_target} 'which sshfs'") + print(out) + if rc != 0: + print("[REBUILD][ERROR] sshfs not installed") + return 1 + + # load fuse + rc, out = run_cmd(f"ssh {ssh_target} 'modprobe fuse'") + print(out) + if rc != 0: + print("[REBUILD][ERROR] modprobe fuse failed") + return 1 + + print("[REBUILD][OK] sshfs-fuse ready") + return 0 + + +def mount_kernel_source(config, run_cmd): + ssh_target = config.ssh_target + kernel_src_dir = config.kernel_src_dir + host_user = config.build_host_user + host_ip = config.build_host_ip + + print("[REBUILD][STEP 1] Mount kernel source via SSHFS") + + run_cmd(f'ssh {ssh_target} "mkdir -p /usr/src/linux"') + run_cmd( + f'ssh {ssh_target}' + f'"mount | grep /usr/src/linux && umount /usr/src/linux || true"' + ) + + rc, out = run_cmd( + f'ssh {ssh_target} ' + f'"sshfs {host_user}@{host_ip}:{kernel_src_dir} /usr/src/linux"' + ) + print(out) + + rc, out = run_cmd( + f'ssh {ssh_target} "test -f /usr/src/linux/Makefile && echo OK"' + ) + print(out) + + if rc != 0: + print("[REBUILD][ERROR] Kernel source not visible at /usr/src/linux") + return 1 + + print("[REBUILD][OK] Kernel source mounted via SSHFS") + return 0 + + +def fix_symlinks(config, run_cmd): + ssh_target = config.ssh_target + + print("[REBUILD][STEP 2] Fix build/source symlinks") + + rc, out = run_cmd( + f"ssh {ssh_target} " + "'cd /lib/modules/$(uname -r) && " + "rm -f build source && " + "ln -s /usr/src/linux source && " + "ln -s source build'" + ) + print(out) + + if rc != 0: + print("[REBUILD][ERROR] Failed to fix build/source symlinks") + return 1 + + # Optional but good verification + rc, out = run_cmd( + f"ssh {ssh_target} " + "'ls -l /lib/modules/$(uname -r)/build " + "/lib/modules/$(uname -r)/source'" + ) + print(out) + + print("[REBUILD][OK] build and source symlinks fixed") + return 0 + + +def prepare_headers(config, run_cmd): + ssh_target = config.ssh_target + + print("[REBUILD][STEP 3] Prepare kernel headers") + + rc, out = run_cmd( + f"ssh {ssh_target} " + "'cd /lib/modules/$(uname -r)/build && " + "make prepare && make modules_prepare'" + ) + print(out) + + if rc != 0: + print("[REBUILD][ERROR] Kernel header preparation failed") + return 1 + + print("[REBUILD][OK] Kernel headers prepared") + return 0 + + +def dkms_autoinstall(config, run_cmd): + ssh_target = config.ssh_target + + print("[REBUILD][STEP 4] DKMS autoinstall") + + rc, out = run_cmd(f"ssh {ssh_target} 'dkms autoinstall'") + print(out) + + if rc != 0: + print("[REBUILD][ERROR] DKMS autoinstall failed") + return 1 + + rc, out = run_cmd(f"ssh {ssh_target} 'dkms status'") + print(out) + + print("[REBUILD][OK] DKMS rebuild complete") + return 0 diff --git a/scripts/dev/upstream_merge/utils/build.py b/scripts/dev/upstream_merge/utils/build.py index e25d8d3d1..4962dfdf9 100644 --- a/scripts/dev/upstream_merge/utils/build.py +++ b/scripts/dev/upstream_merge/utils/build.py @@ -4,9 +4,13 @@ """ import argparse +import os from .shell_commands import execute_and_stream_cmd_output +config = None +KERNEL_DEFCONFIG = "nati_x86_64_defconfig" + def setup_env_and_build_packages(args="", clean_build=False): """ @@ -122,6 +126,106 @@ def build_core_images(args=""): ) +def build_kernel_x86_64(kernel_src_dir): + print("[INFO] Starting x86_64 kernel build") + + if not os.path.isdir(kernel_src_dir): + return 1, f"Kernel source directory not found: {kernel_src_dir}" + + # kernel_src_dir = /nilrt-kernel-build/linux + build_root = os.path.dirname(kernel_src_dir) + + # Read from config + temp_modules_rel = getattr(config, "temp_modules_dir", "tmp-glibc/modules") + + temp_modules_dir = os.path.join(build_root, temp_modules_rel) + os.makedirs(temp_modules_dir, exist_ok=True) + + os.chdir(kernel_src_dir) + + # Ensure ARCH is correct + os.environ["ARCH"] = "x86_64" + + # Step 1: Configure kernel + ret = os.system(f"make {KERNEL_DEFCONFIG}") + if ret != 0: + return 1, "Kernel defconfig failed" + + # Step 2: Build kernel and modules + jobs = getattr(config, "kernel_build_jobs", None) + + if jobs is None: + jobs = "$(nproc)" # fallback to all available cores + + ret = os.system(f"make -j{jobs} bzImage modules") + if ret != 0: + return 1, "Kernel build failed (bzImage/modules)" + + # Step 3: Install modules to temp directory + ret = os.system( + f"make modules_install INSTALL_MOD_PATH={temp_modules_dir}") + if ret != 0: + return 1, "Kernel modules_install failed" + + return 0, ( + "Kernel built successfully (bzImage + modules)\n" + f"Modules staged at: {temp_modules_dir}" + ) + + +def regenerate_defconfig(kernel_src_dir): + """ + US-4: + Regenerate nati_x86_64_defconfig and create a commit if it changes. + Returns True if a commit was created, False otherwise. + """ + print("[US4] Regenerating nati_x86_64_defconfig") + + original_cwd = os.getcwd() + os.chdir(kernel_src_dir) + + cmds = [ + "make mrproper", + f"make {KERNEL_DEFCONFIG}", + "make savedefconfig", + f"mv defconfig arch/x86/configs/{KERNEL_DEFCONFIG}", + ] + + for cmd in cmds: + rc = os.system(cmd) + if rc != 0: + os.chdir(original_cwd) + raise RuntimeError(f"[US4][ERROR] Command failed: {cmd}") + + # Check for diff + diff_rc = os.system( + f"git diff --quiet arch/x86/configs/{KERNEL_DEFCONFIG}" + ) + + if diff_rc == 0: + print("[US4] Defconfig unchanged") + os.chdir(original_cwd) + return False + + print("[US4] Defconfig changed, creating commit") + + rc = os.system(f"git add arch/x86/configs/{KERNEL_DEFCONFIG}") + if rc != 0: + os.chdir(original_cwd) + raise RuntimeError("[US4][ERROR] git add failed") + + rc = os.system( + 'git commit -s -m ' + '"nati_x86_64_defconfig: regenerate; no functional changes"' + ) + if rc != 0: + os.chdir(original_cwd) + raise RuntimeError("[US4][ERROR] git commit failed") + + os.chdir(original_cwd) + return True + + def parse_args(): """ Parse command-line arguments for building the core feeds and images, diff --git a/scripts/dev/upstream_merge/utils/git_commands.py b/scripts/dev/upstream_merge/utils/git_commands.py index fe0270862..ad822d422 100644 --- a/scripts/dev/upstream_merge/utils/git_commands.py +++ b/scripts/dev/upstream_merge/utils/git_commands.py @@ -282,3 +282,62 @@ def send_email( f'--confirm=never --encoding=UTF-8 {file}' ) return run_command(command) + + +def git_tag(list_pattern=None): + """ + List git tags, optionally filtered by a pattern. + """ + command = "git tag" + if list_pattern: + command += f" -l '{list_pattern}'" + return run_command(command, capture_output=True) + + +def git_merge_abort(): + """ + Abort a merge in progress. + """ + return run_command("git merge --abort", capture_output=True) + + +def git_status(capture_output=True): + """ + Show the working tree status. + """ + return run_command("git status", capture_output) + + +def git_reset(hard=False, target="HEAD", capture_output=True): + """ + Reset current HEAD to the specified state. + :param hard: Whether to do a hard reset (--hard). + :param target: The commit/branch to reset to (default: HEAD). + """ + command = "git reset" + if hard: + command += " --hard" + command += f" {target}" + return run_command(command, capture_output) + + +def git_clean( + force=False, + directories=False, + ignored_files=False, + capture_output=True + ): + """ + Remove untracked files from the working tree. + :param force: Required by git to actually delete files. + :param directories: Remove untracked directories as well (-d). + :param ignored_files: Remove ignored files as well (-x). + """ + command = "git clean" + if force: + command += " -f" + if directories: + command += "d" + if ignored_files: + command += "x" + return run_command(command, capture_output)