winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }