deb-mock/deb_mock/cli.py
2025-08-03 22:16:04 +00:00

611 lines
No EOL
20 KiB
Python

#!/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()