Files
rpmbuild/make_rpm_ubuntu26.py
T

460 lines
17 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',
'Lts/Proxies/TrainingAreaBase', # Required for training_area_telegrams.h used by TetraIF
'Lts/Services/ProjectDataManagement/stubs', # Stub headers for pdm.hpp (needed by TrainingAreaBase)
'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
# Need to handle multi-level paths like 'Lts/Proxies/TrainingAreaBase'
def should_keep_dir(d):
# Check if directory is directly in release_items
if d in release_items:
return True
# Check if directory is the first part of a multi-level release item
for item in release_items:
if '/' in item:
first_part = item.split('/')[0]
if d == first_part:
return True
return False
kept_dirs = [d for d in dirs if should_keep_dir(d)]
info(f"Root level: keeping {kept_dirs} from {dirs}")
dirs[:] = kept_dirs
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, Proxies (for TrainingAreaBase), and Services (for ProjectDataManagement stubs)
kept_dirs = [d for d in dirs if d in ('Common', 'Proxies', 'Services')]
info(f"Lts level: keeping {kept_dirs} from {dirs}")
dirs[:] = kept_dirs
elif rel_root == 'Lts/Proxies':
# In Lts/Proxies, only keep TrainingAreaBase
kept_dirs = [d for d in dirs if d == 'TrainingAreaBase']
info(f"Lts/Proxies level: keeping {kept_dirs} from {dirs}")
dirs[:] = kept_dirs
elif rel_root.startswith('Lts/Proxies/TrainingAreaBase'):
# Keep all subdirectories under TrainingAreaBase (DDS code wrapped in conditionals)
pass
elif rel_root == 'Lts/Services':
# In Lts/Services, only keep ProjectDataManagement (for stub headers)
kept_dirs = [d for d in dirs if d == 'ProjectDataManagement']
info(f"Lts/Services level: keeping {kept_dirs} from {dirs}")
dirs[:] = kept_dirs
elif rel_root == 'Lts/Services/ProjectDataManagement':
# In ProjectDataManagement, only keep stubs
kept_dirs = [d for d in dirs if d == 'stubs']
info(f"Lts/Services/ProjectDataManagement level: keeping {kept_dirs} from {dirs}")
dirs[:] = kept_dirs
elif rel_root.startswith('Lts/Services/ProjectDataManagement/stubs'):
# Keep all subdirectories under stubs (contains ProjectDataManagement/pdm.hpp)
pass
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, cmake_flags="", release_scope=False):
"""Create debian/rules file
Args:
deb_dir: debian directory path
cmake_flags: Additional CMake flags to pass
release_scope: If True, skip tests for release build
"""
# Build CMake flags string
cmake_opts = "-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr"
if cmake_flags:
cmake_opts = f"{cmake_opts} {cmake_flags}"
# For release builds, skip tests to avoid test infrastructure issues
test_override = ""
if release_scope:
test_override = """
override_dh_auto_test:
# Skip tests for release build - tests run separately in CI
@echo "Skipping tests for release build"
"""
rules_content = f"""#!/usr/bin/make -f
%:
dh $@ --buildsystem=cmake --builddirectory=build
override_dh_clean:
dh_clean
rm -rf build
override_dh_auto_configure:
dh_auto_configure -- {cmake_opts}
override_dh_auto_build:
dh_auto_build --parallel
override_dh_auto_install:
dh_auto_install
{test_override}"""
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)
# Add RELEASE_BUILD=ON for release scope builds
deb_cmake_flags = cmake_flags
if release_scope:
if deb_cmake_flags:
deb_cmake_flags = f"{deb_cmake_flags} -DRELEASE_BUILD=ON"
else:
deb_cmake_flags = "-DRELEASE_BUILD=ON"
create_debian_rules(deb_dir, deb_cmake_flags, release_scope)
# 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()