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, GlobalsBuilder,
40 Module,
41 },
42 eval::Evaluator,
43 syntax::{
44 AstModule, 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 source: Option<String>,
75}
76
77#[derive(Debug, Deserialize)]
78struct DependencyManifest {
79 version: Option<String>,
80 registry: Option<String>,
81 channel: Option<String>,
82 path: Option<String>,
84 git: Option<String>,
86 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 hash: String,
173 manifest: String,
174 recipe: String,
175 }
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 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(®istry) {
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 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 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 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 )?; 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 )?;
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 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}