diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt
index 121b0fca54..5c3827e284 100644
--- a/.github/actions/spelling/allow.txt
+++ b/.github/actions/spelling/allow.txt
@@ -450,6 +450,7 @@ VOS
vso
wapproj
wchar
+wcout
wcsicmp
webpage
wekyb
@@ -465,11 +466,14 @@ winsqlite
wix
wmain
woah
+wofstream
workflow
+wostream
wpfn
wrl
WStr
wstring
+wstringstream
www
xamarin
xlang
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index 537c02caf7..cdba6a4d0d 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -245,6 +245,9 @@ usersettingstest
USHORT
Utils
UWP
+validator
+valijson
+valueiterator
vamus
VERSI
VERSIE
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/schemas/JSON/packages/packages.schema.1.0.json b/schemas/JSON/packages/packages.schema.1.0.json
new file mode 100644
index 0000000000..9efff5ea4e
--- /dev/null
+++ b/schemas/JSON/packages/packages.schema.1.0.json
@@ -0,0 +1,97 @@
+{
+ "$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",
+
+ "type": "object",
+ "required": [ "WinGetVersion", "Sources" ],
+ "additionalProperties": true,
+
+ "properties": {
+ "WinGetVersion": {
+ "description": "Version of winget that generated this file",
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]\\.[0-9]$"
+ },
+
+ "CreationDate": {
+ "description": "Date when this list was generated",
+ "type": "string",
+ "format": "date-time"
+ },
+
+ "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": [ "SourceDetails", "Packages" ],
+ "additionalProperties": true,
+
+ "properties": {
+ "SourceDetails": {
+ "description": "Details about this source",
+ "type": "object",
+ "required": [ "Name", "Identifier", "Argument", "Type" ],
+ "additionalProperties": true,
+
+ "properties": {
+ "Name": {
+ "description": "Name of the source",
+ "type": "string"
+ },
+
+ "Identifier": {
+ "description": "Identifier for the source",
+ "type": "string"
+ },
+
+ "Argument": {
+ "description": "Argument used to install the source",
+ "type": "string"
+ },
+
+ "Type": {
+ "description": "Type of the source",
+ "type": "string"
+ }
+ }
+ },
+
+ "Packages": {
+ "description": "Packages installed from this source",
+ "type": "array",
+ "required": [ "Id" ],
+ "minItems": 1,
+
+ "items": {
+ "description": "A package to be installed from this source",
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "Id": {
+ "description": "Package ID",
+ "type": "string"
+ },
+
+ "Version": {
+ "description": "Package version",
+ "type": "string"
+ },
+
+ "Channel": {
+ "description": "Package channel",
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/doc/settings.schema.json b/schemas/JSON/settings/settings.schema.0.2.json
similarity index 94%
rename from doc/settings.schema.json
rename to schemas/JSON/settings/settings.schema.0.2.json
index 45072807b1..16f43b6a02 100644
--- a/doc/settings.schema.json
+++ b/schemas/JSON/settings/settings.schema.0.2.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..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}
@@ -141,6 +137,21 @@
Workflows
+
+ Commands
+
+
+ Header Files
+
+
+ Workflows
+
+
+ Commands
+
+
+ Header Files
+
@@ -248,6 +259,18 @@
Workflows
+
+ Commands
+
+
+ Source Files
+
+
+ Workflows
+
+
+ Commands
+
diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp
new file mode 100644
index 0000000000..15d0923281
--- /dev/null
+++ b/src/AppInstallerCLICore/Commands/ExportCommand.cpp
@@ -0,0 +1,66 @@
+// 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{ "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 },
+ };
+ }
+
+ 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;
+ }
+
+ if (valueType == Execution::Args::Type::Source)
+ {
+ context << Workflow::CompleteSourceName;
+ return;
+ }
+ }
+
+ std::string ExportCommand::HelpLink() const
+ {
+ // TODO: point to correct location
+ return "https://aka.ms/winget-command-export";
+ }
+
+ 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/ExportCommand.h b/src/AppInstallerCLICore/Commands/ExportCommand.h
new file mode 100644
index 0000000000..0c11977b4a
--- /dev/null
+++ b/src/AppInstallerCLICore/Commands/ExportCommand.h
@@ -0,0 +1,25 @@
+// 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 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..7d7c9c2a19
--- /dev/null
+++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp
@@ -0,0 +1,52 @@
+// 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{ "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 },
+ };
+ }
+
+ 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::ExecuteInternal(Execution::Context& context) const
+ {
+ context <<
+ Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) <<
+ Workflow::VerifyFile(Execution::Args::Type::ImportFile) <<
+ Workflow::ReadImportFile <<
+ Workflow::OpenSourcesForImport <<
+ Workflow::OpenPredefinedSource(Repository::PredefinedSource::Installed) <<
+ Workflow::SearchPackagesForImport <<
+ Workflow::ReportExecutionStage(Workflow::ExecutionStage::Execution) <<
+ Workflow::InstallMultiple;
+ }
+}
diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.h b/src/AppInstallerCLICore/Commands/ImportCommand.h
new file mode 100644
index 0000000000..d2a58db14c
--- /dev/null
+++ b/src/AppInstallerCLICore/Commands/ImportCommand.h
@@ -0,0 +1,23 @@
+// 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 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..eb0e67fcea 100644
--- a/src/AppInstallerCLICore/ExecutionArgs.h
+++ b/src/AppInstallerCLICore/ExecutionArgs.h
@@ -55,6 +55,15 @@ namespace AppInstaller::CLI::Execution
CommandLine,
Position,
+ // Export Command
+ OutputFile,
+ IncludeVersions,
+
+ // Import Command
+ ImportFile,
+ IgnoreUnavailable,
+ IgnoreVersions,
+
// 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..2ec8f7a927 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
+
+
+
+
+
+
+
+
diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs
index abce1b5929..50d9781fd9 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;
@@ -104,6 +107,19 @@ 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_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);
}
}
}
diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs
new file mode 100644
index 0000000000..912c7cc083
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs
@@ -0,0 +1,133 @@
+// 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);
+ CleanupTestExe();
+ }
+
+ [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"));
+ 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")}");
+ Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
+ Assert.True(result.StdOut.Contains("Package is already installed"));
+ Assert.False(VerifyTestExeInstalled());
+ UninstallTestExe();
+ }
+
+ [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", Constants.ExeInstallerPackageId);
+
+ var jsonFile = TestCommon.GetRandomTestFile(".json");
+ TestCommon.RunAICLICommand("export", $"{jsonFile} -s TestSource");
+
+ // 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, Constants.TestExeInstalledFileName));
+ }
+
+ private void UninstallTestExe()
+ {
+ ConfigureFeature("uninstall", true);
+ TestCommon.RunAICLICommand("uninstall", Constants.ExeInstallerPackageId);
+ }
+
+ private void CleanupTestExe()
+ {
+ UninstallTestExe();
+ 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;
}
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..317c9f299b
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.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": "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..5ba77b275b
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.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": "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..13c63b2ce1
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.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": "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..f513feaa9a
--- /dev/null
+++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.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": "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/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/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
index 456fae16e0..e251ade0c8 100644
--- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
+++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
@@ -756,4 +756,59 @@ They can be configured through the settings file 'winget settings'.
Uninstall failed with exit code:
+
+ Exports a list of the installed packages
+
+
+ Installs all the packages listed in a file.
+
+
+ Installs all the packages in a file
+
+
+ File where the result is to be written
+
+
+ File describing the packages to install
+
+
+ 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
+
+
+ JSON file is not valid
+
+
+ Package is already installed:
+
+
+ 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/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
index 251385568a..22288f3f7a 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
@@ -186,6 +186,7 @@
+
@@ -233,6 +234,27 @@
true
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
index 858f4f4700..74416c87d6 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
@@ -113,6 +113,9 @@
Source Files
+
+ Source Files
+
Source Files
@@ -339,6 +342,27 @@
TestData
+
+ TestData
+
+
+ TestData
+
+
+ TestData
+
+
+ TestData
+
+
+ TestData
+
+
+ TestData
+
+
+ TestData
+
TestData
diff --git a/src/AppInstallerCLITests/PackageCollection.cpp b/src/AppInstallerCLITests/PackageCollection.cpp
new file mode 100644
index 0000000000..ce549d562e
--- /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_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";
+
+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_v1_0);
+ 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.schema.1.0.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::TryParseJson(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.schema.1.0.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::TryParseJson(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.schema.1.0.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::TryParseJson(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/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..4ea305296a
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://aka.ms/winget-packages.schema.1.0.json",
+ "CreationDate": "2021-01-01T12:00:00.000",
+ "Sources": [
+ {
+ "Packages": [
+ {
+ "Id": "AppInstallerCliTest.TestExeInstaller",
+ "Version": "2.0.0.0"
+ },
+ {
+ "Id": "MissingPackage",
+ "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..4d2b076e5a
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.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": "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..213cc6eff2
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.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": "//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-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
new file mode 100644
index 0000000000..42c46904d6
--- /dev/null
+++ b/src/AppInstallerCLITests/TestData/ImportFile-Good.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://aka.ms/winget-packages.schema.1.0.json",
+ "CreationDate": "2021-01-01T12:00:00.000",
+ "Sources": [
+ {
+ "Packages": [
+ {
+ "Id": "AppInstallerCliTest.TestExeInstaller",
+ "Version": "2.0.0.0"
+ },
+ {
+ "Id": "AppInstallerCliTest.TestMsixInstaller",
+ "Version": "2.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..b98d7084c7 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,240 @@ 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().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)
+ {
+ 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-AlreadyInstalled.json").GetPath().string());
+
+ 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_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");
+
+ 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_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);
+}
+
+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());
+
+ 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());
+
+ // Command should have failed
+ 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({});
+ // 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)
{
std::filesystem::path motwFile(testFile);
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/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp
index ce849d984a..3052b82527 100644
--- a/src/AppInstallerCommonCore/Errors.cpp
+++ b/src/AppInstallerCommonCore/Errors.cpp
@@ -107,12 +107,20 @@ 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_ICU_BREAK_ITERATOR_ERROR:
return "ICU break iterator error";
case APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR:
return "ICU casemap error";
case APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR:
return "ICU regex error";
+ 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/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/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();
diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h
index 8cb137bb97..33cc55e8a5 100644
--- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h
+++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h
@@ -64,6 +64,9 @@
#define APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR ((HRESULT)0x8A150031)
#define APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR ((HRESULT)0x8A150032)
#define APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR ((HRESULT)0x8A150033)
+#define APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED ((HRESULT)0x8a150034)
+#define APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND ((HRESULT)0x8a150035)
+#define APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE ((HRESULT)0x8a150036)
namespace AppInstaller
{
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)
diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp
index a48d4de937..cdf0a64c94 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
@@ -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, CompositeSearchBehavior searchBehavior)
{
m_installedSource = std::move(source);
+ m_searchBehavior = searchBehavior;
}
// An installed search first finds all installed packages that match the request, then correlates with available sources.
@@ -466,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
@@ -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_searchBehavior == CompositeSearchBehavior::AllPackages && !foundInstalledMatch)
+ {
+ result.Matches.push_back(std::move(match));
+ }
}
SortResultMatches(result.Matches);
diff --git a/src/AppInstallerRepositoryCore/CompositeSource.h b/src/AppInstallerRepositoryCore/CompositeSource.h
index 905c89a98a..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);
+ 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;
- std::string m_identifier;
+ CompositeSearchBehavior m_searchBehavior;
};
}
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 86176a0220..c938c9faba 100644
--- a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h
+++ b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h
@@ -44,9 +44,12 @@ 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 source's unique identifier.
+ std::string Identifier;
+
// The last time that this source was updated.
std::chrono::system_clock::time_point LastUpdateTime = {};
@@ -124,8 +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.
- 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,
+ 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 5471d0653a..1aedf1e5ac 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;
}
@@ -374,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);
@@ -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);
}
@@ -724,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, CompositeSearchBehavior searchBehavior)
{
std::shared_ptr result = std::dynamic_pointer_cast(availableSource);
@@ -734,7 +742,7 @@ namespace AppInstaller::Repository
result->AddAvailableSource(availableSource);
}
- result->SetInstalledSource(installedSource);
+ result->SetInstalledSource(installedSource, searchBehavior);
return result;
}
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;
}