chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
//            Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using OpenTap.Cli;
using OpenTap.Package.PackageInstallHelpers;
 
#pragma warning disable 1591 // TODO: Add XML Comments in this file, then remove this
namespace OpenTap.Package
{
    [Display("install", Group: "package", Description: "Install one or more packages.")]
    public class PackageInstallAction : IsolatedPackageAction
    {
        static readonly TraceSource Log = OpenTap.Log.CreateSource("Install");
 
        [Obsolete("Use Force instead.")]
        public bool ForceInstall { get => Force; set => Force = value; }
 
        [CommandLineArgument("dependencies", Description = "Install dependencies without asking. This is always enabled when installing bundle packages.", ShortName = "y")]
        public bool InstallDependencies { get; set; }
 
        [Obsolete("It is no longer supported to ignore dependencies as it causes a broken installation when used.")]
        [CommandLineArgument("no-dependencies", Description = "Don't install dependencies. This is implied when using --force.")]
        public bool IgnoreDependencies { get; set; }
 
        [CommandLineArgument("overwrite", Description = "Overwrite files that already exist without asking. This is implied when using --force.")]
        public bool Overwrite { get; set; }
 
        [CommandLineArgument("repository", Description = CommandLineArgumentRepositoryDescription, ShortName = "r")]
        public string[] Repository { get; set; }
 
        [CommandLineArgument("no-cache", Description = CommandLineArgumentNoCacheDescription)]
        public bool NoCache { get; set; }
 
        [CommandLineArgument("version", Description = CommandLineArgumentVersionDescription)]
        public string Version { get; set; }
 
        [CommandLineArgument("os", Description = CommandLineArgumentOsDescription)]
        public string OS { get; set; }
 
        [CommandLineArgument("architecture", Description = CommandLineArgumentArchitectureDescription)]
        public CpuArchitecture Architecture { get; set; }
 
        /// <summary>
        /// This is used when specifying the install action through the CLI. If you need to specify multiple packages with different version numbers, use <see cref="PackageReferences"/>
        /// </summary>
        [UnnamedCommandLineArgument("package(s)", Required = true, Description = "One or more packages to install. A package can refer to a .TapPackage file, or a name to be resolved from the specified repositories.")]
        public string[] Packages { get; set; }
 
        [CommandLineArgument("check-only", Description = "Checks if the selected package(s) can be installed, but does not install or download them.")]
        public bool CheckOnly { get; set; }
 
        [Obsolete("Interactive is the default. Use --non-interactive to disable.")]
        [CommandLineArgument("interactive", Description = "More user responsive.")]
        [Browsable(false)]
        public bool Interactive { get; set; }
 
        /// <summary>
        /// Never prompt for user input.
        /// </summary>
        [CommandLineArgument("non-interactive", Description = "Never prompt for user input.")]
        public bool NonInteractive { get; set; } = false;
 
        [CommandLineArgument("no-downgrade", Description = "Don't install if the same or a newer version is already installed.")]
        public bool NoDowngrade { get; set; }
 
        [CommandLineArgument("unpack-only", Description = "Only unpack the package payload into the installation directory and\n" +
                                                          "skip any additional install actions the package might have defined.\n" +
                                                          "This can leave the installed package unusable.")]
        public bool UnpackOnly { get; set; }
 
        // This is set when system wide packages are being installed.
        public bool SystemWideOnly { get; set; }
 
        /// <summary>
        /// This is used when specifying multiple packages with different version numbers. In that case <see cref="Packages"/> can be left null.
        /// </summary>
        public PackageSpecifier[] PackageReferences { get; set; }
 
 
        private string DefaultOs;
 
        /// <summary>
        /// This will be set only if the install action was started by <see cref="PackageInstallStep"/>
        /// This is supposed to solve an issue where OpenTAP fails to detect that the process was elevated.
        /// If one level of elevation was already attempted, it is unlikely that further attempts will cause the check to succeed.
        /// In this instance, it is better to just try the installation with the current privileges, and fail with whatever
        /// error if those privileges are not sufficient.
        /// </summary>
        internal bool AlreadyElevated { get; set; }
 
        public PackageInstallAction()
        {
            Architecture = ArchitectureHelper.GuessBaseArchitecture;
            OS = GuessHostOS();
 
            DefaultOs = OS;
        }
 
