1use std::fs;
4use std::path::Path;
5
6use anyhow::{Result, anyhow};
7use colored::Colorize;
8use mlua::LuaSerdeExt;
9use tar::Archive;
10use tempfile::Builder;
11use zoi_core::{types, utils};
12use zoi_package as package;
13use zoi_plugins::PluginManager;
14use zoi_resolver as resolver;
15use zstd::stream::read::Decoder as ZstdDecoder;
16
17fn install_app_from_archive(
19 archive_path: &Path,
20 destination_dir: &Path
21) -> Result<()> {
22 println!(
23 "Extracting app to '{}'...",
24 destination_dir.display().to_string().cyan()
25 );
26 let file = fs::File::open(archive_path)?;
27 let decoder = ZstdDecoder::new(file)?;
28 let mut archive = Archive::new(decoder);
29
30 let temp_extract_dir =
31 Builder::new().prefix("zoi-create-extract-").tempdir()?;
32
33 archive.unpack(temp_extract_dir.path())?;
34
35 let manifest_path = temp_extract_dir.path().join("manifest.json");
36 if !manifest_path.exists() {
37 let create_pkg_dir = temp_extract_dir.path().join("data/createpkgdir");
39 if !create_pkg_dir.exists() {
40 return Err(anyhow!(
41 "Archive is not a valid app package: missing 'manifest.json' \
42 or legacy 'data/createpkgdir'."
43 ));
44 }
45 utils::copy_dir_all(&create_pkg_dir, destination_dir)?;
46 return Ok(());
47 }
48
49 let content = fs::read_to_string(&manifest_path)?;
51 let pooled_manifest =
52 serde_json::from_str::<types::PooledZpaManifest>(&content)?;
53 let pool_dir = temp_extract_dir.path().join("pool");
54
55 if let Some(sub_mapping) = pooled_manifest.mappings.get("")
57 && let Some(scope_mapping) =
58 sub_mapping.scopes.get(&types::Scope::Project)
59 {
60 for mapped_dir in &scope_mapping.dirs {
61 if let Some(rel) = mapped_dir.path.strip_prefix("${createpkgdir}/")
62 {
63 fs::create_dir_all(destination_dir.join(rel))?;
64 }
65 }
66 for mapped_file in &scope_mapping.files {
67 if let Some(rel) = mapped_file.dest.strip_prefix("${createpkgdir}/")
68 {
69 let dest_path = destination_dir.join(rel);
70 if let Some(parent) = dest_path.parent() {
71 fs::create_dir_all(parent)?;
72 }
73 fs::copy(pool_dir.join(&mapped_file.hash), &dest_path)?;
74 }
75 }
76 for mapped_link in &scope_mapping.symlinks {
77 if let Some(rel) = mapped_link.link.strip_prefix("${createpkgdir}/")
78 {
79 let dest_path = destination_dir.join(rel);
80 if let Some(parent) = dest_path.parent() {
81 fs::create_dir_all(parent)?;
82 }
83 utils::symlink_file(
84 Path::new(&mapped_link.target),
85 &dest_path
86 )?;
87 }
88 }
89 }
90
91 Ok(())
92}
93
94pub fn run(
107 source: &str,
108 app_name: Option<String>,
109 yes: bool,
110 plugin_manager: Option<&PluginManager>
111) -> Result<()> {
112 let (pkg, _, _, pkg_lua_path, _, _, _) =
113 resolver::resolve::resolve_package_and_version(
114 source, None, false, false
115 )?;
116
117 if pkg.package_type != types::PackageType::App {
118 return Err(anyhow!(
119 "Package '{}' is not of type 'app'. Use 'zoi install' for \
120 packages and collections.",
121 pkg.name
122 ));
123 }
124
125 let mut pkg_val = None;
126 if let Some(pm) = plugin_manager {
127 let v = pm
128 .lua
129 .to_value(&pkg)
130 .map_err(|e: mlua::Error| anyhow!(e.to_string()))?;
131 pm.trigger_hook("on_pre_create", Some(&v.clone()))?;
132 pkg_val = Some(v);
133 }
134
135 let dest_name = app_name.unwrap_or_else(|| pkg.name.clone());
136 let app_dir = Path::new(&dest_name);
137
138 if app_dir.exists() {
139 if app_dir.is_dir() {
140 if fs::read_dir(app_dir)?.next().is_some() {
141 println!(
142 "{}",
143 format!(
144 "Warning: Directory '{dest_name}' already exists and \
145 is not empty."
146 )
147 .yellow()
148 );
149 if !utils::ask_for_confirmation("Do you want to continue?", yes)
150 {
151 return Err(anyhow!("Operation aborted by user."));
152 }
153 }
154 } else {
155 return Err(anyhow!(
156 "A file with the name '{dest_name}' already exists."
157 ));
158 }
159 }
160
161 println!(
162 "Creating app '{}' using template '{}'...",
163 dest_name.cyan(),
164 pkg.name.green()
165 );
166
167 let build_dir = Builder::new().prefix("zoi-create-build-").tempdir()?;
168
169 package::build::run(
170 &pkg_lua_path,
171 Some("source"),
172 &[utils::get_platform()?],
173 None,
174 Some(build_dir.path()),
175 pkg.version.as_deref(),
176 None,
177 false,
178 "native",
179 None,
180 false,
181 false,
182 false
183 )?;
184
185 let archive_filename = format!(
186 "{}-{}-{}.zpa",
187 pkg.name,
188 pkg.version.as_deref().unwrap_or_default(),
189 utils::get_platform()?,
190 );
191 let archive_path = build_dir.path().join(archive_filename);
192
193 if !archive_path.exists() {
194 return Err(anyhow!("Build failed to produce an archive."));
195 }
196
197 install_app_from_archive(&archive_path, app_dir)?;
198
199 if let (Some(pm), Some(v)) = (plugin_manager, pkg_val) {
200 pm.trigger_hook_nonfatal("on_post_create", Some(&v));
201 }
202
203 println!("\n{}", "App created successfully.".green());
204
205 Ok(())
206}