#!/usr/bin/python3 # -*- coding: utf-8 -*- """ DEB package builder for Ubuntu 26.04 - Qt6 Release Creates 5 separate DEB packages as per customer requirements: 1. qt6-release-runtime - binaries and libraries from S 2. qt6-release-sources - Qt6 C++ sources from S 3. qt6-release-ctest-runtime - CTest executables 4. qt6-release-ctest-sources - CTest source files 5. qt6-release-build-pipeline - Dockerfiles, build scripts, CI/CD files """ 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 remove_directory(path): """Remove directory tree""" if os.path.exists(path): shutil.rmtree(path) def copy_file_to_dest_dir(src_file, dest_dir): """Copy a single file to destination directory""" os.makedirs(dest_dir, exist_ok=True) shutil.copy2(src_file, dest_dir) def is_dds_file(filepath): """Check if a file or directory path contains DDS-related content that should be excluded Args: filepath: Path to check Returns: True if file should be excluded (is DDS-related), False otherwise """ # Convert to lowercase for case-insensitive matching path_lower = filepath.lower() # Exclude files/directories with 'dds' in the name if 'dds' in path_lower: return True return False def copy_src_dir_to_dest_dir(src_dir, dest_dir, release_scope=False, file_filter=None): """Copy source directory to destination with optional filtering Args: src_dir: Source directory dest_dir: Destination directory release_scope: If True, only copy release scope directories file_filter: Optional function to filter files (filepath) -> bool """ os.makedirs(dest_dir, exist_ok=True) # Release scope directories release_dirs = [ "TetraIF", "Z/src", "Lts/Common" ] if release_scope: info(f"Copying only release scope directories: {release_dirs}") # Also copy root-level build files needed for CMake root_build_files = ['CMakeLists.txt', 'CTestCustom.ctest'] root_build_dirs = ['cmake'] for bf in root_build_files: src_file = os.path.join(src_dir, bf) if os.path.exists(src_file): debug(f" Copying root file {bf}...") shutil.copy2(src_file, os.path.join(dest_dir, bf)) for bd in root_build_dirs: src_bdir = os.path.join(src_dir, bd) dest_bdir = os.path.join(dest_dir, bd) if os.path.exists(src_bdir): debug(f" Copying root directory {bd}...") shutil.copytree(src_bdir, dest_bdir, dirs_exist_ok=True) for rel_dir in release_dirs: src_subdir = os.path.join(src_dir, rel_dir) dest_subdir = os.path.join(dest_dir, rel_dir) if os.path.exists(src_subdir): debug(f" Copying {rel_dir}...") for root, dirs, files in os.walk(src_subdir): # Skip build directories, hidden directories, deb_build_tmp (prevent recursion), and DDS-related directories dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['build', 'cmake-build-debug', 'cmake-build-release', 'deb_build_tmp'] and (file_filter is None or not file_filter(os.path.join(root, d)))] for file in files: src_file = os.path.join(root, file) rel_path = os.path.relpath(src_file, src_subdir) dest_file = os.path.join(dest_subdir, rel_path) # Apply file filter if provided (skip file if filter returns True) if file_filter and file_filter(src_file): continue os.makedirs(os.path.dirname(dest_file), exist_ok=True) shutil.copy2(src_file, dest_file) else: # Copy entire source tree for root, dirs, files in os.walk(src_dir): # Skip build directories, hidden directories, deb_build_tmp (prevent recursion), and DDS-related directories dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['build', 'cmake-build-debug', 'cmake-build-release', 'deb_build_tmp'] and (file_filter is None or not file_filter(os.path.join(root, d)))] for file in files: src_file = os.path.join(root, file) rel_path = os.path.relpath(src_file, src_dir) dest_file = os.path.join(dest_dir, rel_path) # Apply file filter if provided (skip file if filter returns True) if file_filter and file_filter(src_file): continue os.makedirs(os.path.dirname(dest_file), exist_ok=True) shutil.copy2(src_file, dest_file) def find_test_executables(pkg_dir, release_dirs): """Find test executables from release scope modules in build4/bin/ Args: pkg_dir: Package staging directory release_dirs: List of release scope directory paths Returns: List of (executable_path, module_path) tuples for tests in release scope """ test_executables = [] build_bin_dir = os.path.join(pkg_dir, "build4", "bin") if not os.path.exists(build_bin_dir): warning(f"Build bin directory not found: {build_bin_dir}") return test_executables # Get all executable files in build4/bin/ for filename in os.listdir(build_bin_dir): file_path = os.path.join(build_bin_dir, filename) if not os.path.isfile(file_path) or not os.access(file_path, os.X_OK): continue # Skip non-test executables (common patterns) if not ('test' in filename.lower() or 'Test' in filename): continue # Find which module this test belongs to by searching CMakeLists.txt module_path = find_test_module_source(pkg_dir, filename, release_dirs) if module_path: test_executables.append((file_path, module_path)) debug(f" Found test executable: {filename} from {module_path}") else: debug(f" Skipping test executable (no module found): {filename}") return test_executables def find_test_module_source(pkg_dir, test_name, release_dirs): """Find the source module directory for a test executable Args: pkg_dir: Package staging directory test_name: Name of the test executable release_dirs: List of release scope directory paths Returns: Relative path to module directory, or None if not found """ # Search for CMakeLists.txt files that define this test for rel_dir in release_dirs: module_dir = os.path.join(pkg_dir, rel_dir) if not os.path.exists(module_dir): continue for root, dirs, files in os.walk(module_dir): # Skip build directories dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['build', 'cmake-build-debug', 'cmake-build-release']] if 'CMakeLists.txt' in files: cmake_file = os.path.join(root, 'CMakeLists.txt') try: with open(cmake_file, 'r') as f: content = f.read() # Look for ADD_EXECUTABLE or ADD_TEST with this test name if f'ADD_EXECUTABLE({test_name}' in content or f'ADD_TEST(NAME {test_name}' in content: # Return the module directory relative to pkg_dir return os.path.relpath(root, pkg_dir) except Exception: pass return None def find_test_sources(pkg_dir, release_dirs): """Find test source files in release scope modules Args: pkg_dir: Package staging directory release_dirs: List of release scope directory paths Returns: List of relative paths to test source files """ test_sources = [] test_patterns = ['*test*.cpp', '*test*.h', '*Test*.cpp', '*Test*.h', 'test_*.cpp', 'test_*.h'] for rel_dir in release_dirs: module_dir = os.path.join(pkg_dir, rel_dir) if not os.path.exists(module_dir): continue for root, dirs, files in os.walk(module_dir): # Skip build directories and hidden directories dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['build', 'cmake-build-debug', 'cmake-build-release']] for filename in files: # Check if this is a test file if 'test' in filename.lower() and (filename.endswith('.cpp') or filename.endswith('.h')): file_path = os.path.join(root, filename) rel_path = os.path.relpath(file_path, pkg_dir) test_sources.append(rel_path) debug(f" Found test source: {rel_path}") return test_sources def create_debian_control(deb_dir, package_name, version, build_number, description, depends="", section="devel"): """Create debian/control file for a package""" control_content = f"""Source: {package_name} Section: {section} Priority: optional Maintainer: Legatus Team Build-Depends: debhelper (>= 13), cmake, qt6-base-dev, qt6-tools-dev Standards-Version: 4.6.0 Package: {package_name} Architecture: any Depends: ${{shlibs:Depends}}, ${{misc:Depends}}{", " + depends if depends else ""} Description: {description} Legatus Qt6 Release Package This package is part of the Legatus Qt6 migration release. """ 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, package_name, version, build_number): """Create debian/changelog file""" # Use +0000 as timezone offset (UTC) for reproducibility from email.utils import formatdate timestamp = formatdate(localtime=True) changelog_content = f"""{package_name} ({version}-{build_number}) unstable; urgency=medium * Qt6 Release Build -- 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, cmake_flags="", skip_tests=False): """Create debian/rules file""" test_override = "" if skip_tests: test_override = """ override_dh_auto_test: # Tests skipped for this package type """ rules_content = f"""#!/usr/bin/make -f %: dh $@ --buildsystem=cmake override_dh_auto_configure: dh_auto_configure -- -DCMAKE_BUILD_TYPE=Release {cmake_flags} override_dh_auto_build: dh_auto_build --parallel override_dh_auto_install: dh_auto_install --destdir=debian/tmp {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 create_debian_rules_nobuild(deb_dir, skip_shlibdeps=False): """Create debian/rules file for packages that don't need building (pre-built files only) Args: deb_dir: Path to debian directory skip_shlibdeps: If True, skip dh_shlibdeps (useful for test executables with external deps) """ rules_content = """#!/usr/bin/make -f %: dh $@ override_dh_auto_build: # No build step needed - pre-built files only override_dh_auto_configure: # No configure step needed - pre-built files only override_dh_auto_install: dh_auto_install """ if skip_shlibdeps: rules_content += """ override_dh_shlibdeps: # Skip shlibdeps for test executables - Qt6 deps are external system packages """ 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}") 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 create_debian_install(deb_dir, install_lines): """Create debian/install file from list of file entries""" install_file = os.path.join(deb_dir, "install") with open(install_file, "w") as f: for line in install_lines: f.write(line + "\n") info(f"Created {install_file} with {len(install_lines)} entries") def create_manifest(output_dir, package_name, pkg_dir): """Create manifest file listing all package contents from actual DEB file""" manifest_file = os.path.join(output_dir, "manifests", f"{package_name}.manifest") os.makedirs(os.path.dirname(manifest_file), exist_ok=True) # Find the DEB file for this package deb_file = None for f in os.listdir(output_dir): if f.startswith(package_name) and f.endswith(".deb"): deb_file = os.path.join(output_dir, f) break manifest_lines = [] if deb_file and os.path.exists(deb_file): # Extract DEB contents using dpkg-deb result = subprocess.run( ["dpkg-deb", "--contents", deb_file], capture_output=True, text=True ) if result.returncode == 0: for line in result.stdout.strip().split("\n"): if line.strip(): # dpkg-deb --contents output format: -rw-r--r-- root/root 1234 2024-01-01 00:00 ./path/to/file # Extract the path (last field) parts = line.split() if len(parts) >= 6: file_path = parts[-1] # Remove leading ./ or / if file_path.startswith("./"): file_path = file_path[2:] elif file_path.startswith("/"): file_path = file_path[1:] if file_path: manifest_lines.append(file_path) elif os.path.exists(pkg_dir): # Fallback: scan directory if DEB not found info(f"Warning: DEB file not found for {package_name}, scanning directory instead") for root, dirs, files in os.walk(pkg_dir): for file in files: file_path = os.path.join(root, file) rel_path = os.path.relpath(file_path, pkg_dir) manifest_lines.append(rel_path) manifest_lines.sort() with open(manifest_file, "w") as f: for line in manifest_lines: f.write(line + "\n") info(f"Created manifest: {manifest_file} ({len(manifest_lines)} entries)") return manifest_lines def validate_package_contents(package_name, manifest_lines, allowed_patterns, forbidden_patterns=None): """Validate package contents against allowlist/forbidden list""" import fnmatch errors = [] warnings = [] for file_path in manifest_lines: # Check forbidden patterns if forbidden_patterns: for pattern in forbidden_patterns: if fnmatch.fnmatch(file_path, pattern): errors.append(f"FORBIDDEN file in {package_name}: {file_path} (matches {pattern})") # Check if file matches any allowed pattern allowed = False if allowed_patterns: for pattern in allowed_patterns: if fnmatch.fnmatch(file_path, pattern): allowed = True break if not allowed and allowed_patterns: warnings.append(f"Unexpected file in {package_name}: {file_path}") if errors: for err in errors: error(f"Validation error: {err}") return False if warnings: for warn in warnings: warning(f"Validation warning: {warn}") return True def build_single_deb(pkg_name, pkg_dir, output_dir, version, build_number, cmake_flags, skip_tests, install_lines, description, section="devel", depends=""): """Build a single DEB package""" info(f"Building DEB package: {pkg_name}") deb_dir = os.path.join(pkg_dir, "debian") os.makedirs(deb_dir) create_debian_control(deb_dir, pkg_name, version, build_number, description, depends, section) create_debian_changelog(deb_dir, pkg_name, version, build_number) create_debian_rules(deb_dir, cmake_flags, skip_tests) # 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 with open(os.path.join(deb_dir, "compat"), "w") as f: f.write("13\n") # Create debian/install if install_lines: create_debian_install(deb_dir, install_lines) # Build the package info(f"Running debuild for {pkg_name}...") os.chdir(pkg_dir) 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, capture_output=True, text=True) if ret.returncode != 0: print(ret.stdout) print(ret.stderr) die(f"debuild failed for {pkg_name} 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}") return True def build_single_deb_nobuild(pkg_name, pkg_dir, output_dir, version, build_number, install_lines, description, section="devel", depends="", skip_shlibdeps=False): """Build a single DEB package without running cmake/make (pre-built files only) Args: skip_shlibdeps: If True, skip dh_shlibdeps (useful for test executables with external deps) """ info(f"Building DEB package (no-build): {pkg_name}") deb_dir = os.path.join(pkg_dir, "debian") os.makedirs(deb_dir) create_debian_control(deb_dir, pkg_name, version, build_number, description, depends, section) create_debian_changelog(deb_dir, pkg_name, version, build_number) create_debian_rules_nobuild(deb_dir, skip_shlibdeps=skip_shlibdeps) # 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 with open(os.path.join(deb_dir, "compat"), "w") as f: f.write("13\n") # Create debian/install if install_lines: create_debian_install(deb_dir, install_lines) # Build the package info(f"Running debuild for {pkg_name}...") os.chdir(pkg_dir) 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, capture_output=True, text=True) if ret.returncode != 0: print(ret.stdout) print(ret.stderr) die(f"debuild failed for {pkg_name} 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}") return True def build_all_debs(source_dir, output_dir, project_name, version, build_number, cmake_flags, release_scope=True): """Build all 5 required DEB packages - build once, then package""" info("========================================") info("Building 5 DEB packages for Qt6 Release") info("========================================") os.makedirs(output_dir, exist_ok=True) os.makedirs(os.path.join(output_dir, "manifests"), exist_ok=True) build_base = os.path.join(source_dir, "deb_build_tmp") remove_directory(build_base) os.makedirs(build_base) # Release scope directories release_dirs = ["TetraIF", "Z/src", "Lts/Common"] # Source file extensions source_extensions = ['.cpp', '.h', '.hpp', '.cc', '.cxx', '.hxx'] script_extensions = ['.py', '.sh', '.cmake', '.txt'] # ============================================ # STEP 1: Build once from full source tree # ============================================ info("\n=== Step 1: Building from full source tree ===") build_dir = os.path.join(source_dir, "build") os.makedirs(build_dir, exist_ok=True) os.chdir(build_dir) # Configure info("Running CMake configuration...") cmake_cmd = f"cmake -DCMAKE_BUILD_TYPE=Release {cmake_flags} .." ret = subprocess.run(cmake_cmd, shell=True, capture_output=True, text=True) if ret.returncode != 0: print(ret.stdout) print(ret.stderr) die("CMake configuration failed") # Build info("Running build...") build_cmd = f"make -j$(nproc)" ret = subprocess.run(build_cmd, shell=True, capture_output=True, text=True) if ret.returncode != 0: print(ret.stdout) print(ret.stderr) die("Build failed") info("Build completed successfully") # ============================================ # Package 1: qt6-release-runtime # ============================================ info("\n=== Building qt6-release-runtime ===") pkg1_name = f"qt6-release-runtime" pkg1_dir = os.path.join(build_base, f"{pkg1_name}-{version}") os.makedirs(pkg1_dir) # Copy FULL source tree (needed for debian/rules cmake reference) copy_src_dir_to_dest_dir(source_dir, pkg1_dir, release_scope=False) # Install lines for runtime: binaries and libraries from release scope only # CMake install uses component prefixes: ALL->(none), AAR/SV/HLA->SV/, WS->WS/ install_lines_1 = [] # Install from all possible component install paths # ALL component: bin/, lib/ # AAR/SV/HLA components: SV/bin/, SV/lib/ # WS component: WS/bin/, WS/lib/ install_lines_1.append("usr/bin/* usr/bin/") install_lines_1.append("usr/lib/*.so* usr/lib/") install_lines_1.append("usr/SV/bin/* usr/bin/") install_lines_1.append("usr/SV/lib/*.so* usr/lib/") install_lines_1.append("usr/WS/bin/* usr/bin/") install_lines_1.append("usr/WS/lib/*.so* usr/lib/") build_single_deb( pkg_name=pkg1_name, pkg_dir=pkg1_dir, output_dir=output_dir, version=version, build_number=build_number, cmake_flags=cmake_flags, skip_tests=True, install_lines=install_lines_1, description="Qt6 Release Runtime - binaries and libraries from release scope S", section="libs" ) # Create manifest manifest_1 = create_manifest(output_dir, pkg1_name, pkg1_dir) validate_package_contents(pkg1_name, manifest_1, allowed_patterns=["usr/bin/*", "usr/lib/*", "usr/SV/*", "usr/WS/*", "debian/*"], forbidden_patterns=["*.cpp", "*.h", "*.hpp", "tests/*", "jenkins/*", "Dockerfile*"]) # ============================================ # Package 2: qt6-release-sources # ============================================ info("\n=== Building qt6-release-sources ===") pkg2_name = f"qt6-release-sources" pkg2_dir = os.path.join(build_base, f"{pkg2_name}-{version}") os.makedirs(pkg2_dir) # Copy FULL source tree (excluding DDS-related files) copy_src_dir_to_dest_dir(source_dir, pkg2_dir, release_scope=False, file_filter=is_dds_file) # Install lines for sources: only from release scope directories install_lines_2 = [] for rel_dir in release_dirs: src_dir = os.path.join(pkg2_dir, rel_dir) if os.path.exists(src_dir): for root, dirs, files in os.walk(src_dir): dirs[:] = [d for d in dirs if not d.startswith('.')] for file in files: ext = os.path.splitext(file)[1].lower() if ext in source_extensions or file == "CMakeLists.txt": rel_path = os.path.relpath(os.path.join(root, file), pkg2_dir) dest_dir = os.path.join("usr/src", os.path.dirname(rel_path)) install_lines_2.append(f"{rel_path} {dest_dir}/") # Use no-build rules for source package build_single_deb_nobuild( pkg_name=pkg2_name, pkg_dir=pkg2_dir, output_dir=output_dir, version=version, build_number=build_number, install_lines=install_lines_2, description="Qt6 Release Sources - C++ source files from release scope S", section="devel" ) manifest_2 = create_manifest(output_dir, pkg2_name, pkg2_dir) validate_package_contents(pkg2_name, manifest_2, allowed_patterns=["usr/src/**/*.cpp", "usr/src/**/*.h", "usr/src/**/CMakeLists.txt", "debian/*"], forbidden_patterns=["build/*", "tests/*", "jenkins/*"]) # ============================================ # Package 3: qt6-release-ctest-runtime # ============================================ info("\n=== Building qt6-release-ctest-runtime ===") pkg3_name = f"qt6-release-ctest-runtime" pkg3_dir = os.path.join(build_base, f"{pkg3_name}-{version}") os.makedirs(pkg3_dir) # Copy FULL source tree copy_src_dir_to_dest_dir(source_dir, pkg3_dir, release_scope=False) # Install lines for CTest runtime: test executables from release scope only install_lines_3 = [] # Find test executables in build4/bin/ and map to release scope modules test_executables = find_test_executables(pkg3_dir, release_dirs) for exe_path, module_path in test_executables: rel_path = os.path.relpath(exe_path, pkg3_dir) dest_dir = "usr/lib/ctest" install_lines_3.append(f"{rel_path} {dest_dir}/") info(f" Including test executable: {os.path.basename(exe_path)} from {module_path}") # Also include CTest configuration files ctest_files = [ "build4/CTestTestfile.cmake", "build4/DartConfiguration.tcl", "build4/Testing/TAG", ] for ctest_file in ctest_files: if os.path.exists(os.path.join(pkg3_dir, ctest_file)): install_lines_3.append(f"{ctest_file} usr/lib/ctest/") build_single_deb_nobuild( pkg_name=pkg3_name, pkg_dir=pkg3_dir, output_dir=output_dir, version=version, build_number=build_number, install_lines=install_lines_3, description="Qt6 Release CTest Runtime - executable test programs from release scope", section="devel", skip_shlibdeps=True ) manifest_3 = create_manifest(output_dir, pkg3_name, pkg3_dir) validate_package_contents(pkg3_name, manifest_3, allowed_patterns=["usr/lib/ctest/*", "debian/*"], forbidden_patterns=["*.cpp", "*.h", "jenkins/*"]) # ============================================ # Package 4: qt6-release-ctest-sources # ============================================ info("\n=== Building qt6-release-ctest-sources ===") pkg4_name = f"qt6-release-ctest-sources" pkg4_dir = os.path.join(build_base, f"{pkg4_name}-{version}") os.makedirs(pkg4_dir) # Copy FULL source tree copy_src_dir_to_dest_dir(source_dir, pkg4_dir, release_scope=False) # Install lines for test sources from release scope only install_lines_4 = [] # Find test source files in release scope modules test_sources = find_test_sources(pkg4_dir, release_dirs) for rel_path in test_sources: dest_dir = os.path.dirname(rel_path) install_lines_4.append(f"{rel_path} {dest_dir}/") build_single_deb_nobuild( pkg_name=pkg4_name, pkg_dir=pkg4_dir, output_dir=output_dir, version=version, build_number=build_number, install_lines=install_lines_4, description="Qt6 Release CTest Sources - test source files from release scope", section="devel" ) manifest_4 = create_manifest(output_dir, pkg4_name, pkg4_dir) validate_package_contents(pkg4_name, manifest_4, allowed_patterns=["usr/src/**/tests/*", "debian/*"], forbidden_patterns=["build/*", "jenkins/*"]) # ============================================ # Package 5: qt6-release-build-pipeline # ============================================ info("\n=== Building qt6-release-build-pipeline ===") pkg5_name = f"qt6-release-build-pipeline" pkg5_dir = os.path.join(build_base, f"{pkg5_name}-{version}") os.makedirs(pkg5_dir) # Copy FULL source tree copy_src_dir_to_dest_dir(source_dir, pkg5_dir, release_scope=False) # Install lines for pipeline files only install_lines_5 = [] pipeline_files = [ "new_docker_build.sh", "Dockerfile", "Dockerfile.ubuntu26", "jenkins-ci-rme/release-16-10-2026.sh", "rpmbuild/make_rpm_ubuntu26.py", "CMakeLists.txt", "cmake/FindGeographicLib.cmake", "cmake/FindRTI.cmake", "cmake/FindZLIB.cmake", ] for pipeline_file in pipeline_files: src_file = os.path.join(pkg5_dir, pipeline_file) if os.path.exists(src_file): dest_dir = os.path.join("usr/src/pipeline", os.path.dirname(pipeline_file)) install_lines_5.append(f"{pipeline_file} {dest_dir}/") info(f" Added pipeline file: {pipeline_file}") build_single_deb_nobuild( pkg_name=pkg5_name, pkg_dir=pkg5_dir, output_dir=output_dir, version=version, build_number=build_number, install_lines=install_lines_5, description="Qt6 Release Build Pipeline - Docker, Jenkins, CMake, and packaging scripts", section="devel" ) manifest_5 = create_manifest(output_dir, pkg5_name, pkg5_dir) validate_package_contents(pkg5_name, manifest_5, allowed_patterns=["usr/src/pipeline/*", "debian/*"], forbidden_patterns=["*.cpp", "*.h", "tests/*"]) # Cleanup os.chdir(source_dir) remove_directory(build_base) info("\n========================================") info(f"DEB package build completed: {output_dir}") info("Generated packages:") for f in os.listdir(output_dir): if f.endswith(".deb"): info(f" - {f}") info("========================================") def main(): parser = argparse.ArgumentParser(description="Build DEB packages for Legatus Qt6 Release") 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") 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}") source_dir = args.source_dir info(f"Source directory: {source_dir}") build_all_debs( 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 ) if __name__ == "__main__": main()