vrc_get_vpm/unity_project/
pending_project_changes.rs

1use crate::io::{DefaultProjectIo, DirEntry, IoTrait};
2use crate::traits::AbortCheck;
3use crate::unity_project::find_legacy_assets::collect_legacy_assets;
4use crate::utils::{PathBufExt, walk_dir_relative};
5use crate::version::DependencyRange;
6use crate::{PackageInfo, UnityProject, unity_compatible};
7use crate::{PackageInstaller, io};
8use either::Either;
9use futures::future::{join, join_all};
10use futures::prelude::*;
11use indexmap::IndexSet;
12use log::debug;
13use std::collections::hash_map::Entry;
14use std::collections::{HashMap, HashSet, VecDeque};
15use std::future::ready;
16use std::marker::PhantomData;
17use std::path::{Path, PathBuf};
18use std::pin::pin;
19use std::sync::OnceLock;
20
21/// Represents Packages to be added and folders / packages to be removed
22///
23/// In vrc-get, Adding package is divided into two phases:
24/// - Collect modifications
25/// - Apply collected changes
26///
27/// This is done to ask users before removing packages
28#[derive(Debug)]
29pub struct PendingProjectChanges<'env> {
30    pub(crate) package_changes: HashMap<Box<str>, PackageChange<'env>>,
31
32    pub(crate) remove_legacy_files: Vec<(Box<Path>, &'env str)>,
33    pub(crate) remove_legacy_folders: Vec<(Box<Path>, &'env str)>,
34
35    pub(crate) conflicts: HashMap<Box<str>, ConflictInfo>,
36}
37
38#[derive(Debug)]
39pub enum PackageChange<'env> {
40    Install(Install<'env>),
41    Remove(Remove<'env>),
42}
43
44impl<'env> PackageChange<'env> {
45    pub fn as_install(&self) -> Option<&Install<'env>> {
46        match self {
47            PackageChange::Install(x) => Some(x),
48            PackageChange::Remove(_) => None,
49        }
50    }
51
52    pub fn as_remove(&self) -> Option<&Remove<'env>> {
53        match self {
54            PackageChange::Install(_) => None,
55            PackageChange::Remove(x) => Some(x),
56        }
57    }
58}
59
60#[derive(Debug)]
61pub struct Install<'env> {
62    package: Option<PackageInfo<'env>>,
63    add_to_locked: bool,
64    to_dependencies: Option<DependencyRange>,
65}
66
67impl<'env> Install<'env> {
68    pub fn install_package(&self) -> Option<PackageInfo<'env>> {
69        self.package
70    }
71
72    pub fn is_adding_to_locked(&self) -> bool {
73        self.add_to_locked
74    }
75
76    pub fn to_dependencies(&self) -> Option<&DependencyRange> {
77        self.to_dependencies.as_ref()
78    }
79}
80
81#[derive(Debug)]
82pub struct Remove<'env> {
83    reason: RemoveReason,
84    _phantom: PhantomData<&'env ()>,
85}
86
87impl Remove<'_> {
88    pub fn reason(&self) -> RemoveReason {
89        self.reason
90    }
91}
92
93#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
94pub enum RemoveReason {
95    Requested,
96    Legacy,
97    Unused,
98}
99
100#[derive(Debug, Default)]
101pub struct ConflictInfo {
102    conflicts_packages: Vec<Box<str>>,
103    conflicts_with_unity: bool,
104    // The value is the name of directory that is installed as unlocked
105    unlocked_names: Vec<Box<str>>,
106}
107
108impl ConflictInfo {
109    pub fn conflicting_packages(&self) -> &[Box<str>] {
110        self.conflicts_packages.as_slice()
111    }
112
113    pub fn conflicts_with_unity(&self) -> bool {
114        self.conflicts_with_unity
115    }
116
117    pub fn unlocked_names(&self) -> &[Box<str>] {
118        self.unlocked_names.as_slice()
119    }
120}
121
122pub(crate) struct Builder<'env> {
123    package_changes: HashMap<Box<str>, PackageChange<'env>>,
124    conflicts: HashMap<Box<str>, ConflictInfo>,
125}
126
127impl<'env> Builder<'env> {
128    pub fn new() -> Self {
129        Self {
130            package_changes: HashMap::new(),
131            conflicts: HashMap::new(),
132        }
133    }
134
135    pub fn add_to_dependencies(&mut self, name: Box<str>, version: DependencyRange) -> &mut Self {
136        match self.package_changes.entry(name) {
137            Entry::Occupied(mut e) => match e.get_mut() {
138                PackageChange::Install(e) => {
139                    if e.to_dependencies.is_none() {
140                        e.to_dependencies = Some(version);
141                    } else {
142                        panic!("INTERNAL ERROR: already add_to_dependencies");
143                    }
144                }
145                PackageChange::Remove(_) => {
146                    panic!("INTERNAL ERROR: add_to_dependencies for removed");
147                }
148            },
149            Entry::Vacant(e) => {
150                e.insert(PackageChange::Install(Install {
151                    package: None,
152                    add_to_locked: false,
153                    to_dependencies: Some(version),
154                }));
155            }
156        }
157
158        self
159    }
160
161    pub fn install_to_locked(&mut self, info: PackageInfo<'env>) -> &mut Self {
162        match self.package_changes.entry(info.name().into()) {
163            Entry::Occupied(mut e) => match e.get_mut() {
164                PackageChange::Install(e) => {
165                    if e.package.is_none() {
166                        e.package = Some(info);
167                        e.add_to_locked = true;
168                    } else {
169                        panic!("INTERNAL ERROR: already install");
170                    }
171                }
172                PackageChange::Remove(_) => {
173                    panic!("INTERNAL ERROR: install for removed");
174                }
175            },
176            Entry::Vacant(e) => {
177                e.insert(PackageChange::Install(Install {
178                    package: Some(info),
179                    add_to_locked: true,
180                    to_dependencies: None,
181                }));
182            }
183        }
184        self
185    }
186
187    pub fn install_already_locked(&mut self, info: PackageInfo<'env>) -> &mut Self {
188        match self.package_changes.entry(info.name().into()) {
189            Entry::Occupied(mut e) => match e.get_mut() {
190                PackageChange::Install(e) => {
191                    if e.package.is_none() {
192                        e.package = Some(info);
193                        e.add_to_locked = false;
194                    } else {
195                        panic!("INTERNAL ERROR: already install");
196                    }
197                }
198                PackageChange::Remove(_) => {
199                    panic!("INTERNAL ERROR: install for removed");
200                }
201            },
202            Entry::Vacant(e) => {
203                e.insert(PackageChange::Install(Install {
204                    package: Some(info),
205                    add_to_locked: false,
206                    to_dependencies: None,
207                }));
208            }
209        }
210        self
211    }
212
213    pub fn unlocked_installation_conflict(
214        &mut self,
215        name: Box<str>,
216        unlocked_name: Box<str>,
217    ) -> &mut Self {
218        self.conflicts
219            .entry(name)
220            .or_default()
221            .unlocked_names
222            .push(unlocked_name);
223        self
224    }
225
226    pub fn conflict_multiple(
227        &mut self,
228        name: Box<str>,
229        conflict: impl IntoIterator<Item = Box<str>>,
230    ) -> &mut Self {
231        self.conflicts
232            .entry(name)
233            .or_default()
234            .conflicts_packages
235            .extend(conflict);
236        self
237    }
238
239    pub fn conflicts(&mut self, name: Box<str>, conflict: Box<str>) -> &mut Self {
240        self.conflicts
241            .entry(name)
242            .or_default()
243            .conflicts_packages
244            .push(conflict);
245        self
246    }
247
248    pub fn conflicts_unity(&mut self, name: Box<str>) -> &mut Self {
249        self.conflicts.entry(name).or_default().conflicts_with_unity = true;
250        self
251    }
252
253    pub fn remove(&mut self, name: Box<str>, reason: RemoveReason) -> &mut Self {
254        match self.package_changes.entry(name) {
255            Entry::Occupied(mut e) => match e.get_mut() {
256                PackageChange::Install(_) => {
257                    panic!("INTERNAL ERROR: remove for installed");
258                }
259                PackageChange::Remove(e) => {
260                    if e.reason != reason {
261                        panic!("INTERNAL ERROR: already remove");
262                    }
263                }
264            },
265            Entry::Vacant(e) => {
266                e.insert(PackageChange::Remove(Remove {
267                    reason,
268                    _phantom: PhantomData,
269                }));
270            }
271        }
272        self
273    }
274
275    fn remove_unused(&mut self, name: Box<str>) -> &mut Self {
276        match self.package_changes.entry(name) {
277            Entry::Occupied(mut e) => match e.get_mut() {
278                PackageChange::Install(_) => {
279                    panic!("INTERNAL ERROR: remove_unused for installed");
280                }
281                PackageChange::Remove(_) => {
282                    // already removed, do nothing
283                }
284            },
285            Entry::Vacant(e) => {
286                e.insert(PackageChange::Remove(Remove {
287                    reason: RemoveReason::Unused,
288                    _phantom: PhantomData,
289                }));
290            }
291        }
292        self
293    }
294
295    pub(crate) fn get_dependencies(&self, name: &str) -> Option<&DependencyRange> {
296        self.package_changes
297            .get(name)
298            .and_then(|x| x.as_install())
299            .and_then(|x| x.to_dependencies.as_ref())
300    }
301
302    pub(crate) fn get_installing(&self, name: &str) -> Option<PackageInfo<'env>> {
303        self.package_changes
304            .get(name)
305            .and_then(|x| x.as_install())
306            .filter(|x| x.add_to_locked)
307            .and_then(|x| x.package)
308    }
309
310    pub(crate) fn get_all_installing(&self) -> impl Iterator<Item = PackageInfo<'env>> + '_ {
311        self.package_changes
312            .values()
313            .filter_map(|x| x.as_install())
314            .filter(|x| x.add_to_locked)
315            .filter_map(|x| x.package)
316    }
317
318    pub fn build_no_resolve(self) -> PendingProjectChanges<'env> {
319        for change in self.package_changes.values() {
320            match change {
321                PackageChange::Install(change) => {
322                    if change.package.is_some() {
323                        panic!("INTERNAL ERROR: install package requires resolve")
324                    }
325                }
326                PackageChange::Remove(_) => {
327                    panic!("INTERNAL ERROR: remove requires resolve")
328                }
329            }
330        }
331
332        PendingProjectChanges {
333            package_changes: self.package_changes,
334            conflicts: self.conflicts,
335
336            remove_legacy_files: vec![],
337            remove_legacy_folders: vec![],
338        }
339    }
340
341    pub async fn build_resolve(
342        mut self,
343        unity_project: &UnityProject,
344    ) -> PendingProjectChanges<'env> {
345        let installs = Vec::from_iter(
346            self.package_changes
347                .values()
348                .filter_map(|x| x.as_install())
349                .filter(|x| x.add_to_locked)
350                .map(|x| x.package.unwrap()),
351        );
352
353        debug!("checking for unity compatibility");
354
355        for package in installs
356            .iter()
357            .filter(|pkg| !unity_compatible(pkg.package_json(), unity_project.unity_version))
358            .map(|pkg| pkg.name().into())
359        {
360            self.conflicts_unity(package);
361        }
362
363        debug!("Finding unused packages");
364
365        self.mark_and_sweep_packages(unity_project);
366
367        debug!("Collecting legacy assets");
368
369        let legacy_assets =
370            collect_legacy_assets(&unity_project.io, &installs, unity_project).await;
371
372        debug!("Building PendingProjectChanges finished!");
373
374        PendingProjectChanges {
375            package_changes: self.package_changes,
376            conflicts: self.conflicts,
377
378            remove_legacy_files: legacy_assets.files,
379            remove_legacy_folders: legacy_assets.folders,
380        }
381    }
382
383    fn mark_and_sweep_packages(&mut self, unity_project: &UnityProject) {
384        fn mark_recursive<'a, F, I>(
385            entrypoint: impl Iterator<Item = &'a str>,
386            get_dependencies: F,
387        ) -> HashSet<&'a str>
388        where
389            F: Fn(&'a str) -> I,
390            I: Iterator<Item = &'a str>,
391        {
392            let mut mark = HashSet::from_iter(entrypoint);
393
394            if mark.is_empty() {
395                return mark;
396            }
397
398            let mut queue = mark.iter().copied().collect::<VecDeque<_>>();
399
400            while let Some(dep_name) = queue.pop_back() {
401                for dep_name in get_dependencies(dep_name) {
402                    if mark.insert(dep_name) {
403                        queue.push_front(dep_name);
404                    }
405                }
406            }
407
408            mark
409        }
410
411        // collect removable packages
412        // if the unused package is not referenced by any packages to be removed, it should not be removed
413        // since it might be because of bug of VPM implementation
414        let removable = {
415            // packages to be removed or overridden are entrypoint
416            let entrypoint =
417                self.package_changes
418                    .iter()
419                    .filter_map(|(name, change)| match change {
420                        PackageChange::Install(change) if change.add_to_locked => {
421                            unity_project.get_locked(name.as_ref()).map(|x| x.name())
422                        }
423                        // packages that is not added to locked are not removable
424                        PackageChange::Install(_) => None,
425                        PackageChange::Remove(_) => {
426                            unity_project.get_locked(name.as_ref()).map(|x| x.name())
427                        }
428                    });
429
430            mark_recursive(entrypoint, |dep_name| {
431                unity_project
432                    .get_locked(dep_name)
433                    .into_iter()
434                    .flat_map(|dep| dep.dependencies.keys())
435                    .map(Box::as_ref)
436            })
437        };
438
439        debug!("removable packages: {removable:?}");
440        // nothing can be removed
441        if removable.is_empty() {
442            return;
443        }
444
445        // copy to avoid borrow checker error
446        let installing_packages = self
447            .package_changes
448            .iter()
449            .filter(|(_, change)| change.as_install().is_some())
450            .map(|(name, _)| name.clone())
451            .collect::<Vec<_>>();
452
453        // collect packages that is used by dependencies or unlocked packages
454        let using_packages = {
455            let unlocked_dependencies = unity_project
456                .unlocked_packages()
457                .iter()
458                .filter_map(|(_, pkg)| pkg.as_ref())
459                .flat_map(|pkg| pkg.vpm_dependencies().keys())
460                .map(Box::as_ref);
461
462            let dependencies = unity_project.dependencies().filter(|name| {
463                self.package_changes
464                    .get(*name)
465                    .and_then(|change| change.as_remove())
466                    .is_none()
467            });
468
469            // keep installing packages even if the package is not used by any dependencies
470            let package_changes = installing_packages.iter().map(Box::as_ref);
471
472            let entry_points = unlocked_dependencies
473                .chain(dependencies)
474                .chain(package_changes);
475
476            mark_recursive(entry_points, |dep_name| {
477                if let Some(to_install) = self
478                    .package_changes
479                    .get(dep_name)
480                    .and_then(|change| change.as_install())
481                    .and_then(|x| x.package)
482                {
483                    Either::Left(to_install.vpm_dependencies().keys().map(Box::as_ref))
484                } else {
485                    Either::Right(
486                        unity_project
487                            .get_locked(dep_name)
488                            .into_iter()
489                            .flat_map(|dep| dep.dependencies.keys())
490                            .map(Box::as_ref),
491                    )
492                }
493            })
494        };
495
496        debug!("using packages: {using_packages:?}");
497
498        // weep
499        for locked in unity_project.locked_packages() {
500            if !using_packages.contains(locked.name()) && removable.contains(locked.name()) {
501                self.remove_unused(locked.name().into());
502            }
503        }
504    }
505}
506
507impl PendingProjectChanges<'_> {
508    pub fn package_changes(&self) -> &HashMap<Box<str>, PackageChange<'_>> {
509        &self.package_changes
510    }
511
512    pub fn remove_legacy_files(&self) -> &[(Box<Path>, &str)] {
513        self.remove_legacy_files.as_slice()
514    }
515
516    pub fn remove_legacy_folders(&self) -> &[(Box<Path>, &str)] {
517        self.remove_legacy_folders.as_slice()
518    }
519
520    pub fn conflicts(&self) -> &HashMap<Box<str>, ConflictInfo> {
521        &self.conflicts
522    }
523}
524
525static TEMP_DIR: &str = "Temp";
526static PKG_TEMP_DIR: &str = "Temp/vrc-get";
527
528impl UnityProject {
529    /// Applies the changes specified in `AddPackageRequest` to the project.
530    ///
531    /// This will also save the manifest changes
532    pub async fn apply_pending_changes<'env, Env: PackageInstaller>(
533        &mut self,
534        env: &'env Env,
535        request: PendingProjectChanges<'env>,
536    ) -> io::Result<()> {
537        /*
538        Apply pending changes consists of following steps:
539        - Move packages to temp directory (remove packages)
540        - Apply changes to manifest (add packages)
541        - Install packages
542        - Remove legacy assets
543
544        This function will do those steps in the order above.
545        There are several things to consider:
546        - We remove package before applying changes to manifest because:
547          - If we update manifest before removing packages,
548            failing to remove packages will leave previously installed packages as unlocked packages.
549          - If we remove packages before updating manifest,
550            failing to install packages will leave packages as uninstalled locked packages,
551            which is easy to fix with Resolve command.
552        - We install packages after applying changes to manifest because:
553          - If we install packages before updating manifest,
554            failing to update manifest will leave packages as unlocked packages.
555          - If we update manifest before installing packages,
556            failing to install packages will leave packages as uninstalled locked packages,
557            which is easy to fix with Resolve command.
558        - We remove legacy assets after installing packages because:
559          - If we remove legacy assets before installing packages,
560            failing to install package will leave legacy assets removed.
561          - If we install packages before removing legacy assets,
562            failing to remove legacy assets will duplicate legacy assets.
563          - Both cases are not desirable, but the latter is less harmful.
564         */
565
566        let mut installs = Vec::new();
567        let mut remove_names = Vec::new();
568        let mut remove_unlocked_names = Vec::new();
569
570        for (name, change) in &request.package_changes {
571            match change {
572                PackageChange::Install(change) => {
573                    if let Some(package) = change.package {
574                        installs.push(package);
575                    }
576                }
577                PackageChange::Remove(_) => {
578                    remove_names.push(name.as_ref());
579                }
580            }
581        }
582
583        for info in request.conflicts.values() {
584            for x in &info.unlocked_names {
585                remove_unlocked_names.push(x.as_ref());
586            }
587        }
588
589        // remove packages
590        let remove_temp_dir = format!("{PKG_TEMP_DIR}/{}", uuid::Uuid::new_v4());
591        let remove_temp_dir = Path::new(&remove_temp_dir);
592
593        self.io.create_dir_all(remove_temp_dir).await?;
594
595        move_packages_to_temp(
596            &self.io,
597            (remove_names.iter().copied())
598                .chain(installs.iter().map(|x| x.name()))
599                .chain(remove_unlocked_names.iter().copied()),
600            remove_temp_dir,
601        )
602        .await?;
603
604        // apply changes to manifest
605        for (name, change) in &request.package_changes {
606            match change {
607                PackageChange::Install(change) => {
608                    if let Some(package) = change.package
609                        && change.add_to_locked
610                    {
611                        self.manifest.add_locked(
612                            package.name(),
613                            package.version().clone(),
614                            package.vpm_dependencies().clone(),
615                        );
616                    }
617
618                    if let Some(version) = &change.to_dependencies {
619                        self.manifest.add_dependency(name, version.clone());
620                    }
621                }
622                PackageChange::Remove(_) => {}
623            }
624        }
625
626        self.manifest.remove_packages(remove_names.iter().copied());
627
628        // save manifest
629
630        self.save().await?;
631
632        // add packages
633
634        install_packages(&self.io, env, &installs).await?;
635
636        self.io.remove_dir_all(remove_temp_dir).await.ok();
637        self.io.remove_dir_all(PKG_TEMP_DIR.as_ref()).await.ok();
638        // remove temp dir also if it's empty
639        self.io.remove_dir(TEMP_DIR.as_ref()).await.ok();
640
641        // remove legacy assets
642
643        remove_assets(
644            &self.io,
645            request.remove_legacy_files.iter().map(|(p, _)| p.as_ref()),
646            request
647                .remove_legacy_folders
648                .iter()
649                .map(|(p, _)| p.as_ref()),
650        )
651        .await;
652
653        Ok(())
654    }
655}
656
657static REMOVED_FILE_PREFIX: &str = ".__removed_";
658
659async fn move_packages_to_temp<'a>(
660    io: &DefaultProjectIo,
661    names: impl Iterator<Item = &'a str>,
662    temp_dir: &Path,
663) -> io::Result<Vec<&'a str>> {
664    // it's expected to cheap to rename (link) packages to temp dir,
665    // so we do it sequentially for simplicity
666
667    let mut moved = IndexSet::new();
668
669    for name in names {
670        if moved.contains(name) {
671            continue;
672        }
673
674        match move_package(io, name, temp_dir).await {
675            Ok(true) => {
676                moved.insert(name);
677            }
678            Ok(false) => {
679                // package not found, do nothing
680            }
681            Err(err) => {
682                // restore moved packages as possible
683                // our package can also be partially moved so insert to moved
684                moved.insert(name);
685                restore_remove(io, temp_dir, moved.iter().copied()).await;
686
687                return Err(err);
688            }
689        }
690    }
691
692    return Ok(moved.into_iter().collect());
693
694    async fn move_package(io: &DefaultProjectIo, name: &str, temp_dir: &Path) -> io::Result<bool> {
695        let package_dir = format!("Packages/{name}");
696        let package_dir = Path::new(&package_dir);
697        let copied_dir = temp_dir.join(name);
698
699        io.create_dir_all(&copied_dir).await?;
700        let mut iterator = pin!(walk_dir_relative(io, vec![package_dir.into()]));
701        while let Some((original, entry)) = iterator.next().await {
702            let relative = original.strip_prefix(package_dir).unwrap();
703            let mut moved = copied_dir.join(relative);
704            if entry.file_type().await?.is_dir() {
705                match io.create_dir_all(&moved).await {
706                    Ok(()) => {}
707                    Err(e) => {
708                        log::error!(gui_toast = false; "error creating directory {}: {e}", moved.display());
709                        return Err(e);
710                    }
711                }
712            } else {
713                if let Some(name) = original.file_name().unwrap().to_str() {
714                    moved.pop();
715                    moved.push(format!("{REMOVED_FILE_PREFIX}{name}"));
716                }
717                log::trace!("move {} to {}", original.display(), moved.display());
718
719                match io.rename(&original, &moved).await {
720                    Ok(()) => {}
721                    Err(e) => {
722                        // ignore error
723                        log::error!(gui_toast = false; "error moving {} to {}: {e}", original.display(), moved.display());
724                    }
725                }
726            }
727        }
728
729        match io.remove_dir_all(package_dir).await {
730            Ok(()) => {}
731            Err(err) if err.kind() == io::ErrorKind::NotFound => {
732                return Ok(false);
733            }
734            Err(err) => {
735                return Err(err);
736            }
737        }
738
739        Ok(true)
740    }
741}
742
743async fn restore_remove(io: &DefaultProjectIo, temp_dir: &Path, names: impl Iterator<Item = &str>) {
744    for name in names {
745        let package_dir = format!("Packages/{name}");
746        let package_dir = Path::new(&package_dir);
747        let temp_package_dir = temp_dir.join(name);
748        if io.metadata(&temp_package_dir).await.is_err() {
749            continue;
750        }
751
752        if io.metadata(package_dir).await.is_ok() {
753            // Process partially moved case
754            let mut iterator = pin!(walk_dir_relative(io, vec![temp_package_dir.clone()]));
755            while let Some((original, entry)) = iterator.next().await {
756                if entry
757                    .file_type()
758                    .await
759                    .map(|x| !x.is_dir())
760                    .unwrap_or(false)
761                {
762                    let relative = original.strip_prefix(&temp_package_dir).unwrap();
763                    if let Some(name) = original.file_name().unwrap().to_str() {
764                        let name = name.strip_prefix(REMOVED_FILE_PREFIX).unwrap_or(name);
765                        let moved = package_dir.join(relative.parent().unwrap()).joined(name);
766                        io.create_dir_all(moved.parent().unwrap()).await.ok();
767                        io.rename(&original, &moved).await.ok();
768                    }
769                }
770            }
771        } else {
772            // Process fully moved case
773            io.rename(&temp_package_dir, package_dir).await.ok();
774
775            let mut iterator = pin!(walk_dir_relative(io, vec![package_dir.into()]));
776            while let Some((original, entry)) = iterator.next().await {
777                if entry
778                    .file_type()
779                    .await
780                    .map(|x| !x.is_dir())
781                    .unwrap_or(false)
782                    && let Some(name) = original.file_name().unwrap().to_str()
783                    && let Some(stripped) = name.strip_prefix(REMOVED_FILE_PREFIX)
784                {
785                    let moved = original.parent().unwrap().join(stripped);
786                    io.rename(&original, &moved).await.ok();
787                }
788            }
789        }
790    }
791    io.remove_dir(temp_dir).await.ok();
792    io.remove_dir(PKG_TEMP_DIR.as_ref()).await.ok();
793    io.remove_dir(TEMP_DIR.as_ref()).await.ok();
794}
795
796async fn install_packages<Env: PackageInstaller>(
797    io: &DefaultProjectIo,
798    env: &Env,
799    packages: &[PackageInfo<'_>],
800) -> io::Result<()> {
801    let abort = AbortCheck::new();
802    let mut error_store = OnceLock::new();
803
804    // resolve all packages
805    join_all(packages.iter().map(|package| {
806        env.install_package(io, *package, &abort).then(|x| {
807            if let Err(e) = x {
808                error_store.set(e).ok();
809                abort.abort();
810            }
811            ready(())
812        })
813    }))
814    .await;
815
816    if let Some(err) = error_store.take() {
817        return Err(err);
818    }
819
820    Ok(())
821}
822
823async fn remove_assets(
824    io: &DefaultProjectIo,
825    legacy_files: impl Iterator<Item = &Path>,
826    legacy_folders: impl Iterator<Item = &Path>,
827) {
828    join(
829        join_all(legacy_files.map(|relative| async move {
830            remove_file(io, relative).await;
831        })),
832        join_all(legacy_folders.map(|relative| async move {
833            remove_folder(io, relative).await;
834        })),
835    )
836    .await;
837
838    async fn remove_meta_file(io: &DefaultProjectIo, path: PathBuf) {
839        let mut building = path.into_os_string();
840        building.push(".meta");
841        let meta = PathBuf::from(building);
842
843        if let Some(err) = io.remove_file(&meta).await.err()
844            && !matches!(err.kind(), io::ErrorKind::NotFound)
845        {
846            log::error!("error removing legacy asset at {}: {err}", meta.display());
847        }
848    }
849
850    async fn remove_file(io: &DefaultProjectIo, path: &Path) {
851        if let Some(err) = io.remove_file(path).await.err() {
852            log::error!("error removing legacy asset at {}: {err}", path.display());
853        }
854        remove_meta_file(io, path.to_owned()).await;
855    }
856
857    async fn remove_folder(io: &DefaultProjectIo, path: &Path) {
858        if let Some(err) = io.remove_dir_all(path).await.err() {
859            log::error!("error removing legacy asset at {}: {err}", path.display());
860        }
861        remove_meta_file(io, path.to_owned()).await;
862    }
863}