#!/usr/bin/python3 # -*- coding: utf-8 -*- """ DEB package builder for Ubuntu 26.04 Migrated from make_rpm.py (openSUSE) to Ubuntu/DEB ecosystem """ import argparse import configparser import getpass import os import re import shutil import stat import subprocess import sys from datetime import datetime from pathlib import Path def verbose(): verb = os.getenv("EMERGE_VERBOSE") if verb is not None and verb.isdigit() and int(verb) > 0: return int(verb) return 0 def debug(message): if verbose() >= 1: print(f"debug: {message}") return True def info(message): if verbose() >= 0: print(f"info: {message}") return True def warning(message): print(f"warning: {message}") return True def error(message): die(message) def die(message): print(f"emerge fatal error: {message}", file=sys.stderr) sys.exit(1) def system(cmdstring, file=None): debug(f"Executing '{cmdstring}'") stdout = subprocess.PIPE if file is not None: stdout = file p = subprocess.Popen(cmdstring, shell=True, stdout=stdout, stderr=sys.stderr) ret = p.wait() if ret != 0: die(f"Error executing {cmdstring} ({ret})") ret = "" if file is None: ret = p.stdout.read().strip() return ret def get_os_name(): """Return Ubuntu version name""" if os.path.exists("/etc/os-release"): with open("/etc/os-release", "r") as f: for line in f.readlines(): if line.startswith("PRETTY_NAME="): name = line.split("=")[1].strip().strip('"') return name.replace(" ", "_") return "Ubuntu_unknown" def remove_directory(dest_dir): info("--------------------------------") info(f"Removing {dest_dir}") info("--------------------------------") if os.path.exists(dest_dir) and os.path.isdir(dest_dir): shutil.rmtree(dest_dir) def copy_src_dir_to_dest_dir(srcdir, dstdir, perm=None, with_subdirs=True, release_scope=False): """Copy source directory to destination directory. Args: srcdir: Source directory path dstdir: Destination directory path perm: Optional permissions to set with_subdirs: Whether to copy subdirectories release_scope: If True, only copy release scope files (TetraIF, Z/src, Lts/Common) """ info("--------------------------------") info(f"Copying {srcdir} to {dstdir}") if release_scope: info("Release scope mode: only copying TetraIF, Z/src, Lts/Common") info("--------------------------------") while srcdir.endswith("/"): srcdir = srcdir[:-1] # Release scope directories and files to include release_items = [ 'TetraIF', 'Z/src', 'Lts/Common', 'CMakeLists.txt', 'cmake', 'tests', 'new_docker_build.sh', 'specs', 'include', 'translations', 'CTestCustom.ctest', '.gitignore', '.envrc', 'doxygen', 'external', 'src', 'lts-cc.conf' ] if with_subdirs: for root, dirs, files in os.walk(srcdir): # Filter directories for release scope if release_scope: rel_root = os.path.relpath(root, srcdir) if rel_root == '.': # At root level, only keep release scope directories dirs[:] = [d for d in dirs if d in release_items or any(d == item.split('/')[0] for item in release_items if '/' in item)] elif rel_root == 'Z': # In Z directory, only keep src dirs[:] = [d for d in dirs if d == 'src'] elif rel_root == 'Lts': # In Lts directory, only keep Common dirs[:] = [d for d in dirs if d == 'Common'] elif rel_root.startswith('Lts/Common'): # Keep all subdirectories under Lts/Common pass elif rel_root.startswith('Z/src'): # Keep all subdirectories under Z/src pass elif rel_root.startswith('TetraIF'): # Keep all subdirectories under TetraIF pass else: # Skip other directories dirs[:] = [] tmpdir = root.replace(srcdir, dstdir) debug(f"dstdir = '{tmpdir}'") if not os.path.exists(tmpdir): debug(f"Creating dir '{tmpdir}'") os.makedirs(tmpdir) if perm: os.chmod(tmpdir, perm | stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH) for file in files: # Skip non-release files at root level if release_scope and rel_root == '.': if file not in release_items and not any(file == item.split('/')[0] for item in release_items if '/' in item): continue src_path = os.path.join(root, file) # Skip broken symlinks if os.path.islink(src_path) and not os.path.exists(src_path): debug(f"Skipping broken symlink: {src_path}") continue if os.path.isfile(tmpdir) and os.path.exists(tmpdir): os.remove(tmpdir) shutil.copy(src_path, tmpdir, follow_symlinks=True) if perm: f = os.path.join(tmpdir, file) add_perm = 0 if os.path.isdir(f): add_perm = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH else: mode = os.stat(f).st_mode if (stat.S_IXUSR & mode) == stat.S_IXUSR: add_perm = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH os.chmod(f, perm | add_perm) else: for f in os.listdir(srcdir): if os.path.isdir(f): continue if release_scope and f not in release_items: continue shutil.copy(os.path.join(srcdir, f), os.path.join(dstdir, f)) def create_debian_control(deb_dir, project_name, version, build_number, cmake_flags): """Create debian/control file for DEB package""" control_content = f"""Source: legatus-{project_name.lower()} Section: devel Priority: optional Maintainer: Legatus Team Build-Depends: debhelper (>= 13), cmake, ninja-build, qt6-base-dev, qt6-tools-dev, libboost-all-dev, libjson-glib-dev, libgeographiclib-dev Package: legatus-{project_name.lower()} Architecture: any Depends: ${{shlibs:Depends}}, ${{misc:Depends}} Description: Legatus {project_name} LTS package Legatus {project_name} component built with Qt6/C++23 Version: {version}-{build_number} Build flags: {cmake_flags} """ control_file = os.path.join(deb_dir, "control") with open(control_file, "w") as f: f.write(control_content) info(f"Created {control_file}") def create_debian_changelog(deb_dir, project_name, version, build_number): """Create debian/changelog file""" timestamp = datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000") changelog_content = f"""legatus-{project_name.lower()} ({version}-{build_number}) unstable; urgency=medium * Automated build for Ubuntu 26.04 * Qt6/C++23 migration -- Legatus Team {timestamp} """ changelog_file = os.path.join(deb_dir, "changelog") with open(changelog_file, "w") as f: f.write(changelog_content) info(f"Created {changelog_file}") def create_debian_rules(deb_dir): """Create debian/rules file""" rules_content = """#!/usr/bin/make -f %: dh $@ --buildsystem=cmake --builddirectory=build override_dh_auto_configure: dh_auto_configure -- -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr override_dh_auto_build: dh_auto_build --parallel override_dh_auto_install: dh_auto_install """ rules_file = os.path.join(deb_dir, "rules") with open(rules_file, "w") as f: f.write(rules_content) os.chmod(rules_file, stat.S_IREAD | stat.S_IWRITE | stat.S_IXUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IXGRP | stat.S_IXOTH) info(f"Created {rules_file}") def build_deb(source_dir, output_dir, project_name, version, build_number, cmake_flags, release_scope=False): """Build DEB package using debuild Args: source_dir: Source directory path output_dir: Output directory for packages project_name: Project name (qatar|ahtc|guez) version: Version string build_number: Build number cmake_flags: CMake build flags release_scope: If True, only include release scope files (TetraIF, Z/src, Lts/Common) """ info("--------------------------------") info(f"Building DEB package for {project_name} {version}-{build_number}") if release_scope: info("Release scope mode: only packaging TetraIF, Z/src, Lts/Common") info("--------------------------------") # Create output directory os.makedirs(output_dir, exist_ok=True) # Create temporary build directory build_base = os.path.join(source_dir, "deb_build_tmp") remove_directory(build_base) os.makedirs(build_base) # Copy source to build directory pkg_name = f"legatus-{project_name.lower()}" pkg_dir = os.path.join(build_base, f"{pkg_name}-{version}") copy_src_dir_to_dest_dir(source_dir, pkg_dir, release_scope=release_scope) # Create debian directory deb_dir = os.path.join(pkg_dir, "debian") os.makedirs(deb_dir) # Create debian control files create_debian_control(deb_dir, project_name, version, build_number, cmake_flags) create_debian_changelog(deb_dir, project_name, version, build_number) create_debian_rules(deb_dir) # Create debian/source/format source_dir_path = os.path.join(deb_dir, "source") os.makedirs(source_dir_path) with open(os.path.join(source_dir_path, "format"), "w") as f: f.write("3.0 (quilt)\n") # Create debian/compat (for older debhelper compatibility) with open(os.path.join(deb_dir, "compat"), "w") as f: f.write("13\n") # Build the package info("Running debuild...") os.chdir(pkg_dir) # Set environment for debuild env = os.environ.copy() env["DEB_BUILD_OPTIONS"] = "parallel=$(nproc)" cmd = f"debuild -us -uc -b" debug(f"Executing: {cmd}") ret = subprocess.run(cmd, shell=True, env=env) if ret.returncode != 0: die(f"debuild failed with code {ret.returncode}") # Move generated packages to output directory parent_dir = os.path.dirname(pkg_dir) for f in os.listdir(parent_dir): if f.endswith(".deb") or f.endswith(".dsc") or f.endswith(".changes") or f.endswith(".buildinfo"): src = os.path.join(parent_dir, f) dst = os.path.join(output_dir, f) if os.path.exists(dst): os.remove(dst) shutil.move(src, output_dir) info(f"Moved {f} to {output_dir}") # Cleanup os.chdir(source_dir) remove_directory(build_base) info(f"DEB package build completed: {output_dir}") def main(): parser = argparse.ArgumentParser(description="Build DEB packages for Legatus LTS") parser.add_argument("-c", "--config", required=True, help="Config type (LTS)") parser.add_argument("--project_name", required=True, help="Project name (qatar|ahtc|guez)") parser.add_argument("--version", required=True, help="Version (Major.minor)") parser.add_argument("--output_dir", required=True, help="Output directory for packages") parser.add_argument("--source_dir", required=True, help="Source directory path") parser.add_argument("--cmake_build_flags", default="", help="CMake build flags") parser.add_argument("--build_number", default="1", help="Build number") parser.add_argument("--release_scope", action="store_true", help="Only include release scope files (TetraIF, Z/src, Lts/Common)") args = parser.parse_args() info(f"Building {args.config} package for {args.project_name} v{args.version}") info(f"Output directory: {args.output_dir}") info(f"Source directory: {args.source_dir}") info(f"CMake flags: {args.cmake_build_flags}") info(f"Release scope: {args.release_scope}") # Use provided source directory source_dir = args.source_dir info(f"Source directory: {source_dir}") build_deb( source_dir=source_dir, output_dir=args.output_dir, project_name=args.project_name, version=args.version, build_number=args.build_number, cmake_flags=args.cmake_build_flags, release_scope=args.release_scope ) info("Build completed successfully!") if __name__ == "__main__": main()