Skip to main content

provenant/cli/run/
mod.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::app::request::ScanRequest;
5use crate::app::scan_pipeline::execute_request;
6use crate::cli::{Cli, Command, ScanArgs};
7use crate::compare::compare_json_files;
8use crate::license_detection::dataset::export_embedded_license_dataset;
9use crate::output::{OutputWriteConfig, write_output_file};
10use crate::progress::ScanProgress;
11use crate::serve::run as run_serve_shell;
12use crate::time::format_scancode_timestamp;
13use anyhow::{Result, anyhow};
14use chrono::Utc;
15use std::path::Path;
16use std::sync::Arc;
17use std::time::Instant;
18
19pub fn run() -> Result<()> {
20    #[cfg(feature = "golden-tests")]
21    touch_license_golden_symbols();
22
23    let cli = Cli::parse();
24    match &cli.command {
25        Command::ShowAttribution => {
26            print!("{}", include_str!("../../../NOTICE"));
27            return Ok(());
28        }
29        Command::Serve(args) => {
30            return run_serve_shell(args);
31        }
32        Command::Compare(args) => {
33            let result = compare_json_files(
34                &args.scancode_json,
35                &args.provenant_json,
36                args.artifact_dir.as_deref(),
37            )?;
38            println!("Comparison status: {}", result.comparison_status);
39            println!("Artifacts:");
40            println!("  Artifact directory: {}", result.artifact_dir.display());
41            println!("  Run manifest:       {}", result.manifest_path.display());
42            println!("  Raw ScanCode JSON:  {}", result.scancode_json.display());
43            println!("  Raw Provenant JSON: {}", result.provenant_json.display());
44            println!("  Summary JSON:       {}", result.summary_json.display());
45            println!("  Summary TSV:        {}", result.summary_tsv.display());
46            println!("  Sample artifacts:   {}", result.samples_dir.display());
47            return Ok(());
48        }
49        Command::ExportLicenseDataset(args) => {
50            export_embedded_license_dataset(Path::new(&args.dir))?;
51            return Ok(());
52        }
53        Command::Scan(_) => {}
54    }
55
56    let cli = cli
57        .scan_args()
58        .expect("scan arguments should exist after command dispatch");
59
60    validate_scan_option_compatibility(cli)?;
61
62    let request = ScanRequest::from(cli);
63    let executed = execute_request(&request)?;
64    let output = executed.output;
65    let progress = executed.progress;
66    let start_time = executed.start_time;
67
68    let output_schema_output =
69        crate::output_schema::Output::from_with_compat_mode(&output, cli.compat_mode);
70    progress.start_output();
71    for target in &request.output_targets {
72        let output_config = OutputWriteConfig {
73            format: target.format,
74            custom_template: target.custom_template.clone(),
75            scanned_path: if request.input_paths.len() == 1 {
76                request.input_paths.first().cloned()
77            } else {
78                None
79            },
80        };
81
82        let timing_name = format!("output:{:?}", target.format).to_lowercase();
83        record_detail_timing(&progress, timing_name, || {
84            write_output_file(&target.file, &output_schema_output, &output_config)
85        })?;
86        progress.output_written(&format!(
87            "{:?} output written to {}",
88            target.format, target.file
89        ));
90    }
91    progress.record_final_counts(&output.files);
92    progress.record_final_header_counts(&output.headers);
93    progress.finish_output();
94
95    let summary_end = Utc::now();
96    progress.display_summary(
97        &format_scancode_timestamp(&start_time),
98        &format_scancode_timestamp(&summary_end),
99    );
100
101    Ok(())
102}
103
104#[cfg(feature = "golden-tests")]
105fn touch_license_golden_symbols() {
106    let _ = crate::license_detection::golden_utils::read_golden_input_content;
107    let _ = crate::license_detection::golden_utils::detect_matches_for_golden;
108    let _ = crate::license_detection::golden_utils::detect_license_expressions_for_golden;
109    let _ = crate::license_detection::LicenseDetectionEngine::detect_matches_with_kind;
110}
111
112fn validate_scan_option_compatibility(cli: &ScanArgs) -> Result<()> {
113    if cli.from_json
114        && (cli.package
115            || cli.system_package
116            || cli.package_in_compiled
117            || cli.package_only
118            || cli.copyright
119            || cli.email
120            || cli.url
121            || cli.generated)
122    {
123        return Err(anyhow!(
124            "When using --from-json, file scan options like --package/--copyright/--email/--url/--generated are not allowed"
125        ));
126    }
127
128    if cli.from_json && !cli.paths_file.is_empty() {
129        return Err(anyhow!(
130            "--paths-file is only supported for native scan mode, not --from-json"
131        ));
132    }
133
134    if cli.from_json && cli.incremental {
135        return Err(anyhow!(
136            "--incremental is only supported for directory scan mode, not --from-json"
137        ));
138    }
139
140    if !cli.paths_file.is_empty() && cli.dir_path.len() != 1 {
141        return Err(anyhow!(
142            "--paths-file requires exactly one positional scan root"
143        ));
144    }
145
146    if !cli.from_json && cli.dir_path.is_empty() {
147        return Err(anyhow!("Directory path is required for scan operations"));
148    }
149
150    if cli.tallies_by_facet && cli.facet.is_empty() {
151        return Err(anyhow!(
152            "--tallies-by-facet requires at least one --facet <facet>=<pattern> definition"
153        ));
154    }
155
156    if cli.mark_source && !cli.info {
157        return Err(anyhow!("--mark-source requires --info"));
158    }
159
160    Ok(())
161}
162
163fn record_detail_timing<T, F>(progress: &Arc<ScanProgress>, name: impl Into<String>, f: F) -> T
164where
165    F: FnOnce() -> T,
166{
167    let started = Instant::now();
168    let result = f();
169    progress.record_detail_timing(name.into(), started.elapsed().as_secs_f64());
170    result
171}
172
173#[cfg(test)]
174mod tests;