1use 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;