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, init_cli_logger};
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
25 match &cli.command {
29 Command::Scan(_) => {}
30 Command::Serve(args) => init_cli_logger(args.verbosity.verbosity().log_level()),
31 Command::Compare(args) => init_cli_logger(args.verbosity.verbosity().log_level()),
32 Command::ExportLicenseDataset(args) => {
33 init_cli_logger(args.verbosity.verbosity().log_level())
34 }
35 Command::ShowAttribution => init_cli_logger(crate::cli::Verbosity::Normal.log_level()),
36 }
37
38 match &cli.command {
39 Command::ShowAttribution => {
40 print!("{}", include_str!("../../../NOTICE"));
41 return Ok(());
42 }
43 Command::Serve(args) => {
44 return run_serve_shell(args);
45 }
46 Command::Compare(args) => {
47 log::info!(
48 "Comparing ScanCode JSON {} against Provenant JSON {}...",
49 args.scancode_json.display(),
50 args.provenant_json.display()
51 );
52 let result = compare_json_files(
53 &args.scancode_json,
54 &args.provenant_json,
55 args.artifact_dir.as_deref(),
56 )?;
57 println!("Comparison status: {}", result.comparison_status);
58 println!("Artifacts:");
59 println!(" Artifact directory: {}", result.artifact_dir.display());
60 println!(" Run manifest: {}", result.manifest_path.display());
61 println!(" Raw ScanCode JSON: {}", result.scancode_json.display());
62 println!(" Raw Provenant JSON: {}", result.provenant_json.display());
63 println!(" Summary JSON: {}", result.summary_json.display());
64 println!(" Summary TSV: {}", result.summary_tsv.display());
65 println!(" Sample artifacts: {}", result.samples_dir.display());
66 return Ok(());
67 }
68 Command::ExportLicenseDataset(args) => {
69 let dir = Path::new(&args.dir);
70 log::info!("Exporting built-in license dataset to {}...", dir.display());
71
72 let started = Instant::now();
73 let outcome = export_embedded_license_dataset(dir)?;
74
75 log::info!(
76 "Exported {} license dataset files to {} in {:.1}s (SPDX list {})",
77 outcome.files_written,
78 dir.display(),
79 started.elapsed().as_secs_f64(),
80 outcome.manifest.spdx_license_list_version
81 );
82 return Ok(());
83 }
84 Command::Scan(_) => {}
85 }
86
87 let cli = cli
88 .scan_args()
89 .expect("scan arguments should exist after command dispatch");
90
91 validate_scan_option_compatibility(cli)?;
92
93 let request = ScanRequest::from(cli);
94 let executed = execute_request(&request)?;
95 let output = executed.output;
96 let progress = executed.progress;
97 let start_time = executed.start_time;
98
99 let output_schema_output =
100 crate::output_schema::Output::from_with_compat_mode(&output, cli.compat_mode);
101 progress.start_output();
102 for target in &request.output_targets {
103 let output_config = OutputWriteConfig {
104 format: target.format,
105 custom_template: target.custom_template.clone(),
106 scanned_path: if request.input_paths.len() == 1 {
107 request.input_paths.first().cloned()
108 } else {
109 None
110 },
111 };
112
113 let timing_name = format!("output:{:?}", target.format).to_lowercase();
114 record_detail_timing(&progress, timing_name, || {
115 write_output_file(&target.file, &output_schema_output, &output_config)
116 })?;
117 progress.output_written(&format!(
118 "{:?} output written to {}",
119 target.format, target.file
120 ));
121 }
122 progress.record_final_counts(&output.files);
123 progress.record_final_header_counts(&output.headers);
124 progress.finish_output();
125
126 let summary_end = Utc::now();
127 progress.display_summary(
128 &format_scancode_timestamp(&start_time),
129 &format_scancode_timestamp(&summary_end),
130 );
131
132 Ok(())
133}
134
135#[cfg(feature = "golden-tests")]
136fn touch_license_golden_symbols() {
137 let _ = crate::license_detection::golden_utils::read_golden_input_content;
138 let _ = crate::license_detection::golden_utils::detect_matches_for_golden;
139 let _ = crate::license_detection::golden_utils::detect_license_expressions_for_golden;
140 let _ = crate::license_detection::LicenseDetectionEngine::detect_matches_with_kind;
141}
142
143fn validate_scan_option_compatibility(cli: &ScanArgs) -> Result<()> {
144 if cli.from_json
145 && (cli.package
146 || cli.system_package
147 || cli.package_in_compiled
148 || cli.package_only
149 || cli.copyright
150 || cli.email
151 || cli.url
152 || cli.generated)
153 {
154 return Err(anyhow!(
155 "When using --from-json, file scan options like --package/--copyright/--email/--url/--generated are not allowed"
156 ));
157 }
158
159 if cli.from_json && !cli.paths_file.is_empty() {
160 return Err(anyhow!(
161 "--paths-file is only supported for native scan mode, not --from-json"
162 ));
163 }
164
165 if cli.from_json && cli.incremental {
166 return Err(anyhow!(
167 "--incremental is only supported for directory scan mode, not --from-json"
168 ));
169 }
170
171 if !cli.paths_file.is_empty() && cli.dir_path.len() != 1 {
172 return Err(anyhow!(
173 "--paths-file requires exactly one positional scan root"
174 ));
175 }
176
177 if !cli.from_json && cli.dir_path.is_empty() {
178 return Err(anyhow!("Directory path is required for scan operations"));
179 }
180
181 if cli.tallies_by_facet && cli.facet.is_empty() {
182 return Err(anyhow!(
183 "--tallies-by-facet requires at least one --facet <facet>=<pattern> definition"
184 ));
185 }
186
187 if cli.mark_source && !cli.info {
188 return Err(anyhow!("--mark-source requires --info"));
189 }
190
191 Ok(())
192}
193
194fn record_detail_timing<T, F>(progress: &Arc<ScanProgress>, name: impl Into<String>, f: F) -> T
195where
196 F: FnOnce() -> T,
197{
198 let started = Instant::now();
199 let result = f();
200 progress.record_detail_timing(name.into(), started.elapsed().as_secs_f64());
201 result
202}
203
204#[cfg(test)]
205mod tests;