Skip to main content

phlow_runtime/
package.rs

1use crate::MODULE_EXTENSION;
2use anyhow::{Context, Result, anyhow, bail};
3use log::info;
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use std::{fs, path::PathBuf, process::Command};
7
8#[derive(Debug)]
9pub struct Package {
10    pub module_dir: PathBuf,
11    pub package_target: String,
12    pub create_tar: bool,
13}
14
15#[derive(Deserialize, Serialize)]
16struct ModuleMetadata {
17    name: String,
18    version: String,
19}
20
21impl Package {
22    pub fn new(module_dir: PathBuf, package_target: String, create_tar: bool) -> Result<Self> {
23        if !module_dir.exists() {
24            bail!("Directory not found: {}", module_dir.display());
25        }
26
27        info!(
28            "Initializing Package struct for directory: {}",
29            module_dir.display()
30        );
31        Ok(Package {
32            module_dir,
33            package_target,
34            create_tar,
35        })
36    }
37
38    pub fn run(&self) -> Result<()> {
39        let archive_name = self.create_package().with_context(|| {
40            format!("Failed to create package in {}", self.module_dir.display())
41        })?;
42
43        info!("Package created: {}", archive_name);
44        Ok(())
45    }
46
47    fn create_package(&self) -> Result<String> {
48        let release_dir = PathBuf::from("target/release");
49
50        info!(
51            "Searching for metadata file in: {}",
52            self.module_dir.display()
53        );
54
55        let metadata_path = ["main.phlow", "phlow.yaml", "phlow.yml"]
56            .iter()
57            .map(|f| self.module_dir.join(f))
58            .find(|p| p.exists())
59            .ok_or_else(|| anyhow!("No main.phlow file found in {}", self.module_dir.display()))?;
60
61        info!("Metadata file found: {}", metadata_path.display());
62
63        let metadata: ModuleMetadata = {
64            let content = fs::read_to_string(&metadata_path)?;
65            serde_yaml::from_str(&content).with_context(|| {
66                format!("Failed to parse YAML file: {}", metadata_path.display())
67            })?
68        };
69
70        info!(
71            "Metadata loaded:\n  - name: {}\n  - version: {}\n ",
72            metadata.name, metadata.version
73        );
74
75        info!("Validating version...");
76        let version_regex = Regex::new(r"^\d+\.\d+\.\d+(?:-[\w\.-]+)?(?:\+[\w\.-]+)?$")?;
77        if !version_regex.is_match(&metadata.version) {
78            bail!("Invalid version: must follow MAJOR.MINOR.PATCH-prerelease+build format");
79        }
80
81        info!("Starting project build...");
82        Command::new("cargo")
83            .args(["build", "--release", "--locked"])
84            .status()
85            .context("Failed to run cargo build")?
86            .success()
87            .then_some(())
88            .context("Build failed")?;
89
90        let so_name = format!("lib{}.{}", metadata.name, MODULE_EXTENSION);
91        let so_path = release_dir.join(&so_name);
92        if !so_path.exists() {
93            bail!("Missing .{} file: {}", MODULE_EXTENSION, so_path.display());
94        }
95
96        if self.create_tar {
97            // Create .tar.gz archive
98            let temp_dir = PathBuf::from(format!(".tmp/{}", metadata.name));
99            info!("Creating temporary directory: {}", temp_dir.display());
100            fs::create_dir_all(&temp_dir)?;
101
102            info!(
103                "Copying .{} file from {} to {}",
104                MODULE_EXTENSION,
105                so_path.display(),
106                temp_dir.display()
107            );
108            fs::copy(
109                &so_path,
110                temp_dir.join(format!("module.{}", MODULE_EXTENSION)),
111            )?;
112
113            info!("Copying metadata file to temp folder");
114            fs::copy(
115                &metadata_path,
116                temp_dir.join(metadata_path.file_name().unwrap()),
117            )?;
118
119            let archive_name = format!("{}-{}.tar.gz", metadata.name, metadata.version);
120
121            info!("Creating archive: {}", archive_name);
122            let status = Command::new("tar")
123                .args(["-czf", &archive_name, "-C"])
124                .arg(temp_dir.to_str().unwrap())
125                .arg(".")
126                .status()
127                .context("Failed to create archive")?;
128
129            if !status.success() {
130                bail!("Failed to generate package: {}", archive_name);
131            }
132
133            info!("Success! Package created: {} 🎉", archive_name);
134
135            info!("Cleaning up temporary directory: {}", temp_dir.display());
136            fs::remove_dir_all(&temp_dir).with_context(|| {
137                format!(
138                    "Failed to remove temporary directory: {}",
139                    temp_dir.display()
140                )
141            })?;
142
143            Ok(archive_name)
144        } else {
145            // Save directly to package_target/module-name
146            let package_dir = PathBuf::from(&self.package_target).join(&metadata.name);
147
148            info!("Creating package directory: {}", package_dir.display());
149            fs::create_dir_all(&package_dir)?;
150
151            info!(
152                "Copying .{} file from {} to {}",
153                MODULE_EXTENSION,
154                so_path.display(),
155                package_dir.display()
156            );
157            fs::copy(
158                &so_path,
159                package_dir.join(format!("module.{}", MODULE_EXTENSION)),
160            )?;
161
162            info!("Copying metadata file to package folder");
163            fs::copy(
164                &metadata_path,
165                package_dir.join(metadata_path.file_name().unwrap()),
166            )?;
167
168            let result = format!("{}/{}", self.package_target, metadata.name);
169            info!("Success! Package created in: {} 🎉", result);
170
171            Ok(result)
172        }
173    }
174}