#!/usr/bin/env python3 """ Command-line interface for deb-mock """ import click import sys import os from pathlib import Path from .core import DebMock from .config import Config from .configs import get_available_configs, load_config from .exceptions import ( DebMockError, ConfigurationError, ChrootError, SbuildError, BuildError, DependencyError, MetadataError, CacheError, PluginError, NetworkError, PermissionError, ValidationError, handle_exception, format_error_context ) @click.group() @click.version_option() @click.option('--config', '-c', type=click.Path(exists=True), help='Configuration file path') @click.option('--chroot', '-r', help='Chroot configuration name (e.g., debian-bookworm-amd64)') @click.option('--verbose', '-v', is_flag=True, help='Enable verbose output') @click.option('--debug', is_flag=True, help='Enable debug output') @click.pass_context def main(ctx, config, chroot, verbose, debug): """ Deb-Mock: A low-level utility to create clean, isolated build environments for Debian packages. This tool is a direct functional replacement for Fedora's Mock, adapted specifically for Debian-based ecosystems. """ ctx.ensure_object(dict) ctx.obj['verbose'] = verbose ctx.obj['debug'] = debug # Load configuration if config: try: ctx.obj['config'] = Config.from_file(config) except ConfigurationError as e: e.print_error() sys.exit(e.get_exit_code()) elif chroot: # Load core config by name (similar to Mock's -r option) try: config_data = load_config(chroot) ctx.obj['config'] = Config(**config_data) except ValueError as e: error = ValidationError( f"Invalid chroot configuration: {e}", field='chroot', value=chroot, expected_format='debian-suite-arch or ubuntu-suite-arch' ) error.print_error() click.echo(f"Available configs: {', '.join(get_available_configs())}") sys.exit(error.get_exit_code()) else: ctx.obj['config'] = Config.default() @main.command() @click.argument('source_package', type=click.Path(exists=True)) @click.option('--chroot', help='Chroot environment to use') @click.option('--arch', help='Target architecture') @click.option('--output-dir', '-o', type=click.Path(), help='Output directory for build artifacts') @click.option('--keep-chroot', is_flag=True, help='Keep chroot after build (for debugging)') @click.option('--no-check', is_flag=True, help='Skip running tests during build') @click.option('--offline', is_flag=True, help='Build in offline mode (no network access)') @click.option('--build-timeout', type=int, help='Build timeout in seconds') @click.option('--force-arch', help='Force target architecture') @click.option('--unique-ext', help='Unique extension for buildroot directory') @click.option('--config-dir', help='Configuration directory') @click.option('--cleanup-after', is_flag=True, help='Clean chroot after build') @click.option('--no-cleanup-after', is_flag=True, help='Don\'t clean chroot after build') @click.pass_context @handle_exception def build(ctx, source_package, chroot, arch, output_dir, keep_chroot, no_check, offline, build_timeout, force_arch, unique_ext, config_dir, cleanup_after, no_cleanup_after): """ Build a Debian source package in an isolated environment. SOURCE_PACKAGE: Path to the .dsc file or source package directory """ deb_mock = DebMock(ctx.obj['config']) # Override config with command line options if chroot: ctx.obj['config'].chroot_name = chroot if arch: ctx.obj['config'].architecture = arch if output_dir: ctx.obj['config'].output_dir = output_dir if keep_chroot: ctx.obj['config'].keep_chroot = keep_chroot if no_check: ctx.obj['config'].run_tests = False if offline: ctx.obj['config'].enable_network = False if build_timeout: ctx.obj['config'].build_timeout = build_timeout if force_arch: ctx.obj['config'].force_architecture = force_arch if unique_ext: ctx.obj['config'].unique_extension = unique_ext if config_dir: ctx.obj['config'].config_dir = config_dir if cleanup_after is not None: ctx.obj['config'].cleanup_after = cleanup_after if no_cleanup_after is not None: ctx.obj['config'].cleanup_after = not no_cleanup_after result = deb_mock.build(source_package) if ctx.obj['verbose']: click.echo(f"Build completed successfully: {result}") else: click.echo("Build completed successfully") @main.command() @click.argument('source_packages', nargs=-1, type=click.Path(exists=True)) @click.option('--chroot', help='Chroot environment to use') @click.option('--arch', help='Target architecture') @click.option('--output-dir', '-o', type=click.Path(), help='Output directory for build artifacts') @click.option('--keep-chroot', is_flag=True, help='Keep chroot after build (for debugging)') @click.option('--continue-on-failure', is_flag=True, help='Continue building remaining packages even if one fails') @click.pass_context @handle_exception def chain(ctx, source_packages, chroot, arch, output_dir, keep_chroot, continue_on_failure): """ Build a chain of packages that depend on each other. SOURCE_PACKAGES: List of .dsc files or source package directories to build in order """ if not source_packages: raise ValidationError( "No source packages specified", field='source_packages', expected_format='list of .dsc files or source directories' ) deb_mock = DebMock(ctx.obj['config']) # Override config with command line options if chroot: ctx.obj['config'].chroot_name = chroot if arch: ctx.obj['config'].architecture = arch if output_dir: ctx.obj['config'].output_dir = output_dir if keep_chroot: ctx.obj['config'].keep_chroot = keep_chroot results = deb_mock.build_chain( list(source_packages), continue_on_failure=continue_on_failure ) # Display results for result in results: if result['success']: click.echo(f"✅ {result['package']} (step {result['order']})") else: click.echo(f"❌ {result['package']} (step {result['order']}): {result['error']}") # Check if all builds succeeded failed_builds = [r for r in results if not r['success']] if failed_builds: sys.exit(1) else: click.echo("All packages built successfully") @main.command() @click.argument('chroot_name') @click.option('--arch', help='Target architecture') @click.option('--suite', help='Debian suite (e.g., bookworm, sid)') @click.option('--bootstrap', is_flag=True, help='Use bootstrap chroot for cross-distribution builds') @click.option('--bootstrap-chroot', help='Name of bootstrap chroot to use') @click.pass_context @handle_exception def init_chroot(ctx, chroot_name, arch, suite, bootstrap, bootstrap_chroot): """ Initialize a new chroot environment for building. CHROOT_NAME: Name of the chroot environment to create The --bootstrap option is useful for building packages for newer distributions on older systems (e.g., building Debian Sid packages on Debian Stable). """ deb_mock = DebMock(ctx.obj['config']) # Override config with command line options if arch: ctx.obj['config'].architecture = arch if suite: ctx.obj['config'].suite = suite if bootstrap: ctx.obj['config'].use_bootstrap_chroot = True if bootstrap_chroot: ctx.obj['config'].bootstrap_chroot_name = bootstrap_chroot deb_mock.init_chroot(chroot_name) click.echo(f"Chroot '{chroot_name}' initialized successfully") if bootstrap: click.echo("Bootstrap chroot was used for cross-distribution compatibility") @main.command() @click.argument('chroot_name') @click.pass_context @handle_exception def clean_chroot(ctx, chroot_name): """ Clean up a chroot environment. CHROOT_NAME: Name of the chroot environment to clean """ deb_mock = DebMock(ctx.obj['config']) deb_mock.clean_chroot(chroot_name) click.echo(f"Chroot '{chroot_name}' cleaned successfully") @main.command() @click.argument('chroot_name') @click.pass_context @handle_exception def scrub_chroot(ctx, chroot_name): """ Clean up a chroot environment without removing it. CHROOT_NAME: Name of the chroot environment to scrub """ deb_mock = DebMock(ctx.obj['config']) deb_mock.chroot_manager.scrub_chroot(chroot_name) click.echo(f"Chroot '{chroot_name}' scrubbed successfully") @main.command() @click.pass_context @handle_exception def scrub_all_chroots(ctx): """ Clean up all chroot environments without removing them. """ deb_mock = DebMock(ctx.obj['config']) deb_mock.chroot_manager.scrub_all_chroots() click.echo("All chroots scrubbed successfully") @main.command() @click.option('--chroot', help='Chroot environment to use') @click.option('--preserve-env', is_flag=True, help='Preserve environment variables in chroot') @click.option('--env-var', multiple=True, help='Specific environment variable to preserve') @click.pass_context @handle_exception def shell(ctx, chroot, preserve_env, env_var): """ Open a shell in the chroot environment. Use --preserve-env to preserve environment variables (addresses common environment variable issues in chroot environments). """ deb_mock = DebMock(ctx.obj['config']) chroot_name = chroot or ctx.obj['config'].chroot_name # Configure environment preservation if preserve_env: ctx.obj['config'].environment_sanitization = False if env_var: ctx.obj['config'].preserve_environment.extend(env_var) deb_mock.shell(chroot_name) @main.command() @click.argument('source_path') @click.argument('dest_path') @click.option('--chroot', help='Chroot environment to use') @click.pass_context @handle_exception def copyin(ctx, source_path, dest_path, chroot): """ Copy files from host to chroot. SOURCE_PATH: Path to file/directory on host DEST_PATH: Path in chroot where to copy """ deb_mock = DebMock(ctx.obj['config']) chroot_name = chroot or ctx.obj['config'].chroot_name deb_mock.copyin(source_path, dest_path, chroot_name) click.echo(f"Copied {source_path} to {dest_path} in chroot '{chroot_name}'") @main.command() @click.argument('source_path') @click.argument('dest_path') @click.option('--chroot', help='Chroot environment to use') @click.pass_context @handle_exception def copyout(ctx, source_path, dest_path, chroot): """ Copy files from chroot to host. SOURCE_PATH: Path to file/directory in chroot DEST_PATH: Path on host where to copy """ deb_mock = DebMock(ctx.obj['config']) chroot_name = chroot or ctx.obj['config'].chroot_name deb_mock.copyout(source_path, dest_path, chroot_name) click.echo(f"Copied {source_path} from chroot '{chroot_name}' to {dest_path}") @main.command() @click.pass_context @handle_exception def list_chroots(ctx): """ List available chroot environments. """ deb_mock = DebMock(ctx.obj['config']) chroots = deb_mock.list_chroots() if not chroots: click.echo("No chroot environments found") return click.echo("Available chroot environments:") for chroot in chroots: click.echo(f" - {chroot}") @main.command() @click.pass_context @handle_exception def list_configs(ctx): """ List available core configurations. """ from .configs import list_configs configs = list_configs() if not configs: click.echo("No core configurations found") return click.echo("Available core configurations:") for config_name, config_info in configs.items(): click.echo(f" - {config_name}: {config_info['description']}") click.echo(f" Suite: {config_info['suite']}, Arch: {config_info['architecture']}") @main.command() @click.pass_context @handle_exception def cleanup_caches(ctx): """ Clean up old cache files (similar to Mock's cache management). """ deb_mock = DebMock(ctx.obj['config']) cleaned = deb_mock.cleanup_caches() if not cleaned: click.echo("No old cache files found to clean") return click.echo("Cleaned up cache files:") for cache_type, count in cleaned.items(): if count > 0: click.echo(f" - {cache_type}: {count} files") @main.command() @click.pass_context @handle_exception def cache_stats(ctx): """ Show cache statistics. """ deb_mock = DebMock(ctx.obj['config']) stats = deb_mock.get_cache_stats() if not stats: click.echo("No cache statistics available") return click.echo("Cache Statistics:") for cache_type, cache_stats in stats.items(): click.echo(f" - {cache_type}:") if isinstance(cache_stats, dict): for key, value in cache_stats.items(): click.echo(f" {key}: {value}") else: click.echo(f" {cache_stats}") @main.command() @click.pass_context @handle_exception def config(ctx): """ Show current configuration. """ config = ctx.obj['config'] click.echo("Current configuration:") click.echo(f" Chroot name: {config.chroot_name}") click.echo(f" Architecture: {config.architecture}") click.echo(f" Suite: {config.suite}") click.echo(f" Output directory: {config.output_dir}") click.echo(f" Keep chroot: {config.keep_chroot}") click.echo(f" Use root cache: {config.use_root_cache}") click.echo(f" Use ccache: {config.use_ccache}") click.echo(f" Parallel jobs: {config.parallel_jobs}") @main.command() @click.argument('source_package', type=click.Path(exists=True)) @click.option('--chroot', help='Chroot environment to use') @click.option('--arch', help='Target architecture') @click.pass_context @handle_exception def install_deps(ctx, source_package, chroot, arch): """ Install build dependencies for a Debian source package. SOURCE_PACKAGE: Path to the .dsc file or source package directory """ deb_mock = DebMock(ctx.obj['config']) # Override config with command line options if chroot: ctx.obj['config'].chroot_name = chroot if arch: ctx.obj['config'].architecture = arch result = deb_mock.install_dependencies(source_package) if ctx.obj['verbose']: click.echo(f"Dependencies installed successfully: {result}") else: click.echo("Dependencies installed successfully") @main.command() @click.argument('packages', nargs=-1, required=True) @click.option('--chroot', help='Chroot environment to use') @click.option('--arch', help='Target architecture') @click.pass_context @handle_exception def install(ctx, packages, chroot, arch): """ Install packages in the chroot environment. PACKAGES: List of packages to install """ deb_mock = DebMock(ctx.obj['config']) # Override config with command line options if chroot: ctx.obj['config'].chroot_name = chroot if arch: ctx.obj['config'].architecture = arch result = deb_mock.install_packages(packages) if ctx.obj['verbose']: click.echo(f"Packages installed successfully: {result}") else: click.echo(f"Packages installed successfully: {', '.join(packages)}") @main.command() @click.argument('packages', nargs=-1) @click.option('--chroot', help='Chroot environment to use') @click.option('--arch', help='Target architecture') @click.pass_context @handle_exception def update(ctx, packages, chroot, arch): """ Update packages in the chroot environment. PACKAGES: List of packages to update (if empty, update all) """ deb_mock = DebMock(ctx.obj['config']) # Override config with command line options if chroot: ctx.obj['config'].chroot_name = chroot if arch: ctx.obj['config'].architecture = arch result = deb_mock.update_packages(packages) if ctx.obj['verbose']: click.echo(f"Packages updated successfully: {result}") else: if packages: click.echo(f"Packages updated successfully: {', '.join(packages)}") else: click.echo("All packages updated successfully") @main.command() @click.argument('packages', nargs=-1, required=True) @click.option('--chroot', help='Chroot environment to use') @click.option('--arch', help='Target architecture') @click.pass_context @handle_exception def remove(ctx, packages, chroot, arch): """ Remove packages from the chroot environment. PACKAGES: List of packages to remove """ deb_mock = DebMock(ctx.obj['config']) # Override config with command line options if chroot: ctx.obj['config'].chroot_name = chroot if arch: ctx.obj['config'].architecture = arch result = deb_mock.remove_packages(packages) if ctx.obj['verbose']: click.echo(f"Packages removed successfully: {result}") else: click.echo(f"Packages removed successfully: {', '.join(packages)}") @main.command() @click.argument('command') @click.option('--chroot', help='Chroot environment to use') @click.option('--arch', help='Target architecture') @click.pass_context @handle_exception def apt_cmd(ctx, command, chroot, arch): """ Execute APT command in the chroot environment. COMMAND: APT command to execute (e.g., "update", "install package") """ deb_mock = DebMock(ctx.obj['config']) # Override config with command line options if chroot: ctx.obj['config'].chroot_name = chroot if arch: ctx.obj['config'].architecture = arch result = deb_mock.execute_apt_command(command) if ctx.obj['verbose']: click.echo(f"APT command executed successfully: {result}") else: click.echo(f"APT command executed: {command}") @main.command() @click.option('--expand', is_flag=True, help='Show expanded configuration values') @click.pass_context @handle_exception def debug_config(ctx, expand): """ Show detailed configuration information for debugging. """ config = ctx.obj['config'] if expand: # Show expanded configuration (with template values resolved) click.echo("Expanded Configuration:") config_dict = config.to_dict() for key, value in config_dict.items(): click.echo(f" {key}: {value}") else: # Show configuration with template placeholders click.echo("Configuration (with templates):") click.echo(f" chroot_name: {config.chroot_name}") click.echo(f" architecture: {config.architecture}") click.echo(f" suite: {config.suite}") click.echo(f" basedir: {config.basedir}") click.echo(f" output_dir: {config.output_dir}") click.echo(f" chroot_dir: {config.chroot_dir}") click.echo(f" cache_dir: {config.cache_dir}") click.echo(f" chroot_home: {config.chroot_home}") # Show plugin configuration if hasattr(config, 'plugins') and config.plugins: click.echo(" plugins:") for plugin_name, plugin_config in config.plugins.items(): click.echo(f" {plugin_name}: {plugin_config}") if __name__ == '__main__': main()