Skip to main content

foch_cli/cli/handler/
data.rs

1use crate::cli::arg::{
2	DataArgs, DataBuildArgs, DataInstallArgs, DataListArgs, FochCliDataCommands,
3};
4use crate::cli::handler::HandlerResult;
5use foch::game::eu4::Eu4;
6use foch::game::eu4::base::snapshot::{
7	BaseBuildObserver, BaseDataSource, INSTALLED_COVERAGE_FILE_NAME,
8	build_base_snapshot_with_observer, default_release_tag, install_built_snapshot,
9	install_snapshot_from_release, list_installed_base_data, resolve_game_root_and_version,
10	write_release_artifacts, write_snapshot_bundle,
11};
12use foch::input::{Config, FileFilter};
13
14pub fn handle_data(data_args: &DataArgs, config: Config) -> HandlerResult {
15	match &data_args.command {
16		FochCliDataCommands::Install(args) => handle_data_install(args, config),
17		FochCliDataCommands::Build(args) => handle_data_build(args, &config),
18		FochCliDataCommands::List(args) => handle_data_list(args),
19	}
20}
21
22fn handle_data_install(args: &DataInstallArgs, config: Config) -> HandlerResult {
23	let game = parse_game(&args.game_name)?;
24	let resolved_version = if args.game_version.eq_ignore_ascii_case("auto") {
25		let (_game_root, version) = resolve_game_root_and_version(&config, &game)
26			.map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
27		version
28	} else {
29		args.game_version.clone()
30	};
31	let installed =
32		install_snapshot_from_release(&game, &resolved_version, args.release_tag.as_deref())
33			.map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
34	println!(
35		"installed base data: game={} version={} source=download path={}",
36		installed.metadata.game,
37		installed.metadata.game_version,
38		installed.install_dir.display()
39	);
40	Ok(0)
41}
42
43fn handle_data_build(args: &DataBuildArgs, config: &Config) -> HandlerResult {
44	let observer = BaseBuildObserver::stderr(&args.game_name);
45	for line in run_data_build(args, config, observer)? {
46		println!("{line}");
47	}
48	Ok(0)
49}
50
51/// Build EU4 base data as `foch data build` does and return its report
52/// lines. Bare `foch` builds through this too, with the arguments it shows
53/// as the command.
54pub fn run_data_build(
55	args: &DataBuildArgs,
56	config: &Config,
57	mut observer: BaseBuildObserver,
58) -> Result<Vec<String>, Box<dyn std::error::Error>> {
59	let mut report = Vec::new();
60	if !args.install && args.output_dir.is_none() && !args.release_asset {
61		return Err(
62			"please specify at least one of --install, --output-dir, or --release-asset".into(),
63		);
64	}
65
66	let game = parse_game(&args.game_name)?;
67	let resolved_version = if args.game_version.eq_ignore_ascii_case("auto") {
68		None
69	} else {
70		Some(args.game_version.as_str())
71	};
72	let filter = FileFilter::new(game, &config.extra_ignore_patterns)
73		.map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
74	let build = build_base_snapshot_with_observer(
75		&game,
76		&args.from_game_path,
77		resolved_version,
78		&filter,
79		&mut observer,
80	)
81	.map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
82
83	observer
84		.run_stage(
85			"write_outputs",
86			|counts: &mut std::collections::BTreeMap<String, u64>| {
87				if args.install {
88					let installed = install_built_snapshot(
89						&build.encoded_snapshot,
90						BaseDataSource::Build,
91						Some(build.snapshot_asset_name.clone()),
92						Some(build.snapshot_sha256.clone()),
93					)?;
94					counts.insert(
95						"install_snapshot_bytes".to_string(),
96						build.encoded_snapshot.len() as u64,
97					);
98					let coverage_bytes =
99						std::fs::metadata(installed.install_dir.join(INSTALLED_COVERAGE_FILE_NAME))
100							.map(|item| item.len())
101							.unwrap_or_default();
102					counts.insert("install_coverage_bytes".to_string(), coverage_bytes);
103					report.push(format!(
104						"built and installed base data: game={} version={} path={}",
105						installed.metadata.game,
106						installed.metadata.game_version,
107						installed.install_dir.display()
108					));
109				}
110
111				if args.release_asset {
112					let output_dir = args.output_dir.as_ref().ok_or_else(|| {
113						"--output-dir is required when using --release-asset".to_string()
114					})?;
115					let release_output = write_release_artifacts(
116						&build.encoded_snapshot,
117						output_dir,
118						&default_release_tag(),
119					)?;
120					let manifest_bytes = std::fs::metadata(&release_output.manifest_path)
121						.map(|item| item.len())
122						.unwrap_or_default();
123					let coverage_bytes = std::fs::metadata(&release_output.coverage_path)
124						.map(|item| item.len())
125						.unwrap_or_default();
126					counts.insert(
127						"release_snapshot_bytes".to_string(),
128						build.encoded_snapshot.len() as u64,
129					);
130					counts.insert("release_manifest_bytes".to_string(), manifest_bytes);
131					counts.insert("release_coverage_bytes".to_string(), coverage_bytes);
132					report.push(format!(
133						"wrote release data asset: snapshot={} manifest={} coverage={}",
134						release_output.snapshot_path.display(),
135						release_output.manifest_path.display(),
136						release_output.coverage_path.display()
137					));
138				} else if let Some(output_dir) = args.output_dir.as_ref() {
139					let bundle = write_snapshot_bundle(
140						&build.encoded_snapshot,
141						output_dir,
142						BaseDataSource::Build,
143						Some(build.snapshot_asset_name.clone()),
144						Some(build.snapshot_sha256.clone()),
145					)?;
146					let metadata_bytes = std::fs::metadata(&bundle.metadata_path)
147						.map(|item| item.len())
148						.unwrap_or_default();
149					let coverage_bytes = std::fs::metadata(&bundle.coverage_path)
150						.map(|item| item.len())
151						.unwrap_or_default();
152					counts.insert(
153						"bundle_snapshot_bytes".to_string(),
154						build.encoded_snapshot.len() as u64,
155					);
156					counts.insert("bundle_metadata_bytes".to_string(), metadata_bytes);
157					counts.insert("bundle_coverage_bytes".to_string(), coverage_bytes);
158					report.push(format!(
159						"wrote snapshot bundle: snapshot={} metadata={} coverage={}",
160						bundle.snapshot_path.display(),
161						bundle.metadata_path.display(),
162						bundle.coverage_path.display()
163					));
164				}
165				Ok(())
166			},
167		)
168		.map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
169
170	let profile = observer.finish();
171	if let Some(path) = args.profile_out.as_ref() {
172		if let Some(parent) = path.parent()
173			&& !parent.as_os_str().is_empty()
174		{
175			std::fs::create_dir_all(parent)?;
176		}
177		std::fs::write(path, serde_json::to_vec_pretty(&profile)?)?;
178		report.push(format!("wrote profiling result: {}", path.display()));
179	}
180
181	Ok(report)
182}
183
184fn handle_data_list(args: &DataListArgs) -> HandlerResult {
185	let entries = list_installed_base_data()
186		.map_err(|err: String| -> Box<dyn std::error::Error> { err.into() })?;
187	if args.json {
188		println!("{}", serde_json::to_string_pretty(&entries)?);
189		return Ok(0);
190	}
191
192	if entries.is_empty() {
193		println!("No installed base data");
194		return Ok(0);
195	}
196
197	for entry in entries {
198		println!(
199			"game={} version={} data={} source={:?} path={}",
200			entry.game,
201			entry.game_version,
202			entry.snapshot_format_version,
203			entry.source,
204			entry.install_path.display()
205		);
206	}
207
208	Ok(0)
209}
210
211fn parse_game(value: &str) -> Result<Eu4, Box<dyn std::error::Error>> {
212	Eu4::from_key(value).ok_or_else(|| format!("unsupported game key: {value}").into())
213}