cabinpkg 0.17.0

A package manager and build system for C/C++
Documentation
//! Glue layer for `cabin explain`.
//!
//! Package, target, source, and feature subcommands map onto
//! the typed explanation model in `cabin-explain`.  The
//! orchestration layer here is responsible for loading the
//! workspace + lockfile + active patches + source-replacement
//! table + (for `build-config`) the full profile / toolchain /
//! build-config preamble, then handing typed inputs to the
//! owning crates.

use std::path::PathBuf;

use anyhow::Result;
use clap::{Args, Subcommand};

use crate::cli::{
    ConfigSelectionArgs, ResolveFormat, ToolchainSelectionArgs, WorkspaceSelectionArgs,
    build_selection_request, build_workspace_selection, compiler_wrapper_override_from_args,
    compute_feature_resolution, lockfile_path_for, profile_selection_for_metadata,
    read_optional_lockfile, resolve_build_configurations, resolve_invocation_manifest,
    toolchain_selection_from_args, workspace_compiler_wrapper_settings,
    workspace_profile_definitions,
};

#[derive(Debug, Args)]
pub(crate) struct ExplainArgs {
    /// Path to the cabin.toml manifest.
    #[arg(long, value_name = "PATH")]
    pub manifest_path: Option<PathBuf>,

    /// Output format. `human` is a concise summary (the
    /// default); `json` is a structured document for tooling.
    #[arg(long, value_name = "FORMAT", default_value = "human", global = true)]
    pub format: ResolveFormat,

    /// Profile to evaluate, when the explanation depends on the
    /// build configuration (`build-config`).  Defaults to `dev`.
    #[arg(long, value_name = "NAME", global = true)]
    pub profile: Option<String>,

    /// Toolchain-selection flags.  Same precedence rules as
    /// `cabin build`.
    #[command(flatten)]
    pub toolchain: ToolchainSelectionArgs,

    /// Feature selection flags.
    #[command(flatten)]
    pub selection: ConfigSelectionArgs,

    /// Workspace package-selection flags.  Restrict the closure
    /// the explanation considers (the same `--package` /
    /// `--workspace` flags `cabin metadata` accepts).
    #[command(flatten)]
    pub workspace_selection: WorkspaceSelectionArgs,

    /// Disable every active patch / source-replacement entry.
    #[arg(long, global = true)]
    pub no_patches: bool,

    #[command(subcommand)]
    pub command: ExplainCommand,
}

#[derive(Debug, Subcommand)]
pub(crate) enum ExplainCommand {
    /// Explain why a package is selected and which selected root
    /// pulls it in.
    Package {
        /// Package name to explain.
        name: String,
    },
    /// Explain a target's owning package, kind, deps, and
    /// language summary.
    Target {
        /// Target name to explain.
        name: String,
    },
    /// Explain where a package's source bytes come from.
    Source {
        /// Package name to explain.
        name: String,
    },
    /// Explain a feature's enablement on the named package.
    Feature {
        /// Query in the form `package/feature`.
        query: String,
    },
    /// Explain the resolved [`cabin_core::BuildConfiguration`]
    /// for a package (profile, toolchain, flags, options,
    /// variants, condition trace, fingerprint).
    BuildConfig {
        /// Package name to explain.
        name: String,
    },
}