        private int DoExecute(CancellationToken cancellationToken)
        {
            if (Target == null)
                Target = FileSystemHelper.GetCurrentInstallationDirectory();
 
 
            if (TryFindParentInstallation(Target, out var parent))
            {
                log.Error($"OpenTAP installation detected in directory '{parent}'. Nested installations are not supported.");
                return 1;
            }
 
            var targetInstallation = new Installation(Target);
 
 
            if (NoCache) PackageManagerSettings.Current.UseLocalPackageCache = false;
            Repository = ExtractRepositoryTokens(Repository, true);
            List<IPackageRepository> repositories = PackageManagerSettings.Current.GetEnabledRepositories(Repository);
            if (!NonInteractive)
                Packages = AutoCorrectPackageNames.Correct(Packages, repositories);
 
            bool installError = false;
            var installer = new Installer(Target, cancellationToken)
            { DoSleep = false, ForceInstall = Force, UnpackOnly = UnpackOnly };
            installer.ProgressUpdate += RaiseProgressUpdate;
            installer.Error += RaiseError;
            installer.Error += ex => installError = true;
 
            try
            {
                Log.Debug("Fetching package information...");
 
                RaiseProgressUpdate(5, "Gathering packages.");
 
                // If exact version is specified, check if it's already installed
                if (Version != null && SemanticVersion.TryParse(Version, out var vs) && Force == false)
                {
                    foreach (var pkg in Packages)
                    {
                        // We should always install when it's a file, this could be part of development
                        if (File.Exists(pkg))
                            break;
 
                        PackageIdentifier pid = new PackageIdentifier(pkg, vs, Architecture, OS);
                        var installedPackage = targetInstallation.GetPackages(validOnly: true).FirstOrDefault(p => p.Name == pid.Name);
                        if (installedPackage != null && pid.Version.Equals(installedPackage.Version))
                        {
                            Log.Info($"Package '{pid.Name}' '{installedPackage.Version}' is already installed.");
                            return (int)ExitCodes.Success;
                        }
                    }
                }
 
                // Get package information
                bool askToInstallDependencies = !NonInteractive;
                if (Force)
                    askToInstallDependencies = false;
 
                List<PackageDef> packagesToInstall;
                try
                {
                    packagesToInstall = PackageActionHelpers.GatherPackagesAndDependencyDefs(
                        targetInstallation, PackageReferences, Packages, Version, Architecture, OS, repositories, Force,
                        NoDowngrade);
                }
                catch (ImageResolveException ex) when (ex.Result is FailedImageResolution fir)
                {
                    // If the problem is exactly that we failed to resolve a compatible release version of a package,
                    // ask the user to retry the resolution with a pre-release instead.
                    // We could also ask for an RC / Any, but if there are no releases, then it is likely also the case that there are no RCs.
                    // 'any' would be the most likely specifier to succeed, but it is probably a bit too extreme to suggest something so bleeding edge.
                    // A beta version strikes a good middle ground. It is pretty likely to resolve, and probably won't be too unstable.
                    if (NonInteractive) throw;
                    if (Packages.Length != 1) throw;
                    if (fir.resolveProblems.Count != 1) throw;
 
                    var problem = fir.resolveProblems[0];
                    if (problem.Version != VersionSpecifier.AnyRelease) throw;
                    // Only show the beta option if the resolution problem was with the package we are trying to install
                    if (problem.Name != Packages[0]) throw;
 
                    // Try to resolve a beta version instead, but only offer the beta version if the user wants it
                    try
                    {
                        packagesToInstall = PackageActionHelpers.GatherPackagesAndDependencyDefs(
                            targetInstallation, PackageReferences, Packages, "beta", Architecture, OS, repositories,
                            Force, NoDowngrade);
                    }
                    catch
                    {
                        // throw original exception if beta resolution fails
                        throw ex;
                    }
 
                    // If the beta resolution succeeded, ask the user if they want to install the beta version
                    var betaVersion = packagesToInstall.First(p => p.Name == problem.Name).Version;
 
                    var req = new AskAboutPrerelease($"Package '{problem.Name}' has no compatible release version, but the beta version '{betaVersion.ToString(4)}' is compatible.\nInstall this version instead?");
                    UserInput.Request(req);
                    if (req.Response == UseBetaQuestion.No) throw;
 
                }
 
                if (SystemWideOnly)
                {
                    // the current process is for installing system wide packages only.
                    packagesToInstall = packagesToInstall.Where(x => x.IsSystemWide()).ToList();
                }
 
                if (packagesToInstall?.Any() != true)
                {
                    if (NoDowngrade)
                    {
                        Log.Info("No package(s) were upgraded.");
                        return (int)ExitCodes.Success;
                    }
 
                    Log.Info("Could not find one or more packages.");
                    return (int)PackageExitCodes.PackageDependencyError;
                }
 
                foreach (var pkg in packagesToInstall)
                {
                    // print a warning if the selected package is incompatible with the host platform.
                    // or return an error if the package does not match.
                    var platformCompatible =
                        pkg.IsPlatformCompatible(targetInstallation.Architecture, targetInstallation.OS);
 
                    if (!Force)
                    {
                        // print a warning if necessary.
                        // --os and --architecture are not really supported when --force is not enabled.
                        // we only allow resolving to installable packages.
                        var differentArch = ArchitectureHelper.GuessBaseArchitecture != Architecture;
 
                        bool printedWarning = false;
                        void maybePrintWarning()
                        {
                            if (printedWarning) return;
                            printedWarning = true;
                            Log.Warning("OS or Architecture were specified without --force. Only compatible packages will be selected.");
 
                        }
                        foreach (var package in packagesToInstall)
                        {
                            if (differentArch)
                            {
                                if (package.Architecture != CpuArchitecture.AnyCPU && Architecture != package.Architecture)
                                {
                                    maybePrintWarning();
                                    Log.Warning("Selected package {2} architecture {0} instead of {1}", package.Architecture, Architecture, package.Name);
                                }
                            }
 
                            if (OS != DefaultOs)
                            {
                                if (!package.IsOsCompatible(OS))
                                {
                                    maybePrintWarning();
                                    Log.Warning("Selected package {2} for {0} instead of {1}", package.Name, package.OS, OS);
                                }
                            }
                        }
 
                    }
 
                    if (!platformCompatible)
                    {
                        var selectedPlatformCompatible = pkg.IsPlatformCompatible(Architecture, OS);
                        var message =
                            $"Selected package {pkg.Name} for {pkg.OS}, {pkg.Architecture} is incompatible with the host platform {targetInstallation.OS}, {targetInstallation.Architecture}.";
                        if (selectedPlatformCompatible || Force)
                            // --OS [arg] was used or --force is specified. Try to install it anyway. 
                            Log.Warning(message);
                        else
                        {
                            Log.Error(message);
                            return (int)ExitCodes.ArgumentError;
                        }
                    }
                }
 
                var installationPackages = targetInstallation.GetPackages(validOnly: true);
 
                var overWriteCheckExitCode = CheckForOverwrittenPackages(installationPackages, packagesToInstall,
                    Force || Overwrite, !(NonInteractive || Overwrite));
                if (overWriteCheckExitCode == InstallationQuestion.Cancel)
                {
                    Log.Info("Install cancelled by user.");
                    return (int)ExitCodes.UserCancelled;
                }
 
                if (overWriteCheckExitCode == InstallationQuestion.OverwriteFile)
                    Log.Warning("Overwriting files. (--{0} option specified).", Overwrite ? "overwrite" : "force");
 
                RaiseProgressUpdate(10, "Gathering dependencies.");
 
                if (!SystemWideOnly)
                {
                    // Sometimes system wide packages depends on non-system wide packages.
                    // when installing system wide packages in a sub-process we dont need to check the dependencies
                    // because that has already been done.
 
                    bool checkDependencies = !Force || CheckOnly;
                    var installedToCheck =
                        installationPackages.Where(p =>
                            p.IsSystemWide() ==
                            false); // don't use system-wide to check, as that would prevent installing older stuff, if a system-wide package depends on newer versions.
                    var issue = DependencyChecker.CheckDependencies(installedToCheck, packagesToInstall,
                        Force ? LogEventType.Information :
                        checkDependencies ? LogEventType.Error : LogEventType.Warning);
 
                    if (checkDependencies)
                    {
                        if (issue == DependencyChecker.Issue.BrokenPackages)
                        {
                            Log.Info("To fix the package conflict uninstall or update the conflicted packages.");
                            Log.Info(
                                "To install packages despite the conflicts, use the --force option. Note that this can break the installation.");
                            return (int)PackageExitCodes.PackageDependencyError;
                        }
 
                        if (CheckOnly)
                        {
                            Log.Info("Check completed with no problems detected.");
                            return (int)ExitCodes.Success;
                        }
                    }
                }
                
                // Download the packages
                // We divide the progress by 2 in the progress update because we assume downloading the packages
                // accounts for half the installation progress. So when all the packages have finished downloading,
                // we have finished 10 + (100/2)% of the installation process.
                packagesToInstall = packagesToInstall.OrderBy(p => p.IsSystemWide()).ToList();
 
                var downloadedPackageFiles = PackageActionHelpers.DownloadPackages(
                    PackageCacheHelper.PackageCacheDirectory, packagesToInstall,
                    progressUpdate: (progress, msg) => RaiseProgressUpdate(10 + progress / 2, msg),
                    ignoreCache: NoCache);
                
                // The downloaded package files will arrive in the same order as the packagesToInstall list.
                // We need to split the list into two parts, one for regular packages and one for systemwide packages.
                var cutoff = packagesToInstall.FindIndex(p => p.IsSystemWide());
                if (cutoff == -1) cutoff = packagesToInstall.Count;
 
                var regularPackages = downloadedPackageFiles.Take(cutoff).ToList();
                var systemwidePackages = downloadedPackageFiles.Skip(cutoff).ToArray();
 
                // We need to elevate if 
                // 1. Elevation was not already attempted, and
                // 2. we need to install systemWide packages, and
                // 3. We are not already running as admin
                bool needElevation = !AlreadyElevated && systemwidePackages.Any() && SubProcessHost.IsAdmin() == false;
 
                // Warn the user if elevation was already attempted, and we are not currently running as admin
                if (AlreadyElevated && SubProcessHost.IsAdmin() == false)
                {
                    log.Warning($"Process elevation failed. Installation will continue without elevation.");
                }
 
                // If we need to install system-wide packages and we are not admin, we should install them in an elevated sub-process
                if (needElevation)
                {
                    RaiseProgressUpdate(20, "Installing system-wide packages.");
                    var installStep = new PackageInstallStep()
                    {
                        Packages = systemwidePackages,
                        Repositories = Array.Empty<string>(),
                        Target = PackageDef.SystemWideInstallationDirectory,
                        Force = Force,
                        SystemWideOnly = true
                    };
 
                    var processRunner = new SubProcessHost
                    {
                        ForwardLogs = true,
                        MutedSources =
                        {
                            "CLI", "Session", "Resolver", "AssemblyFinder", "PluginManager", "TestPlan",
                            "UpdateCheck",
                            "Installation"
                        },
                        // The current install action is a locking package action.
                        // Setting this flag lets the child process bypass the lock on the installation.
                        Unlocked = true,
                    };
 
                    var result = processRunner.Run(installStep, true, cancellationToken);
                    if (result != Verdict.Pass)
                    {
                        var ex = new Exception(
                            $"Failed installing system-wide packages. Try running the command as administrator.");
                        RaiseError(ex);
                        throw ex;
                    }
 
                    var pct = ((double)systemwidePackages.Length / (systemwidePackages.Length + packagesToInstall.Count)) * 100;
                    RaiseProgressUpdate((int)pct, "Installed system-wide packages.");
                }
                // Otherwise if we are admin and we need to install system-wide packages, we can install them in the current process
                else if (systemwidePackages.Any())
                {
                    installer.PackagePaths.AddRange(systemwidePackages);
                }
 
                installer.PackagePaths.AddRange(regularPackages);
            }
            catch (OperationCanceledException e)
            {
                Log.Info(e.Message);
                return (int)ExitCodes.UserCancelled;
            }
            catch (ImageResolveException ex)
            {
                if (Packages != null && Packages.Length > 0)
                {
                    Log.Error("Could not install {0}{1}",
                        string.Join(", ", Packages.Select(x => $"{x}")),
                        string.IsNullOrWhiteSpace(Version) ? "" : $" v{Version}");
 
                    // If the requested package is a bundle, the easiest way to resolve the resolution error is to uninstall the bundle.
                    if (Packages.Length == 1 &&
                        targetInstallation.GetPackages().FirstOrDefault(p => p.Name == Packages[0]) is PackageDef pkg &&
                        pkg.IsBundle())
                    {
                        Log.Error( $"Please try manually uninstalling '{Packages[0]}' and re-installing it.");
                        return (int)PackageExitCodes.PackageDependencyError;
                    }
                }
                else
                {
                    Log.Error("Could not resolve one or more packages.");
                }
 
                var unsatisfiedDependencies = ex.InstalledPackages.Where(x => false == x.Dependencies.All(dep =>
                    ex.InstalledPackages.Any(x2 =>
                        x2.Name == dep.Name && dep.Version.IsSatisfiedBy(x2.Version.AsExactSpecifier())))).ToArray();
                if (unsatisfiedDependencies.Any())
                {
                    Log.Warning("This might be because of the following conflicts:");
 
                    var missingDeps = unsatisfiedDependencies.SelectMany(x => x.Dependencies.Where(dep =>
                            !ex.InstalledPackages.Any(x2 =>
                                x2.Name == dep.Name && dep.Version.IsSatisfiedBy(x2.Version.AsExactSpecifier()))))
                        .ToArray();
 
                    foreach (var grouping in missingDeps.GroupBy(d => d.Name))
                    {
                        var versions = grouping.ToArray();
                        var highest = versions.FindMax(dep => dep.Version);
                        var dependers =
                            unsatisfiedDependencies.Where(dep => dep.Dependencies.Any(d => d.Name == grouping.Key));
                        
                        // Omit the warnings for packages that would have been satisfied by the specified version
                        if (Packages[0] == grouping.Key && !string.IsNullOrWhiteSpace(Version) && VersionSpecifier.TryParse(Version, out var filter))
                        {
                            dependers = dependers.Where(dep =>
                                false == dep.Dependencies.First(d => d.Name == grouping.Key).Version
                                    .IsSatisfiedBy(filter));
                        }
                        var dependString = string.Join(", ", dependers.Select(d => d.Name));
                        Log.Info($"{grouping.Key} version {highest.Version} required by {dependString}");
                    }
                }
 
                // If the problem is not generic, then there are additional details about the resolution problem.
                if (ex.Result is FailedImageResolution f && f.resolveProblems is not GenericResolutionProblem)
                    log.Info(f.resolveProblems.Description());
 
                Log.Debug("{0}", ex.Message);
                return (int)ExitCodes.PackageResolutionError;
            }
            catch (Exception e)
            {
                Log.Info("Could not download one or more packages.");
                Log.Info(e.Message);
                Log.Debug(e);
                RaiseError(e);
                return (int)ExitCodes.NetworkError;
            }
 
            // This happens in cases where only system-wide packages were requested.
            if (installer.PackagePaths.Count == 0)
                return 0;
 
            Log.Info("Installing to {0}", Path.GetFullPath(Target));
 
            // Uninstall old packages before
            var status = UninstallExisting(targetInstallation, installer.PackagePaths, cancellationToken);
            if (status != (int)ExitCodes.Success)
                return status;
 
            var toInstall = ReorderPackages(installer.PackagePaths);
            installer.PackagePaths.Clear();
            installer.PackagePaths.AddRange(toInstall);
 
            // Install the package
            installer.InstallThread();
 
            if (installError)
                return (int)PackageExitCodes.PackageInstallError;
 
            return 0;
        }
 
