1mod error;
2mod install;
3mod sys;
4use crate::error::InstallError::{InstallFailed, InstallerCreatedFailed, LoadingInstallerFailed};
5pub use error::*;
6use install::utils;
7use install::{InstallManifest, Loader};
8use lazy_static::lazy_static;
9use log::{debug, info, trace};
10use ssri::Integrity;
11use std::collections::HashSet;
12use std::ops::{Deref, DerefMut};
13use std::path::{Path, PathBuf};
14use std::{fs, io};
15use sys::create_installer;
16pub use unity_hub::error::UnityError;
17pub use unity_hub::error::UnityHubError;
18pub use unity_hub::unity;
19use unity_hub::unity::hub;
20use unity_hub::unity::hub::editors::EditorInstallation;
21use unity_hub::unity::hub::module::Module;
22use unity_hub::unity::hub::paths::locks_dir;
23use unity_hub::unity::{Installation, UnityInstallation};
24pub use unity_version::error::VersionError;
25pub use unity_version::Version;
26use uvm_install_graph::{InstallGraph, InstallStatus, UnityComponent, Walker};
27pub use uvm_live_platform::error::LivePlatformError;
28pub use uvm_live_platform::fetch_release;
29use uvm_live_platform::Release;
30
31lazy_static! {
32 static ref UNITY_BASE_PATTERN: &'static Path = Path::new("{UNITY_PATH}");
33}
34
35impl AsRef<Path> for UNITY_BASE_PATTERN {
36 fn as_ref(&self) -> &Path {
37 self.deref()
38 }
39}
40
41fn print_graph<'a>(graph: &'a InstallGraph<'a>) {
42 use console::Style;
43
44 for node in graph.topo().iter(graph.context()) {
45 let component = graph.component(node).unwrap();
46 let install_status = graph.install_status(node).unwrap();
47 let prefix: String = [' '].iter().cycle().take(graph.depth(node) * 2).collect();
48
49 let style = match install_status {
50 InstallStatus::Unknown => Style::default().dim(),
51 InstallStatus::Missing => Style::default().yellow().blink(),
52 InstallStatus::Installed => Style::default().green(),
53 };
54
55 info!(
56 "{}- {} ({})",
57 prefix,
58 component,
59 style.apply_to(install_status)
60 );
61 }
62}
63
64pub fn install<V, P, I>(
65 version: V,
66 requested_modules: Option<I>,
67 install_sync: bool,
68 destination: Option<P>,
69) -> Result<UnityInstallation>
70where
71 V: AsRef<Version>,
72 P: AsRef<Path>,
73 I: IntoIterator,
74 I::Item: Into<String>,
75{
76 let version = version.as_ref();
77 let version_string = version.to_string();
78
79 let locks_dir = locks_dir().ok_or_else(|| {
80 InstallError::LockProcessFailure(io::Error::new(
81 io::ErrorKind::NotFound,
82 "Unable to locate locks directory.",
83 ))
84 })?;
85
86 fs::DirBuilder::new().recursive(true).create(&locks_dir)?;
87 lock_process!(locks_dir.join(format!("{}.lock", version_string)));
88
89 let unity_release = fetch_release(version.to_owned())?;
90 eprintln!("{:#?}", unity_release);
91 let mut graph = InstallGraph::from(&unity_release);
92
93 let mut editor_installation: Option<EditorInstallation> = None;
96 let base_dir = if let Some(destination) = destination {
97 let destination = destination.as_ref();
98 if destination.exists() && !destination.is_dir() {
99 return Err(io::Error::new(
100 io::ErrorKind::InvalidInput,
101 "Requested destination is not a directory.",
102 )
103 .into());
104 }
105
106 editor_installation = Some(EditorInstallation::new(
107 version.to_owned(),
108 destination.to_path_buf(),
109 ));
110 destination.to_path_buf()
111 } else {
112 hub::paths::install_path()
113 .map(|path| path.join(format!("{}", version)))
114 .or_else(|| {
115 {
116 #[cfg(any(target_os = "windows", target_os = "macos"))]
117 let application_path = dirs_2::application_dir();
118 #[cfg(target_os = "linux")]
119 let application_path = dirs_2::executable_dir();
120 application_path
121 }
122 .map(|path| path.join(format!("Unity-{}", version)))
123 })
124 .expect("default installation directory")
125 };
126
127 let installation = UnityInstallation::new(&base_dir);
128 if let Ok(ref installation) = installation {
129 let modules = installation.installed_modules()?;
130 let mut module_ids: HashSet<String> =
131 modules.into_iter().map(|m| m.id().to_string()).collect();
132 module_ids.insert("Unity".to_string());
133 graph.mark_installed(&module_ids);
134 } else {
135 info!("\nFresh install");
136 graph.mark_all_missing();
137 }
138
139 let base_iterator = ["Unity".to_string()].into_iter();
143 let all_components: HashSet<String> = match requested_modules {
144 Some(modules) => modules
145 .into_iter()
146 .flat_map(|module| {
147 let module = module.into();
148 let node = graph.get_node_id(&module).ok_or_else(|| {
149 debug!(
150 "Unsupported module '{}' for selected api version {}",
151 module, version
152 );
153 InstallError::UnsupportedModule(module.to_string(), version.to_string())
154 });
155
156 match node {
157 Ok(node) => {
158 let mut out = vec![Ok(module.to_string())];
159 out.append(
160 &mut graph
161 .get_dependend_modules(node)
162 .iter()
163 .map({
164 |((c, _), _)| match c {
165 UnityComponent::Editor(_) => Ok("Unity".to_string()),
166 UnityComponent::Module(m) => Ok(m.id().to_string()),
167 }
168 })
169 .collect(),
170 );
171 if install_sync {
172 out.append(
173 &mut graph
174 .get_sub_modules(node)
175 .iter()
176 .map({
177 |((c, _), _)| match c {
178 UnityComponent::Editor(_) => Ok("Unity".to_string()),
179 UnityComponent::Module(m) => Ok(m.id().to_string()),
180 }
181 })
182 .collect(),
183 );
184 }
185 out
186 }
187 Err(err) => vec![Err(err.into())],
188 }
189 })
190 .chain(base_iterator.map(|c| Ok(c)))
191 .collect::<Result<HashSet<_>>>(),
192 None => base_iterator.map(|c| Ok(c)).collect::<Result<HashSet<_>>>(),
193 }?;
194
195 debug!("\nAll requested components");
196 for c in all_components.iter() {
197 debug!("- {}", c);
198 }
199
200 graph.keep(&all_components);
201
202 info!("\nInstall Graph");
203 print_graph(&graph);
204
205 install_module_and_dependencies(&graph, &base_dir)?;
206 let installation = installation.or_else(|_| UnityInstallation::new(&base_dir))?;
207 let mut modules = match installation.get_modules() {
208 Err(_) => unity_release
209 .downloads
210 .first()
211 .cloned()
212 .map(|d| {
213 let mut modules = vec![];
214 for module in &d.modules {
215 fetch_modules_from_release(&mut modules, module);
216 }
217 modules
218 })
219 .unwrap(),
220 Ok(m) => m,
221 };
222
223 for module in modules.iter_mut() {
224 if module.is_installed == false {
225 module.is_installed = all_components.contains(module.id());
226 trace!("module {} is installed", module.id());
227 }
228 }
229
230 write_modules_json(&installation, modules)?;
231
232 if let Some(installation) = editor_installation {
234 let mut _editors = unity_hub::Editors::load().and_then(|mut editors| {
235 editors.add(&installation);
236 editors.flush()?;
237 Ok(())
238 });
239 }
240
241 Ok(installation)
242}
243
244fn fetch_modules_from_release(
245 modules: &mut Vec<Module>, module: &uvm_live_platform::Module,
246) {
247 modules.push(module.clone().into());
248 for sub_module in module.sub_modules() {
249 fetch_modules_from_release(modules, sub_module);
250 }
251}
252
253fn write_modules_json(
254 installation: &UnityInstallation,
255 modules: Vec<unity_hub::unity::hub::module::Module>,
256) -> io::Result<()> {
257 use console::style;
258 use std::fs::OpenOptions;
259 use std::io::Write;
260
261 let output_path = installation
262 .location()
263 .parent()
264 .unwrap()
265 .join("modules.json");
266 info!(
267 "{}",
268 style(format!("write {}", output_path.display())).green()
269 );
270 let mut f = OpenOptions::new()
271 .write(true)
272 .truncate(true)
273 .create(true)
274 .open(output_path)?;
275
276 let j = serde_json::to_string_pretty(&modules)?;
277 write!(f, "{}", j)?;
278 trace!("{}", j);
279 Ok(())
280}
281
282struct UnityComponent2<'a>(UnityComponent<'a>);
283
284impl<'a> Deref for UnityComponent2<'a> {
285 type Target = UnityComponent<'a>;
286
287 fn deref(&self) -> &Self::Target {
288 &self.0
289 }
290}
291
292impl<'a> DerefMut for UnityComponent2<'a> {
293 fn deref_mut(&mut self) -> &mut Self::Target {
294 &mut self.0
295 }
296}
297
298impl<'a> InstallManifest for UnityComponent2<'a> {
299 fn is_editor(&self) -> bool {
300 match self.0 {
301 UnityComponent::Editor(_) => true,
302 _ => false,
303 }
304 }
305 fn id(&self) -> &str {
306 match self.0 {
307 UnityComponent::Editor(_) => "Unity",
308 UnityComponent::Module(m) => m.id(),
309 }
310 }
311 fn install_size(&self) -> u64 {
312 let download_size = match self.0 {
313 UnityComponent::Editor(e) => e.download_size,
314 UnityComponent::Module(m) => m.download_size,
315 };
316 download_size.to_bytes() as u64
317 }
318
319 fn download_url(&self) -> &str {
320 match self.0 {
321 UnityComponent::Editor(e) => &e.release_file.url,
322 UnityComponent::Module(m) => &m.release_file().url,
323 }
324 }
325
326 fn integrity(&self) -> Option<Integrity> {
328 match self.0 {
329 UnityComponent::Editor(e) => e.release_file.integrity.clone(),
330 UnityComponent::Module(m) => m.release_file().integrity.clone(),
331 }
332 }
333
334 fn install_rename_from_to<P: AsRef<Path>>(&self, base_path: P) -> Option<(PathBuf, PathBuf)> {
335 match self.0 {
336 UnityComponent::Editor(_) => None,
337 UnityComponent::Module(m) => {
338 if let Some(extracted_path_rename) = &m.extracted_path_rename() {
339 Some((
340 strip_unity_base_url(&extracted_path_rename.from, &base_path),
341 strip_unity_base_url(&extracted_path_rename.to, &base_path),
342 ))
343 } else {
344 None
345 }
346 }
347 }
348 }
349
350 fn install_destination<P: AsRef<Path>>(&self, base_path: P) -> Option<PathBuf> {
351 match self.0 {
352 UnityComponent::Editor(_) => Some(base_path.as_ref().to_path_buf()),
353 UnityComponent::Module(m) => {
354 if let Some(destination) = &m.destination() {
355 Some(strip_unity_base_url(destination, &base_path))
356 } else {
357 None
358 }
359 }
360 }
361 }
362}
363
364fn strip_unity_base_url<P: AsRef<Path>, Q: AsRef<Path>>(path: P, base_dir: Q) -> PathBuf {
365 let path = path.as_ref();
366 base_dir
367 .as_ref()
368 .join(&path.strip_prefix(&UNITY_BASE_PATTERN).unwrap_or(path))
369}
370
371fn install_module_and_dependencies<'a, P: AsRef<Path>>(
372 graph: &'a InstallGraph<'a>,
373 base_dir: P,
374) -> Result<()> {
375 let base_dir = base_dir.as_ref();
376 for node in graph.topo().iter(graph.context()) {
377 if let Some(InstallStatus::Missing) = graph.install_status(node) {
378 let component = graph.component(node).unwrap();
379 let module = UnityComponent2(component);
380 let version = &graph.release().version;
381 let hash = &graph.release().short_revision;
382
383 info!("install {}", module.id());
384 info!("download installer for {}", module.id());
385
386 let loader = Loader::new(version, hash, &module);
387 let installer = loader
388 .download()
389 .map_err(|installer_err| LoadingInstallerFailed(installer_err))?;
390
391 info!("create installer for {}", component);
392 let installer = create_installer(base_dir, installer, &module)
393 .map_err(|installer_err| InstallerCreatedFailed(installer_err))?;
394
395 info!("install {}", component);
396 installer
397 .install()
398 .map_err(|installer_err| InstallFailed(module.id().to_string(), installer_err))?;
399 }
400 }
401
402 Ok(())
403}