Skip to main content

libwally/
installation.rs

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    /// Create a new `InstallationContext` for the given path.
36    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    /// Delete the existing index, if it exists.
62    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    /// Install all packages from the given `Resolve` into the package that this
81    /// `InstallationContext` was built for.
82    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            // We do not need to install the root package, but we should create
114            // package links for its dependencies.
115            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    /// Contents of a package-to-package link within the same index.
180    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    /// Contents of a root-to-package link within the same index.
190    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    /// Contents of a link into the shared index from outside the shared index.
200    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    /// Contents of a link into the server index from outside the server index.
226    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
350/// Creates a suitable name for use in file paths that refer to this package.
351fn package_id_file_name(id: &PackageId) -> String {
352    format!(
353        "{}_{}@{}",
354        id.name().scope(),
355        id.name().name(),
356        id.version()
357    )
358}