1#[macro_use]
13extern crate serde;
14
15extern crate core;
16mod building;
17mod cartridge_generation;
18pub mod helpers;
19
20pub mod design_server;
21
22use color_eyre::eyre;
23use color_eyre::eyre::Report;
24use eyre::eyre;
25use fs_extra::dir::{self, CopyOptions};
26use helpers::{copy_dir_recursively, wait_with_output, ERR_SPAWN};
27use pax_manifest::{
28 ComponentDefinition, ComponentTemplate, PaxManifest, TemplateNodeDefinition, TypeId,
29};
30use std::fs;
31use std::io::Write;
32use std::sync::{Arc, Mutex};
33
34#[cfg(unix)]
35use std::os::unix::process::CommandExt;
36
37use crate::building::build_project_with_cartridge;
38
39use crate::cartridge_generation::generate_cartridge_partial_rs;
40use std::path::{Path, PathBuf};
41use std::process::{Command, Output};
42
43use crate::helpers::{
44 get_or_create_pax_directory, update_pax_dependency_versions, INTERFACE_DIR_NAME, PAX_BADGE,
45 PAX_CREATE_LIBDEV_TEMPLATE_DIR_NAME, PAX_CREATE_TEMPLATE, PAX_IOS_INTERFACE_TEMPLATE,
46 PAX_MACOS_INTERFACE_TEMPLATE, PAX_SWIFT_CARTRIDGE_TEMPLATE, PAX_SWIFT_COMMON_TEMPLATE,
47 PAX_WEB_INTERFACE_TEMPLATE,
48};
49
50pub struct RunContext {
51 pub target: RunTarget,
52 pub project_path: PathBuf,
53 pub verbose: bool,
54 pub should_also_run: bool,
55 pub is_libdev_mode: bool,
56 pub process_child_ids: Arc<Mutex<Vec<u64>>>,
57 pub should_run_designer: bool,
58 pub is_release: bool,
59}
60
61#[derive(PartialEq)]
62pub enum RunTarget {
63 #[allow(non_camel_case_types)]
64 macOS,
65 Web,
66 #[allow(non_camel_case_types)]
67 iOS,
68}
69
70pub fn perform_build(ctx: &RunContext) -> eyre::Result<(PaxManifest, Option<PathBuf>), Report> {
74 if ctx.is_libdev_mode && ctx.target == RunTarget::Web {
76 if let Ok(root) = std::env::var("PAX_WORKSPACE_ROOT") {
77 let mut cmd = Command::new("bash");
78 cmd.arg("./build-interface.sh");
79 let web_interface_path = Path::new(&root)
80 .join("pax-compiler")
81 .join("files")
82 .join("interfaces")
83 .join("web");
84 cmd.current_dir(&web_interface_path);
85 if !cmd
86 .output()
87 .expect("failed to start process")
88 .status
89 .success()
90 {
91 panic!(
92 "failed to build js files running ./build-interface.sh at {:?}",
93 web_interface_path
94 );
95 };
96 } else {
97 panic!(
98 "FATAL: PAX_WORKSPACE_ROOT env variable not set - didn't compile typescript files"
99 );
100 }
101 }
102
103 let pax_dir = get_or_create_pax_directory(&ctx.project_path);
104
105 copy_interface_files_for_target(ctx, &pax_dir);
107
108 println!("{} 🛠️ Building parser binary with `cargo`...", *PAX_BADGE);
109
110 let output = run_parser_binary(
112 &ctx.project_path,
113 Arc::clone(&ctx.process_child_ids),
114 ctx.should_run_designer,
115 );
116
117 std::io::stderr()
119 .write_all(output.stderr.as_slice())
120 .unwrap();
121
122 if !output.status.success() {
123 return Err(eyre!(
124 "Parsing failed — there is likely a syntax error in the provided pax"
125 ));
126 }
127
128 let out = String::from_utf8(output.stdout).unwrap();
129
130 let mut manifests: Vec<PaxManifest> =
131 serde_json::from_str(&out).expect(&format!("Malformed JSON from parser: {}", &out));
132
133 let mut userland_manifest = manifests.remove(0);
135
136 let mut merged_manifest = userland_manifest.clone();
137
138 let wrapper_type_id = TypeId::build_singleton("ROOT_COMPONENT", Some("RootComponent"));
140 let mut tnd = TemplateNodeDefinition::default();
141 tnd.type_id = userland_manifest.main_component_type_id.clone();
142 let mut wrapper_component_template = ComponentTemplate::new(wrapper_type_id.clone(), None);
143 wrapper_component_template.add(tnd);
144 userland_manifest.components.insert(
145 wrapper_type_id.clone(),
146 ComponentDefinition {
147 type_id: wrapper_type_id.clone(),
148 is_main_component: false,
149 is_primitive: false,
150 is_struct_only_component: false,
151 module_path: "".to_string(),
152 primitive_instance_import_path: None,
153 template: Some(wrapper_component_template),
154 settings: None,
155 },
156 );
157
158 let designer_manifest = if ctx.should_run_designer {
159 let designer_manifest = manifests.remove(0);
160 merged_manifest.merge_in_place(&designer_manifest);
161
162 userland_manifest
163 .components
164 .extend(designer_manifest.components.clone());
165 userland_manifest
166 .type_table
167 .extend(designer_manifest.type_table.clone());
168
169 Some(designer_manifest)
170 } else {
171 None
172 };
173
174 println!("{} 🦀 Generating Rust", *PAX_BADGE);
175 generate_cartridge_partial_rs(
176 &pax_dir,
177 &merged_manifest,
178 &userland_manifest,
179 designer_manifest,
180 );
181 println!("{} 🧱 Building project with `cargo`", *PAX_BADGE);
185 let build_dir = build_project_with_cartridge(
186 &pax_dir,
187 &ctx,
188 Arc::clone(&ctx.process_child_ids),
189 merged_manifest.assets_dirs,
190 userland_manifest.clone(),
191 )?;
192
193 Ok((userland_manifest, build_dir))
194}
195
196fn copy_interface_files_for_target(ctx: &RunContext, pax_dir: &PathBuf) {
197 let target_str: &str = (&ctx.target).into();
198 let target_str_lower = &target_str.to_lowercase();
199 let interface_path = pax_dir.join(INTERFACE_DIR_NAME).join(target_str_lower);
200
201 let _ = fs::remove_dir_all(&interface_path);
202 let _ = fs::create_dir_all(&interface_path);
203
204 let mut custom_interface = pax_dir
205 .parent()
206 .unwrap()
207 .join("interfaces")
208 .join(target_str_lower);
209 if ctx.target == RunTarget::Web {
210 custom_interface = custom_interface.join("public");
211 }
212
213 if custom_interface.exists() {
214 copy_interface_files(&custom_interface, &interface_path);
215 } else {
216 copy_default_interface_files(&interface_path, ctx);
217 }
218
219 if matches!(ctx.target, RunTarget::macOS | RunTarget::iOS) {
221 let common_dest = pax_dir.join(INTERFACE_DIR_NAME).join("common");
222 copy_common_swift_files(ctx, &common_dest);
223 }
224}
225
226fn copy_interface_files(src: &Path, dest: &Path) {
227 copy_dir_recursively(src, dest, &[]).expect("Failed to copy interface files");
228}
229
230fn copy_default_interface_files(interface_path: &Path, ctx: &RunContext) {
231 if ctx.is_libdev_mode {
232 let pax_compiler_root = Path::new(env!("CARGO_MANIFEST_DIR"));
233 let interface_src = match ctx.target {
234 RunTarget::Web => pax_compiler_root
235 .join("files")
236 .join("interfaces")
237 .join("web")
238 .join("public"),
239 RunTarget::macOS => pax_compiler_root
240 .join("files")
241 .join("interfaces")
242 .join("macos")
243 .join("pax-app-macos"),
244 RunTarget::iOS => pax_compiler_root
245 .join("files")
246 .join("interfaces")
247 .join("ios")
248 .join("pax-app-ios"),
249 };
250
251 copy_dir_recursively(&interface_src, interface_path, &[])
252 .expect("Failed to copy interface files");
253 } else {
254 match ctx.target {
256 RunTarget::Web => PAX_WEB_INTERFACE_TEMPLATE
257 .extract(interface_path)
258 .expect("Failed to extract web interface files"),
259 RunTarget::macOS => PAX_MACOS_INTERFACE_TEMPLATE
260 .extract(interface_path)
261 .expect("Failed to extract macos interface files"),
262 RunTarget::iOS => PAX_IOS_INTERFACE_TEMPLATE
263 .extract(interface_path)
264 .expect("Failed to extract ios interface files"),
265 }
266 }
267}
268
269fn copy_common_swift_files(ctx: &RunContext, common_dest: &Path) {
270 if ctx.is_libdev_mode {
271 let pax_compiler_root = Path::new(env!("CARGO_MANIFEST_DIR"));
272 let common_swift_cartridge_src = pax_compiler_root
273 .join("files")
274 .join("swift")
275 .join("pax-swift-cartridge");
276 let common_swift_common_src = pax_compiler_root
277 .join("files")
278 .join("swift")
279 .join("pax-swift-common");
280 let common_swift_cartridge_dest = common_dest.join("pax-swift-cartridge");
281 let common_swift_common_dest = common_dest.join("pax-swift-common");
282
283 copy_dir_recursively(
284 &common_swift_cartridge_src,
285 &common_swift_cartridge_dest,
286 &[],
287 )
288 .expect("Failed to copy swift cartridge files");
289 copy_dir_recursively(&common_swift_common_src, &common_swift_common_dest, &[])
290 .expect("Failed to copy swift common files");
291 } else {
292 PAX_SWIFT_COMMON_TEMPLATE
293 .extract(common_dest)
294 .expect("Failed to extract swift common template files");
295 PAX_SWIFT_CARTRIDGE_TEMPLATE
296 .extract(common_dest)
297 .expect("Failed to extract swift cartridge template files");
298 }
299}
300
301pub fn perform_eject(ctx: &RunContext) -> eyre::Result<(), Report> {
304 let pax_dir = get_or_create_pax_directory(&ctx.project_path);
305 eject_interface_files(ctx, &pax_dir);
306 Ok(())
307}
308
309fn eject_interface_files(ctx: &RunContext, pax_dir: &PathBuf) {
310 let target_str: &str = (&ctx.target).into();
311 let target_str_lower = &target_str.to_lowercase();
312 let custom_interfaces_dir = pax_dir.parent().unwrap().join("interfaces");
313 let mut target_custom_interface_dir = custom_interfaces_dir.join(target_str_lower);
314 if ctx.target == RunTarget::Web {
315 target_custom_interface_dir = target_custom_interface_dir.join("public");
316 }
317
318 let _ = fs::create_dir_all(&target_custom_interface_dir);
319
320 if ctx.is_libdev_mode {
321 let src_path = get_libdev_interface_path(ctx);
322 let _ = copy_dir_recursively(&src_path, &target_custom_interface_dir, &[]);
323 } else {
324 let _ = extract_interface_template(ctx, &target_custom_interface_dir);
325 }
326
327 println!(
328 "Interface files ejected to: {}",
329 target_custom_interface_dir.display()
330 );
331}
332
333fn get_libdev_interface_path(ctx: &RunContext) -> PathBuf {
334 let pax_compiler_root = Path::new(env!("CARGO_MANIFEST_DIR"));
335 match ctx.target {
336 RunTarget::Web => pax_compiler_root
337 .join("files")
338 .join("interfaces")
339 .join("web")
340 .join("public"),
341 RunTarget::macOS => pax_compiler_root
342 .join("files")
343 .join("interfaces")
344 .join("macos")
345 .join("pax-app-macos"),
346 RunTarget::iOS => pax_compiler_root
347 .join("files")
348 .join("interfaces")
349 .join("ios")
350 .join("pax-app-ios"),
351 }
352}
353
354fn extract_interface_template(ctx: &RunContext, dest: &Path) -> Result<(), std::io::Error> {
355 match ctx.target {
356 RunTarget::Web => PAX_WEB_INTERFACE_TEMPLATE.extract(dest)?,
357 RunTarget::macOS => PAX_MACOS_INTERFACE_TEMPLATE.extract(dest)?,
358 RunTarget::iOS => PAX_IOS_INTERFACE_TEMPLATE.extract(dest)?,
359 }
360 Ok(())
361}
362
363pub fn perform_clean(path: &str) {
365 let path = PathBuf::from(path);
366 let pax_dir = path.join(".pax");
367 fs::remove_dir_all(&pax_dir).ok();
368}
369
370pub struct CreateContext {
371 pub path: String,
372 pub is_libdev_mode: bool,
373 pub version: String,
374}
375
376pub fn perform_create(ctx: &CreateContext) {
377 let full_path = Path::new(&ctx.path);
378
379 if full_path.exists() {
381 panic!("Error: destination `{:?}` already exists", full_path);
382 }
383 let _ = fs::create_dir_all(&full_path);
384
385 if ctx.is_libdev_mode {
387 let pax_compiler_cargo_root = Path::new(env!("CARGO_MANIFEST_DIR"));
391 let template_src = pax_compiler_cargo_root
392 .join("files")
393 .join("new-project")
394 .join(PAX_CREATE_LIBDEV_TEMPLATE_DIR_NAME);
395
396 let mut options = CopyOptions::new();
397 options.overwrite = true;
398
399 for entry in std::fs::read_dir(&template_src).expect("Failed to read template directory") {
400 let entry_path = entry.expect("Failed to read entry").path();
401 if entry_path.is_dir() {
402 dir::copy(&entry_path, &full_path, &options).expect("Failed to copy directory");
403 } else {
404 fs::copy(&entry_path, full_path.join(entry_path.file_name().unwrap()))
405 .expect("Failed to copy file");
406 }
407 }
408 } else {
409 PAX_CREATE_TEMPLATE
411 .extract(&full_path)
412 .expect("Failed to extract files");
413 }
414
415 let cargo_template_path = full_path.join("Cargo.toml.template");
417 let extracted_cargo_toml_path = full_path.join("Cargo.toml");
418 let _ = fs::copy(&cargo_template_path, &extracted_cargo_toml_path);
419 let _ = fs::remove_file(&cargo_template_path);
420
421 let crate_name = full_path.file_name().unwrap().to_str().unwrap().to_string();
422
423 let mut doc = fs::read_to_string(&full_path.join("Cargo.toml"))
425 .expect("Failed to read Cargo.toml")
426 .parse::<toml_edit::Document>()
427 .expect("Failed to parse Cargo.toml");
428
429 update_pax_dependency_versions(&mut doc, &ctx.version);
431
432 if let Some(package) = doc
434 .as_table_mut()
435 .entry("package")
436 .or_insert_with(toml_edit::table)
437 .as_table_mut()
438 {
439 if let Some(name_item) = package.get_mut("name") {
440 *name_item = toml_edit::Item::Value(crate_name.into());
441 }
442 if let Some(version_item) = package.get_mut("version") {
443 *version_item = toml_edit::Item::Value(ctx.version.clone().into());
444 }
445 }
446
447 fs::write(&full_path.join("Cargo.toml"), doc.to_string())
449 .expect("Failed to write modified Cargo.toml");
450
451 println!(
452 "\nCreated new Pax project at {}.\nTo run:\n `cd {} && pax-cli run --target=web`",
453 full_path.to_str().unwrap(),
454 full_path.to_str().unwrap()
455 );
456}
457
458pub fn run_parser_binary(
461 project_path: &PathBuf,
462 process_child_ids: Arc<Mutex<Vec<u64>>>,
463 should_run_designer: bool,
464) -> Output {
465 let mut cmd = Command::new("cargo");
466 cmd.current_dir(project_path)
467 .arg("run")
468 .arg("--bin")
469 .arg("parser")
470 .arg("--features")
471 .arg("parser")
472 .arg("--profile")
473 .arg("parser")
474 .arg("--color")
475 .arg("always")
476 .stdout(std::process::Stdio::piped())
477 .stderr(std::process::Stdio::piped());
478
479 if should_run_designer {
480 cmd.arg("--features").arg("designer");
481 }
482
483 #[cfg(unix)]
484 unsafe {
485 cmd.pre_exec(pre_exec_hook);
486 }
487
488 let child = cmd.spawn().expect(ERR_SPAWN);
489
490 let output = wait_with_output(&process_child_ids, child);
492 output
493}
494
495impl From<&str> for RunTarget {
496 fn from(input: &str) -> Self {
497 match input.to_lowercase().as_str() {
498 "macos" => RunTarget::macOS,
499 "web" => RunTarget::Web,
500 "ios" => RunTarget::iOS,
501 _ => {
502 unreachable!()
503 }
504 }
505 }
506}
507
508impl<'a> Into<&'a str> for &'a RunTarget {
509 fn into(self) -> &'a str {
510 match self {
511 RunTarget::Web => "Web",
512 RunTarget::macOS => "macOS",
513 RunTarget::iOS => "iOS",
514 }
515 }
516}
517
518#[cfg(unix)]
519fn pre_exec_hook() -> Result<(), std::io::Error> {
520 unsafe {
522 libc::setpgid(0, 0);
523 }
524 Ok(())
525}