Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

470 lines
16 KiB
Python

#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Debian package creation functions for ROCm packaging."""
import os
import re
import shutil
import subprocess
import sys
from dataclasses import replace
from datetime import datetime, timezone
from email.utils import format_datetime
from jinja2 import Environment, FileSystemLoader, select_autoescape
from pathlib import Path
from packaging_utils import *
# Setup paths
SCRIPT_DIR = Path(__file__).resolve().parent
def create_nonversioned_deb_package(pkg_name, config: PackageConfig):
"""Create a non-versioned Debian meta package (.deb).
Builds a minimal Debian binary package whose payload is empty and whose primary
purpose is to express dependencies. The package name does not embed a version
Parameters:
pkg_name : Name of the package to be created
config: Configuration object containing package metadata
Returns:
output_list: List of packages created
"""
print_function_name()
# Create immutable config copy with versioned_pkg=False
build_config = replace(config, versioned_pkg=False)
# Use updated package name for build directory to avoid collisions between variants
pkg_info = get_package_info(pkg_name) # Needed for update_package_name
updated_pkg_name = update_package_name(pkg_info.get("Package"), build_config)
package_dir = Path(build_config.dest_dir) / build_config.pkg_type / updated_pkg_name
deb_dir = package_dir / "debian"
# Create package directory and debian directory
os.makedirs(deb_dir, exist_ok=True)
generate_changelog_file(pkg_info, deb_dir, build_config)
generate_rules_file(pkg_info, deb_dir, build_config)
generate_control_file(pkg_info, deb_dir, build_config)
package_with_dpkg_build(package_dir)
# Move packages to destination
updated_pkg_name = update_package_name(pkg_name, build_config)
output_list = move_packages_to_destination(updated_pkg_name, build_config)
return output_list
def create_versioned_deb_package(pkg_name, config: PackageConfig):
"""Create a versioned Debian package (.deb).
This function automates the process of building a Debian package by:
1) Retrieving package metadata and validating required fields.
2) Generating the `DEBIAN/control` file with appropriate fields (Package,
Version, Architecture, Maintainer, Description, and dependencies).
3) Copying the required package contents from an Artifactory repository.
4) Invoking `dpkg-buildpackage` to assemble the final `.deb` file.
Parameters:
pkg_name : Name of the package to be created
config: Configuration object containing package metadata
Returns:
output_list: List of packages created
"""
print_function_name()
# Explicitly ensure versioned_pkg=True
build_config = replace(config, versioned_pkg=True)
# Use updated package name for build directory to avoid collisions between variants
# Each variant (host, device, meta) gets its own build directory
updated_pkg_name = update_package_name(pkg_name, build_config)
package_dir = Path(build_config.dest_dir) / build_config.pkg_type / updated_pkg_name
deb_dir = package_dir / "debian"
# Create package directory and debian directory
os.makedirs(deb_dir, exist_ok=True)
pkg_info = get_package_info(pkg_name) # Raises ValueError if not found
is_meta = is_meta_package(pkg_info)
generate_changelog_file(pkg_info, deb_dir, build_config)
generate_rules_file(pkg_info, deb_dir, build_config)
generate_control_file(pkg_info, deb_dir, build_config)
if is_postinstallscripts_available(pkg_info):
generate_debian_postscripts(pkg_info, deb_dir, build_config)
sourcedir_list = []
dir_list = filter_components_fromartifactory(
pkg_name,
build_config.artifacts_dir,
build_config.gfx_arch,
build_config.enable_kpack,
)
sourcedir_list.extend(dir_list)
print(f"sourcedir_list:\n {sourcedir_list}")
# GFX_META is a versioned meta package (empty content, just dependencies)
is_gfx_meta = build_config.enable_kpack and build_config.gfx_arch == GFX_META
if not sourcedir_list and not is_meta and not is_gfx_meta:
if build_config.enable_kpack:
print(
f"ERROR: {pkg_name}: Empty sourcedir_list and not a meta package, skipping"
)
return []
else:
sys.exit(
f"{pkg_name}: Empty sourcedir_list and not a meta package, exiting"
)
if not sourcedir_list:
print(f"{pkg_name} is a Meta package")
else:
# Copy package contents first
dest_dir = package_dir / Path(build_config.install_prefix).relative_to("/")
for source_path in sourcedir_list:
copy_package_contents(source_path, dest_dir)
# Generate install file after copying, so we can check for hidden files
generate_install_file(pkg_info, deb_dir, build_config, dest_dir)
package_with_dpkg_build(package_dir)
# Move packages to destination
updated_pkg_name = update_package_name(pkg_name, build_config)
output_list = move_packages_to_destination(updated_pkg_name, build_config)
return output_list
def generate_changelog_file(pkg_info, deb_dir, config: PackageConfig):
"""Generate a Debian changelog entry in `debian/changelog`.
Parameters:
pkg_info : Package details from the Json file
deb_dir: Directory where debian package changelog file is saved
config: Configuration object containing package metadata
Returns: None
"""
print_function_name()
changelog = Path(deb_dir) / "changelog"
pkg_name = update_package_name(pkg_info.get("Package"), config)
maintainer = pkg_info.get("Maintainer")
name_part, email_part = maintainer.split("<")
name = name_part.strip()
email = email_part.replace(">", "").strip()
# version is used along with package name
version = str(config.rocm_version)
if config.version_suffix:
version += f"-{str(config.version_suffix)}"
env = Environment(
loader=FileSystemLoader(str(SCRIPT_DIR)),
autoescape=select_autoescape(
enabled_extensions=("html", "htm", "xml"),
default_for_string=True,
default=False,
),
)
template = env.get_template("template/debian_changelog.j2")
# Prepare context dictionary
context = {
"package": pkg_name,
"version": version,
"distribution": "UNRELEASED",
"urgency": "medium",
"changes": ["Initial release"], # TODO: Will get from package.json?
"maintainer_name": name,
"maintainer_email": email,
"date": format_datetime(
datetime.now(timezone.utc)
), # TODO. How to get the date info?
}
with changelog.open("w", encoding="utf-8") as f:
f.write(template.render(context))
def generate_install_file(pkg_info, deb_dir, config: PackageConfig, dest_dir=None):
"""Generate a Debian install entry in `debian/install`.
Parameters:
pkg_info : Package details from the Json file
deb_dir: Directory where debian package control file is saved
config: Configuration object containing package metadata
dest_dir: Optional path to check for hidden files
Returns: None
"""
print_function_name()
# Note: pkg_info is not used currently:
# May be required in future to populate any context
install_file = Path(deb_dir) / "install"
# Check if hidden files and regular files exist in the destination directory
has_hidden_files = False
has_regular_files = False
if dest_dir and Path(dest_dir).exists():
for item in Path(dest_dir).iterdir():
name = item.name # get the filename as a string
# Skip "." and ".."
if name in [".", ".."]:
continue
# Hidden entry
if name.startswith("."):
has_hidden_files = True
else:
has_regular_files = True
env = Environment(
loader=FileSystemLoader(str(SCRIPT_DIR)),
autoescape=select_autoescape(
enabled_extensions=("html", "htm", "xml"),
default_for_string=True,
default=False,
),
)
template = env.get_template("template/debian_install.j2")
# Prepare your context dictionary
context = {
"path": config.install_prefix,
"has_hidden_files": has_hidden_files,
"has_regular_files": has_regular_files,
}
with install_file.open("w", encoding="utf-8") as f:
f.write(template.render(context))
def generate_rules_file(pkg_info, deb_dir, config: PackageConfig):
"""Generate a Debian rules entry in `debian/rules`.
Parameters:
pkg_info : Package details from the Json file
deb_dir: Directory where debian package control file is saved
config: Configuration object containing package metadata
Returns: None
"""
print_function_name()
rules_file = Path(deb_dir) / "rules"
disable_dh_strip = is_key_defined(pkg_info, "Disable_DEB_STRIP")
disable_dwz = is_key_defined(pkg_info, "Disable_DWZ")
# Get package name for changelog installation
pkg_name = update_package_name(pkg_info.get("Package"), config)
# Disable debian dh_strip for multi-arch builds
# WORKAROUND: dh_strip's debugedit incorrectly truncates ELF files with
# unconventional layouts (e.g., program headers at end of file).
# This causes "program header goes past the end of the file" errors.
# See: https://github.com/ROCm/TheRock/issues/4047
if config.enable_kpack:
disable_dh_strip = True
env = Environment(
loader=FileSystemLoader(str(SCRIPT_DIR)),
autoescape=select_autoescape(
enabled_extensions=("html", "htm", "xml"),
default_for_string=True,
default=False,
),
)
template = env.get_template("template/debian_rules.j2")
# Prepare context dictionary
context = {
"disable_dwz": disable_dwz,
"disable_dh_strip": disable_dh_strip,
"install_prefix": config.install_prefix,
"pkg_name": pkg_name,
}
with rules_file.open("w", encoding="utf-8") as f:
f.write(template.render(context))
# set executable permission for rules file
rules_file.chmod(0o755)
def generate_control_file(pkg_info, deb_dir, config: PackageConfig):
"""Generate a Debian control file entry in `debian/control`.
Parameters:
pkg_info: Package details parsed from a JSON file
deb_dir: Directory where the `debian/control` file will be created
config: Configuration object containing package metadata
Returns: None
"""
print_function_name()
control_file = Path(deb_dir) / "control"
pkg_name = pkg_info.get("Package")
is_meta = is_meta_package(pkg_info)
# Initialize optional fields
provides = replaces = conflicts = ""
debrecommends = debsuggests = ""
if config.versioned_pkg:
# Get -> Filter -> Transform
debrecommends = process_secondary_dependencies(
pkg_info, "DEBRecommends", config
)
debsuggests = process_secondary_dependencies(pkg_info, "DEBSuggests", config)
depends = process_main_dependencies(pkg_info, "DEBDepends", config)
else:
# Get -> Transform -> Join
provides = process_name_field(pkg_info, "Provides", debian_replace_devel_name)
replaces = process_name_field(pkg_info, "Replaces", debian_replace_devel_name)
conflicts = process_name_field(pkg_info, "Conflicts", debian_replace_devel_name)
# Non-versioned package depends on versioned package itself
depends = resolve_versioned_dependencies([pkg_name], config, is_meta)
pkg_name = update_package_name(pkg_name, config)
env = Environment(
loader=FileSystemLoader(str(SCRIPT_DIR)),
autoescape=select_autoescape(
enabled_extensions=("html", "htm", "xml"),
default_for_string=True,
default=False,
),
)
template = env.get_template("template/debian_control.j2")
context = {
"source": pkg_name,
"depends": depends,
"pkg_name": pkg_name,
"arch": pkg_info.get("Architecture"),
"description_short": pkg_info.get("Description_Short"),
"description_long": pkg_info.get("Description_Long"),
"homepage": pkg_info.get("Homepage"),
"maintainer": pkg_info.get("Maintainer"),
"priority": pkg_info.get("Priority"),
"section": pkg_info.get("Section"),
"version": config.rocm_version,
"provides": provides,
"replaces": replaces,
"conflicts": conflicts,
"debrecommends": debrecommends,
"debsuggests": debsuggests,
}
with control_file.open("w", encoding="utf-8") as f:
f.write(template.render(context))
f.write("\n") # Adds a blank line. For fixing missing final newline
def generate_debian_postscripts(pkg_info, deb_dir, config: PackageConfig):
"""Generate a Debian postinst/prerm file entry in `debian folder`.
Parameters:
pkg_info: Package details parsed from a JSON file
deb_dir: Directory where the `debian/control` file will be created
config: Configuration object containing package metadata
Returns: None
"""
# Debian maintainer scripts that must be executable
EXEC_SCRIPTS = {"preinst", "postinst", "prerm", "postrm", "config"}
pkg_name = pkg_info.get("Package")
parts = config.rocm_version.split(".")
if len(parts) < 3:
raise ValueError(
f"Version string '{config.rocm_version}' does not have major.minor.patch versions"
)
env = Environment(
loader=FileSystemLoader(str(SCRIPT_DIR)),
autoescape=select_autoescape(
enabled_extensions=("html", "htm", "xml"),
default_for_string=True,
default=False,
),
)
# Prepare your context dictionary
context = {
"install_prefix": config.install_prefix,
"version_major": int(re.match(r"^\d+", parts[0]).group()),
"version_minor": int(re.match(r"^\d+", parts[1]).group()),
"version_patch": int(re.match(r"^\d+", parts[2]).group()),
"target": "deb",
}
templates_root = Path(SCRIPT_DIR) / "template" / "scripts"
# Collect all matching files
for script in EXEC_SCRIPTS:
pattern = f"{pkg_name}-{script}.j2"
for file in templates_root.glob(pattern):
script_file = Path(deb_dir) / script
template = env.get_template(str(file.relative_to(SCRIPT_DIR)))
with script_file.open("w", encoding="utf-8") as f:
f.write(template.render(context))
os.chmod(script_file, 0o755)
def copy_package_contents(source_dir, destination_dir):
"""Copy package contents from artfactory to package build directory
Parameters:
source_dir : Source directory
destination_dir: Local directory where the package contents should be copied
Returns: None
"""
print_function_name()
source_dir = Path(source_dir)
destination_dir = Path(destination_dir)
if not source_dir.is_dir():
print(f"Directory does not exist: {source_dir}")
return
# Ensure destination directory exists
destination_dir.mkdir(parents=True, exist_ok=True)
# Copy each item from source to destination
for item in source_dir.iterdir():
src = item
dst = destination_dir / item.name
if src.is_dir() and not dst.is_symlink():
shutil.copytree(
src,
dst,
dirs_exist_ok=True,
symlinks=True,
ignore_dangling_symlinks=True,
)
elif src.is_symlink():
# Copy the symlink itself (even if dangling)
link_target = src.readlink()
dst.symlink_to(link_target)
else:
shutil.copy2(src, dst)
def package_with_dpkg_build(pkg_dir):
"""Generate a Debian package using `dpkg-buildpackage`
Parameters:
pkg_dir: Path to the directory containing the package contents and the `debian/`
subdirectory (with `control`, `changelog`, `rules`, etc.).
Returns: None
"""
print_function_name()
# Build the command
cmd = ["dpkg-buildpackage", "-uc", "-us", "-b"]
# Execute the command
try:
subprocess.run(cmd, check=True, cwd=pkg_dir)
print(f"Deb Package built successfully: {os.path.basename(pkg_dir)}")
except subprocess.CalledProcessError as e:
print(f"Error building deb package: {os.path.basename(pkg_dir)}: {e}")
sys.exit(e.returncode)