diff --git a/doc/update.md b/doc/update.md index c8f0210a..075840cd 100644 --- a/doc/update.md +++ b/doc/update.md @@ -34,6 +34,32 @@ e.g., `wingetcreate update --urls '|x64|user' '|x64|machine' '|x86|user' '|x86|machine'` +### Installer URL arguments + +The following additional arguments can be provided with the installer URL(s): + +#### Format + +`'||...'` + +#### Override Architecture + +Winget-Create will attempt to determine the architecture of the installer package by performing a regex string match to identify the possible architecture in the installer url. If no match is found, Winget-Create will resort to obtaining the architecture from the downloaded installer. If Winget-Create fails to detect the architecture from the binary or the detected architecture does not match an architecture in the existing manifest, Winget-Create will fail to generate the manifest. In this case, you can explicitly provide the intended architecture and override the detected architecture using the following format: + +`'|'` + +#### Override Scope + +In case there are multiple installers with the same architecture, it may mean the same installer is available for multiple scopes. In this case, you can explicitly provide the installer scope in the update command using the following following argument format: + +`'|'` + +#### Display Version + +In some cases, the publisher of the package may use a different marketing version than the actual version written to Apps & Features. In this case, the manifest will contain `DisplayVersion` field. You can update the `DisplayVersion` field using the `--display-version` CLI arg if all installers use the same display version. If the display version differs for each installer, you can use following argument format: + +`'|'` + ## Usage Examples Search for an existing manifest and update the version: diff --git a/src/WingetCreateCLI/Commands/UpdateCommand.cs b/src/WingetCreateCLI/Commands/UpdateCommand.cs index e2bd101a..46168e6c 100644 --- a/src/WingetCreateCLI/Commands/UpdateCommand.cs +++ b/src/WingetCreateCLI/Commands/UpdateCommand.cs @@ -67,6 +67,12 @@ public static IEnumerable Examples [Option('v', "version", Required = false, HelpText = "Version_HelpText", ResourceType = typeof(Resources))] public string Version { get; set; } + /// + /// Gets or sets the new value used to update the display version field in the manifest. + /// + [Option('d', "display-version", Required = false, HelpText = "DisplayVersion_HelpText", ResourceType = typeof(Resources))] + public string DisplayVersion { get; set; } + /// /// Gets or sets the outputPath where the generated manifest file should be saved to. /// @@ -300,16 +306,57 @@ public async Task UpdateManifestsAutonomously(Manifests manifests) this.InstallerUrls = installerManifest.Installers.Select(i => i.InstallerUrl).Distinct().ToArray(); } - // Generate list of InstallerUpdate objects and parse out any specified architecture or scope overrides. - List installerMetadataList = this.ParseInstallerUrlsForOverrides(this.InstallerUrls.Select(i => i.Trim()).ToList()); + // Generate list of InstallerUpdate objects and parse out any specified installer URL arguments. + List installerMetadataList = this.ParseInstallerUrlsForArguments(this.InstallerUrls.Select(i => i.Trim()).ToList()); - // If the installer update list is null there was an issue when parsing for architecture or scope override. + // If the installer update list is null there was an issue when parsing for additional installer arguments. if (installerMetadataList == null) { return null; } - // Reassign list with parsed installer URLs without architecture or scope overrides. + if (!string.IsNullOrEmpty(this.DisplayVersion)) + { + // Use --display-version value if version was not provided as an argument. + foreach (InstallerMetadata installerUpdate in installerMetadataList) + { + if (string.IsNullOrEmpty(installerUpdate.DisplayVersion)) + { + installerUpdate.DisplayVersion = this.DisplayVersion; + } + } + } + + var originalAppsAndFeaturesEntries = installerManifest.Installers + .Where(i => i.AppsAndFeaturesEntries != null) + .SelectMany(i => i.AppsAndFeaturesEntries); + + int originalDisplayVersionCount = originalAppsAndFeaturesEntries + .Count(entry => entry.DisplayVersion != null); + + int newDisplayVersionCount = installerMetadataList + .Count(entry => entry.DisplayVersion != null); + + if (newDisplayVersionCount < originalDisplayVersionCount) + { + Logger.WarnLocalized(nameof(Resources.UnchangedDisplayVersion_Warning)); + } + + // Check if any single installer has multiple display versions in the original manifest. + bool installerHasMultipleDisplayVersions = originalAppsAndFeaturesEntries + .Where(entry => entry.DisplayVersion != null) + .GroupBy(entry => entry.DisplayVersion) + .Any(group => group.Count() > 1); + + // It is possible for a single installer to have multiple ARP entries having multiple display versions, + // but currently, we only take the primary ARP entry in the community repository. If such a case is detected, + // user will have to manually update the manifest. + if (installerHasMultipleDisplayVersions) + { + Logger.WarnLocalized(nameof(Resources.InstallerWithMultipleDisplayVersions_Warning)); + } + + // Reassign list with parsed installer URLs without installer URL arguments this.InstallerUrls = installerMetadataList.Select(x => x.InstallerUrl).ToList(); foreach (var installerUpdate in installerMetadataList) @@ -713,12 +760,15 @@ private string ObtainMatchingRelativeFilePath(string oldRelativeFilePath, string } /// - /// Parses the installer urls for any architecture or scope overrides. + /// Parses the installer urls for any additional arguments. /// - /// List of installer URLs to be parsed for architecture overrides. + /// List of installer URLs to be parsed for additional arguments. /// List of helper objects used for updating the installers. - private List ParseInstallerUrlsForOverrides(List installerUrlsToBeParsed) + private List ParseInstallerUrlsForArguments(List installerUrlsToBeParsed) { + // There can be at most 4 elements at one time (installerUrl|archOverride|scopeOverride|displayVersion) + const int MaxUrlArgumentLimit = 4; + List installerMetadataList = new List(); foreach (string item in installerUrlsToBeParsed) { @@ -726,26 +776,26 @@ private List ParseInstallerUrlsForOverrides(List inst if (item.Contains('|')) { - // '|' character indicates that an architecture override can be parsed from the installer. - string[] installerUrlOverride = item.Split('|'); + // '|' character indicates that user is providing additional arguments for the installer URL. + string[] installerUrlArguments = item.Split('|'); - // There can be at most 3 elements at one time (installerUrl|archOverride|scopeOverride) - if (installerUrlOverride.Length > 3) + if (installerUrlArguments.Length > MaxUrlArgumentLimit) { - Logger.ErrorLocalized(nameof(Resources.OverrideLimitExceeded_Error), item); + Logger.ErrorLocalized(nameof(Resources.ArgumentLimitExceeded_Error), item); return null; } - installerMetadata.InstallerUrl = installerUrlOverride[0]; + installerMetadata.InstallerUrl = installerUrlArguments[0]; bool archOverridePresent = false; bool scopeOverridePresent = false; + bool displayVersionPresent = false; - for (int i = 1; i < installerUrlOverride.Length; i++) + for (int i = 1; i < installerUrlArguments.Length; i++) { - string overrideString = installerUrlOverride[i]; - Architecture? overrideArch = overrideString.ToEnumOrDefault(); - Scope? overrideScope = overrideString.ToEnumOrDefault(); + string argumentString = installerUrlArguments[i]; + Architecture? overrideArch = argumentString.ToEnumOrDefault(); + Scope? overrideScope = argumentString.ToEnumOrDefault(); if (overrideArch.HasValue) { @@ -773,9 +823,16 @@ private List ParseInstallerUrlsForOverrides(List inst installerMetadata.OverrideScope = overrideScope.Value; } } + + // If value is not a convertible enum, it is assumed to be a display version. + else if (!string.IsNullOrEmpty(argumentString) && !displayVersionPresent) + { + displayVersionPresent = true; + installerMetadata.DisplayVersion = argumentString; + } else { - Logger.ErrorLocalized(nameof(Resources.UnableToParseOverride_Error), overrideString); + Logger.ErrorLocalized(nameof(Resources.UnableToParseArgument_Error), argumentString); return null; } } diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 092129ec..4a4e0a5a 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -222,6 +222,15 @@ public class Resources { } } + /// + /// Looks up a localized string similar to Too many arguments specified for the following installer URL: {0}. + /// + public static string ArgumentLimitExceeded_Error { + get { + return ResourceManager.GetString("ArgumentLimitExceeded_Error", resourceCulture); + } + } + /// /// Looks up a localized string similar to The package author. /// @@ -582,6 +591,15 @@ public class Resources { } } + /// + /// Looks up a localized string similar to Version to be used when updating the display version field. Version provided in the installer URL arguments will take precendence over this value.. + /// + public static string DisplayVersion_HelpText { + get { + return ResourceManager.GetString("DisplayVersion_HelpText", resourceCulture); + } + } + /// /// Looks up a localized string similar to The DisplayVersion registry value. /// @@ -1437,6 +1455,15 @@ public class Resources { } } + /// + /// Looks up a localized string similar to Single installer with multiple display versions detected. Winget-Create will only update the first DisplayVersion for a given installer.. + /// + public static string InstallerWithMultipleDisplayVersions_Warning { + get { + return ResourceManager.GetString("InstallerWithMultipleDisplayVersions_Warning", resourceCulture); + } + } + /// /// Looks up a localized string similar to The installer location redirection path. /// @@ -2130,15 +2157,6 @@ public class Resources { } } - /// - /// Looks up a localized string similar to Too many overrides specified for the following installer URL: {0}. - /// - public static string OverrideLimitExceeded_Error { - get { - return ResourceManager.GetString("OverrideLimitExceeded_Error", resourceCulture); - } - } - /// /// Looks up a localized string similar to Overriding {0} with architecture {1}. /// @@ -3184,11 +3202,11 @@ public class Resources { } /// - /// Looks up a localized string similar to Unable to parse the specified override {0}.. + /// Looks up a localized string similar to Unable to parse the specified argument {0}.. /// - public static string UnableToParseOverride_Error { + public static string UnableToParseArgument_Error { get { - return ResourceManager.GetString("UnableToParseOverride_Error", resourceCulture); + return ResourceManager.GetString("UnableToParseArgument_Error", resourceCulture); } } @@ -3201,6 +3219,15 @@ public class Resources { } } + /// + /// Looks up a localized string similar to Base manifest contains DisplayVersion that has not been updated. Use --display-version CLI arg or provide the version in the installer URL.. + /// + public static string UnchangedDisplayVersion_Warning { + get { + return ResourceManager.GetString("UnchangedDisplayVersion_Warning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unexpected error while loading settings. Please verify your settings by running the settings command.. /// @@ -3390,6 +3417,15 @@ public class Resources { } } + /// + /// Looks up a localized string similar to Using display version '{0}' for {1}. + /// + public static string UsingDisplayVersion_Message { + get { + return ResourceManager.GetString("UsingDisplayVersion_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please check and verify the usage of this command by passing in the --help flag.. /// diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 94069e0a..dcb09166 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -803,9 +803,9 @@ Multiple architectures detected. Only one architecture can be specified for an override. - - Unable to parse the specified override {0}. - {0} - represents the override string that failed to parse. + + Unable to parse the specified argument {0}. + {0} - represents the url argument that failed to parse. Override the architecture of an installer @@ -995,8 +995,8 @@ Multiple scopes detected. Only one scope can be specified for an override. - - Too many overrides specified for the following installer URL: {0} + + Too many arguments specified for the following installer URL: {0} {0} - represents the installer URL argument that the user is providing. The installer URL can be modified by appending '|scope|architecture' to override the detected values for that particular installer. @@ -1357,4 +1357,18 @@ Store a new GitHub token in your local cache + + Base manifest contains DisplayVersion that has not been updated. Use --display-version CLI arg or provide the version in the installer URL. + + + Version to be used when updating the display version field. Version provided in the installer URL arguments will take precendence over this value. + + + Using display version '{0}' for {1} + {0} - will be replaced with the display version provided in the installer URL +{1} - will be replaced by the installer URL + + + Single installer with multiple display versions detected. Winget-Create will only update the first DisplayVersion for a given installer. + \ No newline at end of file diff --git a/src/WingetCreateCore/Common/PackageParser.cs b/src/WingetCreateCore/Common/PackageParser.cs index fa90c3b6..c56312ed 100644 --- a/src/WingetCreateCore/Common/PackageParser.cs +++ b/src/WingetCreateCore/Common/PackageParser.cs @@ -19,6 +19,7 @@ namespace Microsoft.WingetCreateCore using Microsoft.Msix.Utils; using Microsoft.Msix.Utils.AppxPackaging; using Microsoft.Msix.Utils.AppxPackagingInterop; + using Microsoft.Msix.Utils.Logger; using Microsoft.WingetCreateCore.Common; using Microsoft.WingetCreateCore.Common.Exceptions; using Microsoft.WingetCreateCore.Models; @@ -81,7 +82,7 @@ public static void SetHttpMessageHandler(HttpMessageHandler httpMessageHandler) } /// - /// Parses packages for available metadata including Version, Publisher, Name, Descripion, License, etc. + /// Parses packages for available metadata including Version, Publisher, Name, Description, License, etc. /// /// List of . /// Wrapper object for manifest object models. @@ -237,6 +238,18 @@ public static void UpdateInstallerNodesAsync(List installerMe { foreach (var newInstaller in installerUpdate.NewInstallers) { + // Update DisplayVersion for each AppsAndFeaturesEntry + if (!string.IsNullOrEmpty(installerUpdate.DisplayVersion)) + { + newInstaller.AppsAndFeaturesEntries = new List + { + new AppsAndFeaturesEntry + { + DisplayVersion = installerUpdate.DisplayVersion, + }, + }; + } + // if the installerUpdate does not have a binary or url architecture specified, then just use what is specified in the installer. Installer existingInstallerMatch = FindInstallerMatch( newInstaller, @@ -439,10 +452,30 @@ private static void UpdateInstallerMetadata(Installer existingInstaller, Install existingInstaller.PackageFamilyName = newInstaller.PackageFamilyName ?? existingInstaller.PackageFamilyName; existingInstaller.NestedInstallerFiles = newInstaller.NestedInstallerFiles ?? existingInstaller.NestedInstallerFiles; existingInstaller.Platform = newInstaller.Platform ?? existingInstaller.Platform; + + if (existingInstaller.AppsAndFeaturesEntries != null && newInstaller.AppsAndFeaturesEntries != null) + { + // When --display-version is provided, AppsAndFeaturesEntries for the new installer will not be null + // and will contain a single entry. + string newDisplayVersion = newInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + + // Set DisplayVersion for each new installer if it exists in the corresponding existing installer. + foreach (var existingAppsAndFeaturesEntry in existingInstaller.AppsAndFeaturesEntries) + { + if (existingAppsAndFeaturesEntry.DisplayVersion != null) + { + existingAppsAndFeaturesEntry.DisplayVersion = newDisplayVersion ?? existingAppsAndFeaturesEntry.DisplayVersion; + + // Break on first match to avoid setting DisplayVersion for all entries. + // We do not support updating multiple DisplayVersions under the same installer. + break; + } + } + } } /// - /// Parses a package for available metadata including Version, Publisher, Name, Descripion, License, etc. + /// Parses a package for available metadata including Version, Publisher, Name, Description, License, etc. /// /// Helper class for storing an installer's metadata information. /// Wrapper object for manifest object models. diff --git a/src/WingetCreateCore/Models/InstallerMetadata.cs b/src/WingetCreateCore/Models/InstallerMetadata.cs index cfc548ba..e260b37c 100644 --- a/src/WingetCreateCore/Models/InstallerMetadata.cs +++ b/src/WingetCreateCore/Models/InstallerMetadata.cs @@ -46,6 +46,11 @@ public class InstallerMetadata /// public Scope? OverrideScope { get; set; } + /// + /// Gets or sets the display version specified as a CLI arg or an installer url argument. + /// + public string DisplayVersion { get; set; } + /// /// Gets or sets a value indicating whether the installer came from a zip. /// diff --git a/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.ArchAndScopeOverride.yaml b/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.AllUrlArguments.yaml similarity index 69% rename from src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.ArchAndScopeOverride.yaml rename to src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.AllUrlArguments.yaml index d1ecd9b6..f7d1071e 100644 --- a/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.ArchAndScopeOverride.yaml +++ b/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.AllUrlArguments.yaml @@ -1,19 +1,23 @@ -PackageIdentifier: TestPublisher.ArchAndScopeOverride +PackageIdentifier: TestPublisher.AllUrlArguments PackageVersion: 0.1.2 -PackageName: Arch and Scope override test +PackageName: Test for passing all supported url arguments Publisher: Test publisher License: MIT -ShortDescription: A manifest used to test the update command when overriding the architecture and scope of the provided installers. +ShortDescription: A manifest used to test the update command when providing all supported url arguments InstallerLocale: en-US Installers: - Architecture: arm InstallerUrl: https://fakedomain.com/WingetCreateTestExeInstaller.exe Scope: user InstallerType: exe + AppsAndFeaturesEntries: + - DisplayVersion: 1.0.0 InstallerSha256: A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC - Architecture: arm InstallerUrl: https://fakedomain.com/WingetCreateTestExeInstaller.exe Scope: machine + AppsAndFeaturesEntries: + - DisplayVersion: 2.0.0 InstallerType: exe InstallerSha256: A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC PackageLocale: en-US diff --git a/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.SingleInstallerWithMultipleDisplayVersions.yaml b/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.SingleInstallerWithMultipleDisplayVersions.yaml new file mode 100644 index 00000000..774def5b --- /dev/null +++ b/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.SingleInstallerWithMultipleDisplayVersions.yaml @@ -0,0 +1,25 @@ +PackageIdentifier: TestPublisher.SingleInstallerWithMultipleDisplayVersions +PackageVersion: 0.1.2 +PackageName: Test for a single installer with multiple display versions +Publisher: Test publisher +License: MIT +ShortDescription: A manifest used to test whether multiple display versions for a single installer warning is shown +InstallerLocale: en-US +Installers: + - Architecture: x64 + InstallerUrl: https://fakedomain.com/WingetCreateTestExeInstaller.exe + InstallerType: exe + AppsAndFeaturesEntries: + - DisplayVersion: 1.0.0 + - DisplayVersion: 2.0.0 + - DisplayVersion: 3.0.0 + InstallerSha256: A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC + - Architecture: x86 + InstallerUrl: https://fakedomain.com/WingetCreateTestExeInstaller.exe + AppsAndFeaturesEntries: + - DisplayVersion: 2.0.0 + InstallerType: exe + InstallerSha256: A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC +PackageLocale: en-US +ManifestType: singleton +ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.UpdateDisplayVersion.yaml b/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.UpdateDisplayVersion.yaml new file mode 100644 index 00000000..a4f3f9c2 --- /dev/null +++ b/src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.UpdateDisplayVersion.yaml @@ -0,0 +1,37 @@ +PackageIdentifier: TestPublisher.UpdateDisplayVersion +PackageVersion: 0.1.2 +PackageName: Test for updating display version through CLI arg and URL parameter +Publisher: Test publisher +License: MIT +ShortDescription: A manifest used to test whether the display version is correctly updated with the CLI arg and URL parameter +InstallerLocale: en-US +Installers: + # DisplayVersion: 1.0.0 + - Architecture: x64 + InstallerUrl: https://fakedomain.com/WingetCreateTestExeInstaller.exe + InstallerType: exe + AppsAndFeaturesEntries: + - DisplayVersion: 1.0.0 + InstallerSha256: A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC + # DisplayVersion: 2.0.0 + - Architecture: x86 + InstallerUrl: https://fakedomain.com/WingetCreateTestExeInstaller.exe + AppsAndFeaturesEntries: + - DisplayVersion: 2.0.0 + InstallerType: exe + InstallerSha256: A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC + # DisplayVersion: 3.0.0 + - Architecture: arm + InstallerUrl: https://fakedomain.com/WingetCreateTestExeInstaller.exe + AppsAndFeaturesEntries: + - DisplayVersion: 3.0.0 + InstallerType: exe + InstallerSha256: A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC + # No DisplayVersion + - Architecture: arm64 + InstallerUrl: https://fakedomain.com/WingetCreateTestExeInstaller.exe + InstallerType: exe + InstallerSha256: A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC +PackageLocale: en-US +ManifestType: singleton +ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/UpdateCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/UpdateCommandTests.cs index ad3d9cf5..eca41fc6 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/UpdateCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/UpdateCommandTests.cs @@ -174,26 +174,26 @@ public async Task UpdateRemovesEmptyFields() } /// - /// Verify that update command fails if there is a discrepency in the URL count. + /// Verify that update command fails if there is a discrepancy in the URL count. /// /// A representing the asynchronous unit test. [Test] - public async Task UpdateFailsWithInstallerUrlCountDiscrepency() + public async Task UpdateFailsWithInstallerUrlCountDiscrepancy() { TestUtils.InitializeMockDownloads(TestConstants.TestMsixInstaller); (UpdateCommand command, var initialManifestContent) = GetUpdateCommandAndManifestData(TestConstants.TestMultipleInstallerPackageIdentifier, null, this.tempPath, new[] { "fakeurl" }); var updatedManifests = await RunUpdateCommand(command, initialManifestContent); ClassicAssert.IsNull(updatedManifests, "Command should have failed"); string result = this.sw.ToString(); - Assert.That(result, Does.Contain(Resources.MultipleInstallerUpdateDiscrepancy_Error), "Installer discrepency error should be thrown"); + Assert.That(result, Does.Contain(Resources.MultipleInstallerUpdateDiscrepancy_Error), "Installer discrepancy error should be thrown"); } /// - /// Verify that update command fails if there is a discrepency in the package count. + /// Verify that update command fails if there is a discrepancy in the package count. /// /// A representing the asynchronous unit test. [Test] - public async Task UpdateFailsWithPackageCountDiscrepency() + public async Task UpdateFailsWithPackageCountDiscrepancy() { TestUtils.InitializeMockDownloads(TestConstants.TestMsixInstaller); (UpdateCommand command, var initialManifestContent) = GetUpdateCommandAndManifestData("TestPublisher.SingleMsixInExistingBundle", null, this.tempPath, null); @@ -204,7 +204,7 @@ public async Task UpdateFailsWithPackageCountDiscrepency() } /// - /// Verify that update command fails if there is a discrepency in the package types. + /// Verify that update command fails if there is a discrepancy in the package types. /// /// A representing the asynchronous unit test. [Test] @@ -351,52 +351,53 @@ public async Task BlockUpdateSubmissionsWithNoUpdate() } /// - /// Verfies that an error message is shown if the overriding architecture is invalid. + /// Verifies that an error message is shown if multiple architectures are specified for an override. /// /// A representing the result of the asynchronous operation. [Test] - public async Task UpdateWithArchitectureOverrideFailsParsing() + public async Task UpdateFailsOverrideWithMultipleArchitectures() { - string invalidArch = "fakeArch"; string installerUrl = $"https://fakedomain.com/{TestConstants.TestExeInstaller}"; (UpdateCommand badCommand, var manifests) = - GetUpdateCommandAndManifestData("TestPublisher.ArchitectureOverride", "1.2.3.4", this.tempPath, new[] { $"{installerUrl}|{invalidArch}" }); + GetUpdateCommandAndManifestData("TestPublisher.ArchitectureOverride", "1.2.3.4", this.tempPath, new[] { $"{installerUrl}|x86|ARM" }); var failedUpdateManifests = await RunUpdateCommand(badCommand, manifests); - ClassicAssert.IsNull(failedUpdateManifests, "Command should have failed due to invalid architecture specified for override."); + ClassicAssert.IsNull(failedUpdateManifests, "Command should have failed due to multiple architecture overrides specified for a single installer."); string result = this.sw.ToString(); - Assert.That(result, Does.Contain(string.Format(Resources.UnableToParseOverride_Error, invalidArch)), "Failed to show architecture override parsing error."); + Assert.That(result, Does.Contain(Resources.MultipleArchitectureOverride_Error), "Failed to show multiple architecture overrides error."); } /// - /// Verfies that an error message is shown if multiple architectures are specified for an override. + /// Verifies that an error message is shown if multiple architectures are specified for an override. /// /// A representing the result of the asynchronous operation. [Test] - public async Task UpdateFailsOverrideWithMultipleArchitectures() + public async Task UpdateFailsOverrideWithMultipleScopes() { string installerUrl = $"https://fakedomain.com/{TestConstants.TestExeInstaller}"; (UpdateCommand badCommand, var manifests) = - GetUpdateCommandAndManifestData("TestPublisher.ArchitectureOverride", "1.2.3.4", this.tempPath, new[] { $"{installerUrl}|x86|ARM" }); + GetUpdateCommandAndManifestData("TestPublisher.ScopeOverride", "1.2.3.4", this.tempPath, new[] { $"{installerUrl}|user|machine" }); var failedUpdateManifests = await RunUpdateCommand(badCommand, manifests); - ClassicAssert.IsNull(failedUpdateManifests, "Command should have failed due to multiple architecture overrides specified for a single installer."); + ClassicAssert.IsNull(failedUpdateManifests, "Command should have failed due to multiple scope overrides specified for a single installer."); string result = this.sw.ToString(); - Assert.That(result, Does.Contain(Resources.MultipleArchitectureOverride_Error), "Failed to show multiple architecture overrides error."); + Assert.That(result, Does.Contain(Resources.MultipleScopeOverride_Error), "Failed to show multiple scope overrides error."); } /// - /// Verfies that an error message is shown if multiple architectures are specified for an override. + /// Verifies that an error message is shown if multiple architectures are specified for an override. /// /// A representing the result of the asynchronous operation. [Test] - public async Task UpdateFailsOverrideWithMultipleScopes() + public async Task UpdateFailsWithMultipleDisplayVersions() { string installerUrl = $"https://fakedomain.com/{TestConstants.TestExeInstaller}"; + string displayVersion1 = "3.4"; + string displayVersion2 = "1.2"; (UpdateCommand badCommand, var manifests) = - GetUpdateCommandAndManifestData("TestPublisher.ScopeOverride", "1.2.3.4", this.tempPath, new[] { $"{installerUrl}|user|machine" }); + GetUpdateCommandAndManifestData("TestPublisher.ScopeOverride", "1.2.3.4", this.tempPath, new[] { $"{installerUrl}|{displayVersion1}|{displayVersion2}" }); var failedUpdateManifests = await RunUpdateCommand(badCommand, manifests); - ClassicAssert.IsNull(failedUpdateManifests, "Command should have failed due to multiple scope overrides specified for a single installer."); + ClassicAssert.IsNull(failedUpdateManifests, "Command should have failed due to multiple display versions specified for a single installer."); string result = this.sw.ToString(); - Assert.That(result, Does.Contain(Resources.MultipleScopeOverride_Error), "Failed to show multiple scope overrides error."); + Assert.That(result, Does.Contain(string.Format(Resources.UnableToParseArgument_Error, displayVersion2)), "Failed to show parsing error due to multiple string overrides."); } /// @@ -468,42 +469,210 @@ public async Task UpdateWithScopeOverrides() } /// - /// Verifies that the overriding both the architecture and scope is supported and the update succeeds. + /// Verifies that the providing all supported URL arguments will result in a successful update. /// /// A representing the result of the asynchronous operation. [Test] - public async Task UpdateWithArchAndScopeOverrides() + public async Task UpdateWithAllUrlArguments() { TestUtils.InitializeMockDownload(); TestUtils.SetMockHttpResponseContent(TestConstants.TestExeInstaller); string testInstallerUrl = $"https://fakedomain.com/{TestConstants.TestExeInstaller}"; + string newDisplayVersion1 = "2.3"; + string newDisplayVersion2 = "4.5"; // Test without architecture override should fail. (UpdateCommand badCommand, var manifests) = - GetUpdateCommandAndManifestData("TestPublisher.ArchAndScopeOverride", "1.2.3.4", this.tempPath, new[] { testInstallerUrl, testInstallerUrl }); + GetUpdateCommandAndManifestData("TestPublisher.AllUrlArguments", "1.2.3.4", this.tempPath, new[] { testInstallerUrl, testInstallerUrl }); var failedUpdateManifests = await RunUpdateCommand(badCommand, manifests); ClassicAssert.IsNull(failedUpdateManifests, "Command should have failed without overrides"); - // Test with scope and architecture override should pass. + // Test with scope and architecture override should pass. DisplayVersion should also be updated. (UpdateCommand goodCommand, var initialManifestContent) = - GetUpdateCommandAndManifestData("TestPublisher.ArchAndScopeOverride", "1.2.3.4", this.tempPath, new[] { $"{testInstallerUrl}|user|arm", $"{testInstallerUrl}|arm|machine" }); + GetUpdateCommandAndManifestData("TestPublisher.AllUrlArguments", "1.2.3.4", this.tempPath, new[] { $"{testInstallerUrl}|user|arm|{newDisplayVersion1}", $"{testInstallerUrl}|arm|machine|{newDisplayVersion2}" }); var initialManifests = Serialization.DeserializeManifestContents(initialManifestContent); var updatedManifests = await RunUpdateCommand(goodCommand, initialManifestContent); ClassicAssert.IsNotNull(updatedManifests, "Command should have succeeded as installers should be overrided with architecture and scope."); var initialFirstInstaller = initialManifests.SingletonManifest.Installers[0]; var initialSecondInstaller = initialManifests.SingletonManifest.Installers[1]; + var initialFirstDisplayVersion = initialFirstInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + var initialSecondDisplayVersion = initialSecondInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; var updatedFirstInstaller = updatedManifests.InstallerManifest.Installers[0]; var updatedSecondInstaller = updatedManifests.InstallerManifest.Installers[1]; + var updatedFirstDisplayVersion = updatedFirstInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + var updatedSecondDisplayVersion = updatedSecondInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; ClassicAssert.AreEqual(Scope.User, updatedFirstInstaller.Scope, $"Scope should be preserved."); ClassicAssert.AreEqual(Scope.Machine, updatedSecondInstaller.Scope, $"Scope should be preserved."); ClassicAssert.AreEqual(Architecture.Arm, updatedFirstInstaller.Architecture, $"Architecture should be preserved."); ClassicAssert.AreEqual(Architecture.Arm, updatedSecondInstaller.Architecture, $"Architecture should be preserved."); + ClassicAssert.AreEqual(newDisplayVersion1, updatedFirstDisplayVersion, $"DisplayVersion should be updated."); + ClassicAssert.AreEqual(newDisplayVersion2, updatedSecondDisplayVersion, $"DisplayVersion should be updated."); ClassicAssert.AreNotEqual(initialFirstInstaller.InstallerSha256, updatedFirstInstaller.InstallerSha256, "InstallerSha256 should be updated"); ClassicAssert.AreNotEqual(initialSecondInstaller.InstallerSha256, updatedSecondInstaller.InstallerSha256, "InstallerSha256 should be updated"); + ClassicAssert.AreNotEqual(initialFirstDisplayVersion, updatedFirstDisplayVersion, "DisplayVersion should be updated"); + ClassicAssert.AreNotEqual(initialSecondDisplayVersion, updatedSecondDisplayVersion, "DisplayVersion should be updated"); + } + + /// + /// Verifies that display version provided as CLI arg and in the URL arguments correctly updates the display version in the manifest. + /// + /// A representing the result of the asynchronous operation. + [Test] + public async Task UpdateDisplayVersion() + { + TestUtils.InitializeMockDownload(); + TestUtils.SetMockHttpResponseContent(TestConstants.TestExeInstaller); + string testInstallerUrl = $"https://fakedomain.com/{TestConstants.TestExeInstaller}"; + string displayVersionForCLIArg = "2.3"; + string newDisplayVersionForUrl1 = "4.5"; + string newDisplayVersionForUrl2 = "6.7"; + + var initialManifestContent = TestUtils.GetInitialManifestContent("TestPublisher.UpdateDisplayVersion.yaml"); + UpdateCommand command = new UpdateCommand + { + Id = "TestPublisher.UpdateDisplayVersion", + Version = "1.2.3.4", + InstallerUrls = new[] + { + $"{testInstallerUrl}|x64|{newDisplayVersionForUrl1}", + $"{testInstallerUrl}|x86|{newDisplayVersionForUrl2}", + $"{testInstallerUrl}|arm", + $"{testInstallerUrl}|arm64", + }, + DisplayVersion = displayVersionForCLIArg, + }; + var initialManifests = Serialization.DeserializeManifestContents(initialManifestContent); + var updatedManifests = await RunUpdateCommand(command, initialManifestContent); + ClassicAssert.IsNotNull(updatedManifests, "Command should have succeeded."); + + // Initial installers + var initialFirstInstaller = initialManifests.SingletonManifest.Installers[0]; + var initialSecondInstaller = initialManifests.SingletonManifest.Installers[1]; + var initialThirdInstaller = initialManifests.SingletonManifest.Installers[2]; + var initialFourthInstaller = initialManifests.SingletonManifest.Installers[3]; + + // Initial display versions (fourth installer does not have a display version) + var initialFirstDisplayVersion = initialFirstInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + var initialSecondDisplayVersion = initialSecondInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + var initialThirdDisplayVersion = initialThirdInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + + // Updated installers + var updatedFirstInstaller = updatedManifests.InstallerManifest.Installers[0]; + var updatedSecondInstaller = updatedManifests.InstallerManifest.Installers[1]; + var updatedThirdInstaller = updatedManifests.InstallerManifest.Installers[2]; + var updatedFourthInstaller = updatedManifests.InstallerManifest.Installers[3]; + + // Updated display versions (fourth installer does not have a display version) + var updatedFirstDisplayVersion = updatedFirstInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + var updatedSecondDisplayVersion = updatedSecondInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + var updatedThirdDisplayVersion = updatedThirdInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + + string result = this.sw.ToString(); + Assert.That(result, Does.Not.Contain(Resources.UnchangedDisplayVersion_Warning), "Unchanged display version warning should not be shown."); + Assert.That(result, Does.Not.Contain(Resources.InstallerWithMultipleDisplayVersions_Warning), "Single installer with multiple display versions warning should not be shown."); + + ClassicAssert.AreEqual(newDisplayVersionForUrl1, updatedFirstDisplayVersion, $"DisplayVersion should be updated by the value in the URL argument."); + ClassicAssert.AreEqual(newDisplayVersionForUrl2, updatedSecondDisplayVersion, $"DisplayVersion should be updated by the value in the URL argument."); + ClassicAssert.AreEqual(displayVersionForCLIArg, updatedThirdDisplayVersion, $"DisplayVersion should be updated by the value in the CLI arg"); + ClassicAssert.IsNull(updatedFourthInstaller.AppsAndFeaturesEntries); + + ClassicAssert.AreNotEqual(initialFirstInstaller.InstallerSha256, updatedFirstInstaller.InstallerSha256, "InstallerSha256 should be updated"); + ClassicAssert.AreNotEqual(initialSecondInstaller.InstallerSha256, updatedSecondInstaller.InstallerSha256, "InstallerSha256 should be updated"); + ClassicAssert.AreNotEqual(initialThirdInstaller.InstallerSha256, updatedThirdInstaller.InstallerSha256, "InstallerSha256 should be updated"); + ClassicAssert.AreNotEqual(initialFourthInstaller.InstallerSha256, updatedFourthInstaller.InstallerSha256, "InstallerSha256 should be updated"); + ClassicAssert.AreNotEqual(initialFirstDisplayVersion, updatedFirstDisplayVersion, "DisplayVersion should be updated"); + ClassicAssert.AreNotEqual(initialSecondDisplayVersion, updatedSecondDisplayVersion, "DisplayVersion should be updated"); + ClassicAssert.AreNotEqual(updatedThirdDisplayVersion, initialThirdDisplayVersion, "DisplayVersion should be updated"); + } + + /// + /// Verifies that update commands shows a warning if a single installer has multiple display versions. + /// + /// A representing the result of the asynchronous operation. + [Test] + public async Task UpdateShowsWarningForSingleInstallerWithMultipleDisplayVersions() + { + TestUtils.InitializeMockDownload(); + TestUtils.SetMockHttpResponseContent(TestConstants.TestExeInstaller); + string testInstallerUrl = $"https://fakedomain.com/{TestConstants.TestExeInstaller}"; + string newDisplayVersionForUrl1 = "4.5"; + string newDisplayVersionForUrl2 = "6.7"; + + (UpdateCommand command, var initialManifestContent) = + GetUpdateCommandAndManifestData("TestPublisher.SingleInstallerWithMultipleDisplayVersions", null, this.tempPath, new[] + { + $"{testInstallerUrl}|x64|{newDisplayVersionForUrl1}", + $"{testInstallerUrl}|x86|{newDisplayVersionForUrl2}", + }); + + var originalManifests = Serialization.DeserializeManifestContents(initialManifestContent); + var originalInstallers = originalManifests.SingletonManifest.Installers; + var updatedManifests = await RunUpdateCommand(command, initialManifestContent); + ClassicAssert.IsNotNull(updatedManifests, "Command should have succeeded."); + + string result = this.sw.ToString(); + Assert.That(result, Does.Contain(Resources.InstallerWithMultipleDisplayVersions_Warning), "Single installer with multiple display versions warning should be shown."); + + // Get original display versions for a single installer + var originalFirstDisplayVersion = originalInstallers[0].AppsAndFeaturesEntries[0].DisplayVersion; + var originalSecondDisplayVersion = originalInstallers[0].AppsAndFeaturesEntries[1].DisplayVersion; + var originalThirdDisplayVersion = originalInstallers[0].AppsAndFeaturesEntries[2].DisplayVersion; + + var updatedInstaller = updatedManifests.InstallerManifest.Installers[0]; + var updatedFirstDisplayVersion = updatedInstaller.AppsAndFeaturesEntries[0].DisplayVersion; + var updatedSecondDisplayVersion = updatedInstaller.AppsAndFeaturesEntries[1].DisplayVersion; + var updatedThirdDisplayVersion = updatedInstaller.AppsAndFeaturesEntries[2].DisplayVersion; + + // Winget-Create should only update the display version for the first entry and leave the rest unchanged. + ClassicAssert.AreEqual(newDisplayVersionForUrl1, updatedFirstDisplayVersion, "DisplayVersion should be updated by the value in the URL argument."); + ClassicAssert.AreEqual(originalSecondDisplayVersion, updatedSecondDisplayVersion, "DisplayVersion should remain same."); + ClassicAssert.AreEqual(originalThirdDisplayVersion, updatedThirdDisplayVersion, "DisplayVersion should remain same."); + } + + /// + /// Verifies that update commands shows a warning if the display version is unchanged for an installer. + /// + /// A representing the result of the asynchronous operation. + [Test] + public async Task UpdateShowsWarningForUnchangedDisplayVersion() + { + TestUtils.InitializeMockDownload(); + TestUtils.SetMockHttpResponseContent(TestConstants.TestExeInstaller); + string testInstallerUrl = $"https://fakedomain.com/{TestConstants.TestExeInstaller}"; + string newDisplayVersionForUrl1 = "4.5"; + string newDisplayVersionForUrl2 = "6.7"; + + var initialManifestContent = TestUtils.GetInitialManifestContent("TestPublisher.UpdateDisplayVersion.yaml"); + UpdateCommand command = new UpdateCommand + { + Id = "TestPublisher.UpdateDisplayVersion", + Version = "1.2.3.4", + InstallerUrls = new[] + { + $"{testInstallerUrl}|x64|{newDisplayVersionForUrl1}", + $"{testInstallerUrl}|x86|{newDisplayVersionForUrl2}", + $"{testInstallerUrl}|arm", + $"{testInstallerUrl}|arm64", + }, + }; + var initialManifests = Serialization.DeserializeManifestContents(initialManifestContent); + var updatedManifests = await RunUpdateCommand(command, initialManifestContent); + ClassicAssert.IsNotNull(updatedManifests, "Command should have succeeded."); + + var initialThirdInstaller = initialManifests.SingletonManifest.Installers[2]; + var initialThirdDisplayVersion = initialThirdInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + + var updatedThirdInstaller = updatedManifests.InstallerManifest.Installers[2]; + var updatedThirdDisplayVersion = updatedThirdInstaller.AppsAndFeaturesEntries.FirstOrDefault().DisplayVersion; + + // DisplayVersion unchanged for third installer + ClassicAssert.AreEqual(initialThirdDisplayVersion, updatedThirdDisplayVersion, $"DisplayVersion should remain same."); + string result = this.sw.ToString(); + Assert.That(result, Does.Contain(Resources.UnchangedDisplayVersion_Warning), "Unchanged display version warning should be shown."); } /// @@ -561,14 +730,14 @@ public async Task UpdateWithArchitectureOverrideFailsWithErrorMessage() public async Task NumberOfOverridesExceeded() { string installerUrl = $"https://fakedomain.com/{TestConstants.TestExeInstaller}"; - string installerUrlOverride = $"{installerUrl}|x64|user|test"; + string installerUrlOverride = $"{installerUrl}|x64|user|1.3.4|test"; TestUtils.InitializeMockDownloads(TestConstants.TestExeInstaller); (UpdateCommand command, var initialManifestContent) = GetUpdateCommandAndManifestData("TestPublisher.ArchitectureOverride", "1.2.3.4", this.tempPath, new[] { installerUrlOverride }); var updatedManifests = await RunUpdateCommand(command, initialManifestContent); ClassicAssert.IsNull(updatedManifests, "Command should have failed"); string result = this.sw.ToString(); - Assert.That(result, Does.Contain(string.Format(Resources.OverrideLimitExceeded_Error, installerUrlOverride)), "Failed to show warning for override limit exceeded."); + Assert.That(result, Does.Contain(string.Format(Resources.ArgumentLimitExceeded_Error, installerUrlOverride)), "Failed to show error for argument limit exceeded."); } /// @@ -1262,7 +1431,7 @@ public async Task UpdateRetainsNonNullInstallerFields() ClassicAssert.IsTrue(firstInstaller.AppsAndFeaturesEntries[0].ProductCode == "TestProductCode1", "AppsAndFeaturesEntries ProductCode for the first installer should be copied over from root"); ClassicAssert.IsNotNull(firstInstaller.Platform, "Platform for the first installer should not be null"); ClassicAssert.IsTrue(firstInstaller.Platform[0] == Platform.Windows_Desktop, "Platform for the first installer should be copied over from root"); - ClassicAssert.IsNotNull(firstInstaller.ExpectedReturnCodes, "ExpectedReturnCodes afor the first installer should not be null"); + ClassicAssert.IsNotNull(firstInstaller.ExpectedReturnCodes, "ExpectedReturnCodes for the first installer should not be null"); ClassicAssert.IsTrue(firstInstaller.ExpectedReturnCodes[0].InstallerReturnCode == 1001, "ExpectedReturnCodes InstallerReturnCode for the first installer should be copied over from root"); ClassicAssert.IsNotNull(firstInstaller.Commands, "Commands for the first installer should not be null"); ClassicAssert.IsTrue(firstInstaller.Commands[0] == "fakeCommand1", "Commands for the first installer should be copied over from root"); diff --git a/src/WingetCreateTests/WingetCreateTests/WingetCreateTests.csproj b/src/WingetCreateTests/WingetCreateTests/WingetCreateTests.csproj index a4e34738..5f9040d4 100644 --- a/src/WingetCreateTests/WingetCreateTests/WingetCreateTests.csproj +++ b/src/WingetCreateTests/WingetCreateTests/WingetCreateTests.csproj @@ -45,6 +45,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -78,7 +84,7 @@ PreserveNewest - + PreserveNewest