Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/localci/cli/run/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""``localci run`` command package."""

from localci.cli.run.cli import run
from localci.cli.run.patcher import _write_patched_workflow

__all__ = ["run", "_write_patched_workflow"]
60 changes: 60 additions & 0 deletions cli/localci/cli/run/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Click entry point for ``localci run`` (options in ``click_options``, logic in ``run_flow``)."""

from __future__ import annotations

from pathlib import Path

import click

from localci.cli.run.click_options import run_options
from localci.cli.run.container import build_run_container
from localci.cli.run.params import RunOptions
from localci.cli.run.run_flow import execute_run


@click.command()
@run_options
def run(
ctx: click.Context,
workflow: str | None,
jobs: tuple[str, ...],
platform: str | None,
compiler: str | None,
matrix_filters: tuple[str, ...],
parallel: int | None,
timeout: int | None,
dry_run: bool,
no_cache: bool,
cache_dir: Path | None,
rebuild_image: bool,
keep_containers: bool | None,
interactive: bool,
verbose: bool,
github_token: str | None,
offline: bool,
) -> None:
"""Execute selected jobs locally with parallel execution."""
exit_code = execute_run(
cfg=ctx.obj["config"],
options=RunOptions(
workflow=workflow,
jobs=jobs,
platform=platform,
compiler=compiler,
matrix_filters=matrix_filters,
parallel=parallel,
timeout=timeout,
dry_run=dry_run,
no_cache=no_cache,
cache_dir=cache_dir,
rebuild_image=rebuild_image,
keep_containers=keep_containers,
interactive=interactive,
verbose=verbose,
github_token=github_token,
offline=offline,
),
deps=build_run_container(),
)
if exit_code:
ctx.exit(exit_code)
106 changes: 106 additions & 0 deletions cli/localci/cli/run/click_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Click option decorators for ``localci run`` (keeps ``cli.py`` entry ≤50 lines)."""

from __future__ import annotations

from pathlib import Path
from typing import Any, Callable, TypeVar

import click

F = TypeVar("F", bound=Callable[..., Any])


def run_options(command: F) -> F:
"""Apply all ``localci run`` CLI options to *command*."""
opts: list[Callable[[F], F]] = [
click.option(
"--workflow",
"-w",
type=click.Path(exists=True),
default=None,
help="Workflow file (defaults to config value).",
),
click.option(
"--job",
"-j",
"jobs",
multiple=True,
help="Job index or name (can specify multiple).",
),
click.option(
"--platform",
"-p",
type=click.Choice(["linux", "windows", "macos"]),
default=None,
help="Run all jobs for a platform.",
),
click.option("--compiler", type=str, default=None, help="Filter by compiler."),
click.option(
"--matrix",
"-m",
"matrix_filters",
multiple=True,
help="Matrix filter as key=value (repeatable).",
),
click.option(
"--parallel",
type=int,
default=None,
help="Max parallel jobs (overrides config).",
),
click.option(
"--timeout",
type=int,
default=None,
help="Job timeout in seconds (overrides config).",
),
click.option(
"--dry-run", is_flag=True, help="Preview execution plan without running."
),
click.option(
"--no-cache",
is_flag=True,
help="Disable build caching (ccache, boost, b2-source, cmake).",
),
click.option(
"--cache-dir",
type=click.Path(path_type=Path, file_okay=False),
default=None,
help="Override cache root directory (default: config cache.directory).",
),
click.option(
"--rebuild-image", is_flag=True, help="Force rebuild Docker image."
),
click.option(
"--keep-containers/--no-keep-containers",
"keep_containers",
default=None,
help="Keep containers after execution (default: from config).",
),
click.option(
"--interactive", "-i", is_flag=True, help="Interactive job selection."
),
click.option(
"--verbose", "-v", is_flag=True, help="Show verbose act output."
),
click.option(
"--github-token",
"-t",
"github_token",
type=str,
default=None,
help="GitHub token for API access (or set GITHUB_TOKEN env var).",
),
click.option(
"--offline",
is_flag=True,
help=(
"Run in offline mode (no action downloads, requires pre-cached actions)."
),
),
click.pass_context,
]
wrapped: F = command
for opt in reversed(opts):
wrapped = opt(wrapped) # type: ignore[assignment]
return wrapped
53 changes: 53 additions & 0 deletions cli/localci/cli/run/container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Dependency injection container for ``localci run``."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Callable

from localci.core.boost_cache import ensure_boost_cache
from localci.core.ccache_stats import get_ccache_stats
from localci.core.config import LocalCIConfig, resolve_cache_paths
from localci.core.executor import JobExecutor
from localci.core.orchestrator import OrchestratorConfig, ParallelExecutionManager
from localci.core.progress import ProgressTracker
from localci.core.queue import PriorityConfig
from localci.core.queue_builder import QueueBuilder
from localci.core.workflow import WorkflowAnalyzer


@dataclass
class RunDependencies:
"""Injectable collaborators for ``execute_run`` (pass via ``deps=`` in tests)."""

workflow_analyzer: WorkflowAnalyzer
queue_builder_factory: Callable[..., QueueBuilder]
job_executor_factory: Callable[[Path], JobExecutor]
parallel_manager_factory: Callable[..., ParallelExecutionManager]
progress_tracker_factory: Callable[..., ProgressTracker]
orchestrator_config_factory: Callable[[LocalCIConfig], OrchestratorConfig]
priority_config_factory: Callable[[LocalCIConfig], PriorityConfig]
ensure_boost_cache_fn: Callable[..., bool]
get_ccache_stats_fn: Callable[..., str | None]
resolve_cache_paths_fn: Callable[..., object]
workflow_patcher: Callable[..., Path]


def build_run_container() -> RunDependencies:
"""Construct the default production dependency graph for ``localci run``."""
from localci.cli.run.patcher import _write_patched_workflow

return RunDependencies(
workflow_analyzer=WorkflowAnalyzer(),
queue_builder_factory=QueueBuilder,
job_executor_factory=JobExecutor,
parallel_manager_factory=ParallelExecutionManager,
progress_tracker_factory=ProgressTracker,
orchestrator_config_factory=OrchestratorConfig.from_config,
priority_config_factory=PriorityConfig.from_config,
ensure_boost_cache_fn=ensure_boost_cache,
get_ccache_stats_fn=get_ccache_stats,
resolve_cache_paths_fn=resolve_cache_paths,
workflow_patcher=_write_patched_workflow,
)
28 changes: 28 additions & 0 deletions cli/localci/cli/run/params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""CLI option bundle for ``localci run``."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class RunOptions:
"""Normalized options passed from the Click entry point to orchestration."""

workflow: str | None
jobs: tuple[str, ...]
platform: str | None
compiler: str | None
matrix_filters: tuple[str, ...]
parallel: int | None
timeout: int | None
dry_run: bool
no_cache: bool
cache_dir: Path | None
rebuild_image: bool
keep_containers: bool | None
interactive: bool
verbose: bool
github_token: str | None
offline: bool
Loading
Loading