1use std::{
2 fmt::Display,
3 io,
4 path::{Path, PathBuf},
5 time::Duration,
6};
7
8use anyhow::{bail, format_err};
9use crossterm::style::{Color, SetForegroundColor};
10use fs_err as fs;
11use indicatif::{ProgressBar, ProgressStyle};
12use indoc::{formatdoc, indoc};
13
14use crate::{
15 manifest::Realm,
16 package_contents::PackageContents,
17 package_id::PackageId,
18 package_source::{PackageSourceMap, PackageSourceProvider},
19 resolution::Resolve,
20};
21
22#[derive(Clone)]
23pub struct InstallationContext {
24 shared_dir: PathBuf,
25 shared_index_dir: PathBuf,
26 shared_path: Option<String>,
27 server_dir: PathBuf,
28 server_index_dir: PathBuf,
29 server_path: Option<String>,
30 dev_dir: PathBuf,
31 dev_index_dir: PathBuf,
32}
33
34impl InstallationContext {
35 pub fn new(
37 project_path: &Path,
38 shared_path: Option<String>,
39 server_path: Option<String>,
40 ) -> Self {
41 let shared_dir = project_path.join("Packages");
42 let server_dir = project_path.join("ServerPackages");
43 let dev_dir = project_path.join("DevPackages");
44
45 let shared_index_dir = shared_dir.join("_Index");
46 let server_index_dir = server_dir.join("_Index");
47 let dev_index_dir = dev_dir.join("_Index");
48
49 Self {
50 shared_dir,
51 shared_index_dir,
52 shared_path,
53 server_dir,
54 server_index_dir,
55 server_path,
56 dev_dir,
57 dev_index_dir,
58 }
59 }
60
61 pub fn clean(&self) -> anyhow::Result<()> {
63 fn remove_ignore_not_found(path: &Path) -> io::Result<()> {
64 if let Err(err) = fs::remove_dir_all(path) {
65 if err.kind() != io::ErrorKind::NotFound {
66 return Err(err);
67 }
68 }
69
70 Ok(())
71 }
72
73 remove_ignore_not_found(&self.shared_dir)?;
74 remove_ignore_not_found(&self.server_dir)?;
75 remove_ignore_not_found(&self.dev_dir)?;
76
77 Ok(())
78 }
79
80 pub fn install(
83 self,
84 sources: PackageSourceMap,
85 root_package_id: PackageId,
86 resolved: Resolve,
87 ) -> anyhow::Result<()> {
88 let mut handles = Vec::new();
89 let resolved_copy = resolved.clone();
90 let bar = ProgressBar::new((resolved_copy.activated.len() - 1) as u64).with_style(
91 ProgressStyle::with_template(
92 "{spinner:.cyan.bold} {pos}/{len} [{wide_bar:.cyan/blue}]",
93 )
94 .unwrap()
95 .tick_chars("⠁⠈⠐⠠⠄⠂ ")
96 .progress_chars("#>-"),
97 );
98 bar.enable_steady_tick(Duration::from_millis(100));
99
100 let runtime = tokio::runtime::Builder::new_multi_thread()
101 .worker_threads(50)
102 .enable_all()
103 .build()
104 .unwrap();
105
106 for package_id in resolved_copy.activated {
107 log::debug!("Installing {}...", package_id);
108
109 let shared_deps = resolved.shared_dependencies.get(&package_id);
110 let server_deps = resolved.server_dependencies.get(&package_id);
111 let dev_deps = resolved.dev_dependencies.get(&package_id);
112
113 if package_id == root_package_id {
116 if let Some(deps) = shared_deps {
117 self.write_root_package_links(Realm::Shared, deps, &resolved)?;
118 }
119
120 if let Some(deps) = server_deps {
121 self.write_root_package_links(Realm::Server, deps, &resolved)?;
122 }
123
124 if let Some(deps) = dev_deps {
125 self.write_root_package_links(Realm::Dev, deps, &resolved)?;
126 }
127 } else {
128 let metadata = resolved.metadata.get(&package_id).unwrap();
129 let package_realm = metadata.origin_realm;
130
131 if let Some(deps) = shared_deps {
132 self.write_package_links(&package_id, package_realm, deps, &resolved)?;
133 }
134
135 if let Some(deps) = server_deps {
136 self.write_package_links(&package_id, package_realm, deps, &resolved)?;
137 }
138
139 if let Some(deps) = dev_deps {
140 self.write_package_links(&package_id, package_realm, deps, &resolved)?;
141 }
142
143 let source_registry = resolved_copy.metadata[&package_id].source_registry.clone();
144 let source_copy = sources.clone();
145 let context = self.clone();
146 let b = bar.clone();
147
148 let handle = runtime.spawn_blocking(move || {
149 let package_source = source_copy.get(&source_registry).unwrap();
150 let contents = package_source.download_package(&package_id)?;
151 b.println(format!(
152 "{} Downloaded {}{}",
153 SetForegroundColor(Color::DarkGreen),
154 SetForegroundColor(Color::Reset),
155 package_id,
156 ));
157 b.inc(1);
158 context.write_contents(&package_id, &contents, package_realm)
159 });
160
161 handles.push(handle);
162 }
163 }
164
165 let num_packages = handles.len();
166
167 for handle in handles {
168 runtime
169 .block_on(handle)
170 .expect("Package failed to be installed.")?;
171 }
172
173 bar.finish_and_clear();
174 log::info!("Downloaded {} packages!", num_packages);
175
176 Ok(())
177 }
178
179 fn link_sibling_same_index(&self, id: &PackageId) -> String {
181 formatdoc! {r#"
182 return require(script.Parent.Parent["{full_name}"]["{short_name}"])
183 "#,
184 full_name = package_id_file_name(id),
185 short_name = id.name().name()
186 }
187 }
188
189 fn link_root_same_index(&self, id: &PackageId) -> String {
191 formatdoc! {r#"
192 return require(script.Parent._Index["{full_name}"]["{short_name}"])
193 "#,
194 full_name = package_id_file_name(id),
195 short_name = id.name().name()
196 }
197 }
198
199 fn link_shared_index(&self, id: &PackageId) -> anyhow::Result<String> {
201 let shared_path = self.shared_path.as_ref().ok_or_else(|| {
202 format_err!(indoc! {r#"
203 A server or dev dependency is depending on a shared dependency.
204 To link these packages correctly you must declare where shared
205 packages are placed in the roblox datamodel in your wally.toml.
206
207 This typically looks like:
208
209 [place]
210 shared-packages = "game.ReplicatedStorage.Packages"
211 "#})
212 })?;
213
214 let contents = formatdoc! {r#"
215 return require({packages}._Index["{full_name}"]["{short_name}"])
216 "#,
217 packages = shared_path,
218 full_name = package_id_file_name(id),
219 short_name = id.name().name()
220 };
221
222 Ok(contents)
223 }
224
225 fn link_server_index(&self, id: &PackageId) -> anyhow::Result<String> {
227 let server_path = self.server_path.as_ref().ok_or_else(|| {
228 format_err!(indoc! {r#"
229 A dev dependency is depending on a server dependency.
230 To link these packages correctly you must declare where server
231 packages are placed in the roblox datamodel in your wally.toml.
232
233 This typically looks like:
234
235 [place]
236 server-packages = "game.ServerScriptService.Packages"
237 "#})
238 })?;
239
240 let contents = formatdoc! {r#"
241 return require({packages}._Index["{full_name}"]["{short_name}"])
242 "#,
243 packages = server_path,
244 full_name = package_id_file_name(id),
245 short_name = id.name().name()
246 };
247
248 Ok(contents)
249 }
250
251 fn write_root_package_links<'a, K: Display>(
252 &self,
253 root_realm: Realm,
254 dependencies: impl IntoIterator<Item = (K, &'a PackageId)>,
255 resolved: &Resolve,
256 ) -> anyhow::Result<()> {
257 log::debug!("Writing root package links");
258
259 let base_path = match root_realm {
260 Realm::Shared => &self.shared_dir,
261 Realm::Server => &self.server_dir,
262 Realm::Dev => &self.dev_dir,
263 };
264
265 log::trace!("Creating directory {}", base_path.display());
266 fs::create_dir_all(base_path)?;
267
268 for (dep_name, dep_package_id) in dependencies {
269 let dependencies_realm = resolved.metadata.get(dep_package_id).unwrap().origin_realm;
270 let path = base_path.join(format!("{}.lua", dep_name));
271
272 let contents = match (root_realm, dependencies_realm) {
273 (source, dest) if source == dest => self.link_root_same_index(dep_package_id),
274 (_, Realm::Server) => self.link_server_index(dep_package_id)?,
275 (_, Realm::Shared) => self.link_shared_index(dep_package_id)?,
276 (_, Realm::Dev) => {
277 bail!("A dev dependency cannot be depended upon by a non-dev dependency")
278 }
279 };
280
281 log::trace!("Writing {}", path.display());
282 fs::write(path, contents)?;
283 }
284
285 Ok(())
286 }
287
288 fn write_package_links<'a, K: std::fmt::Display>(
289 &self,
290 package_id: &PackageId,
291 package_realm: Realm,
292 dependencies: impl IntoIterator<Item = (K, &'a PackageId)>,
293 resolved: &Resolve,
294 ) -> anyhow::Result<()> {
295 log::debug!("Writing package links for {}", package_id);
296
297 let mut base_path = match package_realm {
298 Realm::Shared => self.shared_index_dir.clone(),
299 Realm::Server => self.server_index_dir.clone(),
300 Realm::Dev => self.dev_index_dir.clone(),
301 };
302
303 base_path.push(package_id_file_name(package_id));
304
305 log::trace!("Creating directory {}", base_path.display());
306 fs::create_dir_all(&base_path)?;
307
308 for (dep_name, dep_package_id) in dependencies {
309 let dependencies_realm = resolved.metadata.get(dep_package_id).unwrap().origin_realm;
310 let path = base_path.join(format!("{}.lua", dep_name));
311
312 let contents = match (package_realm, dependencies_realm) {
313 (source, dest) if source == dest => self.link_sibling_same_index(dep_package_id),
314 (_, Realm::Server) => self.link_server_index(dep_package_id)?,
315 (_, Realm::Shared) => self.link_shared_index(dep_package_id)?,
316 (_, Realm::Dev) => {
317 bail!("A dev dependency cannot be depended upon by a non-dev dependency")
318 }
319 };
320
321 log::trace!("Writing {}", path.display());
322 fs::write(path, contents)?;
323 }
324
325 Ok(())
326 }
327
328 fn write_contents(
329 &self,
330 package_id: &PackageId,
331 contents: &PackageContents,
332 realm: Realm,
333 ) -> anyhow::Result<()> {
334 let mut path = match realm {
335 Realm::Shared => self.shared_index_dir.clone(),
336 Realm::Server => self.server_index_dir.clone(),
337 Realm::Dev => self.dev_index_dir.clone(),
338 };
339
340 path.push(package_id_file_name(package_id));
341 path.push(package_id.name().name());
342
343 fs::create_dir_all(&path)?;
344 contents.unpack_into_path(&path)?;
345
346 Ok(())
347 }
348}
349
350fn package_id_file_name(id: &PackageId) -> String {
352 format!(
353 "{}_{}@{}",
354 id.name().scope(),
355 id.name().name(),
356 id.version()
357 )
358}