pax_compiler/
lib.rs

1//! # The Pax Compiler Library
2//!
3//! `pax-compiler` is a collection of utilities to facilitate compiling Pax templates into Rust code.
4//!
5//! This library is structured into several modules, each providing different
6//! functionality:
7//!
8//! - `building`: Core structures and functions related to building management.
9//! - `utilities`: Helper functions and common routines used across the library.
10//!
11
12#[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
70/// For the specified file path or current working directory, first compile Pax project,
71/// then run it with a patched build of the `chassis` appropriate for the specified platform
72/// See: pax-compiler-sequence-diagram.png
73pub fn perform_build(ctx: &RunContext) -> eyre::Result<(PaxManifest, Option<PathBuf>), Report> {
74    //Compile ts files if applicable (this needs to happen before copying to .pax)
75    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 relevant path
106    copy_interface_files_for_target(ctx, &pax_dir);
107
108    println!("{} 🛠️  Building parser binary with `cargo`...", *PAX_BADGE);
109
110    // Run parser bin from host project with `--features parser`
111    let output = run_parser_binary(
112        &ctx.project_path,
113        Arc::clone(&ctx.process_child_ids),
114        ctx.should_run_designer,
115    );
116
117    // Forward stderr only
118    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    // Simple starting convention: first manifest is userland, second manifest is designer; other schemas are undefined
134    let mut userland_manifest = manifests.remove(0);
135
136    let mut merged_manifest = userland_manifest.clone();
137
138    //Hack: add a wrapper component so UniqueTemplateNodeIdentifier is a suitable uniqueid, even for root nodes
139    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    // source_map.extract_ranges_from_generated_code(cartridge_path.to_str().unwrap());
182
183    //7. Build full project from source
184    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    // Copy common files for macOS and iOS builds
220    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        // File src is include_dir — recursively extract files from include_dir into full_path
255        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
301/// Ejects the interface files for the specified target platform
302/// Interface files will then be used to build the project
303pub 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
363/// Clean all `.pax` temp files
364pub 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    // Abort if directory already exists
380    if full_path.exists() {
381        panic!("Error: destination `{:?}` already exists", full_path);
382    }
383    let _ = fs::create_dir_all(&full_path);
384
385    // clone template into full_path
386    if ctx.is_libdev_mode {
387        //For is_libdev_mode, we copy our monorepo @/pax-compiler/new-project-template directory
388        //to the target directly.  This enables iterating on new-project-template during libdev
389        //without the sticky caches associated with `include_dir`
390        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        // File src is include_dir — recursively extract files from include_dir into full_path
410        PAX_CREATE_TEMPLATE
411            .extract(&full_path)
412            .expect("Failed to extract files");
413    }
414
415    //Patch Cargo.toml
416    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    // Read the Cargo.toml
424    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 the `dependencies` section
430    update_pax_dependency_versions(&mut doc, &ctx.version);
431
432    // Update the `package` section
433    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    // Write the modified Cargo.toml back to disk
448    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
458/// Executes a shell command to run the feature-flagged parser at the specified path
459/// Returns an output object containing bytestreams of stdout/stderr as well as an exit code
460pub 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    // child.stdin.take().map(drop);
491    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    // Set a new process group for this command
521    unsafe {
522        libc::setpgid(0, 0);
523    }
524    Ok(())
525}