Skip to content

Commit 4f44b22

Browse files
authored
Improve matching for ZIP with multiple nested installers (#529)
1 parent 7826f51 commit 4f44b22

7 files changed

Lines changed: 273 additions & 17 deletions

File tree

src/WingetCreateCore/Common/PackageParser.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ public static void UpdateInstallerNodesAsync(List<InstallerMetadata> installerMe
216216
{
217217
parseFailedInstallerUrls.Add(installerMetadata.InstallerUrl);
218218
}
219+
220+
// In case of multiple nested installers in the archive, we expect the new installers to have duplicates
221+
// Remove these duplicates to avoid multiple matches.
222+
installerMetadata.NewInstallers = installerMetadata.NewInstallers.Distinct().ToList();
219223
}
220224

221225
int numOfNewInstallers = installerMetadataList.Sum(x => x.NewInstallers.Count);
@@ -262,6 +266,24 @@ public static void UpdateInstallerNodesAsync(List<InstallerMetadata> installerMe
262266
// If a match is found, add match to dictionary and remove for list of existingInstallers
263267
if (existingInstallerMatch != null)
264268
{
269+
// Remove the nested installers from the new installer that are not present in the existing installer.
270+
if (newInstaller.NestedInstallerFiles != null && existingInstallerMatch.NestedInstallerFiles != null)
271+
{
272+
var matchedFiles = newInstaller.NestedInstallerFiles
273+
.Where(nif =>
274+
{
275+
var fileName = Path.GetFileName(nif.RelativeFilePath);
276+
277+
// If the flow reaches here, there's guaranteed to be a matching file name
278+
// Any mismatches would've been detected earlier in the update flow.
279+
return existingInstallerMatch.NestedInstallerFiles.Any(eif =>
280+
Path.GetFileName(eif.RelativeFilePath) == fileName);
281+
})
282+
.ToList();
283+
284+
newInstaller.NestedInstallerFiles = matchedFiles;
285+
}
286+
265287
installerMatchDict.Add(existingInstallerMatch, newInstaller);
266288
existingInstallers.Remove(existingInstallerMatch);
267289
}

src/WingetCreateTests/WingetCreateTests/Resources/TestPublisher.RetainInstallerFields.yaml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,13 @@ Description: |-
99
1010
1) For the first installer, all root level fields are copied over and root fields are set to null.
1111
2) For the second installer, installer level fields are preserved since they are not null.
12-
3) InstallerType and NestedInstallerType are common across both installers, so they are moved to the root level at the end of the update.
13-
14-
TODO: Use different NestedInstallerType and RelativeFilePath for each installer once logic for handling multiple nested installers is improved.
15-
Reference: https://github.com/microsoft/winget-create/issues/392
12+
3) InstallerType is common across both installers, so it is moved to the root level at the end of the update.
1613
InstallerLocale: en-US
1714
InstallerType: zip
1815
NestedInstallerType: exe
1916
NestedInstallerFiles:
2017
- RelativeFilePath: WingetCreateTestExeInstaller.exe
21-
PortableCommandAlias: TestAlias
18+
PortableCommandAlias: TestExeAlias
2219
AppsAndFeaturesEntries:
2320
- DisplayName: TestDisplayName1
2421
Publisher: TestPublisher1
@@ -87,10 +84,10 @@ Installers:
8784
InstallerType: zip
8885
InstallerUrl: https://fakedomain.com/WingetCreateTestZipInstaller.zip
8986
InstallerSha256: 8A052767127A6E2058BAAE03B551A807777BB1B726650E2C7E92C3E92C8DF80D
90-
NestedInstallerType: exe
87+
NestedInstallerType: msi
9188
NestedInstallerFiles:
92-
- RelativeFilePath: WingetCreateTestExeInstaller.exe
93-
PortableCommandAlias: TestAlias
89+
- RelativeFilePath: WingetCreateTestMsiInstaller.msi
90+
PortableCommandAlias: TestMsiAlias
9491
AppsAndFeaturesEntries:
9592
- DisplayName: TestDisplayName2
9693
Publisher: TestPublisher2
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
PackageIdentifier: TestPublisher.MultipleNestedInstallers
2+
PackageVersion: 0.1.2
3+
PackageName: Test zip app
4+
Publisher: Test publisher
5+
License: MIT
6+
ShortDescription: A manifest used for testing whether multiple nested installers are handled correctly.
7+
Description: The zip archive contains extra nested installers that are intentionally left out in the manifest to test the behavior.
8+
Installers:
9+
- Architecture: x64
10+
InstallerType: zip
11+
InstallerUrl: https://fakedomain.com/WingetCreateTestMultipleNestedInstallers.zip
12+
InstallerSha256: 8A052767127A6E2058BAAE03B551A807777BB1B726650E2C7E92C3E92C8DF80D
13+
NestedInstallerType: portable
14+
NestedInstallerFiles:
15+
- RelativeFilePath: WingetCreateTestExeInstaller.exe
16+
PortableCommandAlias: TestExeInstallerAlias
17+
- RelativeFilePath: WingetCreateTestPortableInstaller.exe
18+
PortableCommandAlias: TestPortableInstallerAlias
19+
- RelativeFilePath: WingetCreateTestPortableInstaller (1).exe
20+
PortableCommandAlias: TestPortableInstallerAlias1
21+
- RelativeFilePath: WingetCreateTestPortableInstaller (2).exe
22+
PortableCommandAlias: TestPortableInstallerAlias2
23+
- Architecture: x86
24+
InstallerType: zip
25+
InstallerUrl: https://fakedomain.com/WingetCreateTestMultipleNestedInstallers.zip
26+
InstallerSha256: 8A052767127A6E2058BAAE03B551A807777BB1B726650E2C7E92C3E92C8DF80D
27+
NestedInstallerType: portable
28+
NestedInstallerFiles:
29+
- RelativeFilePath: WingetCreateTestPortableInstaller.exe
30+
PortableCommandAlias: TestPortableInstallerAlias
31+
- RelativeFilePath: WingetCreateTestPortableInstaller (1).exe
32+
PortableCommandAlias: TestPortableInstallerAlias1
33+
- Architecture: arm
34+
InstallerType: zip
35+
InstallerUrl: https://fakedomain.com/WingetCreateTestMultipleNestedInstallers.zip
36+
InstallerSha256: 8A052767127A6E2058BAAE03B551A807777BB1B726650E2C7E92C3E92C8DF80D
37+
NestedInstallerType: portable
38+
NestedInstallerFiles:
39+
- RelativeFilePath: WingetCreateTestPortableInstaller (2).exe
40+
PortableCommandAlias: TestPortableInstallerAlias2
41+
- Architecture: arm64
42+
InstallerType: zip
43+
Scope: user
44+
InstallerUrl: https://fakedomain.com/WingetCreateTestMultipleNestedInstallers.zip
45+
InstallerSha256: 8A052767127A6E2058BAAE03B551A807777BB1B726650E2C7E92C3E92C8DF80D
46+
NestedInstallerType: exe
47+
NestedInstallerFiles:
48+
- RelativeFilePath: WingetCreateTestExeInstaller.exe
49+
PortableCommandAlias: TestExeInstallerAlias
50+
- Architecture: arm64
51+
InstallerType: zip
52+
Scope: machine
53+
InstallerUrl: https://fakedomain.com/WingetCreateTestMultipleNestedInstallers.zip
54+
InstallerSha256: 8A052767127A6E2058BAAE03B551A807777BB1B726650E2C7E92C3E92C8DF80D
55+
NestedInstallerType: msi
56+
NestedInstallerFiles:
57+
- RelativeFilePath: WingetCreateTestMsiInstaller.msi
58+
PortableCommandAlias: TestMsiInstallerAlias
59+
PackageLocale: en-US
60+
ManifestType: singleton
61+
ManifestVersion: 1.6.0

