catapult/
lib.rs

1mod executable;
2pub mod generator;
3mod interface_library;
4mod link_type;
5mod misc;
6mod object_library;
7pub mod project;
8mod starlark_api;
9mod starlark_context;
10mod starlark_executable;
11mod starlark_fmt;
12mod starlark_generator;
13mod starlark_global;
14mod starlark_interface_library;
15mod starlark_link_target;
16mod starlark_object_library;
17mod starlark_project;
18mod starlark_static_library;
19mod static_library;
20pub mod target;
21pub mod toolchain;
22
23use std::{
24	collections::{BTreeMap, HashMap},
25	fs,
26	path::{Path, PathBuf},
27	sync::{Arc, Mutex},
28	time::Duration,
29};
30
31use anyhow::anyhow;
32use base64::Engine;
33use flate2::read::GzDecoder;
34use reqwest::StatusCode;
35use serde::Deserialize;
36use starlark::{
37	environment::{
38		Globals, //
39		GlobalsBuilder,
40		Module,
41	},
42	eval::Evaluator,
43	syntax::{
44		AstModule, //
45		Dialect,
46		DialectTypes,
47	},
48};
49use tar::Archive;
50
51use project::Project;
52use starlark_api::err_msg;
53use starlark_global::{PkgOpt, StarGlobal};
54use starlark_project::StarProject;
55use toolchain::Toolchain;
56
57type PkgOptMap = HashMap<String, HashMap<String, PkgOpt>>;
58
59const CATAPULT_TOML: &str = "catapult.toml";
60const BUILD_CATAPULT: &str = "build.catapult";
61
62#[derive(Debug, Deserialize)]
63struct Manifest {
64	package: PackageManifest,
65	dependencies: Option<BTreeMap<String, DependencyManifest>>,
66	options: Option<ManifestOptions>,
67	package_options: Option<HashMap<String, PkgOpt>>,
68}
69
70#[derive(Debug, Deserialize)]
71struct PackageManifest {
72	name: String,
73	// version: Option<String>,
74	source: Option<String>,
75}
76
77#[derive(Debug, Deserialize)]
78struct DependencyManifest {
79	version: Option<String>,
80	registry: Option<String>,
81	channel: Option<String>,
82	// ---
83	path: Option<String>,
84	// ---
85	git: Option<String>,
86	// branch: Option<String>,
87	// tag: Option<String>,
88	// rev: Option<String>,
89	options: Option<HashMap<String, PkgOpt>>,
90}
91
92#[derive(Debug, Default, Deserialize)]
93struct ManifestOptions {
94	c_standard: Option<String>,
95	cpp_standard: Option<String>,
96	position_independent_code: Option<bool>,
97}
98
99#[derive(Debug)]
100pub struct GlobalOptions {
101	pub c_standard: Option<String>,
102	pub cpp_standard: Option<String>,
103	pub position_independent_code: Option<bool>,
104}
105
106fn read_manifest(src_dir: &Path) -> Result<Manifest, anyhow::Error> {
107	let manifest_path = src_dir.join(CATAPULT_TOML);
108	let catapult_toml = match fs::read_to_string(&manifest_path) {
109		Ok(x) => x,
110		Err(e) => return err_msg(format!("Error opening {}: {}", manifest_path.display(), e)),
111	};
112
113	let manifest = match toml::from_str::<Manifest>(&catapult_toml) {
114		Ok(x) => x,
115		Err(e) => return err_msg(format!("Error reading {}: {}", manifest_path.display(), e)),
116	};
117
118	Ok(manifest)
119}
120
121fn map_to_pkg_opt_map(opt_map: BTreeMap<String, BTreeMap<String, String>>) -> Result<PkgOptMap, anyhow::Error> {
122	type SerdeErr = toml::de::Error;
123
124	fn deserialize_pkg_opt(kv: (String, String)) -> Result<(String, PkgOpt), SerdeErr> {
125		let deserializer = toml::de::ValueDeserializer::new(&kv.1);
126		let opt_val = PkgOpt::deserialize(deserializer)?;
127		Ok((kv.0, opt_val))
128	}
129
130	match opt_map
131		.into_iter()
132		.map(|(k, im)| {
133			let val = im
134				.into_iter()
135				.map(deserialize_pkg_opt)
136				.collect::<Result<HashMap<String, PkgOpt>, _>>()?;
137			Ok((k, val))
138		})
139		.collect::<Result<PkgOptMap, SerdeErr>>()
140	{
141		Ok(x) => Ok(x),
142		Err(e) => Err(anyhow!(format!("Could not deserialize package option: {}", e))),
143	}
144}
145
146pub fn parse_project(
147	toolchain: &Toolchain,
148	package_options: BTreeMap<String, BTreeMap<String, String>>,
149) -> Result<(Arc<Project>, GlobalOptions), anyhow::Error> {
150	let src_dir = PathBuf::from(".");
151	let manifest_options = read_manifest(&src_dir)?.options.unwrap_or_default();
152	let global_options = GlobalOptions {
153		c_standard: manifest_options.c_standard,
154		cpp_standard: manifest_options.cpp_standard,
155		position_independent_code: manifest_options.position_independent_code,
156	};
157	let mut combined_deps = BTreeMap::new();
158	let package_options = map_to_pkg_opt_map(package_options)?;
159	let project =
160		parse_project_inner(src_dir, &global_options, &package_options, HashMap::new(), toolchain, &mut combined_deps)?;
161
162	match project.into_project() {
163		Ok(x) => Ok((x, global_options)),
164		Err(e) => Err(anyhow!(e)),
165	}
166}
167
168#[derive(Deserialize)]
169struct PackageRecord {
170	// pkg_name: String,
171	// version: String,
172	hash: String,
173	manifest: String,
174	recipe: String,
175	// datetime_added: i64,
176}
177
178fn download_from_registry(
179	mut registry: String,
180	name: &str,
181	info_version: Option<String>,
182	info_channel: Option<String>,
183) -> Result<PathBuf, anyhow::Error> {
184	// Download to tmp dir
185	let version = match &info_version {
186		Some(x) => x,
187		None => return Err(anyhow::anyhow!("Field \"version\" required for dependency \"{}\"", name)),
188	};
189	let channel = match &info_channel {
190		Some(x) => x,
191		None => return Err(anyhow::anyhow!("Field \"channel\" required for dependency \"{}\"", name)),
192	};
193	if !registry.ends_with('/') {
194		registry += "/";
195	}
196	let url = match reqwest::Url::parse(&registry) {
197		Ok(x) => x,
198		Err(e) => return Err(anyhow::anyhow!(e)),
199	};
200	let url = match url.join(&("get".to_owned() + "/" + name + "/" + version + "/" + channel)) {
201		Ok(x) => x,
202		Err(e) => return Err(anyhow::anyhow!(e)),
203	};
204	println!("Fetching dependency \"{}\" from {} ...", name, url);
205	let resp = match reqwest::blocking::Client::builder()
206		.build()?
207		.get(url.clone())
208		.timeout(Duration::from_secs(10))
209		.send()
210	{
211		Ok(resp) => resp,
212		Err(err) => return Err(anyhow!("Error trying to fetch \"{}\" from {}:\n    {}", name, url, err)),
213	};
214	match resp.status() {
215		StatusCode::OK => (),
216		x => return Err(anyhow!("Request GET \"{}\" returned status {}", url, x)),
217	}
218	let resp_json = match resp.json::<PackageRecord>() {
219		Ok(x) => x,
220		Err(e) => return Err(anyhow!(e)),
221	};
222	let cache_dir = match dirs::cache_dir() {
223		Some(x) => x,
224		None => return Err(anyhow!("Could not find a HOME directory")),
225	};
226	let pkg_cache_path = cache_dir.join("catapult").join("cache").join(name).join(channel);
227	println!("pkg_cache_path: {:?}", pkg_cache_path);
228
229	let hash_path = pkg_cache_path.join("catapult.hash");
230	if let Ok(hash) = fs::read_to_string(&hash_path) {
231		if hash.trim() == resp_json.hash.trim() {
232			// This package already exists in the cache. Don't download it again.
233			log::debug!("Package found in cache. It will not be downloaded: {name}");
234			return Ok(pkg_cache_path);
235		} else {
236			log::info!(
237				r#"A cached package was found but its hash does not match the one reported by the registry. It will be re-downloaded.
238      Package: {name}
239 On-disk hash: {}
240Registry hash: {}"#,
241				hash.trim(),
242				resp_json.hash
243			);
244		}
245	}
246
247	let manifest_bytes = base64::engine::general_purpose::STANDARD_NO_PAD.decode(resp_json.manifest)?;
248	let manifest_str = std::str::from_utf8(&manifest_bytes)?;
249	let manifest = match toml::from_str::<Manifest>(manifest_str) {
250		Ok(x) => x,
251		Err(e) => return err_msg(format!("Error reading dependency manifest of {}: {}", name, e)),
252	};
253	let pkg_source_url = match manifest.package.source {
254		Some(x) => x,
255		None => return Err(anyhow!("Dependency manifest did not contain source. ({})", name)),
256	};
257	let src_data_resp = match reqwest::blocking::get(&pkg_source_url) {
258		Ok(resp) => resp,
259		Err(err) => panic!("Error: {}", err),
260	};
261	match src_data_resp.status() {
262		StatusCode::OK => (),
263		x => return Err(anyhow!("Request GET \"{}\" returned status {}", pkg_source_url, x)),
264	}
265	let tar = GzDecoder::new(src_data_resp);
266	let mut archive = Archive::new(tar);
267	archive.unpack(&pkg_cache_path)?;
268
269	let manifest_path = pkg_cache_path.join(CATAPULT_TOML);
270
271	match fs::write(manifest_path, manifest_bytes) {
272		Ok(x) => x,
273		Err(e) => return Err(anyhow!(e)),
274	};
275	let recipe_path = pkg_cache_path.join(BUILD_CATAPULT);
276	let recipe_bytes = base64::engine::general_purpose::STANDARD_NO_PAD.decode(resp_json.recipe)?;
277	match fs::write(recipe_path, recipe_bytes) {
278		Ok(x) => x,
279		Err(e) => return Err(anyhow!(e)),
280	};
281
282	match fs::write(hash_path, resp_json.hash.as_bytes()) {
283		Ok(x) => x,
284		Err(e) => return Err(anyhow!(e)),
285	}
286
287	Ok(pkg_cache_path)
288}
289
290fn parse_project_inner(
291	src_dir: PathBuf,
292	global_options: &GlobalOptions,
293	package_options: &PkgOptMap,
294	mut pkg_opt_underrides: HashMap<String, PkgOpt>,
295	toolchain: &Toolchain,
296	dep_map: &mut BTreeMap<String, Arc<StarProject>>,
297) -> Result<StarProject, anyhow::Error> {
298	log::debug!("parse_project_inner {}", src_dir.display());
299
300	let manifest = read_manifest(&src_dir)?;
301
302	if let Some(pkg_opts) = package_options.get(&manifest.package.name) {
303		for (opt_name, opt_val) in pkg_opts {
304			pkg_opt_underrides.insert(opt_name.clone(), opt_val.clone());
305		}
306	}
307	let mut pkg_opts = package_options.clone();
308	pkg_opts.insert(manifest.package.name.clone(), pkg_opt_underrides);
309
310	let mut dependent_projects = Vec::new();
311
312	// Parse dependencies before parsing the dependent
313	for (name, info) in manifest.dependencies.unwrap_or(BTreeMap::new()) {
314		if let Some(dep_proj) = dep_map.get(&name) {
315			dependent_projects.push(dep_proj.clone());
316		}
317
318		let pkg_opt_underrides = info.options.unwrap_or_default();
319
320		if let Some(registry) = info.registry {
321			let dep_path = download_from_registry(registry, &name, info.version, info.channel)?;
322			let dep_proj =
323				parse_project_inner(dep_path, global_options, &pkg_opts, pkg_opt_underrides, toolchain, dep_map)?;
324			let dep_proj = Arc::new(dep_proj);
325			dependent_projects.push(dep_proj.clone());
326			dep_map.insert(name, dep_proj);
327		} else if info.git.is_some() {
328			// Checkout to tmp dir
329			todo!();
330		} else if let Some(dep_path) = info.path {
331			let dep_proj = parse_project_inner(
332				PathBuf::from(&dep_path),
333				global_options,
334				&pkg_opts,
335				pkg_opt_underrides,
336				toolchain,
337				dep_map,
338			)?; //, globals)?;
339			let dep_proj = Arc::new(dep_proj);
340			dependent_projects.push(dep_proj.clone());
341			dep_map.insert(name, dep_proj);
342		} else {
343			return err_msg("Dependency must specify either \"registry\" or \"git\" or \"path\"".to_owned());
344		}
345	}
346
347	let mut option_overrides = manifest.package_options.unwrap_or_default();
348	if let Some(pkg_opts) = pkg_opts.get(&manifest.package.name) {
349		for (opt_name, opt_val) in pkg_opts {
350			log::debug!("Override option: {opt_name}");
351			if option_overrides.contains_key(opt_name) {
352				option_overrides.insert(opt_name.clone(), opt_val.clone());
353			} else {
354				log::error!("Package \"{}\" does not provide option \"{opt_name}\"", manifest.package.name);
355			}
356		}
357	}
358
359	let recipe_path = src_dir.join(BUILD_CATAPULT);
360	let starlark_code = match fs::read_to_string(&recipe_path) {
361		Ok(x) => x,
362		Err(e) => return err_msg(format!("Error reading \"{}\": {e}", recipe_path.display())),
363	};
364	let this_project = parse_module(
365		manifest.package.name.clone(),
366		dependent_projects,
367		global_options,
368		option_overrides,
369		toolchain,
370		src_dir,
371		starlark_code,
372		// context.clone(),
373	)?;
374
375	Ok(this_project)
376}
377
378pub(crate) fn setup(
379	project: &Arc<Mutex<StarProject>>,
380	global_options: &GlobalOptions,
381	package_options: HashMap<String, PkgOpt>,
382	toolchain: &Toolchain,
383) -> Globals {
384	let mut globals_builder = GlobalsBuilder::standard();
385	starlark::environment::LibraryExtension::Print.add(&mut globals_builder);
386	globals_builder.set("GLOBAL", StarGlobal::new(global_options, package_options, toolchain));
387	starlark_api::build_api(project, &mut globals_builder);
388	globals_builder.build()
389}
390
391pub(crate) fn parse_module(
392	name: String,
393	deps: Vec<Arc<StarProject>>,
394	global_options: &GlobalOptions,
395	package_options: HashMap<String, PkgOpt>,
396	toolchain: &Toolchain,
397	current_dir: PathBuf,
398	starlark_code: String,
399) -> Result<StarProject, anyhow::Error> {
400	let dialect = Dialect {
401		enable_types: DialectTypes::Enable,
402		enable_f_strings: true,
403		..Dialect::default()
404	};
405	let ast = match AstModule::parse(BUILD_CATAPULT, starlark_code, &dialect) {
406		Ok(x) => x,
407		Err(e) => panic!("AstModule::parse: {}", e),
408	};
409	let project_writable = Arc::new(Mutex::new(StarProject::new(name, current_dir, deps.clone())));
410
411	let module = Module::new();
412	for dep_proj in deps {
413		let proj_value = module.heap().alloc(StarProject::clone(&dep_proj));
414		module.set(&dep_proj.name, proj_value);
415	}
416	{
417		let mut eval = Evaluator::new(&module);
418		// eval.enable_static_typechecking(true);
419		// eval.enable_profile(&starlark::eval::ProfileMode::Typecheck)?;
420		let globals = setup(&project_writable, global_options, package_options, toolchain);
421		eval.eval_module(ast, &globals).map_err(|e| e.into_anyhow())?;
422	}
423	let frozen_module = module.freeze()?;
424	let mut project = match project_writable.lock() {
425		Ok(x) => x.clone(),
426		Err(e) => return err_msg(format!("Could not lock project mutex: {e}")),
427	};
428	project.generator_names = frozen_module
429		.names()
430		.filter(|name| name.as_str().starts_with("__gen_"))
431		.map(|name| (name.as_str().to_string(), frozen_module.get(name.as_str()).unwrap()))
432		.collect();
433	Ok(project)
434}