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 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 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}