        protected override int LockedExecute(CancellationToken cancellationToken)
        {
            var currentInterface = UserInput.GetInterface();
            if (NonInteractive)
                UserInput.SetInterface(new NonInteractiveUserInputInterface());
 
            try
            {
                return DoExecute(cancellationToken);
            }
            finally
            {
                IncrementChangeId(Target);
                UserInput.SetInterface(currentInterface);
            }
        }
 
        private int UninstallExisting(Installation installation, List<string> packagePaths, CancellationToken cancellationToken)
        {
            var installed = installation.GetPackages();
 
            var packages = packagePaths.Select(PackageDef.FromPackage).Select(x => x.Name).ToHashSet();
            var existingPackages = installed.Where(kvp => packages.Contains(kvp.Name)).Select(x => (x.PackageSource as XmlPackageDefSource)?.PackageDefFilePath).ToList();
 
            if (existingPackages.Count == 0) return (int)ExitCodes.Success;
 
            var newInstaller = new Installer(Target, cancellationToken);
 
            //newInstaller.ProgressUpdate += RaiseProgressUpdate;
            newInstaller.Error += RaiseError;
            newInstaller.DoSleep = false;
 
            newInstaller.PackagePaths.AddRange(existingPackages);
            return newInstaller.UninstallThread();
        }
 
