Skip to content

Commit

Permalink
Support updating DisplayVersion in autonomous update (#520)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdanish-kh authored Jul 8, 2024
1 parent 8725a01 commit 7826f51
Show file tree
Hide file tree
Showing 11 changed files with 483 additions and 71 deletions.
26 changes: 26 additions & 0 deletions doc/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ e.g.,

`wingetcreate update <PackageIdentifier> --urls '<InstallerUrl1>|x64|user' '<InstallerUrl1>|x64|machine' '<InstallerUrl2>|x86|user' '<InstallerUrl2>|x86|machine'`

### Installer URL arguments

The following additional arguments can be provided with the installer URL(s):

#### Format

`'<InstallerUrl>|<Argument1>|<Argument2>...'`

#### 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:

`'<InstallerUrl>|<InstallerArchitecture>'`

#### 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:

`'<InstallerUrl>|<InstallerScope>'`

#### 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:

`'<InstallerUrl>|<DisplayVersion>'`

## Usage Examples

Search for an existing manifest and update the version:
Expand Down
93 changes: 75 additions & 18 deletions src/WingetCreateCLI/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ public static IEnumerable<Example> Examples
[Option('v', "version", Required = false, HelpText = "Version_HelpText", ResourceType = typeof(Resources))]
public string Version { get; set; }

/// <summary>
/// Gets or sets the new value used to update the display version field in the manifest.
/// </summary>
[Option('d', "display-version", Required = false, HelpText = "DisplayVersion_HelpText", ResourceType = typeof(Resources))]
public string DisplayVersion { get; set; }

/// <summary>
/// Gets or sets the outputPath where the generated manifest file should be saved to.
/// </summary>
Expand Down Expand Up @@ -300,16 +306,57 @@ public async Task<Manifests> 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<InstallerMetadata> installerMetadataList = this.ParseInstallerUrlsForOverrides(this.InstallerUrls.Select(i => i.Trim()).ToList());
// Generate list of InstallerUpdate objects and parse out any specified installer URL arguments.
List<InstallerMetadata> 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)
Expand Down Expand Up @@ -713,39 +760,42 @@ private string ObtainMatchingRelativeFilePath(string oldRelativeFilePath, string
}

/// <summary>
/// Parses the installer urls for any architecture or scope overrides.
/// Parses the installer urls for any additional arguments.
/// </summary>
/// <param name="installerUrlsToBeParsed">List of installer URLs to be parsed for architecture overrides.</param>
/// <param name="installerUrlsToBeParsed">List of installer URLs to be parsed for additional arguments.</param>
/// <returns>List of <see cref="InstallerMetadata"/> helper objects used for updating the installers.</returns>
private List<InstallerMetadata> ParseInstallerUrlsForOverrides(List<string> installerUrlsToBeParsed)
private List<InstallerMetadata> ParseInstallerUrlsForArguments(List<string> installerUrlsToBeParsed)
{
// There can be at most 4 elements at one time (installerUrl|archOverride|scopeOverride|displayVersion)
const int MaxUrlArgumentLimit = 4;

List<InstallerMetadata> installerMetadataList = new List<InstallerMetadata>();
foreach (string item in installerUrlsToBeParsed)
{
InstallerMetadata installerMetadata = new InstallerMetadata();

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<Architecture>();
Scope? overrideScope = overrideString.ToEnumOrDefault<Scope>();
string argumentString = installerUrlArguments[i];
Architecture? overrideArch = argumentString.ToEnumOrDefault<Architecture>();
Scope? overrideScope = argumentString.ToEnumOrDefault<Scope>();

if (overrideArch.HasValue)
{
Expand Down Expand Up @@ -773,9 +823,16 @@ private List<InstallerMetadata> ParseInstallerUrlsForOverrides(List<string> 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;
}
}
Expand Down
60 changes: 48 additions & 12 deletions src/WingetCreateCLI/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 19 additions & 5 deletions src/WingetCreateCLI/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -803,9 +803,9 @@
<data name="MultipleArchitectureOverride_Error" xml:space="preserve">
<value>Multiple architectures detected. Only one architecture can be specified for an override.</value>
</data>
<data name="UnableToParseOverride_Error" xml:space="preserve">
<value>Unable to parse the specified override {0}.</value>
<comment>{0} - represents the override string that failed to parse.</comment>
<data name="UnableToParseArgument_Error" xml:space="preserve">
<value>Unable to parse the specified argument {0}.</value>
<comment>{0} - represents the url argument that failed to parse.</comment>
</data>
<data name="Example_UpdateCommand_OverrideArchitecture" xml:space="preserve">
<value>Override the architecture of an installer</value>
Expand Down Expand Up @@ -995,8 +995,8 @@
<data name="MultipleScopeOverride_Error" xml:space="preserve">
<value>Multiple scopes detected. Only one scope can be specified for an override.</value>
</data>
<data name="OverrideLimitExceeded_Error" xml:space="preserve">
<value>Too many overrides specified for the following installer URL: {0}</value>
<data name="ArgumentLimitExceeded_Error" xml:space="preserve">
<value>Too many arguments specified for the following installer URL: {0}</value>
<comment>{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.</comment>
</data>
<data name="OverridingScope_Warning" xml:space="preserve">
Expand Down Expand Up @@ -1357,4 +1357,18 @@
<data name="Example_TokenCommand_StoreNewToken" xml:space="preserve">
<value>Store a new GitHub token in your local cache</value>
</data>
<data name="UnchangedDisplayVersion_Warning" xml:space="preserve">
<value>Base manifest contains DisplayVersion that has not been updated. Use --display-version CLI arg or provide the version in the installer URL.</value>
</data>
<data name="DisplayVersion_HelpText" xml:space="preserve">
<value>Version to be used when updating the display version field. Version provided in the installer URL arguments will take precendence over this value.</value>
</data>
<data name="UsingDisplayVersion_Message" xml:space="preserve">
<value>Using display version '{0}' for {1}</value>
<comment>{0} - will be replaced with the display version provided in the installer URL
{1} - will be replaced by the installer URL</comment>
</data>
<data name="InstallerWithMultipleDisplayVersions_Warning" xml:space="preserve">
<value>Single installer with multiple display versions detected. Winget-Create will only update the first DisplayVersion for a given installer.</value>
</data>
</root>
Loading

0 comments on commit 7826f51

Please sign in to comment.