From dac55b5f9cf6e42742e72556ce0db3587b9d9bc4 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Wed, 6 Jan 2021 12:39:18 -0800 Subject: [PATCH 01/34] Prototype of import/export --- doc/Settings.md | 10 + doc/packages.schema.json | 76 +++++++ doc/settings.schema.json | 5 + src/AppInstallerCLI.sln | 1 + .../AppInstallerCLICore.vcxproj | 8 + .../AppInstallerCLICore.vcxproj.filters | 24 +++ src/AppInstallerCLICore/Argument.cpp | 20 +- .../Commands/ExportCommand.cpp | 104 ++++++++++ .../Commands/ExportCommand.h | 26 +++ .../Commands/ImportCommand.cpp | 54 +++++ .../Commands/ImportCommand.h | 24 +++ .../Commands/InstallCommand.cpp | 11 +- .../Commands/RootCommand.cpp | 4 + .../Commands/UpgradeCommand.cpp | 4 +- src/AppInstallerCLICore/ExecutionArgs.h | 6 + src/AppInstallerCLICore/ExecutionContext.h | 21 ++ src/AppInstallerCLICore/PackageCollection.cpp | 192 ++++++++++++++++++ src/AppInstallerCLICore/PackageCollection.h | 75 +++++++ src/AppInstallerCLICore/Resources.h | 8 +- .../Workflows/ImportExportFlow.cpp | 105 ++++++++++ .../Workflows/ImportExportFlow.h | 21 ++ .../Workflows/InstallFlow.cpp | 35 ++++ .../Workflows/InstallFlow.h | 12 ++ .../Workflows/WorkflowBase.cpp | 7 +- .../Workflows/WorkflowBase.h | 14 ++ .../Shared/Strings/en-us/winget.resw | 18 ++ .../ExperimentalFeature.cpp | 4 + .../Public/winget/ExperimentalFeature.h | 1 + .../Public/winget/UserSettings.h | 4 +- src/AppInstallerCommonCore/UserSettings.cpp | 8 +- 30 files changed, 878 insertions(+), 24 deletions(-) create mode 100644 doc/packages.schema.json create mode 100644 src/AppInstallerCLICore/Commands/ExportCommand.cpp create mode 100644 src/AppInstallerCLICore/Commands/ExportCommand.h create mode 100644 src/AppInstallerCLICore/Commands/ImportCommand.cpp create mode 100644 src/AppInstallerCLICore/Commands/ImportCommand.h create mode 100644 src/AppInstallerCLICore/PackageCollection.cpp create mode 100644 src/AppInstallerCLICore/PackageCollection.h create mode 100644 src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp create mode 100644 src/AppInstallerCLICore/Workflows/ImportExportFlow.h diff --git a/doc/Settings.md b/doc/Settings.md index f9ba7858e6..231ce7315c 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -97,3 +97,13 @@ While work is in progress on uninstall, the command is hidden behind a feature t "uninstall": true }, ``` + +### importExport + +While work is in progress for import and export, the command is hidden behind a feature toggle. One can enable it as below: + +``` + "experimentalFeatures": { + "importExport": true + }, +``` diff --git a/doc/packages.schema.json b/doc/packages.schema.json new file mode 100644 index 0000000000..3a4923e94e --- /dev/null +++ b/doc/packages.schema.json @@ -0,0 +1,76 @@ +{ + "$id": "https://aka.ms/winget-packages.schema.json", + "$schema": "https://json-schema.org/draft/2019-09/schema#", + + "title": "winget Packages List Schema", + "description": "Describes a list of packages for batch installs", + + "properties": { + "wingetVersion": { + "description": "Version of winget that generated this list", + "type": "string", + "pattern": "^[0-9]+\\.[0-9]\\.[0-9]$" + }, + + "creationDate": { + "description": "Date when this list was generated", + "type": "string", + "format": "date-time" + }, + + "sources": { + "description": "List of sources from wich each package comes from", + "type": "array", + "items": { + "description": "A source and the list of packages to install from it", + "type": "object", + "required": true, + "properties": { + "name": { + "description": "Name of the source", + "type": "string", + "required": true + }, + + "argument": { + "description": "TODO", + "type": "string", + "required": true + }, + + "packages": { + "description": "List of packages installed from this source", + "type": "array", + "required": true, + "minItems": 1, + "items": { + "description": "A package to be installed from this source", + "type": "object", + "properties": { + "id": { + "description": "Package ID", + "type": "string", + "required": true + }, + + "version": { + "description": "Package version", + "type": "string", + "required": true + }, + + "channel": { + "description": "TODO", + "type": "string", + "required": false + } + } + } + } + } + } + } + }, + + "additionalProperties": true +} diff --git a/doc/settings.schema.json b/doc/settings.schema.json index 45072807b1..16f43b6a02 100644 --- a/doc/settings.schema.json +++ b/doc/settings.schema.json @@ -62,6 +62,11 @@ "description": "Enable the uninstall command while it is in development", "type": "boolean", "default": false + }, + "importExport": { + "description": "Enable the import and export commands", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index ee39aea80c..950e04bf30 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project", "Project", "{8D53 ..\azure-pipelines.yml = ..\azure-pipelines.yml ..\cgmanifest.json = ..\cgmanifest.json ..\README.md = ..\README.md + ..\doc\packages.schema.json = ..\doc\packages.schema.json ..\doc\Settings.md = ..\doc\Settings.md ..\doc\settings.schema.json = ..\doc\settings.schema.json EndProjectSection diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 6eff93b8ee..ec96bde19d 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -177,6 +177,8 @@ + + @@ -201,7 +203,9 @@ + + @@ -213,11 +217,14 @@ + + + @@ -241,6 +248,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 324ea67f93..40578c390e 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -141,6 +141,18 @@ Workflows + + Commands + + + Header Files + + + Workflows + + + Commands + @@ -248,6 +260,18 @@ Workflows + + Commands + + + Source Files + + + Workflows + + + Commands + diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index ad1a036a96..e38ac3e2b9 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -51,14 +51,6 @@ namespace AppInstaller::CLI return Argument{ "override", NoAlias, Args::Type::Override, Resource::String::OverrideArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::InstallLocation: return Argument{ "location", 'l', Args::Type::InstallLocation, Resource::String::LocationArgumentDescription, ArgumentType::Standard }; - case Args::Type::HashFile: - return Argument{ "file", 'f', Args::Type::HashFile, Resource::String::FileArgumentDescription, ArgumentType::Positional, true }; - case Args::Type::Msix: - return Argument{ "msix", 'm', Args::Type::Msix, Resource::String::MsixArgumentDescription, ArgumentType::Flag }; - case Args::Type::ListVersions: - return Argument{ "versions", NoAlias, Args::Type::ListVersions, Resource::String::VersionsArgumentDescription, ArgumentType::Flag }; - case Args::Type::Help: - return Argument{ "help", APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_CHAR, Args::Type::Help, Resource::String::HelpArgumentDescription, ArgumentType::Flag }; case Args::Type::SourceName: return Argument{ "name", 'n', Args::Type::SourceName,Resource::String::SourceNameArgumentDescription, ArgumentType::Positional, false }; case Args::Type::SourceArg: @@ -67,6 +59,18 @@ namespace AppInstaller::CLI return Argument{ "type", 't', Args::Type::SourceType, Resource::String::SourceTypeArgumentDescription, ArgumentType::Positional }; case Args::Type::ValidateManifest: return Argument{ "manifest", NoAlias, Args::Type::ValidateManifest, Resource::String::ValidateManifestArgumentDescription, ArgumentType::Positional, true }; + case Args::Type::HashFile: + return Argument{ "file", 'f', Args::Type::HashFile, Resource::String::FileArgumentDescription, ArgumentType::Positional, true }; + case Args::Type::Msix: + return Argument{ "msix", 'm', Args::Type::Msix, Resource::String::MsixArgumentDescription, ArgumentType::Flag }; + case Args::Type::OutputFile: + return Argument{ "output", 'o', Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Positional, true }; + case Args::Type::ImportFile: + return Argument{ "input", 'i', Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }; + case Args::Type::ListVersions: + return Argument{ "versions", NoAlias, Args::Type::ListVersions, Resource::String::VersionsArgumentDescription, ArgumentType::Flag }; + case Args::Type::Help: + return Argument{ "help", APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_CHAR, Args::Type::Help, Resource::String::HelpArgumentDescription, ArgumentType::Flag }; case Args::Type::NoVT: return Argument{ "no-vt", NoAlias, Args::Type::NoVT, Resource::String::NoVTArgumentDescription, ArgumentType::Flag, Argument::Visibility::Hidden }; case Args::Type::RainbowStyle: diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp new file mode 100644 index 0000000000..d6391da12b --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ExportCommand.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/ImportExportFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Resources.h" + +namespace AppInstaller::CLI +{ + using namespace std::string_view_literals; + + std::vector ExportCommand::GetArguments() const + { + return { + Argument::ForType(Execution::Args::Type::OutputFile), + Argument::ForType(Execution::Args::Type::Query), + Argument::ForType(Execution::Args::Type::Id), + Argument::ForType(Execution::Args::Type::Name), + Argument::ForType(Execution::Args::Type::Moniker), + Argument::ForType(Execution::Args::Type::Source), + Argument::ForType(Execution::Args::Type::Tag), + Argument::ForType(Execution::Args::Type::Command), + Argument::ForType(Execution::Args::Type::Count), + Argument::ForType(Execution::Args::Type::Exact), + }; + } + + Resource::LocString ExportCommand::ShortDescription() const + { + return { Resource::String::ExportCommandShortDescription }; + } + + Resource::LocString ExportCommand::LongDescription() const + { + return { Resource::String::ExportCommandLongDescription }; + } + + void ExportCommand::Complete(Execution::Context& context, Execution::Args::Type valueType) const + { + if (valueType == Execution::Args::Type::OutputFile) + { + // Intentionally output nothing to allow pass through to filesystem. + return; + } + + context << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + + switch (valueType) + { + case Execution::Args::Type::Query: + context << + Workflow::RequireCompletionWordNonEmpty << + Workflow::SearchSourceForManyCompletion << + Workflow::CompleteWithMatchedField; + break; + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Source: + case Execution::Args::Type::Tag: + case Execution::Args::Type::Command: + context << + Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); + break; + } + } + + std::string ExportCommand::HelpLink() const + { + // TODO: point to correct location + return "https://aka.ms/winget-command-export"; + } + + void ExportCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::Manifest) && + (execArgs.Contains(Execution::Args::Type::Query) || + execArgs.Contains(Execution::Args::Type::Id) || + execArgs.Contains(Execution::Args::Type::Name) || + execArgs.Contains(Execution::Args::Type::Moniker) || + execArgs.Contains(Execution::Args::Type::Version) || + execArgs.Contains(Execution::Args::Type::Channel) || + execArgs.Contains(Execution::Args::Type::Source) || + execArgs.Contains(Execution::Args::Type::Exact) || + execArgs.Contains(Execution::Args::Type::All))) + { + throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); + } + } + + void ExportCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << + Workflow::SearchSourceForMany << + Workflow::EnsureMatchesFromSearchResult(true) << + Workflow::Export; + } +} diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.h b/src/AppInstallerCLICore/Commands/ExportCommand.h new file mode 100644 index 0000000000..bb93d92a19 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ExportCommand.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + // Command to get the set of installed packages on the system. + struct ExportCommand final : public Command + { + ExportCommand(std::string_view parent) : Command("Export", parent, Settings::ExperimentalFeature::Feature::ExperimentalImportExport) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + + std::string HelpLink() const override; + + protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp new file mode 100644 index 0000000000..7e0e832f87 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ImportCommand.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/ImportExportFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Resources.h" + +namespace AppInstaller::CLI +{ + using namespace std::string_view_literals; + + std::vector ImportCommand::GetArguments() const + { + return { + Argument::ForType(Execution::Args::Type::ImportFile), + }; + } + + Resource::LocString ImportCommand::ShortDescription() const + { + return { Resource::String::ImportCommandShortDescription }; + } + + Resource::LocString ImportCommand::LongDescription() const + { + return { Resource::String::ImportCommandLongDescription }; + } + + std::string ImportCommand::HelpLink() const + { + // TODO: point to correct location + return "https://aka.ms/winget-command-import"; + } + + void ImportCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (!std::filesystem::exists(execArgs.GetArg(Execution::Args::Type::ImportFile))) + { + // TODO + throw CommandException(Resource::String::VerifyFileFailedNotExist, execArgs.GetArg(Execution::Args::Type::ImportFile)); + } + } + + void ImportCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::ReadImportFile << + Workflow::SearchPackagesForImport << + Workflow::InstallMultiple; + } +} diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.h b/src/AppInstallerCLICore/Commands/ImportCommand.h new file mode 100644 index 0000000000..050b132352 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ImportCommand.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + // Command to install a set of packages from a list. + struct ImportCommand final : public Command + { + ImportCommand(std::string_view parent) : Command("Import", parent, Settings::ExperimentalFeature::Feature::ExperimentalImportExport) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + std::string HelpLink() const override; + + protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp index 157f9676bc..a900ac1ce7 100644 --- a/src/AppInstallerCLICore/Commands/InstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -99,15 +99,6 @@ namespace AppInstaller::CLI context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << Workflow::GetManifest << - Workflow::EnsureMinOSVersion << - Workflow::SelectInstaller << - Workflow::EnsureApplicableInstaller << - Workflow::ShowInstallationDisclaimer << - Workflow::ReportExecutionStage(ExecutionStage::Download) << - Workflow::DownloadInstaller << - Workflow::ReportExecutionStage(ExecutionStage::Execution) << - Workflow::ExecuteInstaller << - Workflow::ReportExecutionStage(ExecutionStage::PostExecution) << - Workflow::RemoveInstaller; + Workflow::InstallPackageVersion; } } diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp index 83f2441bc2..aa34ba601e 100644 --- a/src/AppInstallerCLICore/Commands/RootCommand.cpp +++ b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -16,6 +16,8 @@ #include "FeaturesCommand.h" #include "ExperimentalCommand.h" #include "CompleteCommand.h" +#include "ExportCommand.h" +#include "ImportCommand.h" #include "Resources.h" #include "TableOutput.h" @@ -40,6 +42,8 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), + std::make_unique(FullName()), }); } diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp index d2fcbd58bd..10c5534863 100644 --- a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp +++ b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -126,8 +126,8 @@ namespace AppInstaller::CLI context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << - OpenSource << - OpenCompositeSource(Repository::PredefinedSource::Installed); + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); if (ShouldListUpgrade(context)) { diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 0d0191187f..47f829a6ab 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -55,6 +55,12 @@ namespace AppInstaller::CLI::Execution CommandLine, Position, + // Export Command + OutputFile, + + // Import Command + ImportFile, + // Other All, // Used in Update command to update all installed packages to latest Force, // Generic flag to enable a command to skip some check diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index 71ed0e141f..f287e163de 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -8,6 +8,7 @@ #include "ExecutionReporter.h" #include "ExecutionArgs.h" #include "CompletionData.h" +#include "PackageCollection.h" #include #include @@ -57,9 +58,16 @@ namespace AppInstaller::CLI::Execution CompletionData, InstalledPackageVersion, ExecutionStage, + // On Uninstall: Uninstall string to be executed UninstallString, + // On Uninstall: Package Family Names of an MSIX package to uninstall PackageFamilyNames, + // On Uninstall: Product codes of an MSI to uninstall ProductCodes, + // On Import: Packages to be installed from each source + PackageRequests, + + PackagesToInstall, Max }; @@ -184,6 +192,19 @@ namespace AppInstaller::CLI::Execution using value_t = std::vector; }; + template <> + struct DataMapping + { + using value_t = PackageCollectionRequest; + }; + + + template <> + struct DataMapping + { + using value_t = std::vector< std::shared_ptr>; + }; + // Used to deduce the DataVariant type; making a variant that includes std::monostate and all DataMapping types. template inline auto Deduce(std::index_sequence) { return std::variant(I)>::value_t...>{}; } diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp new file mode 100644 index 0000000000..a6f05068a1 --- /dev/null +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PackageCollection.h" +#include "AppInstallerRepositorySource.h" +#include "AppInstallerRuntime.h" + +#include +#include +#include +#include + +namespace AppInstaller::CLI +{ + using namespace AppInstaller::CLI::PackagesJson; + using namespace AppInstaller::Repository; + + namespace + { + // Gets a property of a JSON object by its name. + template + Json::Value& GetJsonProperty(Json::Value& node, const std::string& propertyName) + { + if (!node.isMember(propertyName)) + { + node[propertyName] = Json::Value{ T }; + } + + THROW_HR_IF(E_NOT_VALID_STATE, node[propertyName].type() != T); + return node[propertyName]; + } + + // Reads the description of a package from a Package node in the JSON. + PackageRequest ParsePackageNode(const Json::Value& packageNode) + { + std::string id = packageNode[PACKAGE_ID_PROPERTY].asString(); + std::string version = packageNode[PACKAGE_VERSION_PROPERTY].asString(); + std::string channel = packageNode.isMember(PACKAGE_CHANNEL_PROPERTY) ? packageNode[PACKAGE_CHANNEL_PROPERTY].asString() : ""; + + PackageRequest packageRequest{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; + + return packageRequest; + } + + // Reads the description of a Source and all the packages needed from it, from a Source node in the JSON. + PackageRequestsFromSource ParseSourceNode(const Json::Value& sourceNode) + { + PackageRequestsFromSource requestsFromSource + { + Utility::LocIndString{ sourceNode[SOURCE_NAME_PROPERTY].asString() }, + Utility::LocIndString{ sourceNode[SOURCE_ARGUMENT_PROPERTY].asString() } + }; + + for (const auto& packageNode : sourceNode[PACKAGES_PROPERTY]) + { + requestsFromSource.Packages.push_back(ParsePackageNode(packageNode)); + } + + return requestsFromSource; + } + + // Gets the available PackageVersion that has the same version as the installed version. + // The package must have an installed version. + // Returns null if not available. + std::shared_ptr GetAvailableVersionMatchingInstalled(const IPackage& package) + { + auto installedVersion = package.GetInstalledVersion(); + PackageVersionKey installedVersionKey + { + "", + installedVersion->GetProperty(PackageVersionProperty::Version).get(), + installedVersion->GetProperty(PackageVersionProperty::Channel).get(), + }; + return package.GetAvailableVersion(installedVersionKey); + } + } + + namespace PackagesJson + { + Json::Value CreateRoot() + { + Json::Value root{ Json::ValueType::objectValue }; + root[WINGET_VERSION_PROPERTY] = Runtime::GetClientVersion().get(); + root[SCHEMA_PROPERTY] = SCHEMA_PATH; + + // TODO: Clean up. Do we need this? + std::time_t currentTimeTT = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm currentTimeTM; + std::stringstream currentTimeStream; + gmtime_s(¤tTimeTM, ¤tTimeTT); + currentTimeStream << std::put_time(¤tTimeTM, "%c"); + root[CREATION_DATE_PROPERTY] = currentTimeStream.str(); + + return root; + } + + Json::Value& AddSourceNode(Json::Value& root, const PackageRequestsFromSource& source) + { + Json::Value sourceNode{ Json::ValueType::objectValue }; + sourceNode[SOURCE_NAME_PROPERTY] = source.SourceName.get(); + sourceNode[SOURCE_ARGUMENT_PROPERTY] = source.SourceArg.get(); + sourceNode[PACKAGES_PROPERTY] = Json::Value{ Json::ValueType::arrayValue }; + + auto& sourcesNode = GetJsonProperty(root, SOURCES_PROPERTY); + for (const auto& package : source.Packages) + { + AddPackageToSource(sourceNode, package); + } + + return sourcesNode.append(std::move(sourceNode)); + } + + Json::Value& AddPackageToSource(Json::Value& sourceNode, const PackageRequest& package) + { + Json::Value packageNode{ Json::ValueType::objectValue }; + packageNode[PACKAGE_ID_PROPERTY] = package.Id.get(); + packageNode[PACKAGE_VERSION_PROPERTY] = package.VersionAndChannel.GetVersion().ToString(); + + // Only add channel if present + const std::string& channel = package.VersionAndChannel.GetChannel().ToString(); + if (!channel.empty()) + { + packageNode[PACKAGE_CHANNEL_PROPERTY] = channel; + } + + return sourceNode[PACKAGES_PROPERTY].append(std::move(packageNode)); + } + } + + PackageRequest::PackageRequest(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel) : + Id(std::move(id)), VersionAndChannel(std::move(version), std::move(channel)) {} + + PackageRequest::PackageRequest(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel) : + Id(std::move(id)), VersionAndChannel(std::move(versionAndChannel)) {} + + PackageRequestsFromSource::PackageRequestsFromSource(Utility::LocIndString&& sourceName, Utility::LocIndString&& sourceArg) : + SourceName(std::move(sourceName)), SourceArg(std::move(sourceArg)) {} + + std::vector ParsePackageCollection(const Json::Value& root) + { + // TODO: Validate schema. The following assumes the file is already valid. + // TODO: Use version & creation date? + + std::vector requests = {}; + for (const auto& sourceNode : root[SOURCES_PROPERTY]) + { + requests.push_back(ParseSourceNode(sourceNode)); + } + + return requests; + } + + std::vector ConvertSearchResultToPackageRequests(const SearchResult& packages) + { + std::vector requests = {}; + for (const auto& packageMatch : packages.Matches) + { + auto availableVersion = GetAvailableVersionMatchingInstalled(*packageMatch.Package); + if (!availableVersion) + { + AICLI_LOG(CLI, Info, << "No available package found for " << packageMatch.Package->GetProperty(PackageProperty::Id)); + continue; + } + + auto sourceDetails = availableVersion->GetSource()->GetDetails(); + auto sourceItr = std::find_if(requests.begin(), requests.end(), [&](const PackageRequestsFromSource& s) { return s.SourceName == sourceDetails.Name; }); + if (sourceItr == requests.end()) + { + requests.emplace_back(Utility::LocIndString{ sourceDetails.Name }, Utility::LocIndString{ sourceDetails.Arg }); + sourceItr = std::prev(requests.end()); + } + + sourceItr->Packages.emplace_back( + availableVersion->GetProperty(PackageVersionProperty::Id), + availableVersion->GetProperty(PackageVersionProperty::Version).get(), + availableVersion->GetProperty(PackageVersionProperty::Channel).get()); + } + + return requests; + } + + Json::Value ConvertPackageRequestsToJson(const PackageCollectionRequest& packages) + { + Json::Value root = CreateRoot(); + for (const auto& source : packages) + { + AddSourceNode(root, source); + } + + return root; + } +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h new file mode 100644 index 0000000000..23eba4a578 --- /dev/null +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include "AppInstallerRepositorySearch.h" + +#include +#include + +namespace AppInstaller::CLI +{ + using namespace AppInstaller::Repository; + + // Container for data used to identify a package to be installed. + struct PackageRequest + { + PackageRequest() = default; + PackageRequest(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel); + PackageRequest(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel); + + Utility::LocIndString Id; + Utility::VersionAndChannel VersionAndChannel; + }; + + // Container for data to identify multiple packages to be installed from a single source. + struct PackageRequestsFromSource + { + PackageRequestsFromSource() = default; + PackageRequestsFromSource(Utility::LocIndString&& sourceName, Utility::LocIndString&& sourceArg); + + Utility::LocIndString SourceName; + Utility::LocIndString SourceArg; + std::vector Packages; + }; + + using PackageCollectionRequest = std::vector; + + // Parses a package collection from a JSON file. + PackageCollectionRequest ParsePackageCollection(const Json::Value& root); + + // Converts the result of a search to a collection of package + // requests adequate for exporting. + PackageCollectionRequest ConvertSearchResultToPackageRequests(const SearchResult& packages); + + // Creates a JSON representing a package collection. + Json::Value ConvertPackageRequestsToJson(const PackageCollectionRequest& packages); + + namespace PackagesJson + { + // Strings used in the Packages JSON file. + // Most will be used to access a JSON value, so they need to be std::string + const std::string SCHEMA_PROPERTY = "$schema"; + const std::string SCHEMA_PATH = "https://aka.ms/winget-packages.schema.json"; + const std::string WINGET_VERSION_PROPERTY = "wingetVersion"; + const std::string CREATION_DATE_PROPERTY = "creationDate"; + + const std::string SOURCES_PROPERTY = "sources"; + const std::string SOURCE_NAME_PROPERTY = "name"; + const std::string SOURCE_ARGUMENT_PROPERTY = "argument"; + + const std::string PACKAGES_PROPERTY = "packages"; + const std::string PACKAGE_ID_PROPERTY = "id"; + const std::string PACKAGE_VERSION_PROPERTY = "version"; + const std::string PACKAGE_CHANNEL_PROPERTY = "channel"; + + // Creates a minimal root object of a Packages JSON file. + Json::Value CreateRoot(); + + // Adds a new Source node to the JSON, and returns it. + Json::Value& AddSourceNode(Json::Value& root, const PackageRequestsFromSource& source); + + // Adds a new Package node to a Source node in the Json file, and returns it. + Json::Value& AddPackageToSource(Json::Value& source, const PackageRequest& package); + } +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 095c038566..9a46dc602b 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -48,6 +48,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesCommandLongDescription); @@ -68,6 +70,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(HelpForDetails); WINGET_DEFINE_RESOURCE_STRINGID(HelpLinkPreamble); WINGET_DEFINE_RESOURCE_STRINGID(IdArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer2); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimerMSStore); @@ -127,6 +132,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatchHelp); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoSourceDefined); WINGET_DEFINE_RESOURCE_STRINGID(Options); + WINGET_DEFINE_RESOURCE_STRINGID(OutputFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Package); WINGET_DEFINE_RESOURCE_STRINGID(PendingWorkError); @@ -210,8 +216,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFailedWithCode); - WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowUninstallSuccess); WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowStartingPackageUninstall); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowUninstallSuccess); WINGET_DEFINE_RESOURCE_STRINGID(UnrecognizedCommand); WINGET_DEFINE_RESOURCE_STRINGID(UpdateAllArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicable); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp new file mode 100644 index 0000000000..b68b7fda94 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "InstallFlow.h" +#include "ImportExportFlow.h" +#include "PackageCollection.h" +#include "WorkflowBase.h" +#include "CompositeSource.h" + +namespace AppInstaller::CLI::Workflow +{ + void Export(Execution::Context& context) + { + std::filesystem::path outputFile{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; + auto packages = ConvertSearchResultToPackageRequests(context.Get()); + std::ofstream{ outputFile } << ConvertPackageRequestsToJson(packages); + } + + void ReadImportFile(Execution::Context& context) + { + std::filesystem::path importFile{ context.Args.GetArg(Execution::Args::Type::ImportFile) }; + Json::Value jsonRoot = {}; + std::ifstream{ importFile } >> jsonRoot; + auto packages = ParsePackageCollection(jsonRoot); + context.Add(packages); + + if (packages.empty()) + { + AICLI_LOG(CLI, Warning, << "No packages to install"); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); + } + } + + void SearchPackagesForImport(Execution::Context& context) + { + auto availableSources = Repository::GetSources(); + std::vector> packagesToInstall; + + // Aggregated source with all the required sources. + // We keep it as the source of the root context to keep all the + // source objects alive for install. + auto aggregatedSource = std::make_shared("*ImportSource"); + + for (auto& requiredSource : context.Get()) + { + // Match required sources with the available sources by their arguments, + // as they may have been added with a different name. + std::optional matchingSource = {}; + for (auto& availableSource : availableSources) + { + if (availableSource.Arg == requiredSource.SourceArg.get()) + { + matchingSource = availableSource; + break; + } + } + + if (matchingSource.has_value()) + { + requiredSource.SourceName = Utility::LocIndString{ matchingSource.value().Name }; + } + else + { + // TODO: Add option for ignoring/installing missing sources? + AICLI_LOG(CLI, Error, << "Missing required source: " << requiredSource.SourceName); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); + } + + // Search for all the packages in the source + Logging::SubExecutionTelemetryScope subExecution; + + // We want to do best effort to install all packages regardless of previous failures + auto searchContextPtr = context.Clone(); + Execution::Context& searchContext = *searchContextPtr; + + searchContext << OpenNamedSource(requiredSource.SourceName); + auto source = searchContext.Get(); + aggregatedSource->AddAvailableSource(source); + for (const auto& packageRequest : requiredSource.Packages) + { + // TODO: Case insensitive? + MatchType matchType = MatchType::Exact; + + SearchRequest searchRequest; + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, packageRequest.Id)); + + searchContext.Add(source->Search(searchRequest)); + searchContext << EnsureOneMatchFromSearchResult(false); + + PackageVersionKey requestedVersion + { + "", + packageRequest.VersionAndChannel.GetVersion().ToString(), + packageRequest.VersionAndChannel.GetChannel().ToString(), + }; + + // TODO: handle unavailable version + packagesToInstall.push_back(searchContext.Get()->GetAvailableVersion(requestedVersion)); + } + } + + context.Add(std::move(packagesToInstall)); + context.Add(std::move(aggregatedSource)); + } +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.h b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h new file mode 100644 index 0000000000..8c6c756a17 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // + // Required Args: + // Inputs: + // Outputs: + void Export(Execution::Context& context); + + // Required Args: ImportFile + // Outputs: PackageRequests + void ReadImportFile(Execution::Context& context); + + // Inputs: PackageRequests + // Outputs: PackagesToInstall + void SearchPackagesForImport(Execution::Context& context); +} diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 2091529c83..3f747cb0f4 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -368,4 +368,39 @@ namespace AppInstaller::CLI::Workflow } } } + + void InstallPackageVersion(Execution::Context& context) + { + context << + Workflow::EnsureMinOSVersion << + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller << + Workflow::ShowInstallationDisclaimer << + Workflow::ReportExecutionStage(ExecutionStage::Download) << + Workflow::DownloadInstaller << + Workflow::ReportExecutionStage(ExecutionStage::Execution) << + Workflow::ExecuteInstaller << + Workflow::ReportExecutionStage(ExecutionStage::PostExecution) << + Workflow::RemoveInstaller; + } + + void InstallMultiple(Execution::Context& context) + { + for (auto package : context.Get()) + { + Logging::SubExecutionTelemetryScope subExecution; + + // We want to do best effort to install all packages regardless of previous failures + auto installContextPtr = context.Clone(); + Execution::Context& installContext = *installContextPtr; + + // Extract the data needed for installing + installContext.Add(package); + installContext.Add(package->GetManifest()); + + installContext << InstallPackageVersion; + + // TODO: Handle errors in sub context + } + } } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h index 8e6351fecc..0be8045d10 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.h +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -82,4 +82,16 @@ namespace AppInstaller::CLI::Workflow // Inputs: InstallerPath // Outputs: None void RemoveInstaller(Execution::Context& context); + + // Installs a single package from its manifest + // Required Args: None + // Inputs: Manifest, PackageVersion, Source + // Outputs: Manifest + void InstallPackageVersion(Execution::Context& context); + + // Installs multiple packages. + // Required Args: None + // Inputs: Manifests + // Outputs: None + void InstallMultiple(Execution::Context& context); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 8018e1a044..dbe0c20d65 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -102,10 +102,15 @@ namespace AppInstaller::CLI::Workflow sourceName = context.Args.GetArg(Execution::Args::Type::Source); } + context << OpenNamedSource(sourceName); + } + + void OpenNamedSource::operator()(Execution::Context& context) const + { std::shared_ptr source; try { - auto result = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, sourceName, std::placeholders::_1), true); + auto result = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, m_sourceName, std::placeholders::_1), true); source = result.Source; // We'll only report the source update failure as warning and continue diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index 8f457c2132..84935f78dd 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -60,6 +60,20 @@ namespace AppInstaller::CLI::Workflow // Outputs: Source void OpenSource(Execution::Context& context); + // Creates a source object for a source from its name. + // Required Args: None + // Inputs: None + // Outputs: Source + struct OpenNamedSource : public WorkflowTask + { + OpenNamedSource(std::string_view sourceName) : WorkflowTask("OpenNamedSource"), m_sourceName(sourceName) {} + + void operator()(Execution::Context& context) const override; + + private: + std::string_view m_sourceName; + }; + // Creates a source object for a predefined source. // Required Args: None // Inputs: None diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 456fae16e0..6ac6bb4592 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -756,4 +756,22 @@ They can be configured through the settings file 'winget settings'. Uninstall failed with exit code: + + TODO Export description + + + TODO Export + + + TODO Import description + + + TODO Import + + + TODO + + + TODO + \ No newline at end of file diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index a5b9b437cb..55a8e93c41 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -27,6 +27,8 @@ namespace AppInstaller::Settings return User().Get(); case Feature::ExperimentalUninstall: return User().Get(); + case Feature::ExperimentalImportExport: + return User().Get(); default: THROW_HR(E_UNEXPECTED); } @@ -48,6 +50,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Upgrade Command", "upgrade", "https://aka.ms/winget-settings", Feature::ExperimentalUpgrade }; case Feature::ExperimentalUninstall: return ExperimentalFeature{ "Uninstall Command", "uninstall", "https://aka.ms/winget-settings", Feature::ExperimentalUninstall }; + case Feature::ExperimentalImportExport: + return ExperimentalFeature{ "Import & Export Commands", "importExport", "https://aka.ms/winget-settings", Feature::ExperimentalImportExport }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 23ebf50206..2d09eea653 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -25,6 +25,7 @@ namespace AppInstaller::Settings ExperimentalList = 0x8, ExperimentalUpgrade = 0x10, ExperimentalUninstall = 0x20, + ExperimentalImportExport = 0x40, Max, // This MUST always be last }; diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 5475012d2a..3b33c4d9fe 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -52,6 +52,7 @@ namespace AppInstaller::Settings EFList, EFExperimentalUpgrade, EFUninstall, + EFImportExport, Max }; @@ -86,7 +87,8 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalMSStore, bool, bool, false, ".experimentalFeatures.experimentalMSStore"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFList, bool, bool, false, ".experimentalFeatures.list"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalUpgrade, bool, bool, false, ".experimentalFeatures.upgrade"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::EFUninstall, bool, bool, false, "experimentalFeatures.uninstall"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFUninstall, bool, bool, false, ".experimentalFeatures.uninstall"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFImportExport, bool, bool, false, ".experimentalFeatures.importExport"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index cf030387e1..d9d1308d0c 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -126,7 +126,7 @@ namespace AppInstaller::Settings } else { - AICLI_LOG(Core, Info, << "Setting " << path <<" not found. Using default"); + AICLI_LOG(Core, Info, << "Setting " << path << " not found. Using default"); } } @@ -216,6 +216,12 @@ namespace AppInstaller::Settings { return value; } + + std::optional::value_t> + SettingMapping::Validate(const SettingMapping::json_t& value) + { + return value; + } } UserSettings::UserSettings() : m_type(UserSettingsType::Default) From 4d6c28accf2ba8915fbdbb8b41476543f565653c Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Tue, 12 Jan 2021 16:56:03 -0800 Subject: [PATCH 02/34] Preserve unavailable packages on export; add option for exact versions --- doc/packages.schema.json | 72 ++++--- src/AppInstallerCLICore/Argument.cpp | 2 + .../Commands/ExportCommand.cpp | 3 +- .../Commands/ImportCommand.cpp | 1 + src/AppInstallerCLICore/ExecutionArgs.h | 1 + src/AppInstallerCLICore/ExecutionContext.h | 14 +- src/AppInstallerCLICore/PackageCollection.cpp | 128 ++++++------ src/AppInstallerCLICore/PackageCollection.h | 54 ++--- src/AppInstallerCLICore/Resources.h | 1 + .../Workflows/ImportExportFlow.cpp | 187 +++++++++++++----- .../Workflows/ImportExportFlow.h | 24 ++- .../Shared/Strings/en-us/winget.resw | 3 + .../Public/AppInstallerRepositorySource.h | 2 +- 13 files changed, 319 insertions(+), 173 deletions(-) diff --git a/doc/packages.schema.json b/doc/packages.schema.json index 3a4923e94e..379b2be4d7 100644 --- a/doc/packages.schema.json +++ b/doc/packages.schema.json @@ -5,62 +5,90 @@ "title": "winget Packages List Schema", "description": "Describes a list of packages for batch installs", + "additionalProperties": true, + "properties": { - "wingetVersion": { - "description": "Version of winget that generated this list", + "WinGetVersion": { + "description": "Version of winget that generated this file", "type": "string", "pattern": "^[0-9]+\\.[0-9]\\.[0-9]$" }, - "creationDate": { + "CreationDate": { "description": "Date when this list was generated", "type": "string", "format": "date-time" }, - "sources": { - "description": "List of sources from wich each package comes from", + "Sources": { + "description": "Sources from which each package comes from", "type": "array", + "items": { "description": "A source and the list of packages to install from it", "type": "object", "required": true, + "additionalProperties": true, + "properties": { - "name": { - "description": "Name of the source", - "type": "string", - "required": true - }, + "SourceDetails": { + "description": "Details about this source", + "type": "object", + "required": false, + "additionalProperties": true, + + "properties": { + "Name": { + "description": "Name of the source", + "type": "string", + "required": true + }, + + "Identifier": { + "description": "Identifier for the source", + "type": "string", + "required": true + }, - "argument": { - "description": "TODO", - "type": "string", - "required": true + "Argument": { + "description": "Argument used to install the source", + "type": "string", + "required": true + }, + + "Type": { + "description": "Type of the source", + "type": "string", + "required": true + } + } }, - "packages": { - "description": "List of packages installed from this source", + "Packages": { + "description": "Packages installed from this source", "type": "array", "required": true, "minItems": 1, + "items": { "description": "A package to be installed from this source", "type": "object", + "additionalProperties": true, "properties": { - "id": { + "Id": { "description": "Package ID", "type": "string", "required": true }, - "version": { + "Version": { "description": "Package version", "type": "string", "required": true }, - "channel": { - "description": "TODO", + "Channel": { + "description": "Package channel", "type": "string", "required": false } @@ -70,7 +98,5 @@ } } } - }, - - "additionalProperties": true + } } diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index e38ac3e2b9..e087fd73fd 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -67,6 +67,8 @@ namespace AppInstaller::CLI return Argument{ "output", 'o', Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Positional, true }; case Args::Type::ImportFile: return Argument{ "input", 'i', Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }; + case Args::Type::ExactVersions: + return Argument{ "exactVersions", NoAlias, Args::Type::ExactVersions, Resource::String::ExactVersionsArgumentDescription, ArgumentType::Flag }; case Args::Type::ListVersions: return Argument{ "versions", NoAlias, Args::Type::ListVersions, Resource::String::VersionsArgumentDescription, ArgumentType::Flag }; case Args::Type::Help: diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp index d6391da12b..f55d95e299 100644 --- a/src/AppInstallerCLICore/Commands/ExportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -99,6 +99,7 @@ namespace AppInstaller::CLI Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << Workflow::SearchSourceForMany << Workflow::EnsureMatchesFromSearchResult(true) << - Workflow::Export; + Workflow::SelectVersionsToExport << + Workflow::WriteImportFile; } } diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 7e0e832f87..09d1959d27 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -16,6 +16,7 @@ namespace AppInstaller::CLI { return { Argument::ForType(Execution::Args::Type::ImportFile), + Argument::ForType(Execution::Args::Type::Exact), }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 47f829a6ab..f54119dd43 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -60,6 +60,7 @@ namespace AppInstaller::CLI::Execution // Import Command ImportFile, + ExactVersions, // Other All, // Used in Update command to update all installed packages to latest diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index f287e163de..0411daa69a 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -58,15 +58,13 @@ namespace AppInstaller::CLI::Execution CompletionData, InstalledPackageVersion, ExecutionStage, - // On Uninstall: Uninstall string to be executed UninstallString, - // On Uninstall: Package Family Names of an MSIX package to uninstall PackageFamilyNames, - // On Uninstall: Product codes of an MSI to uninstall ProductCodes, - // On Import: Packages to be installed from each source - PackageRequests, - + // On export: A collection of packages to be exported to a file + // On import: A collection of packages read from a file + PackageCollection, + // On import: A collection of specific package versions to install PackagesToInstall, Max }; @@ -193,9 +191,9 @@ namespace AppInstaller::CLI::Execution }; template <> - struct DataMapping + struct DataMapping { - using value_t = PackageCollectionRequest; + using value_t = CLI::PackageCollection; }; diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index a6f05068a1..c5e892fe03 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" + #include "PackageCollection.h" -#include "AppInstallerRepositorySource.h" #include "AppInstallerRuntime.h" -#include #include #include -#include namespace AppInstaller::CLI { @@ -30,6 +28,19 @@ namespace AppInstaller::CLI return node[propertyName]; } + // Sets a property of a JSON object to a string if it is not empty + void SetJsonProperty(Json::Value& node, const std::string& propertyName, const std::string& value) + { + if (!value.empty()) + { + node[propertyName] = value; + } + else if (node.isMember(propertyName)) + { + node.removeMember(propertyName); + } + } + // Reads the description of a package from a Package node in the JSON. PackageRequest ParsePackageNode(const Json::Value& packageNode) { @@ -45,11 +56,16 @@ namespace AppInstaller::CLI // Reads the description of a Source and all the packages needed from it, from a Source node in the JSON. PackageRequestsFromSource ParseSourceNode(const Json::Value& sourceNode) { - PackageRequestsFromSource requestsFromSource + PackageRequestsFromSource requestsFromSource; + + if (sourceNode.isMember(SOURCE_DETAILS_PROPERTY)) { - Utility::LocIndString{ sourceNode[SOURCE_NAME_PROPERTY].asString() }, - Utility::LocIndString{ sourceNode[SOURCE_ARGUMENT_PROPERTY].asString() } - }; + auto& detailsNode = sourceNode[SOURCE_DETAILS_PROPERTY]; + requestsFromSource.SourceIdentifier = Utility::LocIndString{ detailsNode[SOURCE_IDENTIFIER_PROPERTY].asString() }; + requestsFromSource.Details.Name = detailsNode[SOURCE_NAME_PROPERTY].asString(); + requestsFromSource.Details.Arg = detailsNode[SOURCE_ARGUMENT_PROPERTY].asString(); + requestsFromSource.Details.Type = detailsNode[SOURCE_TYPE_PROPERTY].asString(); + } for (const auto& packageNode : sourceNode[PACKAGES_PROPERTY]) { @@ -83,12 +99,9 @@ namespace AppInstaller::CLI root[WINGET_VERSION_PROPERTY] = Runtime::GetClientVersion().get(); root[SCHEMA_PROPERTY] = SCHEMA_PATH; - // TODO: Clean up. Do we need this? - std::time_t currentTimeTT = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - std::tm currentTimeTM; + // TODO: This uses localtime. Do we want to use UTC or add timezone? std::stringstream currentTimeStream; - gmtime_s(¤tTimeTM, ¤tTimeTT); - currentTimeStream << std::put_time(¤tTimeTM, "%c"); + Utility::OutputTimePoint(currentTimeStream, std::chrono::system_clock::now()); root[CREATION_DATE_PROPERTY] = currentTimeStream.str(); return root; @@ -97,8 +110,17 @@ namespace AppInstaller::CLI Json::Value& AddSourceNode(Json::Value& root, const PackageRequestsFromSource& source) { Json::Value sourceNode{ Json::ValueType::objectValue }; - sourceNode[SOURCE_NAME_PROPERTY] = source.SourceName.get(); - sourceNode[SOURCE_ARGUMENT_PROPERTY] = source.SourceArg.get(); + + if (!source.Details.Name.empty()) + { + Json::Value sourceDetailsNode{ Json::ValueType::objectValue }; + sourceDetailsNode[SOURCE_NAME_PROPERTY] = source.Details.Name; + sourceDetailsNode[SOURCE_ARGUMENT_PROPERTY] = source.Details.Arg; + sourceDetailsNode[SOURCE_IDENTIFIER_PROPERTY] = source.SourceIdentifier.get(); + sourceDetailsNode[SOURCE_TYPE_PROPERTY] = source.Details.Type; + sourceNode[SOURCE_DETAILS_PROPERTY] = std::move(sourceDetailsNode); + } + sourceNode[PACKAGES_PROPERTY] = Json::Value{ Json::ValueType::arrayValue }; auto& sourcesNode = GetJsonProperty(root, SOURCES_PROPERTY); @@ -125,68 +147,42 @@ namespace AppInstaller::CLI return sourceNode[PACKAGES_PROPERTY].append(std::move(packageNode)); } - } - - PackageRequest::PackageRequest(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel) : - Id(std::move(id)), VersionAndChannel(std::move(version), std::move(channel)) {} - - PackageRequest::PackageRequest(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel) : - Id(std::move(id)), VersionAndChannel(std::move(versionAndChannel)) {} - - PackageRequestsFromSource::PackageRequestsFromSource(Utility::LocIndString&& sourceName, Utility::LocIndString&& sourceArg) : - SourceName(std::move(sourceName)), SourceArg(std::move(sourceArg)) {} - - std::vector ParsePackageCollection(const Json::Value& root) - { - // TODO: Validate schema. The following assumes the file is already valid. - // TODO: Use version & creation date? - - std::vector requests = {}; - for (const auto& sourceNode : root[SOURCES_PROPERTY]) - { - requests.push_back(ParseSourceNode(sourceNode)); - } - - return requests; - } - std::vector ConvertSearchResultToPackageRequests(const SearchResult& packages) - { - std::vector requests = {}; - for (const auto& packageMatch : packages.Matches) + Json::Value CreateJson(const PackageCollection& packages) { - auto availableVersion = GetAvailableVersionMatchingInstalled(*packageMatch.Package); - if (!availableVersion) + Json::Value root = PackagesJson::CreateRoot(); + for (const auto& source : packages.RequestsFromSources) { - AICLI_LOG(CLI, Info, << "No available package found for " << packageMatch.Package->GetProperty(PackageProperty::Id)); - continue; + PackagesJson::AddSourceNode(root, source); } - auto sourceDetails = availableVersion->GetSource()->GetDetails(); - auto sourceItr = std::find_if(requests.begin(), requests.end(), [&](const PackageRequestsFromSource& s) { return s.SourceName == sourceDetails.Name; }); - if (sourceItr == requests.end()) + return root; + } + + PackageCollection ParseJson(const Json::Value& root) + { + // TODO: Validate schema. The following assumes the file is already valid. + PackageCollection packages; + packages.ClientVersion = root[WINGET_VERSION_PROPERTY].asString(); + for (const auto& sourceNode : root[SOURCES_PROPERTY]) { - requests.emplace_back(Utility::LocIndString{ sourceDetails.Name }, Utility::LocIndString{ sourceDetails.Arg }); - sourceItr = std::prev(requests.end()); + // TODO: Prevent duplicates? + packages.RequestsFromSources.push_back(ParseSourceNode(sourceNode)); } - sourceItr->Packages.emplace_back( - availableVersion->GetProperty(PackageVersionProperty::Id), - availableVersion->GetProperty(PackageVersionProperty::Version).get(), - availableVersion->GetProperty(PackageVersionProperty::Channel).get()); + return packages; } - - return requests; } - Json::Value ConvertPackageRequestsToJson(const PackageCollectionRequest& packages) - { - Json::Value root = CreateRoot(); - for (const auto& source : packages) - { - AddSourceNode(root, source); - } + PackageRequest::PackageRequest(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel) : + Id(std::move(id)), VersionAndChannel(std::move(version), std::move(channel)) {} - return root; - } + PackageRequest::PackageRequest(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel) : + Id(std::move(id)), VersionAndChannel(std::move(versionAndChannel)) {} + + PackageRequestsFromSource::PackageRequestsFromSource(const Utility::LocIndString& sourceIdentifier, const SourceDetails& sourceDetails) : + SourceIdentifier(sourceIdentifier), Details(sourceDetails) {} + + PackageRequestsFromSource::PackageRequestsFromSource(Utility::LocIndString&& sourceIdentifier, SourceDetails&& sourceDetails) : + SourceIdentifier(std::move(sourceIdentifier)), Details(std::move(sourceDetails)) {} } \ No newline at end of file diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h index 23eba4a578..ff52b49f62 100644 --- a/src/AppInstallerCLICore/PackageCollection.h +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -2,9 +2,10 @@ // Licensed under the MIT License. #pragma once -#include "AppInstallerRepositorySearch.h" +#include "AppInstallerDateTime.h" #include + #include namespace AppInstaller::CLI @@ -26,24 +27,23 @@ namespace AppInstaller::CLI struct PackageRequestsFromSource { PackageRequestsFromSource() = default; - PackageRequestsFromSource(Utility::LocIndString&& sourceName, Utility::LocIndString&& sourceArg); + PackageRequestsFromSource(const Utility::LocIndString& sourceIdentifier, const SourceDetails& sourceDetails); + PackageRequestsFromSource(Utility::LocIndString&& sourceIdentifier, SourceDetails&& sourceDetails); - Utility::LocIndString SourceName; - Utility::LocIndString SourceArg; + Utility::LocIndString SourceIdentifier; + SourceDetails Details; std::vector Packages; }; - using PackageCollectionRequest = std::vector; - - // Parses a package collection from a JSON file. - PackageCollectionRequest ParsePackageCollection(const Json::Value& root); - - // Converts the result of a search to a collection of package - // requests adequate for exporting. - PackageCollectionRequest ConvertSearchResultToPackageRequests(const SearchResult& packages); + // Container for data to identify multiple packages to be installed from multiple sources. + struct PackageCollection + { + // Version of the WinGet client that produced this request. + std::string ClientVersion; - // Creates a JSON representing a package collection. - Json::Value ConvertPackageRequestsToJson(const PackageCollectionRequest& packages); + // Requests from each individual source. + std::vector RequestsFromSources; + }; namespace PackagesJson { @@ -51,17 +51,20 @@ namespace AppInstaller::CLI // Most will be used to access a JSON value, so they need to be std::string const std::string SCHEMA_PROPERTY = "$schema"; const std::string SCHEMA_PATH = "https://aka.ms/winget-packages.schema.json"; - const std::string WINGET_VERSION_PROPERTY = "wingetVersion"; - const std::string CREATION_DATE_PROPERTY = "creationDate"; + const std::string WINGET_VERSION_PROPERTY = "WinGetVersion"; + const std::string CREATION_DATE_PROPERTY = "CreationDate"; - const std::string SOURCES_PROPERTY = "sources"; - const std::string SOURCE_NAME_PROPERTY = "name"; - const std::string SOURCE_ARGUMENT_PROPERTY = "argument"; + const std::string SOURCES_PROPERTY = "Sources"; + const std::string SOURCE_DETAILS_PROPERTY = "SourceDetails"; + const std::string SOURCE_NAME_PROPERTY = "Name"; + const std::string SOURCE_IDENTIFIER_PROPERTY = "Identifier"; + const std::string SOURCE_ARGUMENT_PROPERTY = "Argument"; + const std::string SOURCE_TYPE_PROPERTY = "Type"; - const std::string PACKAGES_PROPERTY = "packages"; - const std::string PACKAGE_ID_PROPERTY = "id"; - const std::string PACKAGE_VERSION_PROPERTY = "version"; - const std::string PACKAGE_CHANNEL_PROPERTY = "channel"; + const std::string PACKAGES_PROPERTY = "Packages"; + const std::string PACKAGE_ID_PROPERTY = "Id"; + const std::string PACKAGE_VERSION_PROPERTY = "Version"; + const std::string PACKAGE_CHANNEL_PROPERTY = "Channel"; // Creates a minimal root object of a Packages JSON file. Json::Value CreateRoot(); @@ -71,5 +74,10 @@ namespace AppInstaller::CLI // Adds a new Package node to a Source node in the Json file, and returns it. Json::Value& AddPackageToSource(Json::Value& source, const PackageRequest& package); + + // Converts a collection of packages to its JSON representation for exporting. + Json::Value CreateJson(const PackageCollection& packages); + + PackageCollection ParseJson(const Json::Value& root); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 9a46dc602b..4f49af68b1 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -45,6 +45,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Done); WINGET_DEFINE_RESOURCE_STRINGID(ExactArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExactVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index b68b7fda94..53c9d31ae8 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -6,25 +6,104 @@ #include "PackageCollection.h" #include "WorkflowBase.h" #include "CompositeSource.h" +#include "AppInstallerRepositorySearch.h" namespace AppInstaller::CLI::Workflow { - void Export(Execution::Context& context) + namespace { - std::filesystem::path outputFile{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; - auto packages = ConvertSearchResultToPackageRequests(context.Get()); - std::ofstream{ outputFile } << ConvertPackageRequestsToJson(packages); + // Gets the available PackageVersion that has the same version as the installed version. + // The package must have an installed version. + std::shared_ptr GetAvailableVersionMatchingInstalledVersion(const IPackage& package) + { + auto installedVersion = package.GetInstalledVersion(); + PackageVersionKey installedVersionKey + { + "", + installedVersion->GetProperty(PackageVersionProperty::Version).get(), + installedVersion->GetProperty(PackageVersionProperty::Channel).get(), + }; + return package.GetAvailableVersion(installedVersionKey); + } + + // Selects which version of an installed package to list when exporting. + std::shared_ptr SelectPackageVersionToExport(const IPackage& package) + { + // See if the installed version is available from some source + auto availableVersion = GetAvailableVersionMatchingInstalledVersion(package); + if (!availableVersion) + { + // If the exact version isn't available, list the latest. + availableVersion = package.GetLatestAvailableVersion(); + } + + if (availableVersion) + { + AICLI_LOG( + CLI, + Info, + << "Found package " << availableVersion->GetProperty(PackageVersionProperty::Id) + << " " << availableVersion->GetProperty(PackageVersionProperty::Version) + << " available from " << availableVersion->GetSource()->GetIdentifier()); + return availableVersion; + } + + // If there is no available version, we didn't have a mapping for the ARP + // entry of the package. List the installed version so it can be later installed + // if we get a better mapping. + auto installedVersion = package.GetInstalledVersion(); + AICLI_LOG(CLI, Warning, << "No available version of package " << installedVersion->GetProperty(PackageVersionProperty::Id) << " was found to export"); + return installedVersion; + } + } + + void SelectVersionsToExport(Execution::Context& context) + { + const auto& searchResult = context.Get(); + PackageCollection versionsToExport = {}; + auto& requestsFromSource = versionsToExport.RequestsFromSources; + for (const auto& packageMatch : searchResult.Matches) + { + auto packageVersion = SelectPackageVersionToExport(*packageMatch.Package); + + const auto& sourceIdentifier = packageVersion->GetSource()->GetIdentifier(); + auto sourceItr = std::find_if(requestsFromSource.begin(), requestsFromSource.end(), [&](const PackageRequestsFromSource& s) { return s.SourceIdentifier == sourceIdentifier; }); + if (sourceItr == requestsFromSource.end()) + { + requestsFromSource.emplace_back(Utility::LocIndString{ sourceIdentifier }, packageVersion->GetSource()->GetDetails()); + sourceItr = std::prev(requestsFromSource.end()); + } + + sourceItr->Packages.emplace_back( + packageVersion->GetProperty(PackageVersionProperty::Id), + packageVersion->GetProperty(PackageVersionProperty::Version).get(), + packageVersion->GetProperty(PackageVersionProperty::Channel).get()); + } + + context.Add(std::move(versionsToExport)); + } + + void WriteImportFile(Execution::Context& context) + { + auto packages = PackagesJson::CreateJson(context.Get()); + + std::filesystem::path outputFilePath{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; + std::ofstream outputFileStream{ outputFilePath }; + outputFileStream << packages; } void ReadImportFile(Execution::Context& context) { - std::filesystem::path importFile{ context.Args.GetArg(Execution::Args::Type::ImportFile) }; - Json::Value jsonRoot = {}; - std::ifstream{ importFile } >> jsonRoot; - auto packages = ParsePackageCollection(jsonRoot); - context.Add(packages); + std::filesystem::path importFilePath{ context.Args.GetArg(Execution::Args::Type::ImportFile) }; - if (packages.empty()) + // TODO: Handle errors + Json::Value jsonRoot; + std::ifstream{ importFilePath } >> jsonRoot; + + auto packages = PackagesJson::ParseJson(jsonRoot); + context.Add(packages); + + if (packages.RequestsFromSources.empty()) { AICLI_LOG(CLI, Warning, << "No packages to install"); AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); @@ -41,43 +120,50 @@ namespace AppInstaller::CLI::Workflow // source objects alive for install. auto aggregatedSource = std::make_shared("*ImportSource"); - for (auto& requiredSource : context.Get()) + // Look for the packages needed from each source independently. + // If a package is available from multiple sources, this ensures we will get it from the right one. + for (auto& requiredSource : context.Get().RequestsFromSources) { - // Match required sources with the available sources by their arguments, - // as they may have been added with a different name. - std::optional matchingSource = {}; - for (auto& availableSource : availableSources) + if (!requiredSource.Details.Name.empty()) { - if (availableSource.Arg == requiredSource.SourceArg.get()) + // For packages that come from a specific source, find the matching available source. + // Match required sources with the available sources by their arguments, as they may have been added with a different name. + // TODO: Can we do this with identifiers without opening the source? + std::optional matchingSource = {}; + for (auto& availableSource : availableSources) { - matchingSource = availableSource; - break; + if (availableSource.Arg == requiredSource.Details.Arg) + { + matchingSource = availableSource; + break; + } } - } - if (matchingSource.has_value()) - { - requiredSource.SourceName = Utility::LocIndString{ matchingSource.value().Name }; - } - else - { - // TODO: Add option for ignoring/installing missing sources? - AICLI_LOG(CLI, Error, << "Missing required source: " << requiredSource.SourceName); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); + if (matchingSource.has_value()) + { + requiredSource.Details.Name = matchingSource.value().Name; + } + else + { + // TODO: Add option for ignoring/installing missing sources? + AICLI_LOG(CLI, Error, << "Missing required source: " << requiredSource.Details.Name); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); + } } - // Search for all the packages in the source - Logging::SubExecutionTelemetryScope subExecution; - - // We want to do best effort to install all packages regardless of previous failures - auto searchContextPtr = context.Clone(); - Execution::Context& searchContext = *searchContextPtr; - - searchContext << OpenNamedSource(requiredSource.SourceName); - auto source = searchContext.Get(); + context << OpenNamedSource(requiredSource.Details.Name); + auto source = context.Get(); aggregatedSource->AddAvailableSource(source); for (const auto& packageRequest : requiredSource.Packages) { + // Search for all the packages in the source + Logging::SubExecutionTelemetryScope subExecution; + + // We want to do best effort to install all packages regardless of previous failures + auto searchContextPtr = context.Clone(); + Execution::Context& searchContext = *searchContextPtr; + searchContext.Add(source); + // TODO: Case insensitive? MatchType matchType = MatchType::Exact; @@ -87,15 +173,28 @@ namespace AppInstaller::CLI::Workflow searchContext.Add(source->Search(searchRequest)); searchContext << EnsureOneMatchFromSearchResult(false); - PackageVersionKey requestedVersion + if (searchContext.IsTerminated()) { - "", - packageRequest.VersionAndChannel.GetVersion().ToString(), - packageRequest.VersionAndChannel.GetChannel().ToString(), - }; + AICLI_LOG(CLI, Warning, << "Package not found [" << packageRequest.Id << "] in source [" << source->GetIdentifier() << "]"); + continue; + } - // TODO: handle unavailable version - packagesToInstall.push_back(searchContext.Get()->GetAvailableVersion(requestedVersion)); + if (context.Args.Contains(Execution::Args::Type::ExactVersions)) + { + PackageVersionKey requestedVersion + { + requiredSource.SourceIdentifier.get(), + packageRequest.VersionAndChannel.GetVersion().ToString(), + packageRequest.VersionAndChannel.GetChannel().ToString(), + }; + + // TODO: handle unavailable version + packagesToInstall.push_back(searchContext.Get()->GetAvailableVersion(requestedVersion)); + } + else + { + packagesToInstall.push_back(searchContext.Get()->GetLatestAvailableVersion()); + } } } diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.h b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h index 8c6c756a17..40890207c4 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.h +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h @@ -5,17 +5,27 @@ namespace AppInstaller::CLI::Workflow { - // - // Required Args: - // Inputs: - // Outputs: - void Export(Execution::Context& context); + // Selects the package versions to list on the exported file + // Required Args: None + // Inputs: SearchResult + // Outputs: PackageCollection + void SelectVersionsToExport(Execution::Context& context); + // Exports a collection of packages to a JSON import file + // Required Args: OutputFile + // Inputs: PackageCollection + // Outputs: None + void WriteImportFile(Execution::Context& context); + + // Reads the contents of an import file // Required Args: ImportFile - // Outputs: PackageRequests + // Inputs: None + // Outputs: PackageCollection void ReadImportFile(Execution::Context& context); - // Inputs: PackageRequests + // Finds the package versions to install matching their descriptions + // Required Args: None + // Inputs: PackageCollection // Outputs: PackagesToInstall void SearchPackagesForImport(Execution::Context& context); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 6ac6bb4592..9feda57e8a 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -774,4 +774,7 @@ They can be configured through the settings file 'winget settings'. TODO + + TODO + \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h index 86176a0220..376b89a17f 100644 --- a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -44,7 +44,7 @@ namespace AppInstaller::Repository // The argument used when adding the source. std::string Arg; - // The sources extra data string. + // The source's extra data string. std::string Data; // The last time that this source was updated. From 60109c5743ba771652878c07c257df64c2c4cf74 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Tue, 12 Jan 2021 17:31:20 -0800 Subject: [PATCH 03/34] typo --- src/AppInstallerCLICore/PackageCollection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index c5e892fe03..a0ff460e1f 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -99,7 +99,7 @@ namespace AppInstaller::CLI root[WINGET_VERSION_PROPERTY] = Runtime::GetClientVersion().get(); root[SCHEMA_PROPERTY] = SCHEMA_PATH; - // TODO: This uses localtime. Do we want to use UTC or add timezone? + // TODO: This uses localtime. Do we want to use UTC or add time zone? std::stringstream currentTimeStream; Utility::OutputTimePoint(currentTimeStream, std::chrono::system_clock::now()); root[CREATION_DATE_PROPERTY] = currentTimeStream.str(); From 01c6b8671a05174551eb82d665c78d58861a7849 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Tue, 12 Jan 2021 18:59:55 -0800 Subject: [PATCH 04/34] Lowercase command names --- src/AppInstallerCLICore/Commands/ExportCommand.h | 2 +- src/AppInstallerCLICore/Commands/ImportCommand.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.h b/src/AppInstallerCLICore/Commands/ExportCommand.h index bb93d92a19..5383e0a4f7 100644 --- a/src/AppInstallerCLICore/Commands/ExportCommand.h +++ b/src/AppInstallerCLICore/Commands/ExportCommand.h @@ -8,7 +8,7 @@ namespace AppInstaller::CLI // Command to get the set of installed packages on the system. struct ExportCommand final : public Command { - ExportCommand(std::string_view parent) : Command("Export", parent, Settings::ExperimentalFeature::Feature::ExperimentalImportExport) {} + ExportCommand(std::string_view parent) : Command("export", parent, Settings::ExperimentalFeature::Feature::ExperimentalImportExport) {} std::vector GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.h b/src/AppInstallerCLICore/Commands/ImportCommand.h index 050b132352..219c0bee4e 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.h +++ b/src/AppInstallerCLICore/Commands/ImportCommand.h @@ -8,7 +8,7 @@ namespace AppInstaller::CLI // Command to install a set of packages from a list. struct ImportCommand final : public Command { - ImportCommand(std::string_view parent) : Command("Import", parent, Settings::ExperimentalFeature::Feature::ExperimentalImportExport) {} + ImportCommand(std::string_view parent) : Command("import", parent, Settings::ExperimentalFeature::Feature::ExperimentalImportExport) {} std::vector GetArguments() const override; From c89e8a0b312b895d579bba5c204eb1e3ab7fb7a0 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 21 Jan 2021 12:22:30 -0800 Subject: [PATCH 05/34] Remove query arguments for export (except source) --- .../Commands/ExportCommand.cpp | 48 ++----------------- .../Commands/ExportCommand.h | 1 - 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp index f55d95e299..308ec7f246 100644 --- a/src/AppInstallerCLICore/Commands/ExportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -15,15 +15,7 @@ namespace AppInstaller::CLI { return { Argument::ForType(Execution::Args::Type::OutputFile), - Argument::ForType(Execution::Args::Type::Query), - Argument::ForType(Execution::Args::Type::Id), - Argument::ForType(Execution::Args::Type::Name), - Argument::ForType(Execution::Args::Type::Moniker), Argument::ForType(Execution::Args::Type::Source), - Argument::ForType(Execution::Args::Type::Tag), - Argument::ForType(Execution::Args::Type::Command), - Argument::ForType(Execution::Args::Type::Count), - Argument::ForType(Execution::Args::Type::Exact), }; } @@ -45,27 +37,10 @@ namespace AppInstaller::CLI return; } - context << - Workflow::OpenSource << - Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); - - switch (valueType) + if (valueType == Execution::Args::Type::Source) { - case Execution::Args::Type::Query: - context << - Workflow::RequireCompletionWordNonEmpty << - Workflow::SearchSourceForManyCompletion << - Workflow::CompleteWithMatchedField; - break; - case Execution::Args::Type::Id: - case Execution::Args::Type::Name: - case Execution::Args::Type::Moniker: - case Execution::Args::Type::Source: - case Execution::Args::Type::Tag: - case Execution::Args::Type::Command: - context << - Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); - break; + context << Workflow::CompleteSourceName; + return; } } @@ -75,23 +50,6 @@ namespace AppInstaller::CLI return "https://aka.ms/winget-command-export"; } - void ExportCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const - { - if (execArgs.Contains(Execution::Args::Type::Manifest) && - (execArgs.Contains(Execution::Args::Type::Query) || - execArgs.Contains(Execution::Args::Type::Id) || - execArgs.Contains(Execution::Args::Type::Name) || - execArgs.Contains(Execution::Args::Type::Moniker) || - execArgs.Contains(Execution::Args::Type::Version) || - execArgs.Contains(Execution::Args::Type::Channel) || - execArgs.Contains(Execution::Args::Type::Source) || - execArgs.Contains(Execution::Args::Type::Exact) || - execArgs.Contains(Execution::Args::Type::All))) - { - throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); - } - } - void ExportCommand::ExecuteInternal(Execution::Context& context) const { context << diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.h b/src/AppInstallerCLICore/Commands/ExportCommand.h index 5383e0a4f7..0c11977b4a 100644 --- a/src/AppInstallerCLICore/Commands/ExportCommand.h +++ b/src/AppInstallerCLICore/Commands/ExportCommand.h @@ -20,7 +20,6 @@ namespace AppInstaller::CLI std::string HelpLink() const override; protected: - void ValidateArgumentsInternal(Execution::Args& execArgs) const override; void ExecuteInternal(Execution::Context& context) const override; }; } From 6e0830f6d61504e96a47a24c2a3eb223b9dd151a Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 21 Jan 2021 12:26:45 -0800 Subject: [PATCH 06/34] Use Workflow::VerifyFile for import --- src/AppInstallerCLICore/Commands/ImportCommand.cpp | 10 +--------- src/AppInstallerCLICore/Commands/ImportCommand.h | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 09d1959d27..693f26f6bc 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -36,18 +36,10 @@ namespace AppInstaller::CLI return "https://aka.ms/winget-command-import"; } - void ImportCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const - { - if (!std::filesystem::exists(execArgs.GetArg(Execution::Args::Type::ImportFile))) - { - // TODO - throw CommandException(Resource::String::VerifyFileFailedNotExist, execArgs.GetArg(Execution::Args::Type::ImportFile)); - } - } - void ImportCommand::ExecuteInternal(Execution::Context& context) const { context << + Workflow::VerifyFile(Execution::Args::Type::ImportFile) << Workflow::ReadImportFile << Workflow::SearchPackagesForImport << Workflow::InstallMultiple; diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.h b/src/AppInstallerCLICore/Commands/ImportCommand.h index 219c0bee4e..d2a58db14c 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.h +++ b/src/AppInstallerCLICore/Commands/ImportCommand.h @@ -18,7 +18,6 @@ namespace AppInstaller::CLI std::string HelpLink() const override; protected: - void ValidateArgumentsInternal(Execution::Args& execArgs) const override; void ExecuteInternal(Execution::Context& context) const override; }; } From d015f374b0a83a9c2d9ff34da32cf627a575f4c6 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 21 Jan 2021 12:35:07 -0800 Subject: [PATCH 07/34] Move argument definitions to be in command definitions --- src/AppInstallerCLICore/Argument.cpp | 22 +++++++------------ .../Commands/ExportCommand.cpp | 4 ++-- .../Commands/ImportCommand.cpp | 4 ++-- src/AppInstallerCLICore/Resources.h | 1 + .../Shared/Strings/en-us/winget.resw | 3 +++ 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index e087fd73fd..ad1a036a96 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -51,28 +51,22 @@ namespace AppInstaller::CLI return Argument{ "override", NoAlias, Args::Type::Override, Resource::String::OverrideArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::InstallLocation: return Argument{ "location", 'l', Args::Type::InstallLocation, Resource::String::LocationArgumentDescription, ArgumentType::Standard }; - case Args::Type::SourceName: - return Argument{ "name", 'n', Args::Type::SourceName,Resource::String::SourceNameArgumentDescription, ArgumentType::Positional, false }; - case Args::Type::SourceArg: - return Argument{ "arg", 'a', Args::Type::SourceArg, Resource::String::SourceArgArgumentDescription, ArgumentType::Positional, true }; - case Args::Type::SourceType: - return Argument{ "type", 't', Args::Type::SourceType, Resource::String::SourceTypeArgumentDescription, ArgumentType::Positional }; - case Args::Type::ValidateManifest: - return Argument{ "manifest", NoAlias, Args::Type::ValidateManifest, Resource::String::ValidateManifestArgumentDescription, ArgumentType::Positional, true }; case Args::Type::HashFile: return Argument{ "file", 'f', Args::Type::HashFile, Resource::String::FileArgumentDescription, ArgumentType::Positional, true }; case Args::Type::Msix: return Argument{ "msix", 'm', Args::Type::Msix, Resource::String::MsixArgumentDescription, ArgumentType::Flag }; - case Args::Type::OutputFile: - return Argument{ "output", 'o', Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Positional, true }; - case Args::Type::ImportFile: - return Argument{ "input", 'i', Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }; - case Args::Type::ExactVersions: - return Argument{ "exactVersions", NoAlias, Args::Type::ExactVersions, Resource::String::ExactVersionsArgumentDescription, ArgumentType::Flag }; case Args::Type::ListVersions: return Argument{ "versions", NoAlias, Args::Type::ListVersions, Resource::String::VersionsArgumentDescription, ArgumentType::Flag }; case Args::Type::Help: return Argument{ "help", APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_CHAR, Args::Type::Help, Resource::String::HelpArgumentDescription, ArgumentType::Flag }; + case Args::Type::SourceName: + return Argument{ "name", 'n', Args::Type::SourceName,Resource::String::SourceNameArgumentDescription, ArgumentType::Positional, false }; + case Args::Type::SourceArg: + return Argument{ "arg", 'a', Args::Type::SourceArg, Resource::String::SourceArgArgumentDescription, ArgumentType::Positional, true }; + case Args::Type::SourceType: + return Argument{ "type", 't', Args::Type::SourceType, Resource::String::SourceTypeArgumentDescription, ArgumentType::Positional }; + case Args::Type::ValidateManifest: + return Argument{ "manifest", NoAlias, Args::Type::ValidateManifest, Resource::String::ValidateManifestArgumentDescription, ArgumentType::Positional, true }; case Args::Type::NoVT: return Argument{ "no-vt", NoAlias, Args::Type::NoVT, Resource::String::NoVTArgumentDescription, ArgumentType::Flag, Argument::Visibility::Hidden }; case Args::Type::RainbowStyle: diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp index 308ec7f246..e0a756e9d9 100644 --- a/src/AppInstallerCLICore/Commands/ExportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -14,8 +14,8 @@ namespace AppInstaller::CLI std::vector ExportCommand::GetArguments() const { return { - Argument::ForType(Execution::Args::Type::OutputFile), - Argument::ForType(Execution::Args::Type::Source), + Argument{ "output", 'o', Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ "source", 's', Execution::Args::Type::Source, Resource::String::ExportSourceArgumentDescription, ArgumentType::Standard }, }; } diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 693f26f6bc..db1c534bc6 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -15,8 +15,8 @@ namespace AppInstaller::CLI std::vector ImportCommand::GetArguments() const { return { - Argument::ForType(Execution::Args::Type::ImportFile), - Argument::ForType(Execution::Args::Type::Exact), + Argument{ "importFile", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ "exactVersions", Argument::NoAlias, Execution::Args::Type::ExactVersions, Resource::String::ExactVersionsArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 4f49af68b1..3587ea76d4 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -51,6 +51,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesCommandLongDescription); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 9feda57e8a..c2c1bbe229 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -777,4 +777,7 @@ They can be configured through the settings file 'winget settings'. TODO + + Export packages from the specified source + \ No newline at end of file From 9842b1f219e8b5905a5e324d999261d38ee83363 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 21 Jan 2021 18:02:22 -0800 Subject: [PATCH 08/34] Find sources by identifier instead of argument --- src/AppInstallerCLICore/ExecutionContext.h | 1 - src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 4 +--- src/AppInstallerRepositoryCore/CompositeSource.cpp | 6 +++--- src/AppInstallerRepositoryCore/CompositeSource.h | 1 - .../Microsoft/PreIndexedPackageSourceFactory.cpp | 5 +++++ .../Microsoft/SQLiteIndexSource.cpp | 5 +++-- .../Microsoft/SQLiteIndexSource.h | 1 - .../Public/AppInstallerRepositorySource.h | 3 +++ src/AppInstallerRepositoryCore/RepositorySource.cpp | 8 ++++++++ 9 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index 0411daa69a..5b6ef0e5fa 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -196,7 +196,6 @@ namespace AppInstaller::CLI::Execution using value_t = CLI::PackageCollection; }; - template <> struct DataMapping { diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 53c9d31ae8..93d5c04bc1 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -127,12 +127,10 @@ namespace AppInstaller::CLI::Workflow if (!requiredSource.Details.Name.empty()) { // For packages that come from a specific source, find the matching available source. - // Match required sources with the available sources by their arguments, as they may have been added with a different name. - // TODO: Can we do this with identifiers without opening the source? std::optional matchingSource = {}; for (auto& availableSource : availableSources) { - if (availableSource.Arg == requiredSource.Details.Arg) + if (availableSource.Identifier == requiredSource.Details.Identifier) { matchingSource = availableSource; break; diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index a48d4de937..41a79d8cbc 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -364,10 +364,10 @@ namespace AppInstaller::Repository }; } - CompositeSource::CompositeSource(std::string identifier) : - m_identifier(identifier) + CompositeSource::CompositeSource(std::string identifier) { m_details.Name = "CompositeSource"; + m_details.Identifier = std::move(identifier); } const SourceDetails& CompositeSource::GetDetails() const @@ -377,7 +377,7 @@ namespace AppInstaller::Repository const std::string& CompositeSource::GetIdentifier() const { - return m_identifier; + return m_details.Identifier; } // The composite search needs to take several steps to get results, and due to the diff --git a/src/AppInstallerRepositoryCore/CompositeSource.h b/src/AppInstallerRepositoryCore/CompositeSource.h index 905c89a98a..57e84503c1 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.h +++ b/src/AppInstallerRepositoryCore/CompositeSource.h @@ -57,7 +57,6 @@ namespace AppInstaller::Repository std::shared_ptr m_installedSource; std::vector> m_availableSources; SourceDetails m_details; - std::string m_identifier; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index cd6646cc82..0948198217 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -86,6 +86,7 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Info, << "Found package full name: " << details.Name << " => " << fullName); details.Data = Msix::GetPackageFamilyNameFromFullName(fullName); + details.Identifier = Msix::GetPackageFamilyNameFromFullName(fullName); auto lock = Synchronization::CrossProcessReaderWriteLock::LockForWrite(CreateNameForCPRWL(details)); @@ -146,6 +147,8 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex index = SQLiteIndex::Open(indexLocation.u8string(), SQLiteIndex::OpenDisposition::Immutable); + // We didn't use to store the source identifier, so we compute it here in case it's + // missing from the details. return std::make_shared(details, GetPackageFamilyNameFromDetails(details), std::move(index), std::move(lock)); } @@ -252,6 +255,8 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex index = SQLiteIndex::Open(packageLocation.u8string(), SQLiteIndex::OpenDisposition::Read); + // We didn't use to store the source identifier, so we compute it here in case it's + // missing from the details. return std::make_shared(details, GetPackageFamilyNameFromDetails(details), std::move(index), std::move(lock)); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp index 4520ab48d9..93274ffb2c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -293,8 +293,9 @@ namespace AppInstaller::Repository::Microsoft } SQLiteIndexSource::SQLiteIndexSource(const SourceDetails& details, std::string identifier, SQLiteIndex&& index, Synchronization::CrossProcessReaderWriteLock&& lock, bool isInstalledSource) : - m_details(details), m_identifier(std::move(identifier)), m_lock(std::move(lock)), m_isInstalled(isInstalledSource), m_index(std::move(index)) + m_details(details), m_lock(std::move(lock)), m_isInstalled(isInstalledSource), m_index(std::move(index)) { + m_details.Identifier = std::move(identifier); } const SourceDetails& SQLiteIndexSource::GetDetails() const @@ -304,7 +305,7 @@ namespace AppInstaller::Repository::Microsoft const std::string& SQLiteIndexSource::GetIdentifier() const { - return m_identifier; + return m_details.Identifier; } SearchResult SQLiteIndexSource::Search(const SearchRequest& request) const diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h index 69f854d043..71b7821a2d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h @@ -39,7 +39,6 @@ namespace AppInstaller::Repository::Microsoft private: SourceDetails m_details; - std::string m_identifier; Synchronization::CrossProcessReaderWriteLock m_lock; bool m_isInstalled; SQLiteIndex m_index; diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h index 376b89a17f..eaaef2170a 100644 --- a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -47,6 +47,9 @@ namespace AppInstaller::Repository // The source's extra data string. std::string Data; + // The source's unique identifier. + std::string Identifier; + // The last time that this source was updated. std::chrono::system_clock::time_point LastUpdateTime = {}; diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index 5471d0653a..be7bad52c6 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -20,6 +20,7 @@ namespace AppInstaller::Repository constexpr std::string_view s_SourcesYaml_Source_Type = "Type"sv; constexpr std::string_view s_SourcesYaml_Source_Arg = "Arg"sv; constexpr std::string_view s_SourcesYaml_Source_Data = "Data"sv; + constexpr std::string_view s_SourcesYaml_Source_Identifier = "Identifier"sv; constexpr std::string_view s_SourcesYaml_Source_IsTombstone = "IsTombstone"sv; constexpr std::string_view s_MetadataYaml_Sources = "Sources"sv; @@ -29,10 +30,12 @@ namespace AppInstaller::Repository constexpr std::string_view s_Source_WingetCommunityDefault_Name = "winget"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Arg = "https://winget.azureedge.net/cache"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Data = "Microsoft.Winget.Source_8wekyb3d8bbwe"sv; + constexpr std::string_view s_Source_WingetCommunityDefault_Identifier = "Microsoft.Winget.Source_8wekyb3d8bbwe"sv; constexpr std::string_view s_Source_WingetMSStoreDefault_Name = "msstore"sv; constexpr std::string_view s_Source_WingetMSStoreDefault_Arg = "https://winget.azureedge.net/msstore"sv; constexpr std::string_view s_Source_WingetMSStoreDefault_Data = "Microsoft.Winget.MSStore.Source_8wekyb3d8bbwe"sv; + constexpr std::string_view s_Source_WingetMSStoreDefault_Identifier = "Microsoft.Winget.MSStore.Source_8wekyb3d8bbwe"sv; namespace { @@ -187,6 +190,7 @@ namespace AppInstaller::Repository details.Type = Microsoft::PreIndexedPackageSourceFactory::Type(); details.Arg = s_Source_WingetCommunityDefault_Arg; details.Data = s_Source_WingetCommunityDefault_Data; + details.Identifier = s_Source_WingetCommunityDefault_Identifier; details.TrustLevel = SourceTrustLevel::Trusted; result.emplace_back(std::move(details)); @@ -197,6 +201,7 @@ namespace AppInstaller::Repository storeDetails.Type = Microsoft::PreIndexedPackageSourceFactory::Type(); storeDetails.Arg = s_Source_WingetMSStoreDefault_Arg; storeDetails.Data = s_Source_WingetMSStoreDefault_Data; + storeDetails.Identifier = s_Source_WingetMSStoreDefault_Identifier; storeDetails.TrustLevel = SourceTrustLevel::Trusted; result.emplace_back(std::move(storeDetails)); } @@ -214,6 +219,7 @@ namespace AppInstaller::Repository if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Arg, details.Arg)) { return false; } if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Data, details.Data)) { return false; } if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_IsTombstone, details.IsTombstone)) { return false; } + TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Identifier, details.Identifier); return true; }); break; @@ -246,6 +252,7 @@ namespace AppInstaller::Repository out << YAML::Key << s_SourcesYaml_Source_Type << YAML::Value << details.Type; out << YAML::Key << s_SourcesYaml_Source_Arg << YAML::Value << details.Arg; out << YAML::Key << s_SourcesYaml_Source_Data << YAML::Value << details.Data; + out << YAML::Key << s_SourcesYaml_Source_Identifier << YAML::Value << details.Identifier; out << YAML::Key << s_SourcesYaml_Source_IsTombstone << YAML::Value << details.IsTombstone; out << YAML::EndMap; } @@ -603,6 +610,7 @@ namespace AppInstaller::Repository AddSourceFromDetails(details, progress); AICLI_LOG(Repo, Info, << "Source created with extra data: " << details.Data); + AICLI_LOG(Repo, Info, << "Source created with identifier: " << details.Identifier); sourceList.AddSource(details); } From 2f4634890a7b2177af84c987e2210c8129a07710 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Mon, 25 Jan 2021 16:08:06 -0800 Subject: [PATCH 09/34] Style changes from review & better logging/reporting * Made source details required in schema. * Stopped using CompositeSource in ImportFlow and instead moved to keeping a list of sources in the context. * Stopped selecting version to export from available ones, and instead always use installed one, warning if it is not available. * Stopped exporting unavailable packages. * Stopped allowing to import packages with no source. * Use existing code to select version to install. * Moved strings for JSON from .h to .cpp * Renamed PackageRequest class --- doc/packages.schema.json | 2 +- .../Commands/ImportCommand.cpp | 4 +- src/AppInstallerCLICore/ExecutionContext.h | 10 +- src/AppInstallerCLICore/PackageCollection.cpp | 169 +++++++------- src/AppInstallerCLICore/PackageCollection.h | 80 +++---- src/AppInstallerCLICore/Resources.h | 6 + .../Workflows/ImportExportFlow.cpp | 215 ++++++++---------- .../Workflows/InstallFlow.cpp | 11 +- .../Workflows/WorkflowBase.cpp | 20 +- .../Workflows/WorkflowBase.h | 19 ++ .../Shared/Strings/en-us/winget.resw | 37 ++- src/AppInstallerCommonCore/Errors.cpp | 6 + .../Public/AppInstallerErrors.h | 1 + 13 files changed, 298 insertions(+), 282 deletions(-) diff --git a/doc/packages.schema.json b/doc/packages.schema.json index 379b2be4d7..627541f0d5 100644 --- a/doc/packages.schema.json +++ b/doc/packages.schema.json @@ -34,7 +34,7 @@ "SourceDetails": { "description": "Details about this source", "type": "object", - "required": false, + "required": true, "additionalProperties": true, "properties": { diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index db1c534bc6..0cb30c895d 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -15,8 +15,8 @@ namespace AppInstaller::CLI std::vector ImportCommand::GetArguments() const { return { - Argument{ "importFile", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, - Argument{ "exactVersions", Argument::NoAlias, Execution::Args::Type::ExactVersions, Resource::String::ExactVersionsArgumentDescription, ArgumentType::Flag }, + Argument{ "import-file", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ "exact-versions", Argument::NoAlias, Execution::Args::Type::ExactVersions, Resource::String::ExactVersionsArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index 5b6ef0e5fa..d651d73084 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -66,6 +66,8 @@ namespace AppInstaller::CLI::Execution PackageCollection, // On import: A collection of specific package versions to install PackagesToInstall, + // On import: Sources for the imported packages + Sources, Max }; @@ -199,7 +201,13 @@ namespace AppInstaller::CLI::Execution template <> struct DataMapping { - using value_t = std::vector< std::shared_ptr>; + using value_t = std::vector>; + }; + + template <> + struct DataMapping + { + using value_t = std::vector>; }; // Used to deduce the DataVariant type; making a variant that includes std::monostate and all DataMapping types. diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index a0ff460e1f..7e8f502087 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -10,69 +10,70 @@ namespace AppInstaller::CLI { - using namespace AppInstaller::CLI::PackagesJson; using namespace AppInstaller::Repository; namespace { - // Gets a property of a JSON object by its name. - template - Json::Value& GetJsonProperty(Json::Value& node, const std::string& propertyName) + // Strings used in the Packages JSON file. + // Most will be used to access a JSON value, so they need to be std::string + const std::string s_PackagesJson_Schema = "$schema"; + const std::string s_PackagesJson_SchemaUri_V0 = "https://aka.ms/winget-packages-v0.schema.json"; + const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; + const std::string s_PackagesJson_CreationDate = "CreationDate"; + + const std::string s_PackagesJson_Sources = "Sources"; + const std::string s_PackagesJson_Source_Details = "SourceDetails"; + const std::string s_PackagesJson_Source_Name = "Name"; + const std::string s_PackagesJson_Source_Identifier = "Identifier"; + const std::string s_PackagesJson_Source_Argument = "Argument"; + const std::string s_PackagesJson_Source_Type = "Type"; + + const std::string s_PackagesJson_Packages = "Packages"; + const std::string s_PackagesJson_Package_Id = "Id"; + const std::string s_PackagesJson_Package_Version = "Version"; + const std::string s_PackagesJson_Package_Channel = "Channel"; + + // Gets or creates a property of a JSON object by its name. + Json::Value& GetJsonProperty(Json::Value& node, const std::string& propertyName, Json::ValueType valueType) { if (!node.isMember(propertyName)) { - node[propertyName] = Json::Value{ T }; + node[propertyName] = Json::Value{ valueType }; } - THROW_HR_IF(E_NOT_VALID_STATE, node[propertyName].type() != T); + THROW_HR_IF(E_NOT_VALID_STATE, node[propertyName].type() != valueType); return node[propertyName]; } - // Sets a property of a JSON object to a string if it is not empty - void SetJsonProperty(Json::Value& node, const std::string& propertyName, const std::string& value) - { - if (!value.empty()) - { - node[propertyName] = value; - } - else if (node.isMember(propertyName)) - { - node.removeMember(propertyName); - } - } - // Reads the description of a package from a Package node in the JSON. - PackageRequest ParsePackageNode(const Json::Value& packageNode) + PackageCollection::Package ParsePackageNode(const Json::Value& packageNode) { - std::string id = packageNode[PACKAGE_ID_PROPERTY].asString(); - std::string version = packageNode[PACKAGE_VERSION_PROPERTY].asString(); - std::string channel = packageNode.isMember(PACKAGE_CHANNEL_PROPERTY) ? packageNode[PACKAGE_CHANNEL_PROPERTY].asString() : ""; + std::string id = packageNode[s_PackagesJson_Package_Id].asString(); + std::string version = packageNode[s_PackagesJson_Package_Version].asString(); + std::string channel = packageNode.isMember(s_PackagesJson_Package_Channel) ? packageNode[s_PackagesJson_Package_Channel].asString() : ""; - PackageRequest packageRequest{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; + PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; - return packageRequest; + return package; } // Reads the description of a Source and all the packages needed from it, from a Source node in the JSON. - PackageRequestsFromSource ParseSourceNode(const Json::Value& sourceNode) + PackageCollection::Source ParseSourceNode(const Json::Value& sourceNode) { - PackageRequestsFromSource requestsFromSource; - - if (sourceNode.isMember(SOURCE_DETAILS_PROPERTY)) - { - auto& detailsNode = sourceNode[SOURCE_DETAILS_PROPERTY]; - requestsFromSource.SourceIdentifier = Utility::LocIndString{ detailsNode[SOURCE_IDENTIFIER_PROPERTY].asString() }; - requestsFromSource.Details.Name = detailsNode[SOURCE_NAME_PROPERTY].asString(); - requestsFromSource.Details.Arg = detailsNode[SOURCE_ARGUMENT_PROPERTY].asString(); - requestsFromSource.Details.Type = detailsNode[SOURCE_TYPE_PROPERTY].asString(); - } - - for (const auto& packageNode : sourceNode[PACKAGES_PROPERTY]) + SourceDetails sourceDetails; + auto& detailsNode = sourceNode[s_PackagesJson_Source_Details]; + sourceDetails.Identifier = Utility::LocIndString{ detailsNode[s_PackagesJson_Source_Identifier].asString() }; + sourceDetails.Name = detailsNode[s_PackagesJson_Source_Name].asString(); + sourceDetails.Arg = detailsNode[s_PackagesJson_Source_Argument].asString(); + sourceDetails.Type = detailsNode[s_PackagesJson_Source_Type].asString(); + + PackageCollection::Source source{ std::move(sourceDetails) }; + for (const auto& packageNode : sourceNode[s_PackagesJson_Packages]) { - requestsFromSource.Packages.push_back(ParsePackageNode(packageNode)); + source.Packages.push_back(ParsePackageNode(packageNode)); } - return requestsFromSource; + return source; } // Gets the available PackageVersion that has the same version as the installed version. @@ -89,41 +90,56 @@ namespace AppInstaller::CLI }; return package.GetAvailableVersion(installedVersionKey); } - } - - namespace PackagesJson - { + // Creates a minimal root object of a Packages JSON file. Json::Value CreateRoot() { Json::Value root{ Json::ValueType::objectValue }; - root[WINGET_VERSION_PROPERTY] = Runtime::GetClientVersion().get(); - root[SCHEMA_PROPERTY] = SCHEMA_PATH; + root[s_PackagesJson_WinGetVersion] = Runtime::GetClientVersion().get(); + root[s_PackagesJson_Schema] = s_PackagesJson_SchemaUri_V0; // TODO: This uses localtime. Do we want to use UTC or add time zone? std::stringstream currentTimeStream; Utility::OutputTimePoint(currentTimeStream, std::chrono::system_clock::now()); - root[CREATION_DATE_PROPERTY] = currentTimeStream.str(); + root[s_PackagesJson_CreationDate] = currentTimeStream.str(); return root; } - Json::Value& AddSourceNode(Json::Value& root, const PackageRequestsFromSource& source) + // Adds a new Package node to a Source node in the Json file, and returns it. + Json::Value& AddPackageToSource(Json::Value& sourceNode, const PackageCollection::Package& package) + { + Json::Value packageNode{ Json::ValueType::objectValue }; + packageNode[s_PackagesJson_Package_Id] = package.Id.get(); + packageNode[s_PackagesJson_Package_Version] = package.VersionAndChannel.GetVersion().ToString(); + + // Only add channel if present + const std::string& channel = package.VersionAndChannel.GetChannel().ToString(); + if (!channel.empty()) + { + packageNode[s_PackagesJson_Package_Channel] = channel; + } + + return sourceNode[s_PackagesJson_Packages].append(std::move(packageNode)); + } + + // Adds a new Source node to the JSON, and returns it. + Json::Value& AddSourceNode(Json::Value& root, const PackageCollection::Source& source) { Json::Value sourceNode{ Json::ValueType::objectValue }; if (!source.Details.Name.empty()) { Json::Value sourceDetailsNode{ Json::ValueType::objectValue }; - sourceDetailsNode[SOURCE_NAME_PROPERTY] = source.Details.Name; - sourceDetailsNode[SOURCE_ARGUMENT_PROPERTY] = source.Details.Arg; - sourceDetailsNode[SOURCE_IDENTIFIER_PROPERTY] = source.SourceIdentifier.get(); - sourceDetailsNode[SOURCE_TYPE_PROPERTY] = source.Details.Type; - sourceNode[SOURCE_DETAILS_PROPERTY] = std::move(sourceDetailsNode); + sourceDetailsNode[s_PackagesJson_Source_Name] = source.Details.Name; + sourceDetailsNode[s_PackagesJson_Source_Argument] = source.Details.Arg; + sourceDetailsNode[s_PackagesJson_Source_Identifier] = source.Details.Identifier; + sourceDetailsNode[s_PackagesJson_Source_Type] = source.Details.Type; + sourceNode[s_PackagesJson_Source_Details] = std::move(sourceDetailsNode); } - sourceNode[PACKAGES_PROPERTY] = Json::Value{ Json::ValueType::arrayValue }; + sourceNode[s_PackagesJson_Packages] = Json::Value{ Json::ValueType::arrayValue }; - auto& sourcesNode = GetJsonProperty(root, SOURCES_PROPERTY); + auto& sourcesNode = GetJsonProperty(root, s_PackagesJson_Sources, Json::ValueType::arrayValue); for (const auto& package : source.Packages) { AddPackageToSource(sourceNode, package); @@ -131,29 +147,16 @@ namespace AppInstaller::CLI return sourcesNode.append(std::move(sourceNode)); } + } - Json::Value& AddPackageToSource(Json::Value& sourceNode, const PackageRequest& package) - { - Json::Value packageNode{ Json::ValueType::objectValue }; - packageNode[PACKAGE_ID_PROPERTY] = package.Id.get(); - packageNode[PACKAGE_VERSION_PROPERTY] = package.VersionAndChannel.GetVersion().ToString(); - - // Only add channel if present - const std::string& channel = package.VersionAndChannel.GetChannel().ToString(); - if (!channel.empty()) - { - packageNode[PACKAGE_CHANNEL_PROPERTY] = channel; - } - - return sourceNode[PACKAGES_PROPERTY].append(std::move(packageNode)); - } - + namespace PackagesJson + { Json::Value CreateJson(const PackageCollection& packages) { - Json::Value root = PackagesJson::CreateRoot(); - for (const auto& source : packages.RequestsFromSources) + Json::Value root = CreateRoot(); + for (const auto& source : packages.Sources) { - PackagesJson::AddSourceNode(root, source); + AddSourceNode(root, source); } return root; @@ -163,26 +166,14 @@ namespace AppInstaller::CLI { // TODO: Validate schema. The following assumes the file is already valid. PackageCollection packages; - packages.ClientVersion = root[WINGET_VERSION_PROPERTY].asString(); - for (const auto& sourceNode : root[SOURCES_PROPERTY]) + packages.ClientVersion = root[s_PackagesJson_WinGetVersion].asString(); + for (const auto& sourceNode : root[s_PackagesJson_Sources]) { // TODO: Prevent duplicates? - packages.RequestsFromSources.push_back(ParseSourceNode(sourceNode)); + packages.Sources.push_back(ParseSourceNode(sourceNode)); } return packages; } } - - PackageRequest::PackageRequest(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel) : - Id(std::move(id)), VersionAndChannel(std::move(version), std::move(channel)) {} - - PackageRequest::PackageRequest(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel) : - Id(std::move(id)), VersionAndChannel(std::move(versionAndChannel)) {} - - PackageRequestsFromSource::PackageRequestsFromSource(const Utility::LocIndString& sourceIdentifier, const SourceDetails& sourceDetails) : - SourceIdentifier(sourceIdentifier), Details(sourceDetails) {} - - PackageRequestsFromSource::PackageRequestsFromSource(Utility::LocIndString&& sourceIdentifier, SourceDetails&& sourceDetails) : - SourceIdentifier(std::move(sourceIdentifier)), Details(std::move(sourceDetails)) {} } \ No newline at end of file diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h index ff52b49f62..c3fd9b841c 100644 --- a/src/AppInstallerCLICore/PackageCollection.h +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -12,69 +12,43 @@ namespace AppInstaller::CLI { using namespace AppInstaller::Repository; - // Container for data used to identify a package to be installed. - struct PackageRequest - { - PackageRequest() = default; - PackageRequest(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel); - PackageRequest(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel); - - Utility::LocIndString Id; - Utility::VersionAndChannel VersionAndChannel; - }; - - // Container for data to identify multiple packages to be installed from a single source. - struct PackageRequestsFromSource - { - PackageRequestsFromSource() = default; - PackageRequestsFromSource(const Utility::LocIndString& sourceIdentifier, const SourceDetails& sourceDetails); - PackageRequestsFromSource(Utility::LocIndString&& sourceIdentifier, SourceDetails&& sourceDetails); - - Utility::LocIndString SourceIdentifier; - SourceDetails Details; - std::vector Packages; - }; - // Container for data to identify multiple packages to be installed from multiple sources. struct PackageCollection { - // Version of the WinGet client that produced this request. + // Description of a package. + // Does not represent the actual package, just enough to find and install it. + struct Package + { + Package() = default; + Package(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel) : + Id(std::move(id)), VersionAndChannel(std::move(version), std::move(channel)) {} + Package(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel) : + Id(std::move(id)), VersionAndChannel(std::move(versionAndChannel)) {} + + Utility::LocIndString Id; + Utility::VersionAndChannel VersionAndChannel; + }; + + // A source along with a set of packages available from it. + struct Source + { + Source() = default; + Source(const SourceDetails& sourceDetails) : Details(sourceDetails) {} + Source(SourceDetails&& sourceDetails) : Details(std::move(sourceDetails)) {} + + SourceDetails Details; + std::vector Packages; + }; + + // Version of the WinGet client that produced this collection. std::string ClientVersion; // Requests from each individual source. - std::vector RequestsFromSources; + std::vector Sources; }; namespace PackagesJson { - // Strings used in the Packages JSON file. - // Most will be used to access a JSON value, so they need to be std::string - const std::string SCHEMA_PROPERTY = "$schema"; - const std::string SCHEMA_PATH = "https://aka.ms/winget-packages.schema.json"; - const std::string WINGET_VERSION_PROPERTY = "WinGetVersion"; - const std::string CREATION_DATE_PROPERTY = "CreationDate"; - - const std::string SOURCES_PROPERTY = "Sources"; - const std::string SOURCE_DETAILS_PROPERTY = "SourceDetails"; - const std::string SOURCE_NAME_PROPERTY = "Name"; - const std::string SOURCE_IDENTIFIER_PROPERTY = "Identifier"; - const std::string SOURCE_ARGUMENT_PROPERTY = "Argument"; - const std::string SOURCE_TYPE_PROPERTY = "Type"; - - const std::string PACKAGES_PROPERTY = "Packages"; - const std::string PACKAGE_ID_PROPERTY = "Id"; - const std::string PACKAGE_VERSION_PROPERTY = "Version"; - const std::string PACKAGE_CHANNEL_PROPERTY = "Channel"; - - // Creates a minimal root object of a Packages JSON file. - Json::Value CreateRoot(); - - // Adds a new Source node to the JSON, and returns it. - Json::Value& AddSourceNode(Json::Value& root, const PackageRequestsFromSource& source); - - // Adds a new Package node to a Source node in the Json file, and returns it. - Json::Value& AddPackageToSource(Json::Value& source, const PackageRequest& package); - // Converts a collection of packages to its JSON representation for exporting. Json::Value CreateJson(const PackageCollection& packages); diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 3587ea76d4..d1ffe7b217 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -75,12 +75,17 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ImportSearchFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ImportSourceNotInstalled); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer2); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimerMSStore); WINGET_DEFINE_RESOURCE_STRINGID(InstallationRequiresHigherWindows); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageNotAvailable); + WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageVersionNotAvailable); WINGET_DEFINE_RESOURCE_STRINGID(InstallerBlockedByPolicy); WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedSecurityCheck); WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedVirusScan); @@ -128,6 +133,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage); WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoPackageFound); + WINGET_DEFINE_RESOURCE_STRINGID(NoPackagesFoundInImportFile); WINGET_DEFINE_RESOURCE_STRINGID(NoUninstallInfoFound); WINGET_DEFINE_RESOURCE_STRINGID(NoVTArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatch); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 93d5c04bc1..4cdba89ac6 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -5,82 +5,68 @@ #include "ImportExportFlow.h" #include "PackageCollection.h" #include "WorkflowBase.h" -#include "CompositeSource.h" #include "AppInstallerRepositorySearch.h" namespace AppInstaller::CLI::Workflow { - namespace - { - // Gets the available PackageVersion that has the same version as the installed version. - // The package must have an installed version. - std::shared_ptr GetAvailableVersionMatchingInstalledVersion(const IPackage& package) - { - auto installedVersion = package.GetInstalledVersion(); - PackageVersionKey installedVersionKey - { - "", - installedVersion->GetProperty(PackageVersionProperty::Version).get(), - installedVersion->GetProperty(PackageVersionProperty::Channel).get(), - }; - return package.GetAvailableVersion(installedVersionKey); - } - - // Selects which version of an installed package to list when exporting. - std::shared_ptr SelectPackageVersionToExport(const IPackage& package) - { - // See if the installed version is available from some source - auto availableVersion = GetAvailableVersionMatchingInstalledVersion(package); - if (!availableVersion) - { - // If the exact version isn't available, list the latest. - availableVersion = package.GetLatestAvailableVersion(); - } - - if (availableVersion) - { - AICLI_LOG( - CLI, - Info, - << "Found package " << availableVersion->GetProperty(PackageVersionProperty::Id) - << " " << availableVersion->GetProperty(PackageVersionProperty::Version) - << " available from " << availableVersion->GetSource()->GetIdentifier()); - return availableVersion; - } - - // If there is no available version, we didn't have a mapping for the ARP - // entry of the package. List the installed version so it can be later installed - // if we get a better mapping. - auto installedVersion = package.GetInstalledVersion(); - AICLI_LOG(CLI, Warning, << "No available version of package " << installedVersion->GetProperty(PackageVersionProperty::Id) << " was found to export"); - return installedVersion; - } - } + using namespace AppInstaller::Repository; void SelectVersionsToExport(Execution::Context& context) { const auto& searchResult = context.Get(); - PackageCollection versionsToExport = {}; - auto& requestsFromSource = versionsToExport.RequestsFromSources; + PackageCollection exportedPackages = {}; + auto& exportedSources = exportedPackages.Sources; for (const auto& packageMatch : searchResult.Matches) { - auto packageVersion = SelectPackageVersionToExport(*packageMatch.Package); + auto installedPackageVersion = packageMatch.Package->GetInstalledVersion(); + auto version = installedPackageVersion->GetProperty(PackageVersionProperty::Version); + auto channel = installedPackageVersion->GetProperty(PackageVersionProperty::Channel); - const auto& sourceIdentifier = packageVersion->GetSource()->GetIdentifier(); - auto sourceItr = std::find_if(requestsFromSource.begin(), requestsFromSource.end(), [&](const PackageRequestsFromSource& s) { return s.SourceIdentifier == sourceIdentifier; }); - if (sourceItr == requestsFromSource.end()) + // Find an available version of this package to determine its source. + auto availablePackageVersion = packageMatch.Package->GetAvailableVersion({ "", version.get(), channel.get() }); + if (!availablePackageVersion) { - requestsFromSource.emplace_back(Utility::LocIndString{ sourceIdentifier }, packageVersion->GetSource()->GetDetails()); - sourceItr = std::prev(requestsFromSource.end()); + availablePackageVersion = packageMatch.Package->GetLatestAvailableVersion(); + if (!availablePackageVersion) + { + // Report package not found and move to next package. + AICLI_LOG(CLI, Warning, << "No available version of package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] was found to export"); + context.Reporter.Warn() << Resource::String::InstalledPackageNotAvailable << ' ' << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << std::endl; + continue; + } + else + { + // Warn installed version is not available. + AICLI_LOG( + CLI, + Info, + << "Installed package version not available. Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "]" + << ", Version [" << version << "], Channel [" << channel << "]"); + context.Reporter.Warn() << Resource::String::InstalledPackageVersionNotAvailable << ' ' << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << std::endl; + } + } + + const auto& sourceIdentifier = availablePackageVersion->GetSource()->GetIdentifier(); + AICLI_LOG(CLI, Info, + << "Installed package is available. Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Source [" << sourceIdentifier << "]"); + + // Find the exported source for this package + auto sourceItr = std::find_if(exportedSources.begin(), exportedSources.end(), [&](const PackageCollection::Source& s) { return s.Details.Identifier == sourceIdentifier; }); + if (sourceItr == exportedSources.end()) + { + exportedSources.emplace_back(availablePackageVersion->GetSource()->GetDetails()); + sourceItr = std::prev(exportedSources.end()); } + // Take the Id from the available package because that is the one used in the source, + // but take the exported version from the installed package. sourceItr->Packages.emplace_back( - packageVersion->GetProperty(PackageVersionProperty::Id), - packageVersion->GetProperty(PackageVersionProperty::Version).get(), - packageVersion->GetProperty(PackageVersionProperty::Channel).get()); + availablePackageVersion->GetProperty(PackageVersionProperty::Id), + version.get(), + channel.get()); } - context.Add(std::move(versionsToExport)); + context.Add(std::move(exportedPackages)); } void WriteImportFile(Execution::Context& context) @@ -101,102 +87,97 @@ namespace AppInstaller::CLI::Workflow std::ifstream{ importFilePath } >> jsonRoot; auto packages = PackagesJson::ParseJson(jsonRoot); - context.Add(packages); - - if (packages.RequestsFromSources.empty()) + if (packages.Sources.empty()) { AICLI_LOG(CLI, Warning, << "No packages to install"); + context.Reporter.Info() << Resource::String::NoPackagesFoundInImportFile << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); } + + context.Add(packages); } void SearchPackagesForImport(Execution::Context& context) { auto availableSources = Repository::GetSources(); - std::vector> packagesToInstall; + std::vector> packagesToInstall = {}; + bool foundAll = true; - // Aggregated source with all the required sources. - // We keep it as the source of the root context to keep all the - // source objects alive for install. - auto aggregatedSource = std::make_shared("*ImportSource"); + // List of all the sources used for import. + // Needed to keep all the source objects alive for install. + std::vector> sources = {}; // Look for the packages needed from each source independently. // If a package is available from multiple sources, this ensures we will get it from the right one. - for (auto& requiredSource : context.Get().RequestsFromSources) + for (auto& requiredSource : context.Get().Sources) { - if (!requiredSource.Details.Name.empty()) + // Find the installed source matching the one described in the collection. + auto matchingSource = std::find_if( + availableSources.begin(), + availableSources.end(), + [&](const SourceDetails& s) { return s.Identifier == requiredSource.Details.Identifier; }); + if (matchingSource != availableSources.end()) { - // For packages that come from a specific source, find the matching available source. - std::optional matchingSource = {}; - for (auto& availableSource : availableSources) - { - if (availableSource.Identifier == requiredSource.Details.Identifier) - { - matchingSource = availableSource; - break; - } - } - - if (matchingSource.has_value()) - { - requiredSource.Details.Name = matchingSource.value().Name; - } - else - { - // TODO: Add option for ignoring/installing missing sources? - AICLI_LOG(CLI, Error, << "Missing required source: " << requiredSource.Details.Name); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); - } + requiredSource.Details.Name = matchingSource->Name; + } + else + { + AICLI_LOG(CLI, Error, << "Missing required source: " << requiredSource.Details.Name); + context.Reporter.Warn() << Resource::String::ImportSourceNotInstalled << ' ' << requiredSource.Details.Name << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); } + // Search for all the packages in the source. + // Each search is done in a sub context to search everything regardless of previous failures. context << OpenNamedSource(requiredSource.Details.Name); auto source = context.Get(); - aggregatedSource->AddAvailableSource(source); for (const auto& packageRequest : requiredSource.Packages) { - // Search for all the packages in the source Logging::SubExecutionTelemetryScope subExecution; - // We want to do best effort to install all packages regardless of previous failures auto searchContextPtr = context.Clone(); Execution::Context& searchContext = *searchContextPtr; searchContext.Add(source); - // TODO: Case insensitive? - MatchType matchType = MatchType::Exact; - + // Search for the current package SearchRequest searchRequest; - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, packageRequest.Id)); - + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, packageRequest.Id)); searchContext.Add(source->Search(searchRequest)); - searchContext << EnsureOneMatchFromSearchResult(false); - - if (searchContext.IsTerminated()) - { - AICLI_LOG(CLI, Warning, << "Package not found [" << packageRequest.Id << "] in source [" << source->GetIdentifier() << "]"); - continue; - } + Utility::VersionAndChannel versionAndChannel = {}; if (context.Args.Contains(Execution::Args::Type::ExactVersions)) { - PackageVersionKey requestedVersion - { - requiredSource.SourceIdentifier.get(), - packageRequest.VersionAndChannel.GetVersion().ToString(), - packageRequest.VersionAndChannel.GetChannel().ToString(), - }; - - // TODO: handle unavailable version - packagesToInstall.push_back(searchContext.Get()->GetAvailableVersion(requestedVersion)); + versionAndChannel = packageRequest.VersionAndChannel; } - else + + // Find the single version we want is available + searchContext << + Workflow::EnsureOneMatchFromSearchResult(false) << + Workflow::GetManifestWithVersionFromPackage(versionAndChannel); + + if (searchContext.IsTerminated()) { - packagesToInstall.push_back(searchContext.Get()->GetLatestAvailableVersion()); + AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << versionAndChannel.ToString()); + context.Reporter.Info() << Resource::String::ImportSearchFailed << ' ' << packageRequest.Id << std::endl; + + // Keep searching for the remaining packages and only fail at the end. + foundAll = false; + continue; } + + packagesToInstall.push_back(std::move(searchContext.Get())); } + + sources.push_back(std::move(source)); + } + + if (!foundAll) + { + // TODO: Set better error; report; log + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR); } context.Add(std::move(packagesToInstall)); - context.Add(std::move(aggregatedSource)); + context.Add(std::move(sources)); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 3f747cb0f4..cb3de2a22b 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -386,6 +386,7 @@ namespace AppInstaller::CLI::Workflow void InstallMultiple(Execution::Context& context) { + bool allSucceeded = true; for (auto package : context.Get()) { Logging::SubExecutionTelemetryScope subExecution; @@ -399,8 +400,16 @@ namespace AppInstaller::CLI::Workflow installContext.Add(package->GetManifest()); installContext << InstallPackageVersion; + if (installContext.IsTerminated()) + { + allSucceeded = false; + } + } - // TODO: Handle errors in sub context + if (!allSucceeded) + { + context.Reporter.Error() << Resource::String::ImportInstallFailed << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED); } } } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index dbe0c20d65..3e1ca02db6 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -477,12 +477,9 @@ namespace AppInstaller::CLI::Workflow }; } - void GetManifestFromPackage(Execution::Context& context) + void GetManifestWithVersionFromPackage::operator()(Execution::Context& context) const { - std::string_view version = context.Args.GetArg(Execution::Args::Type::Version); - std::string_view channel = context.Args.GetArg(Execution::Args::Type::Channel); - - PackageVersionKey key("", version, channel); + PackageVersionKey key("", m_version, m_channel); auto requestedVersion = context.Get()->GetAvailableVersion(key); std::optional manifest; @@ -494,13 +491,13 @@ namespace AppInstaller::CLI::Workflow if (!manifest) { context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound << ' '; - if (!version.empty()) + if (!m_version.empty()) { - context.Reporter.Error() << version; + context.Reporter.Error() << m_version; } - if (!channel.empty()) + if (!m_channel.empty()) { - context.Reporter.Error() << '[' << channel << ']'; + context.Reporter.Error() << '[' << m_channel << ']'; } context.Reporter.Error() << std::endl; @@ -512,6 +509,11 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(requestedVersion)); } + void GetManifestFromPackage(Execution::Context& context) + { + context << GetManifestWithVersionFromPackage(context.Args.GetArg(Execution::Args::Type::Version), context.Args.GetArg(Execution::Args::Type::Channel)); + } + void VerifyFile::operator()(Execution::Context& context) const { std::filesystem::path path = Utility::ConvertToUTF16(context.Args.GetArg(m_arg)); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index 84935f78dd..eca91a82a2 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -198,6 +198,25 @@ namespace AppInstaller::CLI::Workflow bool m_isFromInstalledSource; }; + // Gets the manifest from package. + // Required Args: Version and channel; can be empty + // Inputs: Package + // Outputs: Manifest, PackageVersion + struct GetManifestWithVersionFromPackage : public WorkflowTask + { + GetManifestWithVersionFromPackage(const Utility::VersionAndChannel& versionAndChannel) : + WorkflowTask("GetManifestWithVersionFromPackage"), m_version(versionAndChannel.GetVersion().ToString()), m_channel(versionAndChannel.GetChannel().ToString()) {} + + GetManifestWithVersionFromPackage(std::string_view version, std::string_view channel) : + WorkflowTask("GetManifestWithVersionFromPackage"), m_version(version), m_channel(channel) {} + + void operator()(Execution::Context& context) const override; + + private: + std::string_view m_version; + std::string_view m_channel; + }; + // Gets the manifest from package. // Required Args: None // Inputs: Package diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index c2c1bbe229..4b89844a24 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -756,28 +756,47 @@ They can be configured through the settings file 'winget settings'. Uninstall failed with exit code: - - TODO Export description - - TODO Export + Exports a list of the installed packages - TODO Import description + Installs all the packages listed in a file. - TODO Import + Installs all the packages in a file - TODO + File where the result is to be written - TODO + File describing the packages to install - TODO + Install the exact versions listed in the import file Export packages from the specified source + + Writes a list of the installed packages to a file. The packages can then be installed with the import command. + {Locked="import"} + + + One or more imported packages failed to install + + + Package not found for import: + + + Source required for import is not installed: + + + Installed package is not available from any source: + + + Installed version of package is not available from any source: + + + No packages found in import file + \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp index d0a9db2495..cc169c9154 100644 --- a/src/AppInstallerCommonCore/Errors.cpp +++ b/src/AppInstallerCommonCore/Errors.cpp @@ -107,6 +107,12 @@ namespace AppInstaller return "Installer failed security check"; case APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH: return "Download size does not match expected content length"; + case APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND: + return "Uninstall command not found"; + case APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED: + return "Running uninstall command failed"; + case APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED: + return "Failed to install one or more imported packages"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h index 358cd2570f..75b11a0fdf 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -61,6 +61,7 @@ #define APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH ((HRESULT)0x8A15002E) #define APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND ((HRESULT)0x8a15002F) #define APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED ((HRESULT)0x8a150030) +#define APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED ((HRESULT)0x8a150031) namespace AppInstaller { From c6408d74473b27cdad54f1d63134bb7972228074 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 28 Jan 2021 18:52:00 -0800 Subject: [PATCH 10/34] Add tests for workflow & detect installed packages --- .../Commands/ImportCommand.cpp | 2 + src/AppInstallerCLICore/PackageCollection.cpp | 16 +- src/AppInstallerCLICore/Resources.h | 2 + .../Workflows/ImportExportFlow.cpp | 95 ++++++--- .../Workflows/ImportExportFlow.h | 9 +- .../Shared/Strings/en-us/winget.resw | 6 + .../AppInstallerCLITests.vcxproj | 19 ++ .../AppInstallerCLITests.vcxproj.filters | 21 ++ .../TestData/ImportFile-Bad-Invalid.json | 1 + .../TestData/ImportFile-Bad-Malformed.json | 1 + .../ImportFile-Bad-UnknownPackage.json | 21 ++ .../ImportFile-Bad-UnknownPackageVersion.json | 21 ++ .../ImportFile-Bad-UnknownSource.json | 21 ++ .../TestData/ImportFile-Good.json | 25 +++ src/AppInstallerCLITests/TestSource.cpp | 15 +- src/AppInstallerCLITests/TestSource.h | 15 +- src/AppInstallerCLITests/WorkFlow.cpp | 196 +++++++++++++++++- src/AppInstallerCommonCore/Errors.cpp | 2 + .../Public/AppInstallerErrors.h | 2 + 19 files changed, 433 insertions(+), 57 deletions(-) create mode 100644 src/AppInstallerCLITests/TestData/ImportFile-Bad-Invalid.json create mode 100644 src/AppInstallerCLITests/TestData/ImportFile-Bad-Malformed.json create mode 100644 src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json create mode 100644 src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json create mode 100644 src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json create mode 100644 src/AppInstallerCLITests/TestData/ImportFile-Good.json diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 0cb30c895d..82436d27b5 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -41,6 +41,8 @@ namespace AppInstaller::CLI context << Workflow::VerifyFile(Execution::Args::Type::ImportFile) << Workflow::ReadImportFile << + Workflow::OpenSourcesForImport << + Workflow::OpenPredefinedSource(Repository::PredefinedSource::Installed) << Workflow::SearchPackagesForImport << Workflow::InstallMultiple; } diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index 7e8f502087..ad4b06f700 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -127,15 +127,13 @@ namespace AppInstaller::CLI { Json::Value sourceNode{ Json::ValueType::objectValue }; - if (!source.Details.Name.empty()) - { - Json::Value sourceDetailsNode{ Json::ValueType::objectValue }; - sourceDetailsNode[s_PackagesJson_Source_Name] = source.Details.Name; - sourceDetailsNode[s_PackagesJson_Source_Argument] = source.Details.Arg; - sourceDetailsNode[s_PackagesJson_Source_Identifier] = source.Details.Identifier; - sourceDetailsNode[s_PackagesJson_Source_Type] = source.Details.Type; - sourceNode[s_PackagesJson_Source_Details] = std::move(sourceDetailsNode); - } + + Json::Value sourceDetailsNode{ Json::ValueType::objectValue }; + sourceDetailsNode[s_PackagesJson_Source_Name] = source.Details.Name; + sourceDetailsNode[s_PackagesJson_Source_Argument] = source.Details.Arg; + sourceDetailsNode[s_PackagesJson_Source_Identifier] = source.Details.Identifier; + sourceDetailsNode[s_PackagesJson_Source_Type] = source.Details.Type; + sourceNode[s_PackagesJson_Source_Details] = std::move(sourceDetailsNode); sourceNode[s_PackagesJson_Packages] = Json::Value{ Json::ValueType::arrayValue }; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index d1ffe7b217..9efb612f5e 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -76,6 +76,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ImportPackageAlreadyInstalled); WINGET_DEFINE_RESOURCE_STRINGID(ImportSearchFailed); WINGET_DEFINE_RESOURCE_STRINGID(ImportSourceNotInstalled); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1); @@ -99,6 +100,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InteractiveArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InvalidAliasError); WINGET_DEFINE_RESOURCE_STRINGID(InvalidArgumentSpecifierError); + WINGET_DEFINE_RESOURCE_STRINGID(InvalidJsonFile); WINGET_DEFINE_RESOURCE_STRINGID(InvalidNameError); WINGET_DEFINE_RESOURCE_STRINGID(LanguageArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(LicenseAgreement); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 4cdba89ac6..7287b32cc5 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "InstallFlow.h" #include "ImportExportFlow.h" +#include "UpdateFlow.h" #include "PackageCollection.h" #include "WorkflowBase.h" #include "AppInstallerRepositorySearch.h" @@ -80,11 +81,18 @@ namespace AppInstaller::CLI::Workflow void ReadImportFile(Execution::Context& context) { - std::filesystem::path importFilePath{ context.Args.GetArg(Execution::Args::Type::ImportFile) }; + std::ifstream importFile{ context.Args.GetArg(Execution::Args::Type::ImportFile) }; + THROW_LAST_ERROR_IF(importFile.fail()); - // TODO: Handle errors Json::Value jsonRoot; - std::ifstream{ importFilePath } >> jsonRoot; + Json::CharReaderBuilder builder; + Json::String errors; + if (!Json::parseFromStream(builder, importFile, &jsonRoot, &errors)) + { + AICLI_LOG(CLI, Error, << "Failed to read JSON: " << errors); + context.Reporter.Error() << Resource::String::InvalidJsonFile << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); + } auto packages = PackagesJson::ParseJson(jsonRoot); if (packages.Sources.empty()) @@ -97,18 +105,14 @@ namespace AppInstaller::CLI::Workflow context.Add(packages); } - void SearchPackagesForImport(Execution::Context& context) + void OpenSourcesForImport(Execution::Context& context) { auto availableSources = Repository::GetSources(); - std::vector> packagesToInstall = {}; - bool foundAll = true; // List of all the sources used for import. // Needed to keep all the source objects alive for install. std::vector> sources = {}; - // Look for the packages needed from each source independently. - // If a package is available from multiple sources, this ensures we will get it from the right one. for (auto& requiredSource : context.Get().Sources) { // Find the installed source matching the one described in the collection. @@ -127,21 +131,52 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); } + context << Workflow::OpenNamedSource(requiredSource.Details.Name); + if (context.IsTerminated()) + { + return; + } + + sources.push_back(context.Get()); + } + + context.Add(std::move(sources)); + } + + void SearchPackagesForImport(Execution::Context& context) + { + const auto& sources = context.Get(); + std::vector> packagesToInstall = {}; + bool foundAll = true; + + // Look for the packages needed from each source independently. + // If a package is available from multiple sources, this ensures we will get it from the right one. + for (auto& requiredSource : context.Get().Sources) + { + // Find the required source among the open sources. This must exist as we already found them. + auto sourceItr = std::find_if( + sources.begin(), + sources.end(), + [&](const std::shared_ptr& s) { return s->GetIdentifier() == requiredSource.Details.Identifier; }); + if (sourceItr == sources.end()) + { + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR); + } + // Search for all the packages in the source. // Each search is done in a sub context to search everything regardless of previous failures. - context << OpenNamedSource(requiredSource.Details.Name); - auto source = context.Get(); + auto source = Repository::CreateCompositeSource(context.Get(), *sourceItr); for (const auto& packageRequest : requiredSource.Packages) { Logging::SubExecutionTelemetryScope subExecution; - auto searchContextPtr = context.Clone(); - Execution::Context& searchContext = *searchContextPtr; - searchContext.Add(source); - // Search for the current package SearchRequest searchRequest; searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, packageRequest.Id)); + + auto searchContextPtr = context.Clone(); + Execution::Context& searchContext = *searchContextPtr; + searchContext.Add(source); searchContext.Add(source->Search(searchRequest)); Utility::VersionAndChannel versionAndChannel = {}; @@ -153,31 +188,39 @@ namespace AppInstaller::CLI::Workflow // Find the single version we want is available searchContext << Workflow::EnsureOneMatchFromSearchResult(false) << - Workflow::GetManifestWithVersionFromPackage(versionAndChannel); + Workflow::GetManifestWithVersionFromPackage(versionAndChannel) << + Workflow::GetInstalledPackageVersion << + Workflow::EnsureUpdateVersionApplicable; if (searchContext.IsTerminated()) { - AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << versionAndChannel.ToString()); - context.Reporter.Info() << Resource::String::ImportSearchFailed << ' ' << packageRequest.Id << std::endl; - - // Keep searching for the remaining packages and only fail at the end. - foundAll = false; - continue; + if (searchContext.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE) + { + AICLI_LOG(CLI, Info, << "Package is already installed: [" << packageRequest.Id << "]"); + context.Reporter.Info() << Resource::String::ImportPackageAlreadyInstalled << ' ' << packageRequest.Id << std::endl; + continue; + } + else + { + AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << versionAndChannel.ToString()); + context.Reporter.Info() << Resource::String::ImportSearchFailed << ' ' << packageRequest.Id << std::endl; + + // Keep searching for the remaining packages and only fail at the end. + foundAll = false; + continue; + } } packagesToInstall.push_back(std::move(searchContext.Get())); } - - sources.push_back(std::move(source)); } if (!foundAll) { - // TODO: Set better error; report; log - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR); + AICLI_LOG(CLI, Info, << "Could not find one or more packages for import"); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); } context.Add(std::move(packagesToInstall)); - context.Add(std::move(sources)); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.h b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h index 40890207c4..f89381a955 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.h +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h @@ -23,9 +23,16 @@ namespace AppInstaller::CLI::Workflow // Outputs: PackageCollection void ReadImportFile(Execution::Context& context); - // Finds the package versions to install matching their descriptions + // Opens the sources specified in an import file // Required Args: None // Inputs: PackageCollection + // Outputs: Sources + void OpenSourcesForImport(Execution::Context& context); + + // Finds the package versions to install matching their descriptions + // Needs the sources for all packages and the installed source + // Required Args: None + // Inputs: PackageCollection, Sources, Source // Outputs: PackagesToInstall void SearchPackagesForImport(Execution::Context& context); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 4b89844a24..ea30aa820f 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -799,4 +799,10 @@ They can be configured through the settings file 'winget settings'. No packages found in import file + + JSON file is not valid + + + Package is already installed: + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index fc733cf476..3b4ec9a6fa 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -185,6 +185,7 @@ + @@ -231,6 +232,24 @@ true + + true + + + true + + + true + + + true + + + true + + + true + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 64bc5e2ff0..61d1b9c75e 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -110,6 +110,9 @@ Source Files + + Source Files + @@ -333,5 +336,23 @@ TestData + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-Invalid.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-Invalid.json new file mode 100644 index 0000000000..6bfedac997 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-Invalid.json @@ -0,0 +1 @@ +"A valid JSON file that does not conform to the schema" \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-Malformed.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-Malformed.json new file mode 100644 index 0000000000..7e50697753 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-Malformed.json @@ -0,0 +1 @@ +This is not a valid JSON file. \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json new file mode 100644 index 0000000000..467cfd6ddf --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "MissingFile", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json new file mode 100644 index 0000000000..273bb3005e --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "4.3.2.1" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json new file mode 100644 index 0000000000..5a422810b1 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//bad", + "Identifier": "*BadSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good.json b/src/AppInstallerCLITests/TestData/ImportFile-Good.json new file mode 100644 index 0000000000..9709a27873 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Good.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "1.0.0.0" + }, + { + "Id": "AppInstallerCliTest.TestMsixInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp index 5fc1ca3513..81c6279ab5 100644 --- a/src/AppInstallerCLITests/TestSource.cpp +++ b/src/AppInstallerCLITests/TestSource.cpp @@ -12,6 +12,9 @@ namespace TestCommon TestPackageVersion::TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata) : VersionManifest(manifest), Metadata(std::move(installationMetadata)) {} + TestPackageVersion::TestPackageVersion(const Manifest& manifest, std::weak_ptr source) : + VersionManifest(manifest), Source(source) {} + TestPackageVersion::LocIndString TestPackageVersion::GetProperty(PackageVersionProperty property) const { switch (property) @@ -59,7 +62,7 @@ namespace TestCommon std::shared_ptr TestPackageVersion::GetSource() const { - return Source; + return Source.lock(); } TestPackageVersion::MetadataMap TestPackageVersion::GetMetadata() const @@ -80,20 +83,20 @@ namespace TestCommon } } - TestPackage::TestPackage(const std::vector& available) + TestPackage::TestPackage(const std::vector& available, std::weak_ptr source) { for (const auto& manifest : available) { - AvailableVersions.emplace_back(TestPackageVersion::Make(manifest)); + AvailableVersions.emplace_back(TestPackageVersion::Make(manifest, source)); } } - TestPackage::TestPackage(const Manifest& installed, MetadataMap installationMetadata, const std::vector& available) : + TestPackage::TestPackage(const Manifest& installed, MetadataMap installationMetadata, const std::vector& available, std::weak_ptr source) : InstalledVersion(TestPackageVersion::Make(installed, std::move(installationMetadata))) { for (const auto& manifest : available) { - AvailableVersions.emplace_back(TestPackageVersion::Make(manifest)); + AvailableVersions.emplace_back(TestPackageVersion::Make(manifest, source)); } } @@ -185,7 +188,7 @@ namespace TestCommon const std::string& TestSource::GetIdentifier() const { - return Identifier; + return Details.Identifier; } SearchResult TestSource::Search(const SearchRequest& request) const diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h index daee38ca28..59f05e6dbe 100644 --- a/src/AppInstallerCLITests/TestSource.h +++ b/src/AppInstallerCLITests/TestSource.h @@ -17,7 +17,8 @@ namespace TestCommon using LocIndString = AppInstaller::Utility::LocIndString; using MetadataMap = AppInstaller::Repository::IPackageVersion::Metadata; - TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata = {}); + TestPackageVersion(const Manifest& manifest, std::weak_ptr source = {}); + TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata); template static std::shared_ptr Make(Args&&... args) @@ -33,7 +34,7 @@ namespace TestCommon Manifest VersionManifest; MetadataMap Metadata; - std::shared_ptr Source; + std::weak_ptr Source; protected: static void AddFoldedIfHasValueAndNotPresent(const AppInstaller::Utility::NormalizedString& value, std::vector& target); @@ -43,14 +44,15 @@ namespace TestCommon struct TestPackage : public AppInstaller::Repository::IPackage { using Manifest = AppInstaller::Manifest::Manifest; + using ISource = AppInstaller::Repository::ISource; using LocIndString = AppInstaller::Utility::LocIndString; using MetadataMap = TestPackageVersion::MetadataMap; // Create a package with only available versions using these manifests. - TestPackage(const std::vector& available); + TestPackage(const std::vector& available, std::weak_ptr source = {}); // Create a package with an installed version, metadata, and optionally available versions. - TestPackage(const Manifest& installed, MetadataMap installationMetadata, const std::vector& available = {}); + TestPackage(const Manifest& installed, MetadataMap installationMetadata, const std::vector& available = {}, std::weak_ptr source = {}); template static std::shared_ptr Make(Args&&... args) @@ -70,15 +72,14 @@ namespace TestCommon }; // An ISource implementation for use across the test code. - struct TestSource : public AppInstaller::Repository::ISource + struct TestSource : public AppInstaller::Repository::ISource, public std::enable_shared_from_this { const AppInstaller::Repository::SourceDetails& GetDetails() const override; const std::string& GetIdentifier() const override; AppInstaller::Repository::SearchResult Search(const AppInstaller::Repository::SearchRequest& request) const override; bool IsComposite() const override; - AppInstaller::Repository::SourceDetails Details; - std::string Identifier = "*TestSource"; + AppInstaller::Repository::SourceDetails Details = { "TestSource", "Microsoft.TestSource", "//arg", "", "*TestSource" }; std::function SearchFunction; bool Composite = false; }; diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index e236c3eba8..bcfda9840c 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -65,7 +68,7 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector{ manifest }), + TestPackage::Make(std::vector{ manifest }, this->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); } else if (input == "TestQueryReturnTwo") @@ -73,13 +76,13 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector{ manifest }), + TestPackage::Make(std::vector{ manifest }, this->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector{ manifest2 }), + TestPackage::Make(std::vector{ manifest2 }, this->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); } @@ -119,7 +122,8 @@ namespace { PackageVersionMetadata::StandardUninstallCommand, "C:\\uninstall.exe" }, { PackageVersionMetadata::SilentUninstallCommand, "C:\\uninstall.exe /silence" }, }, - std::vector{ manifest2, manifest } + std::vector{ manifest2, manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -133,7 +137,8 @@ namespace TestPackage::Make( manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Msix" } }, - std::vector{ manifest2, manifest } + std::vector{ manifest2, manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller"))); } @@ -146,7 +151,8 @@ namespace TestPackage::Make( manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "MSStore" } }, - std::vector{ manifest } + std::vector{ manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMSStoreInstaller"))); } @@ -160,7 +166,8 @@ namespace TestPackage::Make( manifest2, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, - std::vector{ manifest2, manifest } + std::vector{ manifest2, manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -174,7 +181,8 @@ namespace TestPackage::Make( manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Msix" } }, - std::vector{ manifest2, manifest } + std::vector{ manifest2, manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -294,6 +302,19 @@ void OverrideForCompositeInstalledSource(TestContext& context) } }); } +void OverrideForImportSource(TestContext& context) +{ + context.Override({ "OpenPredefinedSource", [](TestContext& context) + { + context.Add({}); + } }); + + context.Override({ Workflow::OpenSourcesForImport, [](TestContext& context) + { + context.Add(std::vector>{ std::make_shared() }); + } }); +} + void OverrideForUpdateInstallerMotw(TestContext& context) { context.Override({ UpdateInstallerFileMotwIfApplicable, [](TestContext&) @@ -1068,6 +1089,165 @@ TEST_CASE("UninstallFlow_UninstallExeNotFound", "[UninstallFlow][workflow]") REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); } +TEST_CASE("ExportFlow_ExportAll", "[ExportFlow][workflow]") +{ + TestCommon::TempFile exportResultPath("TestExport.json"); + + std::ostringstream exportOutput; + TestContext context{ exportOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::OutputFile, exportResultPath); + + ExportCommand exportCommand({}); + exportCommand.Execute(context); + INFO(exportOutput.str()); + + // Verify contents of exported collection + const auto& exportedCollection = context.Get(); + REQUIRE(exportedCollection.Sources.size() == 1); + REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); + + const auto& exportedPackages = exportedCollection.Sources[0].Packages; + REQUIRE(exportedPackages.size() == 3); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestExeInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; + })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestMsixInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; + })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestMSStoreInstaller" && p.VersionAndChannel.GetVersion().ToString() == "Latest"; + })); +} + +TEST_CASE("ImportFlow_Successful", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + TestCommon::TempFile msixInstallResultPath("TestMsixInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForMSIX(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Verify all packages were installed + REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(std::filesystem::exists(msixInstallResultPath.GetPath())); +} + +TEST_CASE("ImportFlow_PackageAlreadyInstalled", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good.json").GetPath().string()); + context.Args.AddArg(Execution::Args::Type::ExactVersions); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Exe should not have been installed again + REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportPackageAlreadyInstalled).get()) != std::string::npos); +} + +TEST_CASE("ImportFlow_MissingSource", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownSource.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Installer should not be called + REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSourceNotInstalled).get()) != std::string::npos); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); +} + +TEST_CASE("ImportFlow_MissingPackage", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownPackage.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Installer should not be called + REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); +} + +TEST_CASE("ImportFlow_MissingVersion", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownPackageVersion.json").GetPath().string()); + context.Args.AddArg(Execution::Args::Type::ExactVersions); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Installer should not be called + REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); +} + +TEST_CASE("ImportFlow_MalformedJsonFile", "[ImportFlow][workflow]") +{ + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-Malformed.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Installer should not be called + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); +} + +TEST_CASE("ImportFlow_InvalidJsonFile", "[ImportFlow][workflow]") +{ + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-Invalid.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Installer should not be called + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); +} + void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) { std::filesystem::path motwFile(testFile); diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp index cc169c9154..27d914b3e8 100644 --- a/src/AppInstallerCommonCore/Errors.cpp +++ b/src/AppInstallerCommonCore/Errors.cpp @@ -113,6 +113,8 @@ namespace AppInstaller return "Running uninstall command failed"; case APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED: return "Failed to install one or more imported packages"; + case APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND: + return "Could not find one or more requested packages"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h index 75b11a0fdf..78cbd6a063 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -62,6 +62,8 @@ #define APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND ((HRESULT)0x8a15002F) #define APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED ((HRESULT)0x8a150030) #define APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED ((HRESULT)0x8a150031) +#define APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND ((HRESULT)0x8a150032) +#define APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE ((HRESULT)0x8a150033) namespace AppInstaller { From 8122ea8d59b7e9ad1507eed9cd0115155fc242d5 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 28 Jan 2021 19:21:08 -0800 Subject: [PATCH 11/34] Add unit tests for JSON read/write --- doc/packages.schema.json | 29 +- .../AppInstallerCLICore.vcxproj | 17 +- .../AppInstallerCLICore.vcxproj.filters | 7 +- src/AppInstallerCLICore/PackageCollection.cpp | 72 +++- src/AppInstallerCLICore/PackageCollection.h | 6 +- .../Workflows/ImportExportFlow.cpp | 13 +- .../AppInstallerCLITests.vcxproj | 8 +- .../PackageCollection.cpp | 360 ++++++++++++++++++ src/AppInstallerCLITests/WorkFlow.cpp | 4 +- src/AppInstallerCommonCore/DateTime.cpp | 10 +- .../Public/AppInstallerDateTime.h | 2 +- 11 files changed, 475 insertions(+), 53 deletions(-) create mode 100644 src/AppInstallerCLITests/PackageCollection.cpp diff --git a/doc/packages.schema.json b/doc/packages.schema.json index 627541f0d5..9efff5ea4e 100644 --- a/doc/packages.schema.json +++ b/doc/packages.schema.json @@ -5,6 +5,8 @@ "title": "winget Packages List Schema", "description": "Describes a list of packages for batch installs", + "type": "object", + "required": [ "WinGetVersion", "Sources" ], "additionalProperties": true, "properties": { @@ -27,39 +29,35 @@ "items": { "description": "A source and the list of packages to install from it", "type": "object", - "required": true, + "required": [ "SourceDetails", "Packages" ], "additionalProperties": true, "properties": { "SourceDetails": { "description": "Details about this source", "type": "object", - "required": true, + "required": [ "Name", "Identifier", "Argument", "Type" ], "additionalProperties": true, "properties": { "Name": { "description": "Name of the source", - "type": "string", - "required": true + "type": "string" }, "Identifier": { "description": "Identifier for the source", - "type": "string", - "required": true + "type": "string" }, "Argument": { "description": "Argument used to install the source", - "type": "string", - "required": true + "type": "string" }, "Type": { "description": "Type of the source", - "type": "string", - "required": true + "type": "string" } } }, @@ -67,7 +65,7 @@ "Packages": { "description": "Packages installed from this source", "type": "array", - "required": true, + "required": [ "Id" ], "minItems": 1, "items": { @@ -77,20 +75,17 @@ "properties": { "Id": { "description": "Package ID", - "type": "string", - "required": true + "type": "string" }, "Version": { "description": "Package version", - "type": "string", - "required": true + "type": "string" }, "Channel": { "description": "Package channel", - "type": "string", - "required": false + "type": "string" } } } diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index ec96bde19d..3671a4d030 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -122,9 +122,9 @@ Disabled _DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) true true true @@ -139,7 +139,7 @@ WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) true @@ -152,10 +152,10 @@ true true NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) true true true @@ -199,6 +199,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 40578c390e..9a17abfdbe 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -9,10 +9,6 @@ {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hh;hpp;hxx;hm;inl;inc;xsd - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - {4b0dcf8b-b4a1-47e5-9c28-e8a3440178e6} @@ -153,6 +149,9 @@ Commands + + Header Files + diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index ad4b06f700..8b1710ade3 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -5,6 +5,14 @@ #include "PackageCollection.h" #include "AppInstallerRuntime.h" +#pragma warning (push, 0) +#include "valijson/adapters/jsoncpp_adapter.hpp" +#include "valijson/utils/jsoncpp_utils.hpp" +#include "valijson/schema.hpp" +#include "valijson/schema_parser.hpp" +#include "valijson/validator.hpp" +#pragma warning (pop) + #include #include @@ -49,7 +57,7 @@ namespace AppInstaller::CLI PackageCollection::Package ParsePackageNode(const Json::Value& packageNode) { std::string id = packageNode[s_PackagesJson_Package_Id].asString(); - std::string version = packageNode[s_PackagesJson_Package_Version].asString(); + std::string version = packageNode.isMember(s_PackagesJson_Package_Version) ? packageNode[s_PackagesJson_Package_Version].asString() : ""; std::string channel = packageNode.isMember(s_PackagesJson_Package_Channel) ? packageNode[s_PackagesJson_Package_Channel].asString() : ""; PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; @@ -90,11 +98,12 @@ namespace AppInstaller::CLI }; return package.GetAvailableVersion(installedVersionKey); } + // Creates a minimal root object of a Packages JSON file. - Json::Value CreateRoot() + Json::Value CreateRoot(const std::string& wingetVersion) { Json::Value root{ Json::ValueType::objectValue }; - root[s_PackagesJson_WinGetVersion] = Runtime::GetClientVersion().get(); + root[s_PackagesJson_WinGetVersion] = wingetVersion; root[s_PackagesJson_Schema] = s_PackagesJson_SchemaUri_V0; // TODO: This uses localtime. Do we want to use UTC or add time zone? @@ -145,13 +154,47 @@ namespace AppInstaller::CLI return sourcesNode.append(std::move(sourceNode)); } + + bool IsValidJson(const Json::Value& document, const std::filesystem::path& schemaPath) + { + Json::Value schemaDocument; + if (!valijson::utils::loadDocument(schemaPath.string(), schemaDocument)) + { + // TODO: fail + } + + valijson::Schema schema; + valijson::SchemaParser parser; + valijson::adapters::JsonCppAdapter schemaAdapter{ schemaDocument }; + parser.populateSchema(schemaAdapter, schema); + + valijson::Validator validator; + valijson::ValidationResults results; + if (validator.validate(schema, valijson::adapters::JsonCppAdapter{ document }, &results)) { + return true; + } + + for (const auto& result : results) + { + std::stringstream ss; + ss << result.description << ' '; + for (const auto& context : result.context) + { + ss << '\\' << context; + } + + AICLI_LOG(CLI, Error, << "JSON file is invalid: " << ss.str()); + } + + return false; + } } namespace PackagesJson { Json::Value CreateJson(const PackageCollection& packages) { - Json::Value root = CreateRoot(); + Json::Value root = CreateRoot(packages.ClientVersion); for (const auto& source : packages.Sources) { AddSourceNode(root, source); @@ -160,15 +203,28 @@ namespace AppInstaller::CLI return root; } - PackageCollection ParseJson(const Json::Value& root) + std::optional ParseJson(const Json::Value& root) { - // TODO: Validate schema. The following assumes the file is already valid. + // TODO: Embed schema in binaries + if (!IsValidJson(root, "C:\\src\\winget-cli\\doc\\packages.schema.json")) + { + return {}; + } + PackageCollection packages; packages.ClientVersion = root[s_PackagesJson_WinGetVersion].asString(); for (const auto& sourceNode : root[s_PackagesJson_Sources]) { - // TODO: Prevent duplicates? - packages.Sources.push_back(ParseSourceNode(sourceNode)); + auto newSource = ParseSourceNode(sourceNode); + auto existingSource = std::find_if(packages.Sources.begin(), packages.Sources.end(), [&](const PackageCollection::Source& s) { return s.Details.Identifier == newSource.Details.Identifier; }); + if (existingSource == packages.Sources.end()) + { + packages.Sources.push_back(std::move(newSource)); + } + else + { + existingSource->Packages.insert(existingSource->Packages.end(), newSource.Packages.begin(), newSource.Packages.end()); + } } return packages; diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h index c3fd9b841c..32a3d7e198 100644 --- a/src/AppInstallerCLICore/PackageCollection.h +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -3,8 +3,10 @@ #pragma once #include "AppInstallerDateTime.h" +#include "AppInstallerLanguageUtilities.h" +#include "AppInstallerRepositorySource.h" -#include +#include #include @@ -52,6 +54,6 @@ namespace AppInstaller::CLI // Converts a collection of packages to its JSON representation for exporting. Json::Value CreateJson(const PackageCollection& packages); - PackageCollection ParseJson(const Json::Value& root); + std::optional ParseJson(const Json::Value& root); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 7287b32cc5..ae8269c08d 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -15,7 +15,8 @@ namespace AppInstaller::CLI::Workflow void SelectVersionsToExport(Execution::Context& context) { const auto& searchResult = context.Get(); - PackageCollection exportedPackages = {}; + PackageCollection exportedPackages; + exportedPackages.ClientVersion = Runtime::GetClientVersion().get(); auto& exportedSources = exportedPackages.Sources; for (const auto& packageMatch : searchResult.Matches) { @@ -95,14 +96,20 @@ namespace AppInstaller::CLI::Workflow } auto packages = PackagesJson::ParseJson(jsonRoot); - if (packages.Sources.empty()) + if (!packages.has_value()) + { + context.Reporter.Error() << Resource::String::InvalidJsonFile << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); + } + + if (packages.value().Sources.empty()) { AICLI_LOG(CLI, Warning, << "No packages to install"); context.Reporter.Info() << Resource::String::NoPackagesFoundInImportFile << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); } - context.Add(packages); + context.Add(packages.value()); } void OpenSourcesForImport(Execution::Context& context) diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 3b4ec9a6fa..d437901ed2 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -108,7 +108,7 @@ Disabled _DEBUG;%(PreprocessorDefinitions) - $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true @@ -126,7 +126,7 @@ WIN32;%(PreprocessorDefinitions) - $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true @@ -145,8 +145,8 @@ true true NDEBUG;%(PreprocessorDefinitions) - $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true true diff --git a/src/AppInstallerCLITests/PackageCollection.cpp b/src/AppInstallerCLITests/PackageCollection.cpp new file mode 100644 index 0000000000..994eacfe03 --- /dev/null +++ b/src/AppInstallerCLITests/PackageCollection.cpp @@ -0,0 +1,360 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" + +#include + +// Duplicating here because a change to these values in the product *REALLY* needs to be thought through. +using namespace std::string_literals; +using namespace std::string_view_literals; + +using namespace AppInstaller::CLI; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Utility; + +const std::string s_PackagesJson_Schema = "$schema"; +const std::string s_PackagesJson_SchemaUri_V0 = "https://aka.ms/winget-packages-v0.schema.json"; +const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; +const std::string s_PackagesJson_CreationDate = "CreationDate"; + +const std::string s_PackagesJson_Sources = "Sources"; +const std::string s_PackagesJson_Source_Details = "SourceDetails"; +const std::string s_PackagesJson_Source_Name = "Name"; +const std::string s_PackagesJson_Source_Identifier = "Identifier"; +const std::string s_PackagesJson_Source_Argument = "Argument"; +const std::string s_PackagesJson_Source_Type = "Type"; + +const std::string s_PackagesJson_Packages = "Packages"; +const std::string s_PackagesJson_Package_Id = "Id"; +const std::string s_PackagesJson_Package_Version = "Version"; +const std::string s_PackagesJson_Package_Channel = "Channel"; + +namespace +{ + + Json::Value ParseJsonString(const std::string& jsonString) + { + Json::Value root; + std::stringstream{ jsonString } >> root; + return root; + } + + void ValidateJsonStringProperty(const Json::Value& node, const std::string& propertyName, std::string_view expectedValue, bool allowMissing = false) + { + if (allowMissing && expectedValue.empty() && !node.isMember(propertyName)) + { + return; + } + + REQUIRE(node.isMember(propertyName)); + REQUIRE(node[propertyName].isString()); + REQUIRE(node[propertyName].asString() == expectedValue); + } + + const Json::Value& GetAndValidateJsonProperty(const Json::Value& node, const std::string& propertyName, Json::ValueType valueType) + { + REQUIRE(node.isMember(propertyName)); + REQUIRE(node[propertyName].type() == valueType); + return node[propertyName]; + } + + void ValidateJsonWithCollection(const Json::Value& root, const PackageCollection& collection) + { + ValidateJsonStringProperty(root, s_PackagesJson_Schema, s_PackagesJson_SchemaUri_V0); + ValidateJsonStringProperty(root, s_PackagesJson_WinGetVersion, collection.ClientVersion); + REQUIRE(root.isMember(s_PackagesJson_CreationDate)); + + const auto& jsonSources = GetAndValidateJsonProperty(root, s_PackagesJson_Sources, Json::ValueType::arrayValue); + REQUIRE(jsonSources.size() == collection.Sources.size()); + + // Expect the order to be the same. Not really needed, but it makes things easier. + auto jsonSourceItr = jsonSources.begin(); + auto sourceItr = collection.Sources.begin(); + for (; jsonSourceItr != jsonSources.end(); ++jsonSourceItr, ++sourceItr) + { + REQUIRE(jsonSourceItr->isObject()); + const auto& jsonSourceDetails = GetAndValidateJsonProperty(*jsonSourceItr, s_PackagesJson_Source_Details, Json::ValueType::objectValue); + ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Name, sourceItr->Details.Name); + ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Argument, sourceItr->Details.Arg); + ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Type, sourceItr->Details.Type); + ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Identifier, sourceItr->Details.Identifier); + + const auto& jsonPackages = GetAndValidateJsonProperty(*jsonSourceItr, s_PackagesJson_Packages, Json::ValueType::arrayValue); + REQUIRE(jsonPackages.size() == sourceItr->Packages.size()); + + auto jsonPackageItr = jsonPackages.begin(); + auto packageItr = sourceItr->Packages.begin(); + for (; jsonPackageItr != jsonPackages.end(); ++jsonPackageItr, ++packageItr) + { + REQUIRE(jsonPackageItr->isObject()); + ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Id, packageItr->Id); + ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Version, packageItr->VersionAndChannel.GetVersion().ToString(), true); + ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Channel, packageItr->VersionAndChannel.GetChannel().ToString(), true); + } + } + } + + void ValidateEqualCollections(const PackageCollection& first, const PackageCollection& second) + { + REQUIRE(first.ClientVersion == second.ClientVersion); + REQUIRE(first.Sources.size() == second.Sources.size()); + + auto firstSourceItr = first.Sources.begin(); + auto secondSourceItr = second.Sources.begin(); + for (; firstSourceItr != first.Sources.end(); ++firstSourceItr, ++secondSourceItr) + { + REQUIRE(firstSourceItr->Details.Name == secondSourceItr->Details.Name); + REQUIRE(firstSourceItr->Details.Arg == secondSourceItr->Details.Arg); + REQUIRE(firstSourceItr->Details.Type == secondSourceItr->Details.Type); + REQUIRE(firstSourceItr->Details.Identifier == secondSourceItr->Details.Identifier); + + REQUIRE(firstSourceItr->Packages.size() == secondSourceItr->Packages.size()); + auto firstPackageItr = firstSourceItr->Packages.begin(); + auto secondPackageItr = secondSourceItr->Packages.begin(); + for (; firstPackageItr != firstSourceItr->Packages.end(); ++firstPackageItr, ++secondPackageItr) + { + REQUIRE(firstPackageItr->Id == secondPackageItr->Id); + REQUIRE(firstPackageItr->VersionAndChannel.ToString() == secondPackageItr->VersionAndChannel.ToString()); + } + } + } +} + +TEST_CASE("PackageCollection_Write_SingleSource", "[PackageCollection]") +{ + PackageCollection::Source source; + source.Details.Name = "TestSource"; + source.Details.Arg = "https://aka.ms/winget"; + source.Details.Type = "Microsoft.PreIndexed.Package"; + source.Details.Identifier = "TestSourceId"; + + source.Packages.emplace_back(LocIndString{ "test.package1"sv }, Version{ "1.0.1" }, Channel{ "" }); + source.Packages.emplace_back(LocIndString{ "test.package2"sv }, Version{ "2" }, Channel{ "Public" }); + + PackageCollection pc + { + "0.1.2.3", + std::vector{ source } + }; + + ValidateJsonWithCollection(PackagesJson::CreateJson(pc), pc); +} + +TEST_CASE("PackageCollection_Write_MultipleSources", "[PackageCollection]") +{ + PackageCollection::Source source1; + source1.Details.Name = "TestSource"; + source1.Details.Arg = "https://aka.ms/winget"; + source1.Details.Type = "Microsoft.PreIndexed.Package"; + source1.Details.Identifier = "TestSourceId"; + source1.Packages.emplace_back(LocIndString{ "test.package1"sv }, Version{ "1.0.1" }, Channel{ "" }); + + PackageCollection::Source source2; + source2.Details.Name = "TestSource2"; + source2.Details.Arg = "https://aka.ms/winget"; + source2.Details.Type = "*Test"; + source2.Details.Identifier = "SecondId"; + source2.Packages.emplace_back(LocIndString{ "test.package2"sv }, Version{ "2.1.0" }, Channel{ "Beta" }); + + PackageCollection pc + { + "1.0.0.0", + std::vector{ source1, source2 } + }; + + ValidateJsonWithCollection(PackagesJson::CreateJson(pc), pc); +} + +TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "test.withversion", + "Version": "0.1", + "Channel": "Preview" + }, + { + "Id": "test.noversion" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parsed = PackagesJson::ParseJson(json); + REQUIRE(parsed.has_value()); + + PackageCollection::Source source; + source.Details.Name = "TestSource"; + source.Details.Arg = "https://aka.ms/winget"; + source.Details.Type = "Microsoft.PreIndexed.Package"; + source.Details.Identifier = "TestSourceId"; + + source.Packages.emplace_back(LocIndString{ "test.withversion"sv }, Version{ "0.1" }, Channel{ "Preview" }); + source.Packages.emplace_back(LocIndString{ "test.noversion"sv }, Version{ "" }, Channel{ "" }); + + PackageCollection expected + { + "1.0.0", + std::vector{ source } + }; + + ValidateEqualCollections(parsed.value(), expected); +} + +TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "WinGetVersion": "1.0.0", + "Sources": [ + { + "SourceDetails": { + "Argument": "//firstSource", + "Identifier": "Id1", + "Name": "First", + "Type": "Microsoft.PreIndexed.Package" + }, + "Packages": [ + { + "Id": "test" + } + ] + }, + { + "SourceDetails": { + "Argument": "//secondSource", + "Identifier": "Id2", + "Name": "Second", + "Type": "*TestSource" + }, + "Packages": [ + { + "Id": "test2", + "Version": "1.0" + } + ] + } + ] + })"); + + auto parsed = PackagesJson::ParseJson(json); + REQUIRE(parsed.has_value()); + + PackageCollection::Source source1; + source1.Details.Name = "First"; + source1.Details.Arg = "//firstSource"; + source1.Details.Type = "Microsoft.PreIndexed.Package"; + source1.Details.Identifier = "Id1"; + source1.Packages.emplace_back(LocIndString{ "test"sv }, Version{ "" }, Channel{ "" }); + + PackageCollection::Source source2; + source2.Details.Name = "Second"; + source2.Details.Arg = "//secondSource"; + source2.Details.Type = "*TestSource"; + source2.Details.Identifier = "Id2"; + source2.Packages.emplace_back(LocIndString{ "test2"sv }, Version{ "1.0" }, Channel{ "" }); + + PackageCollection expected + { + "1.0.0", + std::vector{ source1, source2 } + }; + + ValidateEqualCollections(parsed.value(), expected); +} + +TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "WinGetVersion": "1.0.0", + "Sources": [ + { + "SourceDetails": { + "Argument": "//firstSource", + "Identifier": "Id1", + "Name": "First", + "Type": "Microsoft.PreIndexed.Package" + }, + "Packages": [ + { + "Id": "test" + } + ] + }, + { + "SourceDetails": { + "Argument": "//secondSource", + "Identifier": "Id2", + "Name": "Second", + "Type": "*TestSource" + }, + "Packages": [ + { + "Id": "test2", + "Version": "1.0" + } + ] + }, + { + "SourceDetails": { + "Argument": "//secondSource", + "Identifier": "Id2", + "Name": "Second", + "Type": "*TestSource" + }, + "Packages": [ + { + "Id": "test3", + "Version": "1.2" + } + ] + } + ] + })"); + + auto parsed = PackagesJson::ParseJson(json); + REQUIRE(parsed.has_value()); + + PackageCollection::Source source1; + source1.Details.Name = "First"; + source1.Details.Arg = "//firstSource"; + source1.Details.Type = "Microsoft.PreIndexed.Package"; + source1.Details.Identifier = "Id1"; + source1.Packages.emplace_back(LocIndString{ "test"sv }, Version{ "" }, Channel{ "" }); + + PackageCollection::Source source2; + source2.Details.Name = "Second"; + source2.Details.Arg = "//secondSource"; + source2.Details.Type = "*TestSource"; + source2.Details.Identifier = "Id2"; + source2.Packages.emplace_back(LocIndString{ "test2"sv }, Version{ "1.0" }, Channel{ "" }); + source2.Packages.emplace_back(LocIndString{ "test3"sv }, Version{ "1.2" }, Channel{ "" }); + + PackageCollection expected + { + "1.0.0", + std::vector{ source1, source2 } + }; + + ValidateEqualCollections(parsed.value(), expected); +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index bcfda9840c..c64be83df7 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -1230,7 +1230,7 @@ TEST_CASE("ImportFlow_MalformedJsonFile", "[ImportFlow][workflow]") importCommand.Execute(context); INFO(importOutput.str()); - // Installer should not be called + // Command should have failed REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); } @@ -1244,7 +1244,7 @@ TEST_CASE("ImportFlow_InvalidJsonFile", "[ImportFlow][workflow]") importCommand.Execute(context); INFO(importOutput.str()); - // Installer should not be called + // Command should have failed REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); } diff --git a/src/AppInstallerCommonCore/DateTime.cpp b/src/AppInstallerCommonCore/DateTime.cpp index 238ffc462e..06ca51455b 100644 --- a/src/AppInstallerCommonCore/DateTime.cpp +++ b/src/AppInstallerCommonCore/DateTime.cpp @@ -6,7 +6,7 @@ namespace AppInstaller::Utility { // If moved to C++20, this can be replaced with standard library implementations. - void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time) + void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time, bool useRFC3339) { using namespace std::chrono; @@ -14,12 +14,14 @@ namespace AppInstaller::Utility auto tt = system_clock::to_time_t(time); _localtime64_s(&localTime, &tt); - // Don't bother with fill chars for dates, as most of the time this won't be an issue. - stream << (1900 + localTime.tm_year) << '-' << (1 + localTime.tm_mon) << '-' << localTime.tm_mday << ' ' + stream + << std::setw(4) << (1900 + localTime.tm_year) << '-' + << std::setw(2) << (1 + localTime.tm_mon) << '-' + << std::setw(2) << localTime.tm_mday << (useRFC3339 ? 'T' : ' ') << std::setw(2) << std::setfill('0') << localTime.tm_hour << ':' << std::setw(2) << std::setfill('0') << localTime.tm_min << ':' << std::setw(2) << std::setfill('0') << localTime.tm_sec << '.'; - + // Get partial seconds auto sinceEpoch = time.time_since_epoch(); auto leftoverMillis = duration_cast(sinceEpoch) - duration_cast(sinceEpoch); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h b/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h index b4999801ce..f84be6d4dd 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h @@ -10,7 +10,7 @@ namespace AppInstaller::Utility // Writes the given time to the given stream. // Assumes that system_clock uses Linux epoch (as required by C++20 standard). // Time is also assumed to be after the epoch. - void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time); + void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time, bool useRFC3339 = false); // Gets the current time as a string. Can be used as a file name. std::string GetCurrentTimeForFilename(); From 8b07d23f34e1323ff6e6c08ceac1f01434767138 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Mon, 1 Feb 2021 09:21:33 -0800 Subject: [PATCH 12/34] Spell checking --- .github/actions/spelling/expect.txt | 2 ++ src/AppInstallerCLITests/PackageCollection.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index a63346a905..abbd1df131 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -310,6 +310,8 @@ usf USHORT Utils UWP +validator +valijson valueiterator vamus vns diff --git a/src/AppInstallerCLITests/PackageCollection.cpp b/src/AppInstallerCLITests/PackageCollection.cpp index 994eacfe03..8963968efe 100644 --- a/src/AppInstallerCLITests/PackageCollection.cpp +++ b/src/AppInstallerCLITests/PackageCollection.cpp @@ -176,12 +176,12 @@ TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") { "Packages": [ { - "Id": "test.withversion", + "Id": "test.WithVersion", "Version": "0.1", "Channel": "Preview" }, { - "Id": "test.noversion" + "Id": "test.NoVersion" } ], "SourceDetails": { @@ -204,8 +204,8 @@ TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") source.Details.Type = "Microsoft.PreIndexed.Package"; source.Details.Identifier = "TestSourceId"; - source.Packages.emplace_back(LocIndString{ "test.withversion"sv }, Version{ "0.1" }, Channel{ "Preview" }); - source.Packages.emplace_back(LocIndString{ "test.noversion"sv }, Version{ "" }, Channel{ "" }); + source.Packages.emplace_back(LocIndString{ "test.WithVersion"sv }, Version{ "0.1" }, Channel{ "Preview" }); + source.Packages.emplace_back(LocIndString{ "test.NoVersion"sv }, Version{ "" }, Channel{ "" }); PackageCollection expected { From c82d9cb6a5328b787f64ed87bd1d74d53ad1cca0 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Mon, 1 Feb 2021 18:27:56 -0800 Subject: [PATCH 13/34] Disable schema validation --- .../AppInstallerCLICore.vcxproj | 17 ++++--- src/AppInstallerCLICore/PackageCollection.cpp | 48 +------------------ src/AppInstallerCLICore/PackageCollection.h | 2 +- .../AppInstallerCLITests.vcxproj | 8 ++-- src/AppInstallerCLITests/WorkFlow.cpp | 3 ++ 5 files changed, 17 insertions(+), 61 deletions(-) diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 3671a4d030..ec96bde19d 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -122,9 +122,9 @@ Disabled _DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) true true true @@ -139,7 +139,7 @@ WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) true @@ -152,10 +152,10 @@ true true NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib;$(ProjectDir)..\Valijson\valijson\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) true true true @@ -199,7 +199,6 @@ - diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index 8b1710ade3..452bc9a100 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -5,14 +5,6 @@ #include "PackageCollection.h" #include "AppInstallerRuntime.h" -#pragma warning (push, 0) -#include "valijson/adapters/jsoncpp_adapter.hpp" -#include "valijson/utils/jsoncpp_utils.hpp" -#include "valijson/schema.hpp" -#include "valijson/schema_parser.hpp" -#include "valijson/validator.hpp" -#pragma warning (pop) - #include #include @@ -154,40 +146,6 @@ namespace AppInstaller::CLI return sourcesNode.append(std::move(sourceNode)); } - - bool IsValidJson(const Json::Value& document, const std::filesystem::path& schemaPath) - { - Json::Value schemaDocument; - if (!valijson::utils::loadDocument(schemaPath.string(), schemaDocument)) - { - // TODO: fail - } - - valijson::Schema schema; - valijson::SchemaParser parser; - valijson::adapters::JsonCppAdapter schemaAdapter{ schemaDocument }; - parser.populateSchema(schemaAdapter, schema); - - valijson::Validator validator; - valijson::ValidationResults results; - if (validator.validate(schema, valijson::adapters::JsonCppAdapter{ document }, &results)) { - return true; - } - - for (const auto& result : results) - { - std::stringstream ss; - ss << result.description << ' '; - for (const auto& context : result.context) - { - ss << '\\' << context; - } - - AICLI_LOG(CLI, Error, << "JSON file is invalid: " << ss.str()); - } - - return false; - } } namespace PackagesJson @@ -205,11 +163,7 @@ namespace AppInstaller::CLI std::optional ParseJson(const Json::Value& root) { - // TODO: Embed schema in binaries - if (!IsValidJson(root, "C:\\src\\winget-cli\\doc\\packages.schema.json")) - { - return {}; - } + // TODO: Embed schema in binaries & validate file PackageCollection packages; packages.ClientVersion = root[s_PackagesJson_WinGetVersion].asString(); diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h index 32a3d7e198..79aa628424 100644 --- a/src/AppInstallerCLICore/PackageCollection.h +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -6,7 +6,7 @@ #include "AppInstallerLanguageUtilities.h" #include "AppInstallerRepositorySource.h" -#include +#include #include diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index d437901ed2..3b4ec9a6fa 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -108,7 +108,7 @@ Disabled _DEBUG;%(PreprocessorDefinitions) - $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) true @@ -126,7 +126,7 @@ WIN32;%(PreprocessorDefinitions) - $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) true @@ -145,8 +145,8 @@ true true NDEBUG;%(PreprocessorDefinitions) - $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) - $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(MSBuildThisFileDirectory)..\AppInstallerCommonCore;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerRepositoryCore;$(MSBuildThisFileDirectory)..\AppInstallerCommonCore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore\Public;$(MSBuildThisFileDirectory)..\AppInstallerCLICore;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) true true diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index c64be83df7..2e2f97d8ee 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -1241,11 +1241,14 @@ TEST_CASE("ImportFlow_InvalidJsonFile", "[ImportFlow][workflow]") context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-Invalid.json").GetPath().string()); ImportCommand importCommand({}); + // TODO: Enable when we have schema validation + /* importCommand.Execute(context); INFO(importOutput.str()); // Command should have failed REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); + */ } void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) From 3c45df849b029877dc3a2b13846cf03fbee6083d Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Tue, 2 Feb 2021 13:03:10 -0800 Subject: [PATCH 14/34] Add E2E tests --- .../Workflows/ImportExportFlow.cpp | 13 +- .../AppInstallerCLIE2ETests.csproj | 8 ++ src/AppInstallerCLIE2ETests/Constants.cs | 10 ++ src/AppInstallerCLIE2ETests/ImportCommand.cs | 130 ++++++++++++++++++ src/AppInstallerCLIE2ETests/TestCommon.cs | 16 ++- .../ImportFiles/ImportFile-Bad-Invalid.json | 1 + .../ImportFile-Bad-UnknownPackage.json | 21 +++ .../ImportFile-Bad-UnknownPackageVersion.json | 21 +++ .../ImportFile-Bad-UnknownSource.json | 21 +++ .../TestData/ImportFiles/ImportFile-Good.json | 21 +++ .../CompositeSource.cpp | 11 +- .../CompositeSource.h | 3 +- .../Public/AppInstallerRepositorySource.h | 3 +- .../RepositorySource.cpp | 4 +- 14 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 src/AppInstallerCLIE2ETests/ImportCommand.cs create mode 100644 src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-Invalid.json create mode 100644 src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json create mode 100644 src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json create mode 100644 src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json create mode 100644 src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index ae8269c08d..602d58a83d 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -123,6 +123,7 @@ namespace AppInstaller::CLI::Workflow for (auto& requiredSource : context.Get().Sources) { // Find the installed source matching the one described in the collection. + AICLI_LOG(CLI, Info, << "Looking for source [" << requiredSource.Details.Identifier << "]"); auto matchingSource = std::find_if( availableSources.begin(), availableSources.end(), @@ -172,10 +173,12 @@ namespace AppInstaller::CLI::Workflow // Search for all the packages in the source. // Each search is done in a sub context to search everything regardless of previous failures. - auto source = Repository::CreateCompositeSource(context.Get(), *sourceItr); + auto source = Repository::CreateCompositeSource(context.Get(), *sourceItr, false); + AICLI_LOG(CLI, Info, << "Searching for packages requested from source [" << requiredSource.Details.Identifier << "]"); for (const auto& packageRequest : requiredSource.Packages) { Logging::SubExecutionTelemetryScope subExecution; + AICLI_LOG(CLI, Info, << "Searching for package [" << packageRequest.Id << "]"); // Search for the current package SearchRequest searchRequest; @@ -196,8 +199,12 @@ namespace AppInstaller::CLI::Workflow searchContext << Workflow::EnsureOneMatchFromSearchResult(false) << Workflow::GetManifestWithVersionFromPackage(versionAndChannel) << - Workflow::GetInstalledPackageVersion << - Workflow::EnsureUpdateVersionApplicable; + Workflow::GetInstalledPackageVersion; + + if (searchContext.Contains(Execution::Data::InstalledPackageVersion) && searchContext.Get()) + { + searchContext << Workflow::EnsureUpdateVersionApplicable; + } if (searchContext.IsTerminated()) { diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index 40833adb02..c01d6d4bbc 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -22,6 +22,14 @@ + + + + + + + + diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index abce1b5929..cb64f041fe 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -104,6 +104,16 @@ public class ErrorCode public const int ERROR_LIBYAML_ERROR = unchecked((int)0x8A150027); public const int ERROR_MANIFEST_VALIDATION_WARNING = unchecked((int)0x8A150028); public const int ERROR_MANIFEST_VALIDATION_FAILURE = unchecked((int)0x8A150029); + public const int APPINSTALLER_CLI_ERROR_INVALID_MANIFEST = unchecked((int)0x8A15002A); + public const int APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE = unchecked((int)0x8A15002B); + public const int APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE = unchecked((int)0x8A15002C); + public const int APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED = unchecked((int)0x8A15002D); + public const int APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH = unchecked((int)0x8A15002E); + public const int APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND = unchecked((int)0x8a15002F); + public const int APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED = unchecked((int)0x8a150030); + public const int APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED = unchecked((int)0x8a150031); + public const int APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND = unchecked((int)0x8a150032); + public const int APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE = unchecked((int)0x8a150033); } } } diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs new file mode 100644 index 0000000000..48b4a363b2 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using NUnit.Framework; + + public class ImportCommand : BaseCommand + { + [SetUp] + public void Setup() + { + InitializeAllFeatures(false); + ConfigureFeature("importExport", true); + } + + [TearDown] + public void TearDown() + { + InitializeAllFeatures(false); + } + + [Test] + public void ImportSuccessful() + { + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Good.json")); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(VerifyTestExeInstalled()); + UninstallTestExe(); + } + + // Ignore while we don't have schema validation + // [Test] + public void ImportInvalidFile() + { + // Verify failure when trying to import with an invalid file + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-Invalid.json")); + Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, result.ExitCode); + Assert.True(result.StdOut.Contains("JSON file is not valid")); + } + + [Test] + public void ImportUnknownSource() + { + // Verify failure when trying to import from an unknown source + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-UnknownSource.json")); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); + Assert.True(result.StdOut.Contains("Source required for import is not installed")); + } + + [Test] + public void ImportUnavailablePackage() + { + // Verify failure when trying to import an unavailable package + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-UnknownPackage.json")); + Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("Package not found for import")); + } + + [Test] + public void ImportUnavailableVersion() + { + // Verify failure when trying to import an unavailable package + var result = TestCommon.RunAICLICommand("import", $"{GetTestImportFile("ImportFile-Bad-UnknownPackageVersion.json")} --exact-versions"); + Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("Package not found for import")); + } + + [Test] + public void ImportAlreadyInstalled() + { + // Verify success with message when trying to import a package that is already installed + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller -l {installDir}"); + var result = TestCommon.RunAICLICommand("import", $"{GetTestImportFile("ImportFile-Good.json")} --exact-versions"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Package is already installed")); + Assert.False(VerifyTestExeInstalled()); + UninstallTestExe(installDir); + } + + [Test] + public void ImportExportedFile() + { + // Verify success when importing an exported list of packages. + // First install the test package to ensure it is exported. + TestCommon.RunAICLICommand("install", "AppInstallerTest.TestExeInstaller"); + + var jsonFile = TestCommon.GetRandomTestFile(".json"); + TestCommon.RunAICLICommand("export", $"{jsonFile} -s TestSource", timeOut: 180 * 1000); + + // Uninstall the package to ensure we can install it again + UninstallTestExe(); + + // Import the file + var result = TestCommon.RunAICLICommand("import", jsonFile); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(VerifyTestExeInstalled()); + UninstallTestExe(); + } + + private string GetTestImportFile(string importFileName) + { + return TestCommon.GetTestDataFile(Path.Combine("ImportFiles", importFileName)); + } + + private bool VerifyTestExeInstalled(string installDir = null) + { + if (string.IsNullOrEmpty(installDir)) + { + // Default location used by installer + installDir = Path.GetTempPath(); + } + + return File.Exists(Path.Combine(installDir, "TestExeInstalled.txt")); + } + + private void UninstallTestExe(string installDir = null) + { + if (string.IsNullOrEmpty(installDir)) + { + // Default location used by installer + installDir = Path.GetTempPath(); + } + + TestCommon.RunCommand(Path.Combine(installDir, "UninstallTestExe.bat")); + } + } +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs index 6697735d0e..9b1d84f67b 100644 --- a/src/AppInstallerCLIE2ETests/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -190,7 +190,7 @@ public static RunCommandResult RunAICLICommandViaInvokeCommandInDesktopPackage(s return result; } - public static bool RunCommand(string fileName, string args, int timeOut = 60000) + public static bool RunCommand(string fileName, string args = "", int timeOut = 60000) { return RunCommandWithResult(fileName, args, timeOut).ExitCode == 0; } @@ -233,13 +233,25 @@ public static string GetTestDataFile(string fileName) return GetTestFile(Path.Combine("TestData", fileName)); } + public static string GetTestWorkDir() + { + string workDir = Path.Combine(TestContext.CurrentContext.TestDirectory, "WorkDirectory"); + Directory.CreateDirectory(workDir); + return workDir; + } + public static string GetRandomTestDir() { - string randDir = Path.Combine(TestContext.CurrentContext.TestDirectory, Path.Combine("WorkDirectory", Path.GetRandomFileName())); + string randDir = Path.Combine(GetTestWorkDir(), Path.GetRandomFileName()); Directory.CreateDirectory(randDir); return randDir; } + public static string GetRandomTestFile(string extension) + { + return Path.Combine(GetTestWorkDir(), Path.GetRandomFileName() + extension); + } + public static bool InstallMsix(string file) { return RunCommand("powershell", $"Add-AppxPackage \"{file}\""); diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-Invalid.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-Invalid.json new file mode 100644 index 0000000000..6bfedac997 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-Invalid.json @@ -0,0 +1 @@ +"A valid JSON file that does not conform to the schema" \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json new file mode 100644 index 0000000000..1d888819a7 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "MissingPackage", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost:5001/TestKit", + "Identifier": "WingetE2E.Tests_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json new file mode 100644 index 0000000000..aa01f581be --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerTest.TestExeInstaller", + "Version": "4.3.2.1" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost:5001/TestKit", + "Identifier": "WingetE2E.Tests_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json new file mode 100644 index 0000000000..495e9eb432 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost", + "Identifier": "WingetE2E.UnknownTestSource_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json new file mode 100644 index 0000000000..81757355be --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost:5001/TestKit", + "Identifier": "WingetE2E.Tests_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index 41a79d8cbc..71b8024f74 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -404,9 +404,10 @@ namespace AppInstaller::Repository m_availableSources.emplace_back(std::move(source)); } - void CompositeSource::SetInstalledSource(std::shared_ptr source) + void CompositeSource::SetInstalledSource(std::shared_ptr source, bool keepInstalledOnly) { m_installedSource = std::move(source); + m_keepInstalledOnly = keepInstalledOnly; } // An installed search first finds all installed packages that match the request, then correlates with available sources. @@ -515,6 +516,7 @@ namespace AppInstaller::Repository // If no package was found that was already in the results, do a correlation lookup with the installed // source to create a new composite package entry if we find any packages there. + bool foundInstalledMatch = false; if (packageData && !packageData->SystemReferenceStrings.empty()) { // Create a search request to run against the installed source @@ -532,9 +534,16 @@ namespace AppInstaller::Repository auto installedVersion = crossRef.Package->GetInstalledVersion(); auto installedPackageData = result.ReserveInstalledPackageSlot(installedVersion.get()); + foundInstalledMatch = true; result.Matches.emplace_back(std::make_shared(std::move(crossRef.Package), std::move(match.Package)), match.MatchCriteria); } } + + // If there was no correlation for this package, add it without one. + if (!m_keepInstalledOnly && !foundInstalledMatch) + { + result.Matches.push_back(std::move(match)); + } } SortResultMatches(result.Matches); diff --git a/src/AppInstallerRepositoryCore/CompositeSource.h b/src/AppInstallerRepositoryCore/CompositeSource.h index 57e84503c1..b5f05f897c 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.h +++ b/src/AppInstallerRepositoryCore/CompositeSource.h @@ -41,7 +41,7 @@ namespace AppInstaller::Repository void AddAvailableSource(std::shared_ptr source); // Sets the installed source to be composited. - void SetInstalledSource(std::shared_ptr source); + void SetInstalledSource(std::shared_ptr source, bool keepInstalledOnly = true); private: // Performs a search when an installed source is present. @@ -57,6 +57,7 @@ namespace AppInstaller::Repository std::shared_ptr m_installedSource; std::vector> m_availableSources; SourceDetails m_details; + bool m_keepInstalledOnly; }; } diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h index eaaef2170a..507ca33135 100644 --- a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -128,7 +128,8 @@ namespace AppInstaller::Repository std::shared_ptr OpenPredefinedSource(PredefinedSource source, IProgressCallback& progress); // Creates a source that merges the installed packages with the given available packages. - std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource); + // The source can search for installed packages only, or also include non-installed available packages. + std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource, bool installedOnly = true); // Updates an existing source. // Return value indicates whether the named source was found. diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index be7bad52c6..1ccfed438e 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -732,7 +732,7 @@ namespace AppInstaller::Repository THROW_HR(E_UNEXPECTED); } - std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource) + std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource, bool installedOnly) { std::shared_ptr result = std::dynamic_pointer_cast(availableSource); @@ -742,7 +742,7 @@ namespace AppInstaller::Repository result->AddAvailableSource(availableSource); } - result->SetInstalledSource(installedSource); + result->SetInstalledSource(installedSource, installedOnly); return result; } From 86101a8a27d2d8092eab5c1c7e7f3c334409b107 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Tue, 2 Feb 2021 15:07:48 -0800 Subject: [PATCH 15/34] Track installed version in test exe installer --- src/AppInstallerCLIE2ETests/ImportCommand.cs | 12 ++++- .../Manifests/TestExeInstaller.2.0.0.0.yaml | 2 +- src/AppInstallerTestExeInstaller/main.cpp | 54 +++++++++++-------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs index 48b4a363b2..120d7a84bf 100644 --- a/src/AppInstallerCLIE2ETests/ImportCommand.cs +++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -13,6 +13,7 @@ public void Setup() { InitializeAllFeatures(false); ConfigureFeature("importExport", true); + CleanupTestExe(); } [TearDown] @@ -73,7 +74,7 @@ public void ImportAlreadyInstalled() // Verify success with message when trying to import a package that is already installed var installDir = TestCommon.GetRandomTestDir(); TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller -l {installDir}"); - var result = TestCommon.RunAICLICommand("import", $"{GetTestImportFile("ImportFile-Good.json")} --exact-versions"); + var result = TestCommon.RunAICLICommand("import", $"{GetTestImportFile("ImportFile-Good.json")}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Package is already installed")); Assert.False(VerifyTestExeInstalled()); @@ -88,7 +89,7 @@ public void ImportExportedFile() TestCommon.RunAICLICommand("install", "AppInstallerTest.TestExeInstaller"); var jsonFile = TestCommon.GetRandomTestFile(".json"); - TestCommon.RunAICLICommand("export", $"{jsonFile} -s TestSource", timeOut: 180 * 1000); + TestCommon.RunAICLICommand("export", $"{jsonFile} -s TestSource"); // Uninstall the package to ensure we can install it again UninstallTestExe(); @@ -126,5 +127,12 @@ private void UninstallTestExe(string installDir = null) TestCommon.RunCommand(Path.Combine(installDir, "UninstallTestExe.bat")); } + + private void CleanupTestExe() + { + UninstallTestExe(); + File.Delete(Path.Combine(Path.GetTempPath(), "TestExeInstalled.txt")); + File.Delete(Path.Combine(Path.GetTempPath(), "TestExeUninstalled.txt")); + } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml index 1a6e9d673c..f2d6af8541 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml @@ -10,7 +10,7 @@ Installers: InstallerType: exe ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' Switches: - Custom: /execustom + Custom: /execustom /Version 2.0.0.0 SilentWithProgress: /exeswp Silent: /exesilent Interactive: /exeinteractive diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp index 8ba5116466..decb3c623f 100644 --- a/src/AppInstallerTestExeInstaller/main.cpp +++ b/src/AppInstallerTestExeInstaller/main.cpp @@ -11,10 +11,11 @@ using namespace std::filesystem; -std::string_view registrySubkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; -std::string_view defaultProductID = "{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; +std::wstring_view registrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; +std::wstring_view defaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; +std::wstring_view defaultVersion = L"1.0.0.0"; -path GenerateUninstaller(std::ostream& out, const path& installDirectory, const std::string& productID) +path GenerateUninstaller(std::wostream& out, const path& installDirectory, const std::wstring& productID) { path uninstallerPath = installDirectory; uninstallerPath /= "UninstallTestExe.bat"; @@ -24,7 +25,7 @@ path GenerateUninstaller(std::ostream& out, const path& installDirectory, const path uninstallerOutputTextFilePath = installDirectory; uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; - std::string registryKey{ "HKEY_CURRENT_USER\\" }; + std::wstring registryKey{ L"HKEY_CURRENT_USER\\" }; registryKey += registrySubkey; if (!productID.empty()) { @@ -35,7 +36,7 @@ path GenerateUninstaller(std::ostream& out, const path& installDirectory, const registryKey += defaultProductID; } - std::ofstream uninstallerScript(uninstallerPath); + std::wofstream uninstallerScript(uninstallerPath); uninstallerScript << "@echo off\n"; uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; @@ -45,19 +46,18 @@ path GenerateUninstaller(std::ostream& out, const path& installDirectory, const return uninstallerPath; } -void WriteToUninstallRegistry(std::ostream& out, const std::string& productID, const path& uninstallerPath) +void WriteToUninstallRegistry(std::wostream& out, const std::wstring& productID, const path& uninstallerPath, const std::wstring& displayVersion) { HKEY hkey; LONG lReg; // String inputs to registry must be of wide char type const wchar_t* displayName = L"AppInstallerTestExeInstaller"; - const wchar_t* displayVersion = L"1.0.0.0"; const wchar_t* publisher = L"Microsoft Corporation"; const wchar_t* uninstallString = uninstallerPath.c_str(); DWORD version = 1; - std::string registryKey{ registrySubkey }; + std::wstring registryKey{ registrySubkey }; if (!productID.empty()) { @@ -70,7 +70,7 @@ void WriteToUninstallRegistry(std::ostream& out, const std::string& productID, c out << "Default Product Code used: " << registryKey << "\n"; } - lReg = RegCreateKeyExA( + lReg = RegCreateKeyEx( HKEY_CURRENT_USER, registryKey.c_str(), 0, @@ -92,7 +92,7 @@ void WriteToUninstallRegistry(std::ostream& out, const std::string& productID, c } // Set Display Version Property Value - if (LONG res = RegSetValueEx(hkey, L"DisplayVersion", NULL, REG_SZ, (LPBYTE)displayVersion, (DWORD)(wcslen(displayVersion) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + if (LONG res = RegSetValueEx(hkey, L"DisplayVersion", NULL, REG_SZ, (LPBYTE)displayVersion.c_str(), (DWORD)(displayVersion.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { out << "Failed to write DisplayVersion value. Error Code: " << res << "\n"; } @@ -125,44 +125,56 @@ void WriteToUninstallRegistry(std::ostream& out, const std::string& productID, c } // The installer prints all args to an output file and writes to the Uninstall registry key -int main(int argc, const char** argv) +int wmain(int argc, const wchar_t** argv) { path installDirectory = temp_directory_path(); - std::stringstream outContent; - std::string productCode; + std::wstringstream outContent; + std::wstring productCode; + std::wstring version; // Output to cout by default, but swap to a file if requested - std::ostream* out = &std::cout; - std::ofstream logFile; + std::wostream* out = &std::wcout; + std::wofstream logFile; for (int i = 1; i < argc; i++) { outContent << argv[i] << ' '; // Supports custom install path. - if (_stricmp(argv[i], "/InstallDir") == 0 && ++i < argc) + if (_wcsicmp(argv[i], L"/InstallDir") == 0 && ++i < argc) { installDirectory = argv[i]; outContent << argv[i] << ' '; } // Supports custom product code ID - if (_stricmp(argv[i], "/ProductID") == 0 && ++i < argc) + if (_wcsicmp(argv[i], L"/ProductID") == 0 && ++i < argc) { productCode = argv[i]; } + // Supports custom version + if (_wcsicmp(argv[i], L"/Version") == 0 && ++i < argc) + { + version = argv[i]; + } + // Supports log file - if (_stricmp(argv[i], "/LogFile") == 0 && ++i < argc) + if (_wcsicmp(argv[i], L"/LogFile") == 0 && ++i < argc) { - logFile = std::ofstream(argv[i], std::ofstream::out | std::ofstream::trunc); + logFile = std::wofstream(argv[i], std::wofstream::out | std::wofstream::trunc); out = &logFile; } } + if (version.empty()) + { + version = defaultVersion; + } + path outFilePath = installDirectory; outFilePath /= "TestExeInstalled.txt"; - std::ofstream file(outFilePath, std::ofstream::out); + std::wofstream file(outFilePath, std::ofstream::out); file << outContent.str(); @@ -170,7 +182,7 @@ int main(int argc, const char** argv) path uninstallerPath = GenerateUninstaller(*out, installDirectory, productCode); - WriteToUninstallRegistry(*out, productCode, uninstallerPath); + WriteToUninstallRegistry(*out, productCode, uninstallerPath, version); return 0; } From 508d094cb1c5f2f830c8114c7da0a0a06ccea212 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Tue, 2 Feb 2021 17:12:41 -0800 Subject: [PATCH 16/34] Uninstall test exe before tests --- src/AppInstallerCLIE2ETests/Constants.cs | 3 +++ src/AppInstallerCLIE2ETests/ImportCommand.cs | 12 ++++++++---- src/AppInstallerCLIE2ETests/InstallCommand.cs | 6 +++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index cb64f041fe..0a06a6c5aa 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -53,6 +53,9 @@ public class Constants public const string MsiInstallerProductCode = "{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; public const string MsixInstallerPackageFamilyName = "6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e_8wekyb3d8bbwe"; + public const string TestExeInstalledFileName = "TestExeInstalled.txt"; + public const string TestExeUninstallerFileName = "UninstallTestExe.bat"; + public class ErrorCode { public const int S_OK = 0; diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs index 120d7a84bf..d98c0ce55b 100644 --- a/src/AppInstallerCLIE2ETests/ImportCommand.cs +++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -114,7 +114,7 @@ private bool VerifyTestExeInstalled(string installDir = null) installDir = Path.GetTempPath(); } - return File.Exists(Path.Combine(installDir, "TestExeInstalled.txt")); + return File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName)); } private void UninstallTestExe(string installDir = null) @@ -125,14 +125,18 @@ private void UninstallTestExe(string installDir = null) installDir = Path.GetTempPath(); } - TestCommon.RunCommand(Path.Combine(installDir, "UninstallTestExe.bat")); + string uninstaller = Path.Combine(installDir, Constants.TestExeUninstallerFileName); + if (File.Exists(uninstaller)) + { + TestCommon.RunCommand(uninstaller); + } } private void CleanupTestExe() { UninstallTestExe(); - File.Delete(Path.Combine(Path.GetTempPath(), "TestExeInstalled.txt")); - File.Delete(Path.Combine(Path.GetTempPath(), "TestExeUninstalled.txt")); + File.Delete(Path.Combine(Path.GetTempPath(), Constants.TestExeInstalledFileName)); + File.Delete(Path.Combine(Path.GetTempPath(), Constants.TestExeUninstallerFileName)); } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index 99630b2812..8917975ef5 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -8,7 +8,6 @@ namespace AppInstallerCLIE2ETests public class InstallCommand : BaseCommand { - private const string InstallTestExeInstalledFile = @"TestExeInstalled.txt"; private const string InstallTestMsiInstalledFile = @"AppInstallerTestExeInstaller.exe"; private const string InstallTestMsiProductId = @"{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; private const string InstallTestMsixName = @"6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e"; @@ -137,17 +136,18 @@ public void InstallMSIXWithSignatureHashMismatch() private bool VerifyTestExeInstalled(string installDir, string expectedContent = null) { - if (!File.Exists(Path.Combine(installDir, InstallTestExeInstalledFile))) + if (!File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) { return false; } if (!string.IsNullOrEmpty(expectedContent)) { - string content = File.ReadAllText(Path.Combine(installDir, InstallTestExeInstalledFile)); + string content = File.ReadAllText(Path.Combine(installDir, Constants.TestExeInstalledFileName)); return content.Contains(expectedContent); } + TestCommon.RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); return true; } From b176066b205059bfcadad710dd2c395b6ad5a902 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Wed, 3 Feb 2021 09:32:50 -0800 Subject: [PATCH 17/34] Spell checking --- .github/actions/spelling/allow.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index e1eff4a27e..42105c3f12 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -444,6 +444,7 @@ VOS vso wapproj wchar +wcout wcsicmp webpage wekyb @@ -459,7 +460,9 @@ winsqlite wix wmain woah +wofstream workflow +wostream wpfn wrl WStr From 1a3096278929124e9e68ef9e1bafb896f21af7f1 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Wed, 3 Feb 2021 14:08:45 -0800 Subject: [PATCH 18/34] Fix test failure --- src/AppInstallerCLIE2ETests/ImportCommand.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs index d98c0ce55b..35db406102 100644 --- a/src/AppInstallerCLIE2ETests/ImportCommand.cs +++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -78,7 +78,7 @@ public void ImportAlreadyInstalled() Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Package is already installed")); Assert.False(VerifyTestExeInstalled()); - UninstallTestExe(installDir); + UninstallTestExe(); } [Test] @@ -86,7 +86,7 @@ public void ImportExportedFile() { // Verify success when importing an exported list of packages. // First install the test package to ensure it is exported. - TestCommon.RunAICLICommand("install", "AppInstallerTest.TestExeInstaller"); + TestCommon.RunAICLICommand("install", Constants.ExeInstallerPackageId); var jsonFile = TestCommon.GetRandomTestFile(".json"); TestCommon.RunAICLICommand("export", $"{jsonFile} -s TestSource"); @@ -117,19 +117,10 @@ private bool VerifyTestExeInstalled(string installDir = null) return File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName)); } - private void UninstallTestExe(string installDir = null) + private void UninstallTestExe() { - if (string.IsNullOrEmpty(installDir)) - { - // Default location used by installer - installDir = Path.GetTempPath(); - } - - string uninstaller = Path.Combine(installDir, Constants.TestExeUninstallerFileName); - if (File.Exists(uninstaller)) - { - TestCommon.RunCommand(uninstaller); - } + ConfigureFeature("uninstall", true); + TestCommon.RunAICLICommand("uninstall", Constants.ExeInstallerPackageId); } private void CleanupTestExe() From 576c0515708aee8234fd68099b2690aa8b893e9a Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Fri, 5 Feb 2021 15:06:25 -0800 Subject: [PATCH 19/34] Move and rename packages schema file --- .../JSON/packages/packages.schema.1.0.json | 0 .../JSON/settings/settings.schema.0.2.json | 0 src/AppInstallerCLICore/PackageCollection.cpp | 4 ++-- .../ImportFiles/ImportFile-Bad-UnknownPackage.json | 2 +- .../ImportFile-Bad-UnknownPackageVersion.json | 2 +- .../ImportFiles/ImportFile-Bad-UnknownSource.json | 2 +- .../TestData/ImportFiles/ImportFile-Good.json | 2 +- src/AppInstallerCLITests/PackageCollection.cpp | 10 +++++----- .../TestData/ImportFile-Bad-UnknownPackage.json | 2 +- .../TestData/ImportFile-Bad-UnknownPackageVersion.json | 2 +- .../TestData/ImportFile-Bad-UnknownSource.json | 2 +- src/AppInstallerCLITests/TestData/ImportFile-Good.json | 2 +- 12 files changed, 15 insertions(+), 15 deletions(-) rename doc/packages.schema.json => schemas/JSON/packages/packages.schema.1.0.json (100%) rename doc/settings.schema.json => schemas/JSON/settings/settings.schema.0.2.json (100%) diff --git a/doc/packages.schema.json b/schemas/JSON/packages/packages.schema.1.0.json similarity index 100% rename from doc/packages.schema.json rename to schemas/JSON/packages/packages.schema.1.0.json diff --git a/doc/settings.schema.json b/schemas/JSON/settings/settings.schema.0.2.json similarity index 100% rename from doc/settings.schema.json rename to schemas/JSON/settings/settings.schema.0.2.json diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index 452bc9a100..4d4a847a3c 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -17,7 +17,7 @@ namespace AppInstaller::CLI // Strings used in the Packages JSON file. // Most will be used to access a JSON value, so they need to be std::string const std::string s_PackagesJson_Schema = "$schema"; - const std::string s_PackagesJson_SchemaUri_V0 = "https://aka.ms/winget-packages-v0.schema.json"; + const std::string s_PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json"; const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; const std::string s_PackagesJson_CreationDate = "CreationDate"; @@ -96,7 +96,7 @@ namespace AppInstaller::CLI { Json::Value root{ Json::ValueType::objectValue }; root[s_PackagesJson_WinGetVersion] = wingetVersion; - root[s_PackagesJson_Schema] = s_PackagesJson_SchemaUri_V0; + root[s_PackagesJson_Schema] = s_PackagesJson_SchemaUri_v1_0; // TODO: This uses localtime. Do we want to use UTC or add time zone? std::stringstream currentTimeStream; diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json index 1d888819a7..317c9f299b 100644 --- a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json index aa01f581be..5ba77b275b 100644 --- a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json index 495e9eb432..13c63b2ce1 100644 --- a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json index 81757355be..f513feaa9a 100644 --- a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { diff --git a/src/AppInstallerCLITests/PackageCollection.cpp b/src/AppInstallerCLITests/PackageCollection.cpp index 8963968efe..33c16698e0 100644 --- a/src/AppInstallerCLITests/PackageCollection.cpp +++ b/src/AppInstallerCLITests/PackageCollection.cpp @@ -14,7 +14,7 @@ using namespace AppInstaller::Repository; using namespace AppInstaller::Utility; const std::string s_PackagesJson_Schema = "$schema"; -const std::string s_PackagesJson_SchemaUri_V0 = "https://aka.ms/winget-packages-v0.schema.json"; +const std::string s_PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json"; const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; const std::string s_PackagesJson_CreationDate = "CreationDate"; @@ -61,7 +61,7 @@ namespace void ValidateJsonWithCollection(const Json::Value& root, const PackageCollection& collection) { - ValidateJsonStringProperty(root, s_PackagesJson_Schema, s_PackagesJson_SchemaUri_V0); + ValidateJsonStringProperty(root, s_PackagesJson_Schema, s_PackagesJson_SchemaUri_v1_0); ValidateJsonStringProperty(root, s_PackagesJson_WinGetVersion, collection.ClientVersion); REQUIRE(root.isMember(s_PackagesJson_CreationDate)); @@ -170,7 +170,7 @@ TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") { auto json = ParseJsonString(R"( { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { @@ -220,7 +220,7 @@ TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") { auto json = ParseJsonString(R"( { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "WinGetVersion": "1.0.0", "Sources": [ @@ -284,7 +284,7 @@ TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") { auto json = ParseJsonString(R"( { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "WinGetVersion": "1.0.0", "Sources": [ diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json index 467cfd6ddf..a0bb63159c 100644 --- a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json index 273bb3005e..4d2b076e5a 100644 --- a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json index 5a422810b1..213cc6eff2 100644 --- a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good.json b/src/AppInstallerCLITests/TestData/ImportFile-Good.json index 9709a27873..6ac4e286ab 100644 --- a/src/AppInstallerCLITests/TestData/ImportFile-Good.json +++ b/src/AppInstallerCLITests/TestData/ImportFile-Good.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/winget-packages-v0.schema.json", + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", "CreationDate": "2021-01-01T12:00:00.000", "Sources": [ { From 5bcd4a2ee4c27a908c09feddeb3cde2ce33db3a6 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Mon, 8 Feb 2021 15:37:55 -0800 Subject: [PATCH 20/34] Spell checking --- .github/actions/spelling/allow.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index f0dee9bd67..5c3827e284 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -473,6 +473,7 @@ wpfn wrl WStr wstring +wstringstream www xamarin xlang From 33ba20206cb2107adb94b1a1642b8f20d08f039e Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Mon, 8 Feb 2021 16:10:22 -0800 Subject: [PATCH 21/34] Fix error codes in tests after merge --- src/AppInstallerCLIE2ETests/Constants.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 0a06a6c5aa..50d9781fd9 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -114,9 +114,12 @@ public class ErrorCode public const int APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH = unchecked((int)0x8A15002E); public const int APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND = unchecked((int)0x8a15002F); public const int APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED = unchecked((int)0x8a150030); - public const int APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED = unchecked((int)0x8a150031); - public const int APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND = unchecked((int)0x8a150032); - public const int APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE = unchecked((int)0x8a150033); + public const int APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR = unchecked((int)0x8A150031); + public const int APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR = unchecked((int)0x8A150032); + public const int APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR = unchecked((int)0x8A150033); + public const int APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED = unchecked((int)0x8a150034); + public const int APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND = unchecked((int)0x8a150035); + public const int APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE = unchecked((int)0x8a150036); } } } From 660387d71eb44b3d978196abfed48f1c1d3ac7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Chac=C3=B3n?= Date: Wed, 10 Feb 2021 17:19:13 -0800 Subject: [PATCH 22/34] Apply suggestions from code review Co-authored-by: JohnMcPMS --- src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 602d58a83d..669195093b 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -102,7 +102,7 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); } - if (packages.value().Sources.empty()) + if (packages->Sources.empty()) { AICLI_LOG(CLI, Warning, << "No packages to install"); context.Reporter.Info() << Resource::String::NoPackagesFoundInImportFile << std::endl; @@ -182,7 +182,7 @@ namespace AppInstaller::CLI::Workflow // Search for the current package SearchRequest searchRequest; - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, packageRequest.Id)); + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, packageRequest.Id)); auto searchContextPtr = context.Clone(); Execution::Context& searchContext = *searchContextPtr; @@ -237,4 +237,4 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(packagesToInstall)); } -} \ No newline at end of file +} From 20984c612fd5f2c16cdb67fd25a1fe47818cb598 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 09:29:41 -0800 Subject: [PATCH 23/34] Remove exact versions import argument --- src/AppInstallerCLICore/Commands/ImportCommand.cpp | 1 - src/AppInstallerCLICore/ExecutionArgs.h | 1 - src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 10 ++-------- src/AppInstallerCLIE2ETests/ImportCommand.cs | 2 +- .../Shared/Strings/en-us/winget.resw | 3 --- src/AppInstallerCLITests/WorkFlow.cpp | 2 -- 6 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 82436d27b5..04fd8c3d57 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -16,7 +16,6 @@ namespace AppInstaller::CLI { return { Argument{ "import-file", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, - Argument{ "exact-versions", Argument::NoAlias, Execution::Args::Type::ExactVersions, Resource::String::ExactVersionsArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index f54119dd43..47f829a6ab 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -60,7 +60,6 @@ namespace AppInstaller::CLI::Execution // Import Command ImportFile, - ExactVersions, // Other All, // Used in Update command to update all installed packages to latest diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 669195093b..ae5061aef8 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -189,16 +189,10 @@ namespace AppInstaller::CLI::Workflow searchContext.Add(source); searchContext.Add(source->Search(searchRequest)); - Utility::VersionAndChannel versionAndChannel = {}; - if (context.Args.Contains(Execution::Args::Type::ExactVersions)) - { - versionAndChannel = packageRequest.VersionAndChannel; - } - // Find the single version we want is available searchContext << Workflow::EnsureOneMatchFromSearchResult(false) << - Workflow::GetManifestWithVersionFromPackage(versionAndChannel) << + Workflow::GetManifestWithVersionFromPackage(packageRequest.VersionAndChannel) << Workflow::GetInstalledPackageVersion; if (searchContext.Contains(Execution::Data::InstalledPackageVersion) && searchContext.Get()) @@ -216,7 +210,7 @@ namespace AppInstaller::CLI::Workflow } else { - AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << versionAndChannel.ToString()); + AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << packageRequest.VersionAndChannel.ToString()); context.Reporter.Info() << Resource::String::ImportSearchFailed << ' ' << packageRequest.Id << std::endl; // Keep searching for the remaining packages and only fail at the end. diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs index 35db406102..912c7cc083 100644 --- a/src/AppInstallerCLIE2ETests/ImportCommand.cs +++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -63,7 +63,7 @@ public void ImportUnavailablePackage() public void ImportUnavailableVersion() { // Verify failure when trying to import an unavailable package - var result = TestCommon.RunAICLICommand("import", $"{GetTestImportFile("ImportFile-Bad-UnknownPackageVersion.json")} --exact-versions"); + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-UnknownPackageVersion.json")); Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("Package not found for import")); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index ea30aa820f..5a2b082ce5 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -771,9 +771,6 @@ They can be configured through the settings file 'winget settings'. File describing the packages to install - - Install the exact versions listed in the import file - Export packages from the specified source diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index 2e2f97d8ee..385ed00409 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -1152,7 +1152,6 @@ TEST_CASE("ImportFlow_PackageAlreadyInstalled", "[ImportFlow][workflow]") TestContext context{ importOutput, std::cin }; OverrideForImportSource(context); context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good.json").GetPath().string()); - context.Args.AddArg(Execution::Args::Type::ExactVersions); ImportCommand importCommand({}); importCommand.Execute(context); @@ -1208,7 +1207,6 @@ TEST_CASE("ImportFlow_MissingVersion", "[ImportFlow][workflow]") TestContext context{ importOutput, std::cin }; OverrideForImportSource(context); context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownPackageVersion.json").GetPath().string()); - context.Args.AddArg(Execution::Args::Type::ExactVersions); ImportCommand importCommand({}); importCommand.Execute(context); From 7eb3359ab9a0043a63d13e09d2b132b3d3d1da6c Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 10:29:43 -0800 Subject: [PATCH 24/34] Match sources with identifier and type --- src/AppInstallerCLICore/PackageCollection.cpp | 7 ++- .../Workflows/ImportExportFlow.cpp | 61 +++++++++++++++---- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index 4d4a847a3c..d7b97c6166 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -40,8 +40,11 @@ namespace AppInstaller::CLI { node[propertyName] = Json::Value{ valueType }; } + else + { + THROW_HR_IF(E_NOT_VALID_STATE, node[propertyName].type() != valueType); + } - THROW_HR_IF(E_NOT_VALID_STATE, node[propertyName].type() != valueType); return node[propertyName]; } @@ -70,7 +73,7 @@ namespace AppInstaller::CLI PackageCollection::Source source{ std::move(sourceDetails) }; for (const auto& packageNode : sourceNode[s_PackagesJson_Packages]) { - source.Packages.push_back(ParsePackageNode(packageNode)); + source.Packages.emplace_back(ParsePackageNode(packageNode)); } return source; diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index ae5061aef8..59e3b211ac 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -12,6 +12,49 @@ namespace AppInstaller::CLI::Workflow { using namespace AppInstaller::Repository; + namespace + { + SourceDetails GetSourceDetails(const SourceDetails& source) + { + return source; + } + + SourceDetails GetSourceDetails(const PackageCollection::Source& source) + { + return source.Details; + } + + SourceDetails GetSourceDetails(const std::shared_ptr& source) + { + return source->GetDetails(); + } + + // Creates a predicate that determines whether a source matches a description in a SourceDetails. + template + std::function GetSourceDetailsEquivalencePredicate(const SourceDetails& details) + { + return [&](const T& source) + { + SourceDetails sourceDetails = GetSourceDetails(source); + return sourceDetails.Type == details.Type && sourceDetails.Identifier == details.Identifier; + }; + } + + // Finds a source equivalent to the one specified. + template + typename std::vector::const_iterator FindSource(const std::vector& sources, const SourceDetails& details) + { + return std::find_if(sources.begin(), sources.end(), GetSourceDetailsEquivalencePredicate(details)); + } + + // Finds a source equivalent to the one specified. + template + typename std::vector::iterator FindSource(std::vector& sources, const SourceDetails& details) + { + return std::find_if(sources.begin(), sources.end(), GetSourceDetailsEquivalencePredicate(details)); + } + } + void SelectVersionsToExport(Execution::Context& context) { const auto& searchResult = context.Get(); @@ -48,15 +91,15 @@ namespace AppInstaller::CLI::Workflow } } - const auto& sourceIdentifier = availablePackageVersion->GetSource()->GetIdentifier(); + const auto& sourceDetails = availablePackageVersion->GetSource()->GetDetails(); AICLI_LOG(CLI, Info, - << "Installed package is available. Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Source [" << sourceIdentifier << "]"); + << "Installed package is available. Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Source [" << sourceDetails.Identifier << "]"); // Find the exported source for this package - auto sourceItr = std::find_if(exportedSources.begin(), exportedSources.end(), [&](const PackageCollection::Source& s) { return s.Details.Identifier == sourceIdentifier; }); + auto sourceItr = FindSource(exportedSources, sourceDetails); if (sourceItr == exportedSources.end()) { - exportedSources.emplace_back(availablePackageVersion->GetSource()->GetDetails()); + exportedSources.emplace_back(sourceDetails); sourceItr = std::prev(exportedSources.end()); } @@ -124,10 +167,7 @@ namespace AppInstaller::CLI::Workflow { // Find the installed source matching the one described in the collection. AICLI_LOG(CLI, Info, << "Looking for source [" << requiredSource.Details.Identifier << "]"); - auto matchingSource = std::find_if( - availableSources.begin(), - availableSources.end(), - [&](const SourceDetails& s) { return s.Identifier == requiredSource.Details.Identifier; }); + auto matchingSource = FindSource(availableSources, requiredSource.Details); if (matchingSource != availableSources.end()) { requiredSource.Details.Name = matchingSource->Name; @@ -162,10 +202,7 @@ namespace AppInstaller::CLI::Workflow for (auto& requiredSource : context.Get().Sources) { // Find the required source among the open sources. This must exist as we already found them. - auto sourceItr = std::find_if( - sources.begin(), - sources.end(), - [&](const std::shared_ptr& s) { return s->GetIdentifier() == requiredSource.Details.Identifier; }); + auto sourceItr = FindSource(sources, requiredSource.Details); if (sourceItr == sources.end()) { AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR); From 4c93054377cee7ad348b7ba6226e50b840a0ac9b Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 10:50:38 -0800 Subject: [PATCH 25/34] Add execution stages --- src/AppInstallerCLICore/Commands/ExportCommand.cpp | 2 ++ src/AppInstallerCLICore/Commands/ImportCommand.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp index e0a756e9d9..3ffc86eeda 100644 --- a/src/AppInstallerCLICore/Commands/ExportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -53,11 +53,13 @@ namespace AppInstaller::CLI void ExportCommand::ExecuteInternal(Execution::Context& context) const { context << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) << Workflow::OpenSource << Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << Workflow::SearchSourceForMany << Workflow::EnsureMatchesFromSearchResult(true) << Workflow::SelectVersionsToExport << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Execution) << Workflow::WriteImportFile; } } diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 04fd8c3d57..0d3464e549 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -38,6 +38,7 @@ namespace AppInstaller::CLI void ImportCommand::ExecuteInternal(Execution::Context& context) const { context << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) << Workflow::VerifyFile(Execution::Args::Type::ImportFile) << Workflow::ReadImportFile << Workflow::OpenSourcesForImport << From 08b1604379d377e82f712677b7fd2c4c5646b21d Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 10:50:59 -0800 Subject: [PATCH 26/34] Add --force argument to skip missing packages --- src/AppInstallerCLICore/Commands/ImportCommand.cpp | 1 + src/AppInstallerCLICore/Resources.h | 2 +- src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 2 +- src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 3 +++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 0d3464e549..794a281a2f 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -16,6 +16,7 @@ namespace AppInstaller::CLI { return { Argument{ "import-file", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ "force", 'f', Execution::Args::Type::Force, Resource::String::ImportForceArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 9efb612f5e..faa68e93b9 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -45,7 +45,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Done); WINGET_DEFINE_RESOURCE_STRINGID(ExactArgumentDescription); - WINGET_DEFINE_RESOURCE_STRINGID(ExactVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandShortDescription); @@ -75,6 +74,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportForceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed); WINGET_DEFINE_RESOURCE_STRINGID(ImportPackageAlreadyInstalled); WINGET_DEFINE_RESOURCE_STRINGID(ImportSearchFailed); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 59e3b211ac..e2355cdb2c 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -260,7 +260,7 @@ namespace AppInstaller::CLI::Workflow } } - if (!foundAll) + if (!foundAll && !context.Args.Contains(Execution::Args::Type::Force)) { AICLI_LOG(CLI, Info, << "Could not find one or more packages for import"); AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 5a2b082ce5..5ed4ab611b 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -802,4 +802,7 @@ They can be configured through the settings file 'winget settings'. Package is already installed: + + Ignore unavailable packages + \ No newline at end of file From dc8bcadf53eb3a5bba5548ebf31d553d966c2468 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 11:04:57 -0800 Subject: [PATCH 27/34] Rename ParseJson -> TryParseJson --- src/AppInstallerCLICore/PackageCollection.cpp | 4 ++-- src/AppInstallerCLICore/PackageCollection.h | 2 +- src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp | 2 +- src/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 9 +++++---- src/AppInstallerCLITests/PackageCollection.cpp | 6 +++--- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index d7b97c6166..4e403191e7 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -164,9 +164,9 @@ namespace AppInstaller::CLI return root; } - std::optional ParseJson(const Json::Value& root) + std::optional TryParseJson(const Json::Value& root) { - // TODO: Embed schema in binaries & validate file + // TODO: Embed schema in binaries & validate file. This will return nullopt on failure. PackageCollection packages; packages.ClientVersion = root[s_PackagesJson_WinGetVersion].asString(); diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h index 79aa628424..f7e53a7e09 100644 --- a/src/AppInstallerCLICore/PackageCollection.h +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -54,6 +54,6 @@ namespace AppInstaller::CLI // Converts a collection of packages to its JSON representation for exporting. Json::Value CreateJson(const PackageCollection& packages); - std::optional ParseJson(const Json::Value& root); + std::optional TryParseJson(const Json::Value& root); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index e2355cdb2c..d34073cf24 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -138,7 +138,7 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); } - auto packages = PackagesJson::ParseJson(jsonRoot); + auto packages = PackagesJson::TryParseJson(jsonRoot); if (!packages.has_value()) { context.Reporter.Error() << Resource::String::InvalidJsonFile << std::endl; diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 3e1ca02db6..fdde29be56 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -490,17 +490,18 @@ namespace AppInstaller::CLI::Workflow if (!manifest) { - context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound << ' '; + auto errorStream = context.Reporter.Error(); + errorStream << Resource::String::GetManifestResultVersionNotFound << ' '; if (!m_version.empty()) { - context.Reporter.Error() << m_version; + errorStream << m_version; } if (!m_channel.empty()) { - context.Reporter.Error() << '[' << m_channel << ']'; + errorStream << '[' << m_channel << ']'; } - context.Reporter.Error() << std::endl; + errorStream << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); } diff --git a/src/AppInstallerCLITests/PackageCollection.cpp b/src/AppInstallerCLITests/PackageCollection.cpp index 33c16698e0..ce549d562e 100644 --- a/src/AppInstallerCLITests/PackageCollection.cpp +++ b/src/AppInstallerCLITests/PackageCollection.cpp @@ -195,7 +195,7 @@ TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") "WinGetVersion": "1.0.0" })"); - auto parsed = PackagesJson::ParseJson(json); + auto parsed = PackagesJson::TryParseJson(json); REQUIRE(parsed.has_value()); PackageCollection::Source source; @@ -254,7 +254,7 @@ TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") ] })"); - auto parsed = PackagesJson::ParseJson(json); + auto parsed = PackagesJson::TryParseJson(json); REQUIRE(parsed.has_value()); PackageCollection::Source source1; @@ -332,7 +332,7 @@ TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") ] })"); - auto parsed = PackagesJson::ParseJson(json); + auto parsed = PackagesJson::TryParseJson(json); REQUIRE(parsed.has_value()); PackageCollection::Source source1; From f7a9c43f4d418dfe9f5a7cc8e169df4b3d7436b4 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 11:46:12 -0800 Subject: [PATCH 28/34] Use workflow task for building Sources vector --- .../Workflows/ImportExportFlow.cpp | 23 ++++++++----------- .../Workflows/WorkflowBase.cpp | 12 ++++++++++ .../Workflows/WorkflowBase.h | 6 +++++ 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index d34073cf24..11d7749edd 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -85,9 +85,13 @@ namespace AppInstaller::CLI::Workflow AICLI_LOG( CLI, Info, - << "Installed package version not available. Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "]" - << ", Version [" << version << "], Channel [" << channel << "]"); - context.Reporter.Warn() << Resource::String::InstalledPackageVersionNotAvailable << ' ' << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << std::endl; + << "Installed package version is not available." + << " Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Version [" << version << "], Channel [" << channel << "]" + << ". Found Version [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "], Channel [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "]"); + context.Reporter.Warn() + << Resource::String::InstalledPackageVersionNotAvailable + << ' ' << availablePackageVersion->GetProperty(PackageVersionProperty::Id) + << ' ' << version << ' ' << channel << std::endl; } } @@ -158,11 +162,6 @@ namespace AppInstaller::CLI::Workflow void OpenSourcesForImport(Execution::Context& context) { auto availableSources = Repository::GetSources(); - - // List of all the sources used for import. - // Needed to keep all the source objects alive for install. - std::vector> sources = {}; - for (auto& requiredSource : context.Get().Sources) { // Find the installed source matching the one described in the collection. @@ -179,16 +178,14 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); } - context << Workflow::OpenNamedSource(requiredSource.Details.Name); + context << + Workflow::OpenNamedSource(requiredSource.Details.Name) << + Workflow::AddToSources; if (context.IsTerminated()) { return; } - - sources.push_back(context.Get()); } - - context.Add(std::move(sources)); } void SearchPackagesForImport(Execution::Context& context) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index fdde29be56..daf9825e15 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -188,6 +188,18 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(compositeSource)); } + void AddToSources(Execution::Context& context) + { + if (!context.Contains(Execution::Data::Sources)) + { + context.Add({ context.Get() }); + } + else + { + context.Get().emplace_back(context.Get()); + } + } + void SearchSourceForMany(Execution::Context& context) { const auto& args = context.Args; diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index eca91a82a2..4a6f8f1a5e 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -102,6 +102,12 @@ namespace AppInstaller::CLI::Workflow Repository::PredefinedSource m_predefinedSource; }; + // Adds the current open source to a list of open sources. + // Required Args: None + // Inputs: Source + // Outputs: Sources + void AddToSources(Execution::Context& context); + // Performs a search on the source. // Required Args: None // Inputs: Source From 4b59e1a1471b501bf3adb060726b614a879bd587 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 13:05:48 -0800 Subject: [PATCH 29/34] Fix tests --- .../AppInstallerCLITests.vcxproj | 3 +++ .../AppInstallerCLITests.vcxproj.filters | 3 +++ .../ImportFile-Good-AlreadyInstalled.json | 21 +++++++++++++++++++ .../TestData/ImportFile-Good.json | 4 ++-- src/AppInstallerCLITests/WorkFlow.cpp | 2 +- 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/AppInstallerCLITests/TestData/ImportFile-Good-AlreadyInstalled.json diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index de867fd199..22288f3f7a 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -252,6 +252,9 @@ true + + true + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 471ecf9903..74416c87d6 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -345,6 +345,9 @@ TestData + + TestData + TestData diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good-AlreadyInstalled.json b/src/AppInstallerCLITests/TestData/ImportFile-Good-AlreadyInstalled.json new file mode 100644 index 0000000000..4463587609 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Good-AlreadyInstalled.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good.json b/src/AppInstallerCLITests/TestData/ImportFile-Good.json index 6ac4e286ab..42c46904d6 100644 --- a/src/AppInstallerCLITests/TestData/ImportFile-Good.json +++ b/src/AppInstallerCLITests/TestData/ImportFile-Good.json @@ -6,11 +6,11 @@ "Packages": [ { "Id": "AppInstallerCliTest.TestExeInstaller", - "Version": "1.0.0.0" + "Version": "2.0.0.0" }, { "Id": "AppInstallerCliTest.TestMsixInstaller", - "Version": "1.0.0.0" + "Version": "2.0.0.0" } ], "SourceDetails": { diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index 385ed00409..2f4ad985cc 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -1151,7 +1151,7 @@ TEST_CASE("ImportFlow_PackageAlreadyInstalled", "[ImportFlow][workflow]") std::ostringstream importOutput; TestContext context{ importOutput, std::cin }; OverrideForImportSource(context); - context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good.json").GetPath().string()); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good-AlreadyInstalled.json").GetPath().string()); ImportCommand importCommand({}); importCommand.Execute(context); From 8b45f6d3a78da0f138b2d43e1b3474548b7e148c Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 13:06:47 -0800 Subject: [PATCH 30/34] Change composite search behavior from flag to enum --- .../Workflows/ImportExportFlow.cpp | 2 +- .../CompositeSource.cpp | 8 ++++---- src/AppInstallerRepositoryCore/CompositeSource.h | 4 ++-- .../Public/AppInstallerRepositorySource.h | 16 +++++++++++++++- .../RepositorySource.cpp | 6 +++--- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 11d7749edd..77d747ff0e 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -207,7 +207,7 @@ namespace AppInstaller::CLI::Workflow // Search for all the packages in the source. // Each search is done in a sub context to search everything regardless of previous failures. - auto source = Repository::CreateCompositeSource(context.Get(), *sourceItr, false); + auto source = Repository::CreateCompositeSource(context.Get(), *sourceItr, CompositeSearchBehavior::AllPackages); AICLI_LOG(CLI, Info, << "Searching for packages requested from source [" << requiredSource.Details.Identifier << "]"); for (const auto& packageRequest : requiredSource.Packages) { diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index 71b8024f74..cdf0a64c94 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -404,10 +404,10 @@ namespace AppInstaller::Repository m_availableSources.emplace_back(std::move(source)); } - void CompositeSource::SetInstalledSource(std::shared_ptr source, bool keepInstalledOnly) + void CompositeSource::SetInstalledSource(std::shared_ptr source, CompositeSearchBehavior searchBehavior) { m_installedSource = std::move(source); - m_keepInstalledOnly = keepInstalledOnly; + m_searchBehavior = searchBehavior; } // An installed search first finds all installed packages that match the request, then correlates with available sources. @@ -467,7 +467,7 @@ namespace AppInstaller::Repository { auto id = installedVersion->GetProperty(PackageVersionProperty::Id); - AICLI_LOG(Repo, Info, + AICLI_LOG(Repo, Info, << "Found multiple matches for installed package [" << id << "] in source [" << source->GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); // More than one match found for the system reference; run some heuristics to check for a match @@ -540,7 +540,7 @@ namespace AppInstaller::Repository } // If there was no correlation for this package, add it without one. - if (!m_keepInstalledOnly && !foundInstalledMatch) + if (m_searchBehavior == CompositeSearchBehavior::AllPackages && !foundInstalledMatch) { result.Matches.push_back(std::move(match)); } diff --git a/src/AppInstallerRepositoryCore/CompositeSource.h b/src/AppInstallerRepositoryCore/CompositeSource.h index b5f05f897c..1b5dc1e92e 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.h +++ b/src/AppInstallerRepositoryCore/CompositeSource.h @@ -41,7 +41,7 @@ namespace AppInstaller::Repository void AddAvailableSource(std::shared_ptr source); // Sets the installed source to be composited. - void SetInstalledSource(std::shared_ptr source, bool keepInstalledOnly = true); + void SetInstalledSource(std::shared_ptr source, CompositeSearchBehavior searchBehavior = CompositeSearchBehavior::Installed); private: // Performs a search when an installed source is present. @@ -57,7 +57,7 @@ namespace AppInstaller::Repository std::shared_ptr m_installedSource; std::vector> m_availableSources; SourceDetails m_details; - bool m_keepInstalledOnly; + CompositeSearchBehavior m_searchBehavior; }; } diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h index 507ca33135..c938c9faba 100644 --- a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -127,9 +127,23 @@ namespace AppInstaller::Repository // These sources are not under the direct control of the user, such as packages installed on the system. std::shared_ptr OpenPredefinedSource(PredefinedSource source, IProgressCallback& progress); + // Search behavior for composite sources. + // Only relevant for composite sources with an installed source, not for aggregates of multiple available sources. + // Installed and available packages in the result are always correlated when possible. + enum class CompositeSearchBehavior + { + // Search only installed packages. + Installed, + // Search both installed and available packages. + AllPackages, + }; + // Creates a source that merges the installed packages with the given available packages. // The source can search for installed packages only, or also include non-installed available packages. - std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource, bool installedOnly = true); + std::shared_ptr CreateCompositeSource( + const std::shared_ptr& installedSource, + const std::shared_ptr& availableSource, + CompositeSearchBehavior searchBehavior = CompositeSearchBehavior::Installed); // Updates an existing source. // Return value indicates whether the named source was found. diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index 1ccfed438e..1aedf1e5ac 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -381,7 +381,7 @@ namespace AppInstaller::Repository constexpr static auto s_ZeroMins = 0min; auto autoUpdateTime = User().Get(); - // A value of zero means no auto update, to get update the source run `winget update` + // A value of zero means no auto update, to get update the source run `winget update` if (autoUpdateTime != s_ZeroMins) { auto autoUpdateTimeMins = std::chrono::minutes(autoUpdateTime); @@ -732,7 +732,7 @@ namespace AppInstaller::Repository THROW_HR(E_UNEXPECTED); } - std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource, bool installedOnly) + std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource, CompositeSearchBehavior searchBehavior) { std::shared_ptr result = std::dynamic_pointer_cast(availableSource); @@ -742,7 +742,7 @@ namespace AppInstaller::Repository result->AddAvailableSource(availableSource); } - result->SetInstalledSource(installedSource, installedOnly); + result->SetInstalledSource(installedSource, searchBehavior); return result; } From 58f6fe4bdbc18e6dcf737757564f50c9adebfbf6 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 17:24:26 -0800 Subject: [PATCH 31/34] Address comments from review --- .../Commands/ImportCommand.cpp | 3 +- src/AppInstallerCLICore/ExecutionArgs.h | 1 + src/AppInstallerCLICore/ExecutionContext.h | 10 +- src/AppInstallerCLICore/Resources.h | 2 +- .../Workflows/ImportExportFlow.cpp | 15 ++- .../Workflows/WorkflowBase.cpp | 112 ++++++++++-------- .../Workflows/WorkflowBase.h | 16 +-- .../Shared/Strings/en-us/winget.resw | 2 +- 8 files changed, 88 insertions(+), 73 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 794a281a2f..401020d15a 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -16,7 +16,7 @@ namespace AppInstaller::CLI { return { Argument{ "import-file", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, - Argument{ "force", 'f', Execution::Args::Type::Force, Resource::String::ImportForceArgumentDescription, ArgumentType::Flag }, + Argument{ "ignore-unavailable", Argument::NoAlias, Execution::Args::Type::IgnoreUnavailable, Resource::String::ImportIgnoreUnavailableArgumentDescription, ArgumentType::Flag }, }; } @@ -45,6 +45,7 @@ namespace AppInstaller::CLI Workflow::OpenSourcesForImport << Workflow::OpenPredefinedSource(Repository::PredefinedSource::Installed) << Workflow::SearchPackagesForImport << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Execution) << Workflow::InstallMultiple; } } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 47f829a6ab..de26e75dde 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -60,6 +60,7 @@ namespace AppInstaller::CLI::Execution // Import Command ImportFile, + IgnoreUnavailable, // Other All, // Used in Update command to update all installed packages to latest diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index d651d73084..2ec8f7a927 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -20,16 +20,20 @@ // Terminates the Context with some logging to indicate the location. // Also returns from the current function. -#define AICLI_TERMINATE_CONTEXT_ARGS(_context_,_hr_) \ +#define AICLI_TERMINATE_CONTEXT_ARGS(_context_,_hr_,_ret_) \ do { \ HRESULT AICLI_TERMINATE_CONTEXT_ARGS_hr = _hr_; \ _context_.Terminate(AICLI_TERMINATE_CONTEXT_ARGS_hr, __FILE__, __LINE__); \ - return; \ + return _ret_; \ } while(0,0) // Terminates the Context named 'context' with some logging to indicate the location. // Also returns from the current function. -#define AICLI_TERMINATE_CONTEXT(_hr_) AICLI_TERMINATE_CONTEXT_ARGS(context,_hr_) +#define AICLI_TERMINATE_CONTEXT(_hr_) AICLI_TERMINATE_CONTEXT_ARGS(context,_hr_,) + +// Terminates the Context named 'context' with some logging to indicate the location. +// Also returns the specified value from the current function. +#define AICLI_TERMINATE_CONTEXT_RETURN(_hr_,_ret_) AICLI_TERMINATE_CONTEXT_ARGS(context,_hr_,_ret_) namespace AppInstaller::CLI::Workflow { diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index faa68e93b9..f32eec8b28 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -74,7 +74,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); - WINGET_DEFINE_RESOURCE_STRINGID(ImportForceArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnoreUnavailableArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed); WINGET_DEFINE_RESOURCE_STRINGID(ImportPackageAlreadyInstalled); WINGET_DEFINE_RESOURCE_STRINGID(ImportSearchFailed); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 77d747ff0e..0a1a9cdfea 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -178,9 +178,7 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); } - context << - Workflow::OpenNamedSource(requiredSource.Details.Name) << - Workflow::AddToSources; + context << Workflow::OpenNamedSourceForSources(requiredSource.Details.Name); if (context.IsTerminated()) { return; @@ -257,10 +255,17 @@ namespace AppInstaller::CLI::Workflow } } - if (!foundAll && !context.Args.Contains(Execution::Args::Type::Force)) + if (!foundAll) { AICLI_LOG(CLI, Info, << "Could not find one or more packages for import"); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); + if (context.Args.Contains(Execution::Args::Type::IgnoreUnavailable)) + { + AICLI_LOG(CLI, Info, << "Ignoring unavailable packages due to command line argument"); + } + else + { + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); + } } context.Add(std::move(packagesToInstall)); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index daf9825e15..ab34652c0f 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -36,6 +36,53 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::ReportIdentityFound << ' ' << Execution::NameEmphasis << name << " [" << Execution::IdEmphasis << id << ']' << std::endl; } + std::shared_ptr OpenNamedSource(Execution::Context& context, std::string_view sourceName) + { + std::shared_ptr source; + try + { + auto result = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, sourceName, std::placeholders::_1), true); + source = result.Source; + + // We'll only report the source update failure as warning and continue + for (const auto& s : result.SourcesWithUpdateFailure) + { + context.Reporter.Warn() << Resource::String::SourceOpenWithFailedUpdate << ' ' << s.Name << std::endl; + } + } + catch (...) + { + context.Reporter.Error() << Resource::String::SourceOpenFailedSuggestion << std::endl; + throw; + } + + if (!source) + { + std::vector sources = GetSources(); + + if (!sourceName.empty() && !sources.empty()) + { + // A bad name was given, try to help. + context.Reporter.Error() << Resource::String::OpenSourceFailedNoMatch << ' ' << sourceName << std::endl; + context.Reporter.Info() << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; + for (const auto& details : sources) + { + context.Reporter.Info() << " "_liv << details.Name << std::endl; + } + + AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST, {}); + } + else + { + // Even if a name was given, there are no sources + context.Reporter.Error() << Resource::String::OpenSourceFailedNoSourceDefined << std::endl; + AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED, {}); + } + } + + return source; + } + void SearchSourceApplyFilters(Execution::Context& context, SearchRequest& searchRequest, MatchType matchType) { const auto& args = context.Args; @@ -102,55 +149,30 @@ namespace AppInstaller::CLI::Workflow sourceName = context.Args.GetArg(Execution::Args::Type::Source); } - context << OpenNamedSource(sourceName); + auto source = OpenNamedSource(context, sourceName); + if (context.IsTerminated()) + { + return; + } + + context.Add(std::move(source)); } - void OpenNamedSource::operator()(Execution::Context& context) const + void OpenNamedSourceForSources::operator()(Execution::Context& context) const { - std::shared_ptr source; - try - { - auto result = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, m_sourceName, std::placeholders::_1), true); - source = result.Source; - - // We'll only report the source update failure as warning and continue - for (const auto& s : result.SourcesWithUpdateFailure) - { - context.Reporter.Warn() << Resource::String::SourceOpenWithFailedUpdate << ' ' << s.Name << std::endl; - } - } - catch (...) + auto source = OpenNamedSource(context, m_sourceName); + if (context.IsTerminated()) { - context.Reporter.Error() << Resource::String::SourceOpenFailedSuggestion << std::endl; - throw; + return; } - if (!source) + if (!context.Contains(Execution::Data::Sources)) { - std::vector sources = GetSources(); - - if (context.Args.Contains(Execution::Args::Type::Source) && !sources.empty()) - { - // A bad name was given, try to help. - context.Reporter.Error() << Resource::String::OpenSourceFailedNoMatch << ' ' << context.Args.GetArg(Execution::Args::Type::Source) << std::endl; - context.Reporter.Info() << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; - for (const auto& details : sources) - { - context.Reporter.Info() << " "_liv << details.Name << std::endl; - } - - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); - } - else - { - // Even if a name was given, there are no sources - context.Reporter.Error() << Resource::String::OpenSourceFailedNoSourceDefined << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED); - } + context.Add({ std::move(source) }); } else { - context.Add(std::move(source)); + context.Get().emplace_back(std::move(source)); } } @@ -188,18 +210,6 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(compositeSource)); } - void AddToSources(Execution::Context& context) - { - if (!context.Contains(Execution::Data::Sources)) - { - context.Add({ context.Get() }); - } - else - { - context.Get().emplace_back(context.Get()); - } - } - void SearchSourceForMany(Execution::Context& context) { const auto& args = context.Args; diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index 4a6f8f1a5e..848ca9205b 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -60,13 +60,13 @@ namespace AppInstaller::CLI::Workflow // Outputs: Source void OpenSource(Execution::Context& context); - // Creates a source object for a source from its name. + // Creates a source object for a source specified by name, and adds it to the list of open sources. // Required Args: None - // Inputs: None - // Outputs: Source - struct OpenNamedSource : public WorkflowTask + // Inputs: Sources? + // Outputs: Sources + struct OpenNamedSourceForSources : public WorkflowTask { - OpenNamedSource(std::string_view sourceName) : WorkflowTask("OpenNamedSource"), m_sourceName(sourceName) {} + OpenNamedSourceForSources(std::string_view sourceName) : WorkflowTask("OpenNamedSourceForSources"), m_sourceName(sourceName) {} void operator()(Execution::Context& context) const override; @@ -102,12 +102,6 @@ namespace AppInstaller::CLI::Workflow Repository::PredefinedSource m_predefinedSource; }; - // Adds the current open source to a list of open sources. - // Required Args: None - // Inputs: Source - // Outputs: Sources - void AddToSources(Execution::Context& context); - // Performs a search on the source. // Required Args: None // Inputs: Source diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 5ed4ab611b..90c21c5655 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -802,7 +802,7 @@ They can be configured through the settings file 'winget settings'. Package is already installed: - + Ignore unavailable packages \ No newline at end of file From 13574530dde7f29b7f4f56ddfef8e3a8cd6ec270 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Thu, 11 Feb 2021 17:53:52 -0800 Subject: [PATCH 32/34] Update behavior around versions --- .../Commands/ExportCommand.cpp | 1 + .../Commands/ImportCommand.cpp | 1 + src/AppInstallerCLICore/ExecutionArgs.h | 2 + src/AppInstallerCLICore/PackageCollection.cpp | 25 ++----- src/AppInstallerCLICore/PackageCollection.h | 2 + src/AppInstallerCLICore/Resources.h | 2 + .../Workflows/ImportExportFlow.cpp | 29 +++++-- .../Shared/Strings/en-us/winget.resw | 6 ++ .../ImportFile-Bad-UnknownPackage.json | 6 +- src/AppInstallerCLITests/WorkFlow.cpp | 75 +++++++++++++++++++ 10 files changed, 126 insertions(+), 23 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp index 3ffc86eeda..15d0923281 100644 --- a/src/AppInstallerCLICore/Commands/ExportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -16,6 +16,7 @@ namespace AppInstaller::CLI return { Argument{ "output", 'o', Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Positional, true }, Argument{ "source", 's', Execution::Args::Type::Source, Resource::String::ExportSourceArgumentDescription, ArgumentType::Standard }, + Argument{ "include-versions", Argument::NoAlias, Execution::Args::Type::IncludeVersions, Resource::String::ExportIncludeVersionsArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 401020d15a..7d7c9c2a19 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -17,6 +17,7 @@ namespace AppInstaller::CLI return { Argument{ "import-file", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, Argument{ "ignore-unavailable", Argument::NoAlias, Execution::Args::Type::IgnoreUnavailable, Resource::String::ImportIgnoreUnavailableArgumentDescription, ArgumentType::Flag }, + Argument{ "ignore-versions", Argument::NoAlias, Execution::Args::Type::IgnoreVersions, Resource::String::ImportIgnorePackageVersionsArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index de26e75dde..eb0e67fcea 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -57,10 +57,12 @@ namespace AppInstaller::CLI::Execution // Export Command OutputFile, + IncludeVersions, // Import Command ImportFile, IgnoreUnavailable, + IgnoreVersions, // Other All, // Used in Update command to update all installed packages to latest diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index 4e403191e7..bfcba49b44 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -79,21 +79,6 @@ namespace AppInstaller::CLI return source; } - // Gets the available PackageVersion that has the same version as the installed version. - // The package must have an installed version. - // Returns null if not available. - std::shared_ptr GetAvailableVersionMatchingInstalled(const IPackage& package) - { - auto installedVersion = package.GetInstalledVersion(); - PackageVersionKey installedVersionKey - { - "", - installedVersion->GetProperty(PackageVersionProperty::Version).get(), - installedVersion->GetProperty(PackageVersionProperty::Channel).get(), - }; - return package.GetAvailableVersion(installedVersionKey); - } - // Creates a minimal root object of a Packages JSON file. Json::Value CreateRoot(const std::string& wingetVersion) { @@ -114,9 +99,15 @@ namespace AppInstaller::CLI { Json::Value packageNode{ Json::ValueType::objectValue }; packageNode[s_PackagesJson_Package_Id] = package.Id.get(); - packageNode[s_PackagesJson_Package_Version] = package.VersionAndChannel.GetVersion().ToString(); - // Only add channel if present + // Only add version and channel if present. + // Packages may not have a channel, or versions may not have been requested. + const std::string& version = package.VersionAndChannel.GetVersion().ToString(); + if (!version.empty()) + { + packageNode[s_PackagesJson_Package_Version] = version; + } + const std::string& channel = package.VersionAndChannel.GetChannel().ToString(); if (!channel.empty()) { diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h index f7e53a7e09..29b3d6d3e4 100644 --- a/src/AppInstallerCLICore/PackageCollection.h +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -22,6 +22,8 @@ namespace AppInstaller::CLI struct Package { Package() = default; + Package(Utility::LocIndString&& id) : + Id(std::move(id)) {} Package(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel) : Id(std::move(id)), VersionAndChannel(std::move(version), std::move(channel)) {} Package(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel) : diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index f32eec8b28..20ddde8476 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -50,6 +50,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportIncludeVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); @@ -74,6 +75,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnorePackageVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnoreUnavailableArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed); WINGET_DEFINE_RESOURCE_STRINGID(ImportPackageAlreadyInstalled); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 0a1a9cdfea..027cc8728a 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -108,11 +108,18 @@ namespace AppInstaller::CLI::Workflow } // Take the Id from the available package because that is the one used in the source, - // but take the exported version from the installed package. - sourceItr->Packages.emplace_back( - availablePackageVersion->GetProperty(PackageVersionProperty::Id), - version.get(), - channel.get()); + // but take the exported version from the installed package if needed. + if (context.Args.Contains(Execution::Args::Type::IncludeVersions)) + { + sourceItr->Packages.emplace_back( + availablePackageVersion->GetProperty(PackageVersionProperty::Id), + version.get(), + channel.get()); + } + else + { + sourceItr->Packages.emplace_back(availablePackageVersion->GetProperty(PackageVersionProperty::Id)); + } } context.Add(std::move(exportedPackages)); @@ -156,6 +163,18 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); } + if (context.Args.Contains(Execution::Args::Type::IgnoreVersions)) + { + // Strip out all the version information as we don't need it. + for (auto& source : packages->Sources) + { + for (auto& package : source.Packages) + { + package.VersionAndChannel = {}; + } + } + } + context.Add(packages.value()); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 90c21c5655..e251ade0c8 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -805,4 +805,10 @@ They can be configured through the settings file 'winget settings'. Ignore unavailable packages + + Include package versions in produced file + + + Ignore package versions from import file + \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json index a0bb63159c..4ea305296a 100644 --- a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json @@ -5,7 +5,11 @@ { "Packages": [ { - "Id": "MissingFile", + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "2.0.0.0" + }, + { + "Id": "MissingPackage", "Version": "1.0.0.0" } ], diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index 2f4ad985cc..a90cd4558c 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -1107,6 +1107,41 @@ TEST_CASE("ExportFlow_ExportAll", "[ExportFlow][workflow]") REQUIRE(exportedCollection.Sources.size() == 1); REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); + const auto& exportedPackages = exportedCollection.Sources[0].Packages; + REQUIRE(exportedPackages.size() == 3); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestExeInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); + })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestMsixInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); + })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestMSStoreInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); + })); +} + +TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") +{ + TestCommon::TempFile exportResultPath("TestExport.json"); + + std::ostringstream exportOutput; + TestContext context{ exportOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::OutputFile, exportResultPath); + context.Args.AddArg(Execution::Args::Type::IncludeVersions); + + ExportCommand exportCommand({}); + exportCommand.Execute(context); + INFO(exportOutput.str()); + + // Verify contents of exported collection + const auto& exportedCollection = context.Get(); + REQUIRE(exportedCollection.Sources.size() == 1); + REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); + const auto& exportedPackages = exportedCollection.Sources[0].Packages; REQUIRE(exportedPackages.size() == 3); REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) @@ -1162,6 +1197,25 @@ TEST_CASE("ImportFlow_PackageAlreadyInstalled", "[ImportFlow][workflow]") REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportPackageAlreadyInstalled).get()) != std::string::npos); } +TEST_CASE("ImportFlow_IgnoreVersions", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good-AlreadyInstalled.json").GetPath().string()); + context.Args.AddArg(Execution::Args::Type::IgnoreVersions); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Specified version is already installed. It should have been updated since we ignored the version. + REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); +} + TEST_CASE("ImportFlow_MissingSource", "[ImportFlow][workflow]") { TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); @@ -1199,6 +1253,27 @@ TEST_CASE("ImportFlow_MissingPackage", "[ImportFlow][workflow]") REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); } +TEST_CASE("ImportFlow_IgnoreMissingPackage", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownPackage.json").GetPath().string()); + context.Args.AddArg(Execution::Args::Type::IgnoreUnavailable); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Verify installer was called for the package that was available. + REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); + REQUIRE_TERMINATED_WITH(context, S_OK); +} + TEST_CASE("ImportFlow_MissingVersion", "[ImportFlow][workflow]") { TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); From a09ad02e8920b8634807c763080b0ef596eeb1fb Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Fri, 12 Feb 2021 10:52:13 -0800 Subject: [PATCH 33/34] Fix tests --- src/AppInstallerCLITests/WorkFlow.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index a90cd4558c..b98d7084c7 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -1271,7 +1271,6 @@ TEST_CASE("ImportFlow_IgnoreMissingPackage", "[ImportFlow][workflow]") // Verify installer was called for the package that was available. REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); - REQUIRE_TERMINATED_WITH(context, S_OK); } TEST_CASE("ImportFlow_MissingVersion", "[ImportFlow][workflow]") From abd0e4cca07fb3ee76f98a5679b96fc3e045a946 Mon Sep 17 00:00:00 2001 From: Luis Enrique Chacon Ochoa Date: Fri, 12 Feb 2021 17:44:51 -0800 Subject: [PATCH 34/34] Don't check that installed version is available unless needed --- .../Workflows/ImportExportFlow.cpp | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 027cc8728a..6aee4eb594 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -53,11 +53,49 @@ namespace AppInstaller::CLI::Workflow { return std::find_if(sources.begin(), sources.end(), GetSourceDetailsEquivalencePredicate(details)); } + + // Gets the available version of an installed package. + // If requested, checks that the installed version is available and reports a warning if it is not. + std::shared_ptr GetAvailableVersionForInstalledPackage( + Execution::Context& context, + std::shared_ptr package, + std::string_view version, + std::string_view channel, + bool checkVersion) + { + if (!checkVersion) + { + return package->GetLatestAvailableVersion(); + } + + auto availablePackageVersion = package->GetAvailableVersion({ "", version, channel }); + if (!availablePackageVersion) + { + availablePackageVersion = package->GetLatestAvailableVersion(); + if (availablePackageVersion) + { + // Warn installed version is not available. + AICLI_LOG( + CLI, + Info, + << "Installed package version is not available." + << " Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Version [" << version << "], Channel [" << channel << "]" + << ". Found Version [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "], Channel [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "]"); + context.Reporter.Warn() + << Resource::String::InstalledPackageVersionNotAvailable + << ' ' << availablePackageVersion->GetProperty(PackageVersionProperty::Id) + << ' ' << version << ' ' << channel << std::endl; + } + } + + return availablePackageVersion; + } } void SelectVersionsToExport(Execution::Context& context) { const auto& searchResult = context.Get(); + const bool includeVersions = context.Args.Contains(Execution::Args::Type::IncludeVersions); PackageCollection exportedPackages; exportedPackages.ClientVersion = Runtime::GetClientVersion().get(); auto& exportedSources = exportedPackages.Sources; @@ -68,31 +106,13 @@ namespace AppInstaller::CLI::Workflow auto channel = installedPackageVersion->GetProperty(PackageVersionProperty::Channel); // Find an available version of this package to determine its source. - auto availablePackageVersion = packageMatch.Package->GetAvailableVersion({ "", version.get(), channel.get() }); + auto availablePackageVersion = GetAvailableVersionForInstalledPackage(context, packageMatch.Package, version, channel, includeVersions); if (!availablePackageVersion) { - availablePackageVersion = packageMatch.Package->GetLatestAvailableVersion(); - if (!availablePackageVersion) - { - // Report package not found and move to next package. - AICLI_LOG(CLI, Warning, << "No available version of package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] was found to export"); - context.Reporter.Warn() << Resource::String::InstalledPackageNotAvailable << ' ' << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << std::endl; - continue; - } - else - { - // Warn installed version is not available. - AICLI_LOG( - CLI, - Info, - << "Installed package version is not available." - << " Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Version [" << version << "], Channel [" << channel << "]" - << ". Found Version [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "], Channel [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "]"); - context.Reporter.Warn() - << Resource::String::InstalledPackageVersionNotAvailable - << ' ' << availablePackageVersion->GetProperty(PackageVersionProperty::Id) - << ' ' << version << ' ' << channel << std::endl; - } + // Report package not found and move to next package. + AICLI_LOG(CLI, Warning, << "No available version of package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] was found to export"); + context.Reporter.Warn() << Resource::String::InstalledPackageNotAvailable << ' ' << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << std::endl; + continue; } const auto& sourceDetails = availablePackageVersion->GetSource()->GetDetails(); @@ -109,7 +129,7 @@ namespace AppInstaller::CLI::Workflow // Take the Id from the available package because that is the one used in the source, // but take the exported version from the installed package if needed. - if (context.Args.Contains(Execution::Args::Type::IncludeVersions)) + if (includeVersions) { sourceItr->Packages.emplace_back( availablePackageVersion->GetProperty(PackageVersionProperty::Id),