        /// <summary>
        /// Reorder packages to ensure that dependencies are installed before a package needing it.
        /// </summary>
        /// <param name="packagePaths"></param>
        /// <returns></returns>
        private List<string> ReorderPackages(List<string> packagePaths)
        {
            var toInstall = new List<string>();
 
            var packages = packagePaths.ToDictionary(k => k, k => PackageDef.FromPackage(k));
 
            while (packages.Count > 0)
            {
                var next = packages.FirstOrDefault(pkg => pkg.Value.Dependencies.All(dep => !packages.Values.Any(p => p.Name == dep.Name)));
 
                if (next.Value == null) next = packages.First(); // This doesn't matter at this point
 
                toInstall.Add(next.Key);
                packages.Remove(next.Key);
            }
 
            return toInstall;
        }
 
 
        public enum InstallationQuestion
        {
            //KeepExitingFiles = 3,
            [Display("Cancel", Order: 2)]
            Cancel = 2,
            [Display("Overwrite Files", Order: 1)]
            OverwriteFile = 1,
 
            [Browsable(false)]
            Success = 0,
        }
 
        class AskAboutInstallingAnyway
        {
            public string Name { get; } = "Overwrite Files?";
 
            [Browsable(true)]
            [Layout(LayoutMode.FullRow)]
            public string Message { get; private set; }
 
