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
using OpenTap.Cli;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
 
namespace OpenTap.Package
{
    [Display("install", Description: "Install the specified image, overwriting the existing installation.", Group: "image")]
    internal class ImageInstallAction : IsolatedPackageAction
    {
        /// <summary>
        /// Path to Image file containing XML or JSON formatted Image specification, or just the string itself, e.g "REST-API,TUI:beta".
        /// </summary>
        [UnnamedCommandLineArgument("image", Required = true, Description = "The image to install. Supported formats:\n" +
                "A string describing the image. Example: 'OpenTAP:9.24,TUI:beta,CSV'\n" + 
                "A path to a file containing an XML or JSON formatted image specification.")]
        public string ImagePath { get; set; }
 
        /// <summary>
        /// Option to merge with target installation. Default is false, which means overwrite installation
        /// </summary>
        [CommandLineArgument("merge", Description = "Merge the requested image with the existing installation instead of overwriting it.")]
        public bool Merge { get; set; }
 
        /// <summary> Never prompt for user input. </summary>
        [CommandLineArgument("non-interactive", Description = "Never prompt for user input.")]
        public bool NonInteractive { get; set; } = false;
 
        /// <summary> Which operative system to resolve packages for. </summary>
        [CommandLineArgument("os", Description = "The operating system to resolve packages for.")]
        public string Os { get; set; }
 
        /// <summary> Which CPU architecture to resolve packages for. </summary>
        [CommandLineArgument("architecture", Description = "The architecture to resolve packages for.")]
        public CpuArchitecture Architecture { get; set; } = CpuArchitecture.Unspecified;
 
        /// <summary> Resolve and print a summary of the changes that would be applied, but don't apply them. </summary>
        [CommandLineArgument("dry-run", Description = "Only print the result, don't install the packages.")]
        public bool DryRun { get; set; }
 
        /// <summary> Which repositories to use. </summary>
        [CommandLineArgument("repository", ShortName = "r", Description = "Repositories to use for resolving the image.")]
        public string[] Repositories { get; set; } = null;
 
        protected override int LockedExecute(CancellationToken cancellationToken)
        {
            Repositories = ExtractRepositoryTokens(Repositories, true);
            if (NonInteractive)
                UserInput.SetInterface(new NonInteractiveUserInputInterface());
 
            if (Force)
                log.Warning($"Using --force does not force an image installation");
 
            if (string.IsNullOrEmpty(ImagePath))
                throw new ArgumentException("'image' not specified.", nameof(ImagePath));
 
            var imageString = ImagePath;
            if (File.Exists(imageString))
                imageString = File.ReadAllText(imageString);
            var imageSpecifier = ImageSpecifier.FromString(imageString);
 
            // image specifies any repositories?
            if (imageSpecifier.Repositories?.Any() != true)
            {
                if (Repositories?.Any() == true)
                    imageSpecifier.Repositories = Repositories.ToList();
                else
                    imageSpecifier.Repositories = PackageManagerSettings.Current.Repositories
                        .Where(x => x.IsEnabled)
                        .Select(x => x.Url)
                        .ToList();
            }
            else
            {
                if (Repositories?.Any() == true)
                    imageSpecifier.Repositories = Repositories.ToList();
            }
 
            if (!string.IsNullOrWhiteSpace(Os))
                imageSpecifier.OS = Os;
            if (Architecture != CpuArchitecture.Unspecified)
                imageSpecifier.Architecture = Architecture;
 
 
            try
            {
                ImageIdentifier image;
                var sw = Stopwatch.StartNew();
 
                if (TryFindParentInstallation(Target, out var parent))
                {
                    log.Error($"OpenTAP installation detected in directory '{parent}'. Nested installations are not supported.");
                    return 1;
                }
                if (Merge)
                {
                    var deploymentInstallation = new Installation(Target);
                    image = imageSpecifier.MergeAndResolve(deploymentInstallation, cancellationToken);
                }
                else
                {
                    // Here we add the list of currently installed packages to 'AdditionalPackages'.
                    // These packages will be added to the imageSpecifier's internal cache of known packages.
                    // This fixes an edge case where an image fails to resolve because one of the specified
                    // packages is already installed, but is not available in any of the specified repositories
                    // (or the local cache). In that case, if the installed version is compatible with the image,
                    // the image resolution will still succeed even if the package cannot be found.
                    // This is possible e.g. with debug installs where the debug package does not exist in any repository,
                    // or if the package cache has been cleared by the user, and an image install is attempted
                    // without the original source repository of some package.
                    imageSpecifier.AdditionalPackages.AddRange(new Installation(Target).GetPackages());
 
                    image = imageSpecifier.Resolve(TapThread.Current.AbortToken);
                }
 
                if (image == null)
                {
                    log.Error(sw, "Unable to resolve image");
                    return 1;
                }
 
                log.Debug(sw, "Image resolution done");
 
                if (image.Packages.Count == 0)
                {
                    // We should prompt the user and warn them that they are about to completely wipe their installation.
                    var req = new WipeInstallationQuestion();
                    UserInput.Request(req, true);
 
                    if (req.WipeInstallation == WipeInstallationResponse.No)
                    {
                        throw new OperationCanceledException("Image installation was canceled.");
                    }
                }
 
                log.Debug("Image hash: {0}", image.Id);
                if (DryRun)
                {
                    log.Info("Resolved packages:");
                    foreach (var pkg in image.Packages)
                    {
                        log.Info("   {0}:    {1}", pkg.Name, pkg.Version);
                    }
 
                    return 0;
                }
 
                image.Deploy(Target, cancellationToken);
                return 0;
            }
            catch (AggregateException e)
            {
                foreach (var innerException in e.InnerExceptions)
                    log.Error($"- {innerException.Message}");
                throw new ExitCodeException((int)PackageExitCodes.PackageDependencyError, e.Message);
            }
 
        }
 
        enum WipeInstallationResponse
        {
            Yes,
            No,
        }
 
        [Display("Completely wipe installation?")]
        class WipeInstallationQuestion
        {
            [Browsable(true)]
            [Layout(LayoutMode.FullRow)]
            public string Message { get; } = "You are about to completely wipe your installation. Do you wish to Continue?";
 
            [Submit]
            [Layout(LayoutMode.FullRow | LayoutMode.FloatBottom)]
            public WipeInstallationResponse WipeInstallation { get; set; } = WipeInstallationResponse.Yes;
        }
    }
}