Skip to content
Closed
4 changes: 3 additions & 1 deletion ubuntu/99-network-manager.cfg → ubuntu/01-end0.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#cloud-config
network:
version: 2
renderer: NetworkManager
ethernets:
end0:
dhcp4: true
44 changes: 37 additions & 7 deletions ubuntu/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
from build_kernel import build_kernel, reorganize_kernel_debs
from build_dtb import build_dtb
from build_deb import PackageBuilder, PackageNotFoundError, PackageBuildError
from release_debian_changelog_update import process_debian_trees
from constants import *
from datetime import date
from helpers import create_new_directory, umount_dir, check_if_root, check_and_append_line_in_file, cleanup_file, cleanup_directory, change_folder_perm_read_write, print_build_logs, start_local_apt_server, build_deb_package_gz, pull_debs_wget
from helpers import create_new_directory, umount_dir, check_if_root, check_and_append_line_in_file, cleanup_file, cleanup_directory, change_folder_perm_read_write, print_build_logs, start_local_apt_server, build_deb_package_gz, pull_debs_wget, extract_vmlinux
from deb_organize import generate_manifest_map
from pack_deb import PackagePacker
from flat_meta import create_flat_meta
Expand Down Expand Up @@ -94,6 +95,9 @@ def parse_arguments():
help='Generate Debian binary (default: False)')
parser.add_argument('--pack-image', action='store_true', default=False,
help='Pack system.img with generated debians (default: False)')
parser.add_argument('--release-prep-url', type=str, required=False,
help='prepares workspace for release (default: False)',
)
parser.add_argument('--pack-variant', type=str, choices=['base', 'qcom'], default='qcom',
help='Pack variant (only base or qcom, default: qcom)')
parser.add_argument('--packages-manifest-path', type=str, required=False,
Expand Down Expand Up @@ -177,6 +181,7 @@ def parse_arguments():
IF_BUILD_KERNEL = args.build_kernel
IF_GEN_DEBIANS = args.gen_debians
IF_PACK_IMAGE = args.pack_image
IF_RELEASE_PREP_URL = args.release_prep_url
IF_FLAT_META = args.flat_meta
IS_CLEANUP_ENABLED = not args.nocleanup
IS_PREPARE_SOURCE = args.prepare_sources
Expand Down Expand Up @@ -235,6 +240,8 @@ def parse_arguments():
reorganize_kernel_debs(WORKSPACE_DIR, KERNEL_DEB_OUT_DIR)

build_dtb(KERNEL_DEB_OUT_DIR, LINUX_MODULES_DEB, COMBINED_DTB_FILE, OUT_DIR)
logger.info("Building vmlinux as requested")
extract_vmlinux(DEB_OUT_DIR, LINUX_IMAGE_DBGSYM_DEB, VMLINUX_QCOM_FILE, OUT_DIR)

except Exception as e:
logger.critical(f"Exception during kernel build : {e}")
Expand All @@ -246,6 +253,26 @@ def parse_arguments():
logger.critical("Kernel build failed. Exiting.")
exit(1)

if IF_RELEASE_PREP_URL:
logger.info("Running the release preparation phase")
try:
statuses = process_debian_trees(
input_root=SOURCES_DIR,
apt_source_line=IF_RELEASE_PREP_URL,
prefer_debian_changelog=False,
dry_run=False,
)
except Exception as e:
logger.error(f"[FATAL] {e}")
#return 1

logger.info("\nSUMMARY")
for debian_dir, st in statuses.items():
logger.info("-" * 80)
logger.info(f"debian dir: {debian_dir}")
logger.info(f"action: {st.get('action')}")
logger.info(f"details: {st.get('details')}")

if IF_GEN_DEBIANS or IS_PREPARE_SOURCE :
error_during_packages_build = False

Expand Down Expand Up @@ -346,19 +373,22 @@ def parse_arguments():
cleanup_directory(MOUNT_DIR)

create_new_directory(MOUNT_DIR)

packer = PackagePacker(MOUNT_DIR, IMAGE_TYPE, PACK_VARIANT, OUT_DIR, OUT_SYSTEM_IMG, APT_SERVER_CONFIG, DEB_OUT_TEMP_DIR, DEB_OUT_DIR, DEBIAN_INSTALL_DIR, IS_CLEANUP_ENABLED, PACKAGES_MANIFEST_PATH,QC_FOLDER)
files_check = glob.glob(os.path.join(KERNEL_DEB_OUT_DIR, LINUX_MODULES_DEB))
if len(files_check) == 0:
logger.warning(f"No files matching {LINUX_MODULES_DEB} exist in {KERNEL_DEB_OUT_DIR}. Pulling it from pkg.qualcomm.com")
cur_file = os.path.dirname(os.path.realpath(__file__))
manifest_file_path = os.path.join(cur_file, "packages", "base", f"{IMAGE_TYPE}.manifest")

#Get the merged manifest path
manifest_file_path = packer.get_merged_manifest()

pull_debs_wget(manifest_file_path, KERNEL_DEB_OUT_DIR,KERNEL_DEBS,KERNEL_DEB_URL)
else:
logger.info("Linux modules found locally. Skipping pull from pkg.qualcomm.com")

build_dtb(KERNEL_DEB_OUT_DIR, LINUX_MODULES_DEB, COMBINED_DTB_FILE, OUT_DIR)

packer = PackagePacker(MOUNT_DIR, IMAGE_TYPE, PACK_VARIANT, OUT_DIR, OUT_SYSTEM_IMG, APT_SERVER_CONFIG, DEB_OUT_TEMP_DIR, DEB_OUT_DIR, DEBIAN_INSTALL_DIR, IS_CLEANUP_ENABLED, PACKAGES_MANIFEST_PATH,QC_FOLDER)
if not IF_BUILD_KERNEL: #this is needed when user runs both build kernel and pack image extravtion dhouldnt run twice
logger.info("Building vmlinux as requested")
extract_vmlinux(DEB_OUT_DIR, LINUX_IMAGE_DBGSYM_DEB, VMLINUX_QCOM_FILE, OUT_DIR)

packer.build_image()

Expand Down Expand Up @@ -404,4 +434,4 @@ def parse_arguments():
exit(1)

logger.info("Script execution sucessful")
exit(0)
exit(0)
10 changes: 5 additions & 5 deletions ubuntu/build_deb.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,15 @@ def __init__(self, CHROOT_NAME, CHROOT_DIR, SOURCE_DIR, APT_SERVER_CONFIG, \
self.ARCH = ARCH
self.CHROOT_SUFFIX = CHROOT_SUFFIX
self.SOURCE_DIR = SOURCE_DIR
self.DEB_OUT_DIR = DEB_OUT_DIR
self.APT_SERVER_CONFIG = APT_SERVER_CONFIG
self.CHROOT_NAME = CHROOT_NAME
self.MANIFEST_MAP = MANIFEST_MAP
self.DEB_OUT_TEMP_DIR = DEB_OUT_TEMP_DIR
self.IS_CLEANUP_ENABLED = IS_CLEANUP_ENABLED
self.DEB_OUT_DIR = DEB_OUT_DIR
self.DEB_OUT_DIR_APT = DEB_OUT_DIR_APT
self.DEBIAN_INSTALL_DIR_APT = DEBIAN_INSTALL_DIR_APT
self.IS_PREPARE_SOURCE = IS_PREPARE_SOURCE
self.DEBIAN_MIRROR = "http://ports.ubuntu.com"
self.DEBIAN_MIRROR = f"http://ports-ubuntu.qualcomm.com/ports.ubuntu.com/{SNAP_SHOT_DATE}"
self.packages = {}

self.generate_schroot_config()
Expand Down Expand Up @@ -102,16 +100,18 @@ def generate_schroot_config(self):
# this command creates a chroot environment that will be named "{DIST}-{ARCH}-{SUFFIX}"
# We supply our own suffix, otherwise sbuild will use 'sbuild'
cmd = f"sbuild-createchroot --arch={self.ARCH}" \
f" --keyring="" " \
f" --chroot-suffix=-{self.CHROOT_SUFFIX}" \
f" --components=main,universe" \
f" {self.DIST}" \
f" {self.CHROOT_DIR}/{self.CHROOT_NAME}" \
f" {self.DEBIAN_MIRROR}"

logger.debug(f"Creating schroot environment with command: {cmd}")

result = subprocess.run(cmd, shell=True, capture_output=True, text=True)

subprocess.run(["chroot", f"{self.CHROOT_DIR}/{self.CHROOT_NAME}", "bash", "-c", f"sed -i 's|{self.DEBIAN_MIRROR}|[trusted=yes] {self.DEBIAN_MIRROR}|' /etc/apt/sources.list"])

if result.returncode != 0:
raise Exception(f"Error creating schroot environment: {result.stderr}")
else:
Expand Down
3 changes: 3 additions & 0 deletions ubuntu/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import os

LINUX_IMAGE_DBGSYM_DEB = "oss/linux-qcom-tools*/linux-qcom-tools*_arm64.deb"
LINUX_MODULES_DEB = "linux-modules-*-qcom/linux-modules-*_arm64.deb"
SNAP_SHOT_DATE = "2025-09-12" #update date for snapshot date from https://ports-ubuntu.qualcomm.com/ports.ubuntu.com/

KERNEL_DEBS = [
"linux-modules",
Expand All @@ -20,6 +22,7 @@
]

COMBINED_DTB_FILE = "combined-dtb.dtb"
VMLINUX_QCOM_FILE = "vmlinux"
IMAGE_NAME = "system.img"

IMAGE_SIZE_IN_G = 8
Expand Down
85 changes: 83 additions & 2 deletions ubuntu/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from apt_server import AptServer
from constants import TERMINAL, HOST_FS_MOUNT
from color_logger import logger
import tempfile

def check_if_root() -> bool:
"""
Expand Down Expand Up @@ -66,6 +67,72 @@ def check_and_append_line_in_file(file_path, line_to_check, append_if_missing=Fa

return False


def extract_vmlinux(deb_dir, deb_file_regex, vmlinux_filename, out_dir):
"""
Extracts the vmlinux file from a Debian package and places it in the output directory.

Args:
-----
- deb_dir (str): Directory containing the .deb files.
- deb_file_regex (str): Regex pattern to match the .deb file.
- vmlinux_filename (str): Name of the vmlinux file to extract.
- out_dir (str): Directory to copy the extracted vmlinux file to.

Raises:
-------
- SystemExit: If not run as root, if no matching .deb files found, or if errors occur.
"""
if not check_if_root():
logger.error('Please run this script as root user.')
raise Exception('Root privileges required')

vmlinux_path = os.path.join(out_dir, vmlinux_filename)
if os.path.exists(vmlinux_path):
logger.info(f"Removing existing vmlinux at {vmlinux_path}")
os.remove(vmlinux_path)

# Step 0: Check if the .deb file exists
files = glob.glob(os.path.join(deb_dir, deb_file_regex))
if len(files) == 0:
logger.error(f"Error: No files matching {deb_file_regex} exist in {deb_dir}")
raise Exception(f"No files matching {deb_file_regex} found")

# Step 1: Extract the .deb package to a temporary directory
deb_file = files[0] # Assuming only one file matches the regex
try:
temp_dir = tempfile.mkdtemp()
logger.debug(f'Temp path for vmlinux extraction: {temp_dir}')
subprocess.run(["dpkg-deb", '-x', deb_file, temp_dir], check=True)

# Step 2: Find the vmlinux file within the temporary directory
file_path = None
for root, _, files in os.walk(temp_dir):
if vmlinux_filename in files:
file_path = os.path.join(root, vmlinux_filename)
break

# Step 3: Copy the vmlinux file to the output directory
if file_path:
try:
shutil.copy(file_path, vmlinux_path)
os.chmod(vmlinux_path, 0o644)
logger.info(f"{vmlinux_filename} has been copied to {vmlinux_path}")
except Exception as e:
logger.error(f"Error copying file {file_path}")
logger.error(f"Resulted in error: {e}")
else:
logger.error(f"{vmlinux_filename} not found in {deb_file}")

except Exception as e:
logger.error(f"Error extracting vmlinux: {e}")
raise
finally:
# Step 4: Clean up the temporary directory
if temp_dir:
shutil.rmtree(temp_dir)
logger.info(f"Cleaned up temporary directory {temp_dir}")

def parse_debs_manifest(manifest_path):
"""
Parses a manifest file and returns a dictionary of module names and their corresponding versions.
Expand Down Expand Up @@ -431,7 +498,8 @@ def build_deb_package_gz(dir, start_server=True) -> str:

raise Exception(result.stderr)

# Even with a successful exit code, dpkg-scanpackages still outputs the number of entries written to stderr logger.debug(result.stderr.strip())
# Even with a successful exit code, dpkg-scanpackages still outputs the number of entries written to stderr
logger.debug(result.stderr.strip())


cmd = f"gzip -k -f {packages_path}"
Expand All @@ -454,7 +522,6 @@ def build_deb_package_gz(dir, start_server=True) -> str:
return start_local_apt_server(dir)
return None


def pull_debs_wget(manifest_file_path, out_dir,DEBS_to_download_list,base_url):
"""
Downloads Debian packages from a remote repository using wget.
Expand Down Expand Up @@ -492,6 +559,18 @@ def pull_debs_wget(manifest_file_path, out_dir,DEBS_to_download_list,base_url):

# Generate wget links and download
os.makedirs(out_dir, exist_ok=True)
#logger.info(f"version map {version_map}...")

matches = {k: v for k, v in version_map.items() if 'linux-modules' in k}

# Get the first match (you can change this logic if needed)
first_match_value = next(iter(matches.values()))
first_match_key = next(iter(matches))
name_suffix = first_match_value.rsplit('.', 1)[0]

# Construct new key and update version_map
linux_qcom_tools_suffix = 'linux-qcom-tools-' + name_suffix
version_map[linux_qcom_tools_suffix] = version_map[first_match_key]
for module in DEBS_to_download_list:
for name, version in version_map.items():
if name.startswith(module):
Expand All @@ -509,3 +588,5 @@ def pull_debs_wget(manifest_file_path, out_dir,DEBS_to_download_list,base_url):
except subprocess.CalledProcessError as e:
logger.error(f"error: Failed to download {url}: {e}")
break # Stop after first match


56 changes: 50 additions & 6 deletions ubuntu/pack_deb.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from color_logger import logger

class PackagePacker:
def __init__(self, MOUNT_DIR, IMAGE_TYPE, VARIANT, OUT_DIR, OUT_SYSTEM_IMG, APT_SERVER_CONFIG, TEMP_DIR, DEB_OUT_DIR, DEBIAN_INSTALL_DIR, IS_CLEANUP_ENABLED, PACKAGES_MANIFEST_PATH=None,QC_FOLDER=None):
def __init__(self, MOUNT_DIR, IMAGE_TYPE, VARIANT, OUT_DIR, OUT_SYSTEM_IMG, APT_SERVER_CONFIG, TEMP_DIR, DEB_OUT_DIR, DEBIAN_INSTALL_DIR, IS_CLEANUP_ENABLED, PACKAGES_MANIFEST_PATH=None,QC_FOLDER=None,IF_RELEASE_ENABLED=False):
"""
Initializes the PackagePacker instance.

Expand Down Expand Up @@ -61,6 +61,7 @@ def __init__(self, MOUNT_DIR, IMAGE_TYPE, VARIANT, OUT_DIR, OUT_SYSTEM_IMG, APT_
self.OUT_SYSTEM_IMG = OUT_SYSTEM_IMG
self.PACKAGES_MANIFEST_PATH = PACKAGES_MANIFEST_PATH
self.qc_folder = QC_FOLDER
self.IS_RELEASE_ENABLED = IF_RELEASE_ENABLED

self.EFI_BIN_PATH = os.path.join(self.OUT_DIR, "efi.bin")
self.EFI_MOUNT_PATH = os.path.join(self.MOUNT_DIR, "boot", "efi")
Expand Down Expand Up @@ -140,8 +141,6 @@ def merge_manifests_from_folder(self, folder_path, image_type, subdir_filter=Non
logger.error(f"Failed to create or write to merged manifest file: {e}")
return None



def parse_manifests(self):
"""
Parses the base and QCOM manifests to gather the list of packages to include in the image.
Expand Down Expand Up @@ -184,7 +183,14 @@ def parse_manifests(self):
qc_qcom_merged = self.merge_manifests_from_folder(self.qc_folder, self.IMAGE_TYPE, "qcom")
if qc_qcom_merged:
logger.info(f"Using qcom manifests from: {qc_qcom_merged}")
self.DEBS.extend(parse_debs_manifest(qc_qcom_merged))
manifest_debs = parse_debs_manifest(qc_qcom_merged)
if self.IS_RELEASE_ENABLED:
# Append +rel to everyone (no filtering)
manifest_debs[:] = [{**d, "version": (d["version"] if d["version"].endswith("+rel") else d["version"] + "+rel")}
for d in manifest_debs]
logger.info("Assuming a release build, appending +rel to all packages.")
logger.info(manifest_debs)
self.DEBS.extend(manifest_debs)
return

# 3. No manifest found: print message and exit
Expand Down Expand Up @@ -220,7 +226,7 @@ def build_image(self):
bash_command = f"""
sudo mmdebstrap --verbose --logfile={log_file} \
--customize-hook='echo root:password | chroot "$1" chpasswd' \
--customize-hook='cp {self.cur_file}/99-network-manager.cfg "$1/etc/cloud/cloud.cfg.d/99-network-manager.cfg"' \
--customize-hook='cp {self.cur_file}/01-end0.yaml "$1/etc/netplan/01-end0.yaml"' \
--customize-hook='echo "PermitRootLogin yes" >> "$1/etc/ssh/sshd_config"' \
--setup-hook='echo /dev/disk/by-partlabel/system / ext4 defaults 0 1 > "$1/etc/fstab"' \
--arch=arm64 \
Expand All @@ -242,7 +248,8 @@ def build_image(self):
if config.strip():
bash_command += f" \"{config.strip()}\""

bash_command += f" \"deb [arch=arm64 trusted=yes] http://ports.ubuntu.com/ubuntu-ports noble main universe multiverse restricted\""
bash_command += f" \"deb [arch=arm64 trusted=yes] http://ports.ubuntu.com noble main universe multiverse restricted\""
bash_command += f" \"deb [arch=arm64 trusted=yes] https://ports-ubuntu.qualcomm.com/x04_initial_release noble main\""

out = run_command_for_result(bash_command)
if out['returncode'] != 0:
Expand Down Expand Up @@ -296,3 +303,40 @@ def extract_manifest(self, flavor):
else:
logger.info(f"Manifest for {flavor} saved to {manifest_path}")


def get_merged_manifest(self):
if self.PACKAGES_MANIFEST_PATH:
logger.info(f"User provided manifest path: {self.PACKAGES_MANIFEST_PATH}")
return self.PACKAGES_MANIFEST_PATH

manifest_paths = []

# Always merge from qc_folder and packages
search_dirs = [
(self.qc_folder, "base"),
(os.path.join(self.cur_file, "packages"), "base")
]

if self.VARIANT == "qcom":
search_dirs += [
(self.qc_folder, "qcom"),
(os.path.join(self.cur_file, "packages"), "qcom")
]

for folder, subdir in search_dirs:
if folder and os.path.isdir(folder):
for root, _, files in os.walk(folder):
# Only include manifests from subdirectories matching subdir
if subdir in os.path.relpath(root, folder).split(os.sep):
for file in files:
if file == f"{self.IMAGE_TYPE}.manifest":
manifest_paths.append(os.path.join(root, file))

# Merge all found manifest files into one
merged_manifest_path = os.path.join(self.TEMP_DIR, f"{self.IMAGE_TYPE}_merged.manifest")
with open(merged_manifest_path, 'w') as merged_file:
for manifest in manifest_paths:
with open(manifest, 'r') as f:
merged_file.write(f.read())
logger.info(f"Final merged manifest saved to: {merged_manifest_path}")
return merged_manifest_path
Loading
Loading