src/WingetCreateTests/WingetCreateTests/TestConstants.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public static class TestConstants
3535
/// </summary>
3636
public const string TestExeInstaller = "WingetCreateTestExeInstaller.exe";
3737

38+
/// <summary>
39+
/// File name of the test portable installer.
40+
/// </summary>
41+
public const string TestPortableInstaller = "WingetCreateTestPortableInstaller.exe";
42+
3843
/// <summary>
3944
/// File name of the test MSI installer.
4045
/// </summary>

src/WingetCreateTests/WingetCreateTests/TestUtils.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Microsoft.WingetCreateTests
66
using System;
77
using System.Collections.Generic;
88
using System.IO;
9+
using System.IO.Compression;
910
using System.Linq;
1011
using System.Net;
1112
using System.Net.Http;
@@ -138,5 +139,79 @@ public static string MockDownloadFile(string filename)
138139
string downloadedPath = PackageParser.DownloadFileAsync(url).Result;
139140
return downloadedPath;
140141
}
142+
143+
/// <summary>
144+
/// Creates copies of the specified resource file. If multiple copies are requested, the new files will be named with a numeric suffix.
145+
/// </summary>
146+
/// <param name="resource">Name of the resource file to copy.</param>
147+
/// <param name="numberOfCopies">Number of copies to create.</param>
148+
/// <param name="newResourceName">Optional new name for the copied resource file.</param>
149+
/// <returns>List of paths to the newly created files.</returns>
150+
public static List<string> CreateResourceCopy(string resource, int numberOfCopies = 1, string newResourceName = null)
151+
{
152+
string originalResourcePath = GetTestFile(resource);
153+
string newResourcePath = originalResourcePath;
154+
if (!string.IsNullOrEmpty(newResourceName))
155+
{
156+
newResourcePath = Path.Combine(Path.GetDirectoryName(originalResourcePath), newResourceName);
157+
}
158+
159+
List<string> copyPaths = new();
160+
for (int i = 0; i < numberOfCopies; i++)
161+
{
162+
string copyPath = PackageParser.GetNumericFilename(newResourcePath);
163+
File.Copy(originalResourcePath, copyPath);
164+
copyPaths.Add(copyPath);
165+
}
166+
167+
return copyPaths;
168+
}
169+
170+
/// <summary>
171+
/// Adds files to an existing test zip archive.
172+
/// </summary>
173+
/// <param name="zipResourceName">Name of the zip resource file.</param>
174+
/// <param name="filePaths">List of paths for files to be included in the zip archive.</param>
175+
public static void AddFilesToZip(string zipResourceName, List<string> filePaths)
176+
{
177+
string zipPath = GetTestFile(zipResourceName);
178+
using (ZipArchive zipArchive = ZipFile.Open(zipPath, ZipArchiveMode.Update))
179+
{
180+
foreach (string file in filePaths)
181+
{
182+
var fileInfo = new FileInfo(file);
183+
zipArchive.CreateEntryFromFile(fileInfo.FullName, fileInfo.Name);
184+
}
185+
} // The zipArchive is automatically closed and disposed here
186+
}
187+
188+
/// <summary>
189+
/// Removes files from an existing test zip archive.
190+
/// </summary>
191+
/// <param name="zipResourceName">Name of the zip resource file.</param>
192+
/// <param name="fileNames">List of file names to be removed from the zip archive.</param>
193+
public static void RemoveFilesFromZip(string zipResourceName, List<string> fileNames)
194+
{
195+
string zipPath = GetTestFile(zipResourceName);
196+
using (ZipArchive zipArchive = ZipFile.Open(zipPath, ZipArchiveMode.Update))
197+
{
198+
foreach (string fileName in fileNames)
199+
{
200+
zipArchive.GetEntry(fileName)?.Delete();
201+
}
202+
} // ZipArchive is automatically closed and disposed here
203+
}
204+
205+
/// <summary>
206+
/// Delete test resources from cache directory.
207+
/// </summary>
208+
/// <param name="testFileNames">Name of the test files to delete.</param>
209+
public static void DeleteCachedFiles(List<string> testFileNames)
210+
{
211+
foreach (string fileName in testFileNames)
212+
{
213+
File.Delete(Path.Combine(PackageParser.InstallerDownloadPath, fileName));
214+
}
215+
}
141216
}
142217
}

0 commit comments

Comments
 (0)