ImportExportFlow.cpp (16193B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "InstallFlow.h" 5 #include "ImportExportFlow.h" 6 #include "UpdateFlow.h" 7 #include "PackageCollection.h" 8 #include "DependenciesFlow.h" 9 #include "WorkflowBase.h" 10 #include <winget/RepositorySearch.h> 11 #include <winget/Runtime.h> 12 #include <winget/PackageVersionSelection.h> 13 14 namespace AppInstaller::CLI::Workflow 15 { 16 using namespace AppInstaller::Repository; 17 18 namespace 19 { 20 SourceDetails GetSourceDetails(const SourceDetails& source) 21 { 22 return source; 23 } 24 25 SourceDetails GetSourceDetails(const PackageCollection::Source& source) 26 { 27 return source.Details; 28 } 29 30 SourceDetails GetSourceDetails(const Repository::Source& source) 31 { 32 return source.GetDetails(); 33 } 34 35 // Creates a predicate that determines whether a source matches a description in a SourceDetails. 36 template<class T> 37 std::function<bool(const T&)> GetSourceDetailsEquivalencePredicate(const SourceDetails& details) 38 { 39 return [&](const T& source) 40 { 41 SourceDetails sourceDetails = GetSourceDetails(source); 42 return sourceDetails.Type == details.Type && sourceDetails.Identifier == details.Identifier; 43 }; 44 } 45 46 // Finds a source equivalent to the one specified. 47 template<class T> 48 typename std::vector<T>::const_iterator FindSource(const std::vector<T>& sources, const SourceDetails& details) 49 { 50 return std::find_if(sources.begin(), sources.end(), GetSourceDetailsEquivalencePredicate<T>(details)); 51 } 52 53 // Finds a source equivalent to the one specified. 54 template<class T> 55 typename std::vector<T>::iterator FindSource(std::vector<T>& sources, const SourceDetails& details) 56 { 57 return std::find_if(sources.begin(), sources.end(), GetSourceDetailsEquivalencePredicate<T>(details)); 58 } 59 60 // Gets the available version of an installed package. 61 // If requested, checks that the installed version is available and reports a warning if it is not. 62 std::shared_ptr<IPackageVersion> GetAvailableVersionForInstalledPackage( 63 Execution::Context& context, 64 std::shared_ptr<ICompositePackage> package, 65 Utility::LocIndView version, 66 Utility::LocIndView channel, 67 bool checkVersion) 68 { 69 std::shared_ptr<IPackageVersionCollection> availableVersions = GetAvailableVersionsForInstalledVersion(package); 70 71 if (!checkVersion) 72 { 73 return availableVersions->GetLatestVersion(); 74 } 75 76 auto availablePackageVersion = availableVersions->GetVersion({ "", version, channel }); 77 if (!availablePackageVersion) 78 { 79 availablePackageVersion = availableVersions->GetLatestVersion(); 80 if (availablePackageVersion) 81 { 82 // Warn installed version is not available. 83 AICLI_LOG( 84 CLI, 85 Info, 86 << "Installed package version is not available." 87 << " Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Version [" << version << "], Channel [" << channel << "]" 88 << ". Found Version [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "], Channel [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "]"); 89 context.Reporter.Warn() << Resource::String::InstalledPackageVersionNotAvailable(availablePackageVersion->GetProperty(PackageVersionProperty::Id), version, channel) << std::endl; 90 } 91 } 92 93 return availablePackageVersion; 94 } 95 } 96 97 void SelectVersionsToExport(Execution::Context& context) 98 { 99 const auto& searchResult = context.Get<Execution::Data::SearchResult>(); 100 const bool includeVersions = context.Args.Contains(Execution::Args::Type::IncludeVersions); 101 PackageCollection exportedPackages; 102 exportedPackages.ClientVersion = Runtime::GetClientVersion().get(); 103 auto& exportedSources = exportedPackages.Sources; 104 for (const auto& packageMatch : searchResult.Matches) 105 { 106 auto installedPackageVersion = GetInstalledVersion(packageMatch.Package); 107 auto version = installedPackageVersion->GetProperty(PackageVersionProperty::Version); 108 auto channel = installedPackageVersion->GetProperty(PackageVersionProperty::Channel); 109 110 // Find an available version of this package to determine its source. 111 auto availablePackageVersion = GetAvailableVersionForInstalledPackage(context, packageMatch.Package, Utility::LocIndView{ version }, Utility::LocIndView{ channel }, includeVersions); 112 if (!availablePackageVersion) 113 { 114 // Report package not found and move to next package. 115 AICLI_LOG(CLI, Warning, << "No available version of package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] was found to export"); 116 context.Reporter.Warn() << Resource::String::InstalledPackageNotAvailable(installedPackageVersion->GetProperty(PackageVersionProperty::Name)) << std::endl; 117 continue; 118 } 119 120 const auto& sourceDetails = availablePackageVersion->GetSource().GetDetails(); 121 AICLI_LOG(CLI, Info, 122 << "Installed package is available. Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Source [" << sourceDetails.Identifier << "]"); 123 124 if (!availablePackageVersion->GetManifest().DefaultLocalization.Get<Manifest::Localization::Agreements>().empty()) 125 { 126 // Report that the package requires accepting license terms 127 AICLI_LOG(CLI, Warning, << "Package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] requires license agreement to install"); 128 context.Reporter.Warn() << Resource::String::ExportedPackageRequiresLicenseAgreement(installedPackageVersion->GetProperty(PackageVersionProperty::Name)) << std::endl; 129 } 130 131 // Find the exported source for this package 132 auto sourceItr = FindSource(exportedSources, sourceDetails); 133 if (sourceItr == exportedSources.end()) 134 { 135 exportedSources.emplace_back(sourceDetails); 136 sourceItr = std::prev(exportedSources.end()); 137 } 138 139 // Take the Id from the available package because that is the one used in the source, 140 // but take the exported version from the installed package if needed. 141 PackageCollection::Package exportPackage; 142 exportPackage.Id = availablePackageVersion->GetProperty(PackageVersionProperty::Id); 143 exportPackage.InstalledLocation = Utility::ConvertToUTF16(installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledLocation]); 144 if (includeVersions) 145 { 146 exportPackage.VersionAndChannel = { version.get(), channel.get() }; 147 } 148 149 sourceItr->Packages.emplace_back(std::move(exportPackage)); 150 } 151 152 context.Add<Execution::Data::PackageCollection>(std::move(exportedPackages)); 153 } 154 155 void WriteImportFile(Execution::Context& context) 156 { 157 auto packages = PackagesJson::CreateJson(context.Get<Execution::Data::PackageCollection>()); 158 159 std::filesystem::path outputFilePath{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; 160 std::ofstream outputFileStream{ outputFilePath }; 161 outputFileStream << packages; 162 } 163 164 void ReadImportFile(Execution::Context& context) 165 { 166 std::ifstream importFile(Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::ImportFile))); 167 THROW_LAST_ERROR_IF(importFile.fail()); 168 169 Json::Value jsonRoot; 170 Json::CharReaderBuilder builder; 171 Json::String errors; 172 if (!Json::parseFromStream(builder, importFile, &jsonRoot, &errors)) 173 { 174 AICLI_LOG(CLI, Error, << "Failed to read JSON: " << errors); 175 context.Reporter.Error() << Resource::String::InvalidJsonFile << std::endl; 176 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); 177 } 178 179 PackagesJson::ParseResult parseResult = PackagesJson::TryParseJson(jsonRoot); 180 if (parseResult.Result != PackagesJson::ParseResult::Type::Success) 181 { 182 context.Reporter.Error() << Resource::String::InvalidJsonFile << std::endl; 183 if (parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema || 184 parseResult.Result == PackagesJson::ParseResult::Type::UnrecognizedSchema) 185 { 186 context.Reporter.Error() << Resource::String::ImportFileHasInvalidSchema << std::endl; 187 } 188 else if (parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed) 189 { 190 context.Reporter.Error() << parseResult.Errors << std::endl; 191 } 192 193 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); 194 } 195 196 PackageCollection& packages = parseResult.Packages; 197 if (packages.Sources.empty()) 198 { 199 AICLI_LOG(CLI, Warning, << "No packages to install"); 200 context.Reporter.Info() << Resource::String::NoPackagesFoundInImportFile << std::endl; 201 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); 202 } 203 204 if (context.Args.Contains(Execution::Args::Type::IgnoreVersions)) 205 { 206 // Strip out all the version information as we don't need it. 207 for (auto& source : packages.Sources) 208 { 209 for (auto& package : source.Packages) 210 { 211 package.VersionAndChannel = {}; 212 } 213 } 214 } 215 216 context.Add<Execution::Data::PackageCollection>(std::move(packages)); 217 } 218 219 void OpenSourcesForImport(Execution::Context& context) 220 { 221 auto availableSources = Repository::Source::GetCurrentSources(); 222 for (auto& requiredSource : context.Get<Execution::Data::PackageCollection>().Sources) 223 { 224 // Find the installed source matching the one described in the collection. 225 AICLI_LOG(CLI, Info, << "Looking for source [" << requiredSource.Details.Identifier << "]"); 226 auto matchingSource = FindSource(availableSources, requiredSource.Details); 227 if (matchingSource != availableSources.end()) 228 { 229 requiredSource.Details.Name = matchingSource->Name; 230 } 231 else 232 { 233 AICLI_LOG(CLI, Error, << "Missing required source: " << requiredSource.Details.Name); 234 context.Reporter.Warn() 235 << Resource::String::ImportSourceNotInstalled(Utility::LocIndView{ requiredSource.Details.Name }) 236 << std::endl; 237 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); 238 } 239 240 context << Workflow::OpenNamedSourceForSources(requiredSource.Details.Name); 241 if (context.IsTerminated()) 242 { 243 return; 244 } 245 } 246 } 247 248 void GetSearchRequestsForImport(Execution::Context& context) 249 { 250 const auto& sources = context.Get<Execution::Data::Sources>(); 251 std::vector<std::unique_ptr<Execution::Context>> packageSubContexts; 252 253 // Look for the packages needed from each source independently. 254 // If a package is available from multiple sources, this ensures we will get it from the right one. 255 for (auto& requiredSource : context.Get<Execution::Data::PackageCollection>().Sources) 256 { 257 // Find the required source among the open sources. This must exist as we already found them. 258 auto sourceItr = FindSource(sources, requiredSource.Details); 259 if (sourceItr == sources.end()) 260 { 261 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR); 262 } 263 264 // Search for all the packages in the source. 265 // Each search is done in a sub context to search everything regardless of previous failures. 266 Repository::Source source{ context.Get<Execution::Data::Source>(), *sourceItr, CompositeSearchBehavior::AllPackages }; 267 AICLI_LOG(CLI, Info, << "Identifying packages requested from source [" << requiredSource.Details.Identifier << "]"); 268 for (const auto& packageRequest : requiredSource.Packages) 269 { 270 AICLI_LOG(CLI, Info, << "Searching for package [" << packageRequest.Id << "]"); 271 272 // Search for the current package 273 SearchRequest searchRequest; 274 searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, packageRequest.Id.get())); 275 276 auto searchContextPtr = context.CreateSubContext(); 277 Execution::Context& searchContext = *searchContextPtr; 278 auto previousThreadGlobals = searchContext.SetForCurrentThread(); 279 280 searchContext.Add<Execution::Data::Source>(source); 281 searchContext.Add<Execution::Data::SearchRequest>(std::move(searchRequest)); 282 283 if (packageRequest.Scope != Manifest::ScopeEnum::Unknown) 284 { 285 // TODO: In the future, it would be better to not have to convert back and forth from a string 286 searchContext.Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(packageRequest.Scope)); 287 } 288 289 auto versionString = packageRequest.VersionAndChannel.GetVersion().ToString(); 290 if (!versionString.empty()) 291 { 292 searchContext.Args.AddArg(Execution::Args::Type::Version, versionString); 293 } 294 295 auto channelString = packageRequest.VersionAndChannel.GetChannel().ToString(); 296 if (!channelString.empty()) 297 { 298 searchContext.Args.AddArg(Execution::Args::Type::Channel, channelString); 299 } 300 301 packageSubContexts.emplace_back(std::move(searchContextPtr)); 302 } 303 } 304 305 context.Add<Execution::Data::PackageSubContexts>(std::move(packageSubContexts)); 306 } 307 308 void InstallImportedPackages(Execution::Context& context) 309 { 310 // Inform all dependencies here. During SubContexts processing, dependencies are ignored. 311 auto& packageSubContexts = context.Get<Execution::Data::PackageSubContexts>(); 312 Manifest::DependencyList allDependencies; 313 for (auto& packageContext : packageSubContexts) 314 { 315 allDependencies.Add(packageContext->Get<Execution::Data::Installer>().value().Dependencies); 316 } 317 context.Add<Execution::Data::Dependencies>(allDependencies); 318 319 context << 320 ReportDependencies(Resource::String::ImportCommandReportDependencies) << 321 ProcessMultiplePackages( 322 Resource::String::ImportCommandReportDependencies, APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED, ProcessMultiplePackages::Flags::IgnoreDependencies); 323 324 if (context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED) 325 { 326 context.Reporter.Error() << Resource::String::ImportInstallFailed << std::endl; 327 } 328 } 329 }