pub(crate) fn explain(
    args: &ExplainArgs,
    reporter: crate::cli::term_verbosity::Reporter,
) -> Result<()> {
    let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
    // `cabin explain` is read-only inspection: never auto-
    // download foundation ports.  The cache short-circuit serves
    // an already-prepared workspace.
    let explain_selection = build_workspace_selection(&args.workspace_selection);
    let (prepared_ports, initial_graph) = crate::cli::port::prepare_ports_and_load_initial_graph(
        &manifest_path,
        None,
        true,
        false,
        false,
        &explain_selection,
        args.no_patches,
    )?;
    let port_sources: Vec<cabin_workspace::PortPackageSource> = prepared_ports
        .iter()
        .map(crate::cli::port::workspace_source)
        .collect();
    let effective_config = crate::cli::config::load_effective_config(&initial_graph)?;
    let active_patches =
        crate::cli::patch::load_active_patches(&initial_graph, &effective_config, args.no_patches)?;
    let patched_sources = active_patches.workspace_sources();
    let graph = crate::cli::patch::reload_for_patches(
        &manifest_path,
        initial_graph,
        &patched_sources,
        &port_sources,
    )?;

    let lockfile_path = lockfile_path_for(&manifest_path);
    let lockfile = read_optional_lockfile(&lockfile_path)?;

    let request = build_selection_request(
        &args.selection.features,
        args.selection.all_features,
        args.selection.no_default_features,
    );
    let resolved_selection =
        cabin_workspace::resolve_package_selection(&graph, &explain_selection)?;
    let feature_resolution = compute_feature_resolution(
        &graph,
        &resolved_selection,
        &request,
        &std::collections::BTreeSet::new(),
    )?;

    let explanation = match &args.command {
        ExplainCommand::Package { name } => {
            let exp = cabin_explain::explain_package(
                &graph,
                &resolved_selection.packages,
                name,
                Some(&active_patches),
                lockfile.as_ref(),
            )?;
            cabin_explain::Explanation::Package(exp)
        }
        ExplainCommand::Target { name } => {
            let exp = cabin_explain::explain_target(&graph, &resolved_selection.packages, name)?;
            cabin_explain::Explanation::Target(exp)
        }
        ExplainCommand::Source { name } => {
            let exp = cabin_explain::explain_source(
                &graph,
                name,
                Some(&active_patches),
                lockfile.as_ref(),
                &effective_config.source_replacements,
            )?;
            cabin_explain::Explanation::Source(exp)
        }
        ExplainCommand::Feature { query } => {
            // Build a per-package feature view limited to the
            // package the user named.  We look up the package up
            // front so we can map its enabled features into the
            // typed view the cabin-explain crate consumes.
            let pkg_name = query
                .split_once('/')
                .map_or_else(|| query.clone(), |(p, _)| p.to_owned());
            let view = if let Some(idx) = graph.index_of(&pkg_name) {
                let enabled = feature_resolution.for_package(idx).enabled_features.clone();
                Some(cabin_explain::cabin_feature_per_package_view::FeatureView { enabled })
            } else {
                None
            };
            let exp = cabin_explain::explain_feature(&graph, view.as_ref(), query)?;
            cabin_explain::Explanation::Feature(exp)
        }
        ExplainCommand::BuildConfig { name } => {
            // Build-config explanations need the same preamble
            // as `cabin metadata`.  We compute it inline rather
            // than refactoring `metadata()` itself so the
            // existing path stays untouched.
            let manifest_profiles = workspace_profile_definitions(&graph);
            let profile_selection =
                profile_selection_for_metadata(args.profile.as_deref(), &effective_config)?;
            let profile = cabin_core::resolve_profile(&profile_selection, &manifest_profiles)?;
            let host_platform = cabin_core::TargetPlatform::current();
            let toolchain_selection = toolchain_selection_from_args(&args.toolchain)?;
            let toolchain = crate::cli::resolve_toolchain_layered(
                &graph,
                &toolchain_selection,
                &effective_config,
                &host_platform,
            )?;
            // Capability detection mirrors `cabin metadata`: fail-soft
            // so a misbehaving compiler does not block explaining the
            // configuration; compiler cfg conditions then evaluate as
            // `unknown` with no version.
            let detection_report = match cabin_toolchain::detect_toolchain(
                &toolchain,
                &cabin_toolchain::ProcessRunner,
            ) {
                Ok(report) => Some(report),
                Err(err) => {
                    reporter.warning(format_args!("toolchain detection failed: {err}"));
                    None
                }
            };
            let manifest_compiler_wrapper = workspace_compiler_wrapper_settings(&graph);
            let cli_compiler_wrapper = compiler_wrapper_override_from_args(&args.toolchain)?;
            // `cabin explain` does not opt into dev-dep activation;
            // dev-kind system deps stay declaration-only here.
            let dev_for: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
            let prep = crate::cli::build_prep::resolve_build_prep(
                crate::cli::build_prep::BuildConfigInputs {
                    graph: &graph,
                    host_platform: &host_platform,
                    toolchain: &toolchain,
                    detection: detection_report.as_ref(),
                    cli_compiler_wrapper,
                    manifest_compiler_wrapper: manifest_compiler_wrapper.as_ref(),
                    effective_config: &effective_config,
                    profile: &profile,
                    dev_for: &dev_for,
                    feature_resolution: &feature_resolution,
                    reporter,
                },
            )?;
            let configurations = resolve_build_configurations(
                &graph,
                &request,
                &resolved_selection.packages,
                &profile,
                &prep.toolchain_summary,
                &prep.build_flags,
            )?;
            let config = cabin_explain::explain_build_config(&configurations, &graph, name)?;
            // BuildConfiguration already has its own JSON shape
            // documented by `cabin metadata`.  We render it
            // directly rather than wrapping it in our `Explanation`
            // enum so users see exactly the same shape they see
            // in metadata's `configuration` blocks.
            return render_build_config(args.format, name, config);
        }
    };

    match args.format {
        ResolveFormat::Human => {
            let rendered = cabin_explain::render_explanation_human(&explanation);
            print!("{rendered}");
        }
        ResolveFormat::Json => {
            let value = cabin_explain::render_explanation_json(&explanation);
            crate::print_pretty_json(&value, "failed to serialize explanation as JSON")?;
        }
    }
    Ok(())
}

fn render_build_config(
    format: ResolveFormat,
    name: &str,
    config: &cabin_core::BuildConfiguration,
) -> Result<()> {
    match format {
        ResolveFormat::Json => {
            let mut map = serde_json::Map::new();
            map.insert(
                "kind".to_owned(),
                serde_json::Value::String("build-config".to_owned()),
            );
            map.insert(
                "package".to_owned(),
                serde_json::Value::String(name.to_owned()),
            );
            map.insert("configuration".to_owned(), config.as_json());
            crate::print_pretty_json(
                &serde_json::Value::Object(map),
                "failed to serialize build-config explanation",
            )?;
        }
        ResolveFormat::Human => {
            println!("package: {name}");
            println!("profile: {}", config.profile.name);
            if let Some(c) = &config.language.c {
                println!("c standard: {} ({})", c.standard, c.source.as_key());
            }
            if let Some(cxx) = &config.language.cxx {
                println!("cxx standard: {} ({})", cxx.standard, cxx.source.as_key());
            }
            println!("fingerprint: {}", config.fingerprint);
            // Stay terse - JSON is the contract for tooling.
        }
    }
    Ok(())
}