            public AskAboutInstallingAnyway(string message) => Message = message;
 
            [Layout(LayoutMode.FloatBottom | LayoutMode.FullRow)]
            [Submit] public InstallationQuestion Response { get; set; } = InstallationQuestion.Cancel;
        }
 
        internal enum UseBetaQuestion
        {
            [Display("Yes")]
            Beta = 0,
            [Display("No")]
            No = 1,
        }
 
        class AskAboutPrerelease
        {
            [Browsable(true)]
            [Layout(LayoutMode.FullRow)]
            public string Message { get; private set; }
            public AskAboutPrerelease(string message) => Message = message;
            [Layout(LayoutMode.FloatBottom | LayoutMode.FullRow)]
            [Submit] public UseBetaQuestion Response { get; set; } = UseBetaQuestion.No;
        }
 
        internal static InstallationQuestion CheckForOverwrittenPackages(IEnumerable<PackageDef> installedPackages,
            IEnumerable<PackageDef> packagesToInstall, bool force, bool interactive = false)
        {
            var conflicts = VerifyPackageHashes.CalculatePackageInstallConflicts(installedPackages, packagesToInstall);
 
            void buildMessage(Action<string> addLine)
            {
                foreach (var conflictGroup in conflicts.ToLookup(x => x.OffendingPackage))
                {
                    addLine($"  These files will be overwritten if package '{conflictGroup.Key.Name}' is installed:");
                    foreach (var conflict in conflictGroup)
                    {
                        addLine($"    {conflict.File.FileName} (from package '{conflict.Package.Name}').");
                    }
                }
            }
 
            if (conflicts.Any())
            {
                if (interactive)
                {
                    StringBuilder message = new StringBuilder();
                    buildMessage(line => message.AppendLine(line));
                    var question = new AskAboutInstallingAnyway(message.ToString());
                    UserInput.Request(question);
                    return question.Response;
                }
 
                buildMessage(line => Log.Info(line));
 
                if (force)
                {
                    return InstallationQuestion.OverwriteFile;
                }
 
                Log.Error(
                    "Installing these packages will overwrite existing files. " +
                    "Use --overwrite to overwrite existing files, possibly breaking installed packages.");
                return InstallationQuestion.Cancel;
            }
 
            return InstallationQuestion.Success;
        }
    }
}