- Fix environment variable handling in sbuild wrapper - Remove unsupported --log-dir and --env options from sbuild command - Clean up unused imports and fix linting issues - Organize examples directory with official Debian hello package - Fix YAML formatting (trailing spaces, newlines) - Remove placeholder example files - All tests passing (30/30) - Successfully tested build with official Debian hello package
618 lines
19 KiB
Python
618 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Command-line interface for deb-mock
|
|
"""
|
|
|
|
import sys
|
|
|
|
import click
|
|
|
|
from .config import Config
|
|
from .configs import get_available_configs, load_config
|
|
from .core import DebMock
|
|
from .exceptions import ConfigurationError, ValidationError, handle_exception
|
|
|
|
|
|
@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()
|