Files
rpmbuild/make_rpm_ubuntu26.py
T
torstenandCopilot 39b159d67c Exclude LtsUtilsGuiDds from release scope (requires DDS headers)
- LtsUtilsGuiDds depends on DDS/Core headers not in release scope
- Only needed by LtsMultiMonitor which is outside release scope
- Prevents compilation errors during DEB package build

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-09 15:16:08 +00:00

388 lines
13 KiB
Python
Executable File

#!/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',
'translations',
'CTestCustom.ctest',
'.gitignore',
'.envrc',
'doxygen',
'external',
'src',
'lts-cc.conf'
]
# Directories to exclude from include folder (bundled libraries not needed for release)
include_exclude = ['GeographicLib']
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 except LtsUtilsGuiDds (requires DDS)
if rel_root == 'Lts/Common/LtsUtils/LtsUtilsGui':
dirs[:] = [d for d in dirs if d != 'LtsUtilsGuiDds']
else:
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
elif rel_root == 'include':
# In include directory, exclude bundled libraries
dirs[:] = [d for d in dirs if d not in include_exclude]
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
# Include 'include' directory but GeographicLib will be filtered during walk
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 <legatus@example.com>
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 <legatus@example.com> {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()