diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 5cc33bde77..705df91ac8 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -33,6 +33,7 @@ ASwitch ASYNCRTIMP Atest ATL +attr AType AUrl Authenticode @@ -56,6 +57,7 @@ boundparms bpp Browsable BSODs +BUILDOUTDIR buildtransitive BUILTINS cancelledbyuser @@ -121,6 +123,7 @@ ENDSESSION EQU errmsg ERRORONEXIT +ESource ESRB etest etl @@ -224,6 +227,7 @@ Linq liv liwpx localizationpriority +localsource LOWORD LPARAM LPBYTE @@ -394,6 +398,7 @@ rzkzqaqjwj SARL schematab sddl +SECUREFILEPATH seof servercert servercertificate @@ -410,6 +415,7 @@ SLAPI SMTO sortof sourceforge +SOURCESDIRECTORY spamming SPAPI Srinivasan @@ -434,6 +440,7 @@ systemnotsupported Tagit TCpp tcs +TEMPDIRECTORY Templating temppath testdata @@ -476,9 +483,9 @@ userfilesetting userprofile UWP VALUENAMECASE +vclib VERSI VERSIE -vclib vns vsconfig vstest diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4288f1bc08..f7ddc4e59e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -178,18 +178,6 @@ jobs: testResultsFiles: '$(artifactsDir)\TEST-*.xml' failTaskOnFailedTests: true - - task: DownloadSecureFile@1 - name: AppInstallerTest - displayName: 'Download Source Package Certificate' - inputs: - secureFile: 'AppInstallerTest.pfx' - - - task: DownloadSecureFile@1 - name: HTTPSDevCert - displayName: 'Download Kestrel Certificate' - inputs: - secureFile: 'HTTPSDevCertV2.pfx' - - task: MSBuild@1 displayName: Build MSIX Test Installer File inputs: @@ -201,18 +189,6 @@ jobs: /p:UapAppxPackageBuildMode=SideLoadOnly /p:AppxPackageSigningEnabled=false' - - task: PowerShell@2 - displayName: Install Root Certificate - inputs: - filePath: 'src\LocalhostWebServer\InstallDevCert.ps1' - arguments: '-pfxpath $(HTTPSDevCert.secureFilePath) -password microsoft' - - - task: PowerShell@2 - displayName: Launch LocalhostWebServer - inputs: - filePath: 'src\LocalhostWebServer\Run-LocalhostWebServer.ps1' - arguments: '-BuildRoot $(buildOutDir)\LocalhostWebServer -StaticFileRoot $(Agent.TempDirectory)\TestLocalIndex -CertPath $(HTTPSDevCert.secureFilePath) -CertPassword microsoft' - - task: PowerShell@2 displayName: 'Set program files directory' inputs: @@ -240,12 +216,10 @@ jobs: TargetFolder: '$(platformProgramFiles)\dotnet' Contents: Microsoft.Management.Deployment.winmd - - task: PowerShell@2 - displayName: Setup Local PS Repository - inputs: - filePath: 'src\AppInstallerCLIE2ETests\TestData\Configuration\Init-TestRepository.ps1' - arguments: '-Force' - pwsh: true + - template: templates/e2e-setup.yml + parameters: + source: $(Build.SourcesDirectory) + buildOutDir: $(buildOutDir) - template: templates/e2e-test.template.yml parameters: @@ -269,7 +243,21 @@ jobs: displayName: 'Copy E2E Tests Package Log to artifacts folder' inputs: SourceFolder: '$(temp)\E2ETestLogs' - TargetFolder: '$(artifactsDir)\E2ETestsPackagedLog' + TargetFolder: '$(artifactsDir)\E2ETests\PackagedLog' + condition: succeededOrFailed() + + - task: CopyFiles@2 + displayName: 'Copy E2E Test Source' + inputs: + SourceFolder: '$(Agent.TempDirectory)\TestLocalIndex' + TargetFolder: '$(artifactsDir)\E2ETests\TestLocalIndex' + condition: succeededOrFailed() + + - task: CopyFiles@2 + displayName: 'Copy TestData' + inputs: + SourceFolder: '$(Build.SourcesDirectory)\src\AppInstallerCLIE2ETests\TestData\' + TargetFolder: '$(artifactsDir)\E2ETests\TestData' condition: succeededOrFailed() - task: CopyFiles@2 diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index 52cc4f438c..c9745bbe4c 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -175,6 +175,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Management.Config {2B00D362-AC92-41F3-A8D2-5B1599BDCA01} = {2B00D362-AC92-41F3-A8D2-5B1599BDCA01} EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinGetSourceCreator", "WinGetSourceCreator\WinGetSourceCreator.csproj", "{52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -1166,6 +1168,36 @@ Global {2268D5AD-7F2A-485A-8C4B-C574497514C9}.TestRelease|x64.Build.0 = Release|x64 {2268D5AD-7F2A-485A-8C4B-C574497514C9}.TestRelease|x86.ActiveCfg = Release|Win32 {2268D5AD-7F2A-485A-8C4B-C574497514C9}.TestRelease|x86.Build.0 = Release|Win32 + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Debug|ARM64.Build.0 = Debug|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Debug|x64.ActiveCfg = Debug|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Debug|x64.Build.0 = Debug|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Debug|x86.Build.0 = Debug|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Fuzzing|ARM64.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Fuzzing|ARM64.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Fuzzing|x64.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Fuzzing|x64.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Fuzzing|x86.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Fuzzing|x86.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|ARM64.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|ARM64.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|x64.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|x64.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|x86.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|x86.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|ARM64.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|ARM64.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|x64.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|x64.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|x86.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|x86.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.TestRelease|ARM64.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.TestRelease|ARM64.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.TestRelease|x64.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.TestRelease|x64.Build.0 = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.TestRelease|x86.ActiveCfg = Release|Any CPU + {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.TestRelease|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index de89a66040..b985c84a2d 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -62,6 +62,7 @@ + @@ -80,6 +81,7 @@ False + False diff --git a/src/AppInstallerCLIE2ETests/AppShutdownTests.cs b/src/AppInstallerCLIE2ETests/AppShutdownTests.cs index 3c4130916a..a02b97a5d3 100644 --- a/src/AppInstallerCLIE2ETests/AppShutdownTests.cs +++ b/src/AppInstallerCLIE2ETests/AppShutdownTests.cs @@ -11,12 +11,13 @@ namespace AppInstallerCLIE2ETests using System.Threading; using System.Threading.Tasks; using System.Xml; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// /// `test appshutdown` command tests. /// - public class AppShutdownTests : BaseCommand + public class AppShutdownTests { /// /// Runs winget test appshutdown and register the application to force a WM_QUERYENDSESSION message. @@ -25,17 +26,17 @@ public class AppShutdownTests : BaseCommand [Ignore("This test won't work on Window Server")] public void RegisterApplicationTest() { - if (!TestCommon.PackagedContext) + if (!TestSetup.Parameters.PackagedContext) { - return; + Assert.Ignore("Not packaged context."); } - if (string.IsNullOrEmpty(TestCommon.AICLIPackagePath)) + if (string.IsNullOrEmpty(TestSetup.Parameters.AICLIPackagePath)) { throw new NullReferenceException("AICLIPackagePath"); } - var appxManifest = Path.Combine(TestCommon.AICLIPackagePath, "AppxManifest.xml"); + var appxManifest = Path.Combine(TestSetup.Parameters.AICLIPackagePath, "AppxManifest.xml"); if (!File.Exists(appxManifest)) { throw new FileNotFoundException(appxManifest); @@ -73,7 +74,7 @@ public void RegisterApplicationTest() // Register the app with the updated version. var registerTask = new Task(() => { - return TestCommon.InstallMsixRegister(TestCommon.AICLIPackagePath, true, false); + return TestCommon.InstallMsixRegister(TestSetup.Parameters.AICLIPackagePath, true, false); }); // Give it a little time. @@ -97,12 +98,12 @@ public void RegisterApplicationTest() [Test] public void RegisterApplicationTest_Force() { - if (!TestCommon.PackagedContext) + if (!TestSetup.Parameters.PackagedContext) { - return; + Assert.Ignore("Not packaged context."); } - if (string.IsNullOrEmpty(TestCommon.AICLIPackagePath)) + if (string.IsNullOrEmpty(TestSetup.Parameters.AICLIPackagePath)) { throw new NullReferenceException("AICLIPackagePath"); } diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs index 2a8f3560e2..1b28227fd5 100644 --- a/src/AppInstallerCLIE2ETests/BaseCommand.cs +++ b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -8,6 +8,7 @@ namespace AppInstallerCLIE2ETests { using System; using System.IO; + using AppInstallerCLIE2ETests.Helpers; using Newtonsoft.Json.Linq; using NUnit.Framework; diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs index 73df29377f..59b483e04b 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -7,6 +7,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs index 48d73c3703..cddc270d12 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs index 1b78a94d0d..3e7ab142e0 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs @@ -7,6 +7,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Infrastructure; using NUnit.Framework; diff --git a/src/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs index 820ec87bbc..8c17c7420a 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 5cdce2ca98..65e66dae9a 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -22,13 +22,13 @@ public class Constants public const string LooseFileRegistrationParameter = "LooseFileRegistration"; public const string InvokeCommandInDesktopPackageParameter = "InvokeCommandInDesktopPackage"; public const string StaticFileRootPathParameter = "StaticFileRootPath"; + public const string LocalServerCertPathParameter = "LocalServerCertPath"; public const string ExeInstallerPathParameter = "ExeTestInstallerPath"; public const string MsiInstallerPathParameter = "MsiTestInstallerPath"; public const string MsixInstallerPathParameter = "MsixTestInstallerPath"; public const string PackageCertificatePathParameter = "PackageCertificatePath"; public const string PowerShellModulePathParameter = "PowerShellModulePath"; - public const string AppInstallerTestCert = "AppInstallerTest.cer"; - public const string AppInstallerTestCertThumbprint = "d03e7a688b388b1edde8476a627531c49db88017"; + public const string SkipTestSourceParameter = "SkipTestSource"; // Test Sources public const string DefaultWingetSourceName = @"winget"; @@ -42,7 +42,9 @@ public class Constants public const string TestSourceUrl = @"https://localhost:5001/TestKit"; public const string TestSourceType = "Microsoft.PreIndexed.Package"; public const string TestSourceIdentifier = @"WingetE2E.Tests_8wekyb3d8bbwe"; - public const string TestSourceServerCertificateFileName = "servercert.cer"; + + public const string AppInstallerTestCert = "AppInstallerTest.cer"; + public const string AppInstallerTestCertThumbprint = "d03e7a688b388b1edde8476a627531c49db88017"; public const string AICLIPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe"; public const string AICLIPackageName = "WinGetDevCLI"; diff --git a/src/AppInstallerCLIE2ETests/DownloadCommand.cs b/src/AppInstallerCLIE2ETests/DownloadCommand.cs index bb47f84a0a..9573ffe4eb 100644 --- a/src/AppInstallerCLIE2ETests/DownloadCommand.cs +++ b/src/AppInstallerCLIE2ETests/DownloadCommand.cs @@ -6,7 +6,8 @@ namespace AppInstallerCLIE2ETests { - using System.IO; + using System.IO; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; using NUnit.Framework; using Windows.System; diff --git a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs index 17aad536fe..1b15fd8a3a 100644 --- a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs +++ b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/GroupPolicy.cs b/src/AppInstallerCLIE2ETests/GroupPolicy.cs index a9a8ea9964..127dcef8be 100644 --- a/src/AppInstallerCLIE2ETests/GroupPolicy.cs +++ b/src/AppInstallerCLIE2ETests/GroupPolicy.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs b/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs index 978683af29..87e3bf89f6 100644 --- a/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs +++ b/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs @@ -11,6 +11,7 @@ namespace AppInstallerCLIE2ETests using System.IO; using System.Linq; using System.Xml.Linq; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Win32; using Newtonsoft.Json; using NUnit.Framework; diff --git a/src/AppInstallerCLIE2ETests/HashCommand.cs b/src/AppInstallerCLIE2ETests/HashCommand.cs index 8e0ca4c34e..86b8ae5885 100644 --- a/src/AppInstallerCLIE2ETests/HashCommand.cs +++ b/src/AppInstallerCLIE2ETests/HashCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; using NUnit.Framework.Internal; diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs similarity index 87% rename from src/AppInstallerCLIE2ETests/TestCommon.cs rename to src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 7f42838ef9..0797ae0222 100644 --- a/src/AppInstallerCLIE2ETests/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -4,22 +4,22 @@ // // ----------------------------------------------------------------------------- -namespace AppInstallerCLIE2ETests +namespace AppInstallerCLIE2ETests.Helpers { using System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Threading; + using AppInstallerCLIE2ETests; using Microsoft.Management.Deployment; using Microsoft.Win32; using NUnit.Framework; - using Windows.System; /// /// Test common. /// - public class TestCommon + public static class TestCommon { /// /// Scope. @@ -42,76 +42,6 @@ public enum Scope Machine, } - /// - /// Gets or sets the cli path. - /// - public static string AICLIPath { get; set; } - - /// - /// Gets or sets the package path. - /// - public static string AICLIPackagePath { get; set; } - - /// - /// Gets or sets a value indicating whether the test runs in package context. - /// - public static bool PackagedContext { get; set; } - - /// - /// Gets or sets a value indicating whether the test uses verbose logging. - /// - public static bool VerboseLogging { get; set; } - - /// - /// Gets or sets a value indicating whether to use loose file registration. - /// - public static bool LooseFileRegistration { get; set; } - - /// - /// Gets or sets a value indicating whether to invoke command in desktop package. - /// - public static bool InvokeCommandInDesktopPackage { get; set; } - - /// - /// Gets or sets the static file root path. - /// - public static string StaticFileRootPath { get; set; } - - /// - /// Gets or sets the exe installer path. - /// - public static string ExeInstallerPath { get; set; } - - /// - /// Gets or sets the msi installer path. - /// - public static string MsiInstallerPath { get; set; } - - /// - /// Gets or sets the msix installer path. - /// - public static string MsixInstallerPath { get; set; } - - /// - /// Gets or sets the zip installer path. - /// - public static string ZipInstallerPath { get; set; } - - /// - /// Gets or sets the package cert path. - /// - public static string PackageCertificatePath { get; set; } - - /// - /// Gets or sets the PowerShell module path. - /// - public static string PowerShellModulePath { get; set; } - - /// - /// Gets or sets the settings json path. - /// - public static string SettingsJsonFilePath { get; set; } - /// /// Run winget command. /// @@ -123,15 +53,15 @@ public enum Scope public static RunCommandResult RunAICLICommand(string command, string parameters, string stdIn = null, int timeOut = 60000) { string inputMsg = - "AICLI path: " + AICLIPath + + "AICLI path: " + TestSetup.Parameters.AICLIPath + " Command: " + command + " Parameters: " + parameters + (string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) + " Timeout: " + timeOut; - TestContext.Out.WriteLine($"Starting command run. {inputMsg} InvokeCommandInDesktopPackage: {InvokeCommandInDesktopPackage}"); + TestContext.Out.WriteLine($"Starting command run. {inputMsg} InvokeCommandInDesktopPackage: {TestSetup.Parameters.InvokeCommandInDesktopPackage}"); - if (InvokeCommandInDesktopPackage) + if (TestSetup.Parameters.InvokeCommandInDesktopPackage) { return RunAICLICommandViaInvokeCommandInDesktopPackage(command, parameters, stdIn, timeOut); } @@ -153,7 +83,7 @@ public static RunCommandResult RunAICLICommandViaDirectProcess(string command, s { RunCommandResult result = new (); Process p = new Process(); - p.StartInfo = new ProcessStartInfo(AICLIPath, command + ' ' + parameters); + p.StartInfo = new ProcessStartInfo(TestSetup.Parameters.AICLIPath, command + ' ' + parameters); p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardError = true; @@ -183,7 +113,7 @@ public static RunCommandResult RunAICLICommandViaDirectProcess(string command, s TestContext.Error.WriteLine("Command run error. Error: " + result.StdErr); } - if (VerboseLogging && !string.IsNullOrEmpty(result.StdOut)) + if (TestSetup.Parameters.VerboseLogging && !string.IsNullOrEmpty(result.StdOut)) { TestContext.Out.WriteLine("Command run output. Output: " + result.StdOut); } @@ -227,7 +157,7 @@ public static RunCommandResult RunAICLICommandViaInvokeCommandInDesktopPackage(s string stdErrFile = Path.Combine(workDirectory, "StdErr.txt"); // First change the codepage so that the rest of the batch file works - cmdCommandPiped += $"chcp 65001\n{AICLIPath} {command} {parameters} > {stdOutFile} 2> {stdErrFile}\necho %ERRORLEVEL% > {exitCodeFile}"; + cmdCommandPiped += $"chcp 65001\n{TestSetup.Parameters.AICLIPath} {command} {parameters} > {stdOutFile} 2> {stdErrFile}\necho %ERRORLEVEL% > {exitCodeFile}"; File.WriteAllText(tempBatchFile, cmdCommandPiped, new System.Text.UTF8Encoding(false)); string psCommand = $"Invoke-CommandInDesktopPackage -PackageFamilyName {Constants.AICLIPackageFamilyName} -AppId {Constants.AICLIAppId} -PreventBreakaway -Command cmd.exe -Args '/c \"{tempBatchFile}\"'"; @@ -337,7 +267,7 @@ public static RunCommandResult RunCommandWithResult(string fileName, string args result.StdOut = p.StandardOutput.ReadToEnd(); result.StdErr = p.StandardError.ReadToEnd(); - if (VerboseLogging) + if (TestSetup.Parameters.VerboseLogging) { TestContext.Out.WriteLine($"Command run finished. {fileName} {args} {timeOut}. Output: {result.StdOut} Error: {result.StdErr}"); } @@ -359,7 +289,7 @@ public static RunCommandResult RunCommandWithResult(string fileName, string args /// Command result. public static RunCommandResult RunPowerShellCoreCommandWithResult(string cmdlet, string args, int timeOut = 60000) { - return RunCommandWithResult("pwsh.exe", $"-Command ipmo {PowerShellModulePath}; {cmdlet} {args}", timeOut); + return RunCommandWithResult("pwsh.exe", $"-Command ipmo {TestSetup.Parameters.PowerShellModuleManifestPath}; {cmdlet} {args}", timeOut); } /// @@ -523,7 +453,7 @@ public static void VerifyPortablePackage( bool symlinkExists = File.Exists(symlinkPath); bool portableEntryExists; - RegistryKey baseKey = (scope == Scope.User) ? Registry.CurrentUser : Registry.LocalMachine; + RegistryKey baseKey = scope == Scope.User ? Registry.CurrentUser : Registry.LocalMachine; string uninstallSubKey = Constants.UninstallSubKey; using (RegistryKey uninstallRegistryKey = baseKey.OpenSubKey(uninstallSubKey, true)) { @@ -532,7 +462,7 @@ public static void VerifyPortablePackage( } bool isAddedToPath; - string pathSubKey = (scope == Scope.User) ? Constants.PathSubKey_User : Constants.PathSubKey_Machine; + string pathSubKey = scope == Scope.User ? Constants.PathSubKey_User : Constants.PathSubKey_Machine; using (RegistryKey environmentRegistryKey = baseKey.OpenSubKey(pathSubKey, true)) { string pathName = "Path"; @@ -567,12 +497,12 @@ public static void PublishE2ETestLogs() if (Directory.Exists(testLogsPackagedSourcePath)) { - TestIndexSetup.CopyDirectory(testLogsPackagedSourcePath, testLogsPackagedDestPath); + CopyDirectory(testLogsPackagedSourcePath, testLogsPackagedDestPath); } if (Directory.Exists(testLogsUnpackagedSourcePath)) { - TestIndexSetup.CopyDirectory(testLogsUnpackagedSourcePath, testLogsUnpackagedDestPath); + CopyDirectory(testLogsUnpackagedSourcePath, testLogsUnpackagedDestPath); } } @@ -582,7 +512,17 @@ public static void PublishE2ETestLogs() /// Hex string. public static string GetTestServerCertificateHexString() { - return Convert.ToHexString(File.ReadAllBytes(Path.Combine(StaticFileRootPath, Constants.TestSourceServerCertificateFileName))); + if (string.IsNullOrEmpty(TestSetup.Parameters.LocalServerCertPath)) + { + throw new Exception($"{Constants.LocalServerCertPathParameter} not set."); + } + + if (!File.Exists(TestSetup.Parameters.LocalServerCertPath)) + { + throw new FileNotFoundException(TestSetup.Parameters.LocalServerCertPath); + } + + return Convert.ToHexString(File.ReadAllBytes(TestSetup.Parameters.LocalServerCertPath)); } /// @@ -807,9 +747,9 @@ public static void ModifyPortableARPEntryValue(string productCode, string name, /// Use group policy. public static void SetupTestSource(bool useGroupPolicyForTestSource = false) { - TestCommon.RunAICLICommand("source reset", "--force"); - TestCommon.RunAICLICommand("source remove", Constants.DefaultWingetSourceName); - TestCommon.RunAICLICommand("source remove", Constants.DefaultMSStoreSourceName); + RunAICLICommand("source reset", "--force"); + RunAICLICommand("source remove", Constants.DefaultWingetSourceName); + RunAICLICommand("source remove", Constants.DefaultMSStoreSourceName); // TODO: If/when cert pinning is implemented on the packaged index source, useGroupPolicyForTestSource should be set to default true // to enable testing it by default. Until then, leaving this here... @@ -835,7 +775,7 @@ public static void SetupTestSource(bool useGroupPolicyForTestSource = false) new GroupPolicyHelper.GroupPolicyCertificatePinningDetails { Validation = new string[] { "publickey" }, - EmbeddedCertificate = TestCommon.GetTestServerCertificateHexString(), + EmbeddedCertificate = GetTestServerCertificateHexString(), }, }, }, @@ -847,7 +787,7 @@ public static void SetupTestSource(bool useGroupPolicyForTestSource = false) else { GroupPolicyHelper.EnableAdditionalSources.SetNotConfigured(); - TestCommon.RunAICLICommand("source add", $"{Constants.TestSourceName} {Constants.TestSourceUrl}"); + RunAICLICommand("source add", $"{Constants.TestSourceName} {Constants.TestSourceUrl}"); } Thread.Sleep(2000); @@ -901,7 +841,7 @@ public static void CreateARPEntry( object properties, Scope scope = Scope.User) { - RegistryKey baseKey = (scope == Scope.User) ? Registry.CurrentUser : Registry.LocalMachine; + RegistryKey baseKey = scope == Scope.User ? Registry.CurrentUser : Registry.LocalMachine; using (RegistryKey uninstallRegistryKey = baseKey.OpenSubKey(Constants.UninstallSubKey, true)) { RegistryKey entry = uninstallRegistryKey.CreateSubKey(productCode, true); @@ -922,13 +862,42 @@ public static void RemoveARPEntry( string productCode, Scope scope = Scope.User) { - RegistryKey baseKey = (scope == Scope.User) ? Registry.CurrentUser : Registry.LocalMachine; + RegistryKey baseKey = scope == Scope.User ? Registry.CurrentUser : Registry.LocalMachine; using (RegistryKey uninstallRegistryKey = baseKey.OpenSubKey(Constants.UninstallSubKey, true)) { uninstallRegistryKey.DeleteSubKey(productCode); } } + /// + /// Copies the contents of a given directory from a source path to a destination path. + /// + /// Source directory name. + /// Destination directory name. + public static void CopyDirectory(string sourceDirName, string destDirName) + { + DirectoryInfo dir = new DirectoryInfo(sourceDirName); + DirectoryInfo[] dirs = dir.GetDirectories(); + + if (!Directory.Exists(destDirName)) + { + Directory.CreateDirectory(destDirName); + } + + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string temppath = Path.Combine(destDirName, file.Name); + file.CopyTo(temppath, false); + } + + foreach (DirectoryInfo subdir in dirs) + { + string temppath = Path.Combine(destDirName, subdir.Name); + CopyDirectory(subdir.FullName, temppath); + } + } + /// /// Run command result. /// diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs b/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs new file mode 100644 index 0000000000..91931403f8 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/Helpers/TestIndex.cs @@ -0,0 +1,153 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests.Helpers +{ + using System; + using System.IO; + using System.Text.Json; + using Microsoft.WinGetSourceCreator; + using WinGetSourceCreator.Model; + + /// + /// Test index setup. + /// + public static class TestIndex + { + static TestIndex() + { + // Expected path for the installers. + TestIndex.ExeInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.ExeInstaller, Constants.ExeInstallerFileName); + TestIndex.MsiInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsiInstaller, Constants.MsiInstallerFileName); + TestIndex.MsixInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.MsixInstaller, Constants.MsixInstallerFileName); + TestIndex.ZipInstaller = Path.Combine(TestSetup.Parameters.StaticFileRootPath, Constants.ZipInstaller, Constants.ZipInstallerFileName); + } + + /// + /// Gets the signed exe installer path used by the manifests in the E2E test. + /// + public static string ExeInstaller { get; private set; } + + /// + /// Gets the signed msi installer path used by the manifests in the E2E test. + /// + public static string MsiInstaller { get; private set; } + + /// + /// Gets the signed msix installer path used by the manifests in the E2E test. + /// + public static string MsixInstaller { get; private set; } + + /// + /// Gets the zip installer path used by the manifests in the E2E test. + /// + public static string ZipInstaller { get; private set; } + + /// + /// Generate test source. + /// + public static void GenerateE2ESource() + { + var testParams = TestSetup.Parameters; + + if (string.IsNullOrEmpty(testParams.ExeInstallerPath)) + { + throw new ArgumentNullException($"{Constants.ExeInstallerPathParameter} is required"); + } + + if (!File.Exists(testParams.ExeInstallerPath)) + { + throw new FileNotFoundException(testParams.ExeInstallerPath); + } + + if (string.IsNullOrEmpty(testParams.MsiInstallerPath)) + { + throw new ArgumentNullException($"{Constants.MsiInstallerPathParameter} is required"); + } + + if (!File.Exists(testParams.MsiInstallerPath)) + { + throw new FileNotFoundException(testParams.MsiInstallerPath); + } + + if (string.IsNullOrEmpty(testParams.MsixInstallerPath)) + { + throw new ArgumentNullException($"{Constants.MsixInstallerPathParameter} is required"); + } + + if (!File.Exists(testParams.MsixInstallerPath)) + { + throw new FileNotFoundException(testParams.MsixInstallerPath); + } + + if (string.IsNullOrEmpty(testParams.PackageCertificatePath)) + { + throw new ArgumentNullException($"{Constants.PackageCertificatePathParameter} is required"); + } + + if (!File.Exists(testParams.PackageCertificatePath)) + { + throw new FileNotFoundException(testParams.PackageCertificatePath); + } + + LocalSource e2eSource = new () + { + AppxManifest = TestCommon.GetTestDataFile(Path.Combine("Package", "AppxManifest.xml")), + WorkingDirectory = testParams.StaticFileRootPath, + LocalManifests = new () + { + TestCommon.GetTestDataFile("Manifests"), + }, + LocalInstallers = new () + { + new LocalInstaller + { + Type = InstallerType.Exe, + Name = Path.Combine(Constants.ExeInstaller, Constants.ExeInstallerFileName), + Input = testParams.ExeInstallerPath, + HashToken = "", + }, + new LocalInstaller + { + Type = InstallerType.Msi, + Name = Path.Combine(Constants.MsiInstaller, Constants.MsiInstallerFileName), + Input = testParams.MsiInstallerPath, + HashToken = "", + }, + new LocalInstaller + { + Type = InstallerType.Msix, + Name = Path.Combine(Constants.MsixInstaller, Constants.MsixInstallerFileName), + Input = testParams.MsixInstallerPath, + HashToken = "", + SignatureToken = "", + }, + }, + DynamicInstallers = new () + { + new DynamicInstaller + { + Type = InstallerType.Zip, + Name = Path.Combine(Constants.ZipInstaller, Constants.ZipInstallerFileName), + Input = new () + { + ExeInstaller, + MsiInstaller, + MsixInstaller, + }, + HashToken = "", + }, + }, + Signature = new () + { + CertFile = testParams.PackageCertificatePath, + }, + }; + + WinGetLocalSource.CreateLocalSource(e2eSource); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs new file mode 100644 index 0000000000..b8a2f3bc31 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs @@ -0,0 +1,216 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests.Helpers +{ + using System; + using System.IO; + using NUnit.Framework; + + /// + /// Singleton class with test parameters. + /// + internal class TestSetup + { + private static readonly Lazy Lazy = new (() => new TestSetup()); + + private string settingFilePath = null; + + private TestSetup() + { + if (TestContext.Parameters.Count == 0) + { + this.IsDefault = true; + } + + // Read TestParameters and set runtime variables + this.PackagedContext = this.InitializeBoolParam(Constants.PackagedContextParameter, true); + this.VerboseLogging = this.InitializeBoolParam(Constants.VerboseLoggingParameter, true); + this.LooseFileRegistration = this.InitializeBoolParam(Constants.LooseFileRegistrationParameter); + this.InvokeCommandInDesktopPackage = this.InitializeBoolParam(Constants.InvokeCommandInDesktopPackageParameter); + this.SkipTestSource = this.InitializeBoolParam(Constants.SkipTestSourceParameter, this.IsDefault); + + // For packaged context, default to AppExecutionAlias + this.AICLIPath = this.InitializeStringParam(Constants.AICLIPathParameter, this.PackagedContext ? "WinGetDev.exe" : TestCommon.GetTestFile("winget.exe")); + this.AICLIPackagePath = this.InitializeStringParam(Constants.AICLIPackagePathParameter, TestCommon.GetTestFile("AppInstallerCLIPackage.appxbundle")); + + if (this.LooseFileRegistration && this.InvokeCommandInDesktopPackage) + { + this.AICLIPath = Path.Combine(this.AICLIPackagePath, this.AICLIPath); + } + + this.StaticFileRootPath = this.InitializeDirectoryParam(Constants.StaticFileRootPathParameter, Path.GetTempPath()); + + this.PowerShellModuleManifestPath = this.InitializeFileParam(Constants.PowerShellModulePathParameter); + this.LocalServerCertPath = this.InitializeFileParam(Constants.LocalServerCertPathParameter); + this.PackageCertificatePath = this.InitializeFileParam(Constants.PackageCertificatePathParameter); + this.ExeInstallerPath = this.InitializeFileParam(Constants.ExeInstallerPathParameter); + this.MsiInstallerPath = this.InitializeFileParam(Constants.MsiInstallerPathParameter); + this.MsixInstallerPath = this.InitializeFileParam(Constants.MsixInstallerPathParameter); + } + + /// + /// Gets the instance object. + /// + public static TestSetup Parameters + { + get + { + return Lazy.Value; + } + } + + /// + /// Gets the cli path. + /// + public string AICLIPath { get; } + + /// + /// Gets the package path. + /// + public string AICLIPackagePath { get; } + + /// + /// Gets a value indicating whether the test runs in package context. + /// + public bool PackagedContext { get; } + + /// + /// Gets a value indicating whether the test uses verbose logging. + /// + public bool VerboseLogging { get; } + + /// + /// Gets a value indicating whether to use loose file registration. + /// + public bool LooseFileRegistration { get; } + + /// + /// Gets a value indicating whether to invoke command in desktop package. + /// + public bool InvokeCommandInDesktopPackage { get; } + + /// + /// Gets the static file root path. + /// + public string StaticFileRootPath { get; } + + /// + /// Gets the local server cert path. + /// + public string LocalServerCertPath { get; } + + /// + /// Gets the exe installer path. + /// + public string ExeInstallerPath { get; } + + /// + /// Gets the msi installer path. + /// + public string MsiInstallerPath { get; } + + /// + /// Gets the msix installer path. + /// + public string MsixInstallerPath { get; } + + /// + /// Gets the zip installer path. + /// + public string ZipInstallerPath { get; } + + /// + /// Gets the package cert path. + /// + public string PackageCertificatePath { get; } + + /// + /// Gets the PowerShell module path. + /// + public string PowerShellModuleManifestPath { get; } + + /// + /// Gets a value indicating whether to skip creating test source. + /// + public bool SkipTestSource { get; } + + /// + /// Gets the settings json path. + /// + public string SettingsJsonFilePath + { + get + { + if (this.settingFilePath == null) + { + this.settingFilePath = WinGetSettingsHelper.GetUserSettingsPath(); + } + + return this.settingFilePath; + } + } + + /// + /// Gets a value indicating whether is the default parameters. + /// + public bool IsDefault { get; } + + private bool InitializeBoolParam(string paramName, bool defaultValue = false) + { + if (this.IsDefault || !TestContext.Parameters.Exists(paramName)) + { + return defaultValue; + } + + return TestContext.Parameters.Get(paramName).Equals("true", StringComparison.OrdinalIgnoreCase); + } + + private string InitializeStringParam(string paramName, string defaultValue = null) + { + if (this.IsDefault || !TestContext.Parameters.Exists(paramName)) + { + return defaultValue; + } + + return TestContext.Parameters.Get(paramName); + } + + private string InitializeFileParam(string paramName, string defaultValue = null) + { + if (!TestContext.Parameters.Exists(paramName)) + { + return defaultValue; + } + + var value = TestContext.Parameters.Get(paramName); + + if (!File.Exists(value)) + { + throw new FileNotFoundException($"{paramName}: {value}"); + } + + return value; + } + + private string InitializeDirectoryParam(string paramName, string defaultValue = null) + { + if (!TestContext.Parameters.Exists(paramName)) + { + return defaultValue; + } + + var value = TestContext.Parameters.Get(paramName); + + if (!Directory.Exists(value)) + { + throw new DirectoryNotFoundException($"{paramName}: {value}"); + } + + return value; + } + } +} diff --git a/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs similarity index 88% rename from src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs rename to src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs index b45dae67ce..6713582935 100644 --- a/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -4,7 +4,7 @@ // // ----------------------------------------------------------------------------- -namespace AppInstallerCLIE2ETests +namespace AppInstallerCLIE2ETests.Helpers { using System.Collections; using System.IO; @@ -84,7 +84,7 @@ public static void SetWingetSettings(Hashtable settingsJson) /// Settings as string. public static void SetWingetSettings(string settings) { - File.WriteAllText(TestCommon.SettingsJsonFilePath, settings); + File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settings); } /// @@ -94,7 +94,7 @@ public static void SetWingetSettings(string settings) /// Status. public static void ConfigureFeature(string featureName, bool status) { - JObject settingsJson = JObject.Parse(File.ReadAllText(TestCommon.SettingsJsonFilePath)); + JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); if (!settingsJson.ContainsKey("experimentalFeatures")) { @@ -104,7 +104,7 @@ public static void ConfigureFeature(string featureName, bool status) var experimentalFeatures = settingsJson["experimentalFeatures"]; experimentalFeatures[featureName] = status; - File.WriteAllText(TestCommon.SettingsJsonFilePath, settingsJson.ToString()); + File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settingsJson.ToString()); } /// @@ -114,7 +114,7 @@ public static void ConfigureFeature(string featureName, bool status) /// Setting value. public static void ConfigureInstallBehavior(string settingName, string value) { - JObject settingsJson = JObject.Parse(File.ReadAllText(TestCommon.SettingsJsonFilePath)); + JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); if (!settingsJson.ContainsKey("installBehavior")) { @@ -124,7 +124,7 @@ public static void ConfigureInstallBehavior(string settingName, string value) var installBehavior = settingsJson["installBehavior"]; installBehavior[settingName] = value; - File.WriteAllText(TestCommon.SettingsJsonFilePath, settingsJson.ToString()); + File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settingsJson.ToString()); } /// @@ -134,7 +134,7 @@ public static void ConfigureInstallBehavior(string settingName, string value) /// Setting value. public static void ConfigureInstallBehaviorPreferences(string settingName, string value) { - JObject settingsJson = JObject.Parse(File.ReadAllText(TestCommon.SettingsJsonFilePath)); + JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); if (!settingsJson.ContainsKey("installBehavior")) { @@ -151,7 +151,7 @@ public static void ConfigureInstallBehaviorPreferences(string settingName, strin var preferences = installBehavior["preferences"]; preferences[settingName] = value; - File.WriteAllText(TestCommon.SettingsJsonFilePath, settingsJson.ToString()); + File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settingsJson.ToString()); } /// @@ -161,7 +161,7 @@ public static void ConfigureInstallBehaviorPreferences(string settingName, strin /// Setting value. public static void ConfigureInstallBehaviorRequirements(string settingName, string value) { - JObject settingsJson = JObject.Parse(File.ReadAllText(TestCommon.SettingsJsonFilePath)); + JObject settingsJson = JObject.Parse(File.ReadAllText(TestSetup.Parameters.SettingsJsonFilePath)); if (!settingsJson.ContainsKey("installBehavior")) { @@ -178,7 +178,7 @@ public static void ConfigureInstallBehaviorRequirements(string settingName, stri var requirements = installBehavior["requirements"]; requirements[settingName] = value; - File.WriteAllText(TestCommon.SettingsJsonFilePath, settingsJson.ToString()); + File.WriteAllText(TestSetup.Parameters.SettingsJsonFilePath, settingsJson.ToString()); } /// diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs index 66f03f195b..b14777892c 100644 --- a/src/AppInstallerCLIE2ETests/ImportCommand.cs +++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -6,7 +6,8 @@ namespace AppInstallerCLIE2ETests { - using System.IO; + using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index 82f0b4eccc..169cbf445d 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -7,6 +7,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// @@ -698,7 +699,7 @@ public void InstallExeThatInstallsMSIX() Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); // Add the MSIX to simulate an installer doing it - TestCommon.InstallMsix(TestCommon.MsixInstallerPath); + TestCommon.InstallMsix(TestIndex.MsixInstaller); // Install our exe that "installs" the MSIX result = TestCommon.RunAICLICommand("install", $"{targetPackageIdentifier} --force"); diff --git a/src/AppInstallerCLIE2ETests/Interop/CheckInstalledStatusInterop.cs b/src/AppInstallerCLIE2ETests/Interop/CheckInstalledStatusInterop.cs index f63e25dd7c..44223eac3b 100644 --- a/src/AppInstallerCLIE2ETests/Interop/CheckInstalledStatusInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/CheckInstalledStatusInterop.cs @@ -8,7 +8,8 @@ namespace AppInstallerCLIE2ETests.Interop { using System; using System.IO; - using System.Threading.Tasks; + using System.Threading.Tasks; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; using Microsoft.Management.Deployment.Projection; using NUnit.Framework; diff --git a/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs b/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs index 7897ee3c21..240074c58f 100644 --- a/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs @@ -8,7 +8,8 @@ namespace AppInstallerCLIE2ETests.Interop { using System; using System.IO; - using System.Threading.Tasks; + using System.Threading.Tasks; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; using Microsoft.Management.Deployment.Projection; using NUnit.Framework; diff --git a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs index 22eab883cd..924920d1da 100644 --- a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs @@ -9,6 +9,7 @@ namespace AppInstallerCLIE2ETests.Interop using System; using System.IO; using System.Threading.Tasks; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; using Microsoft.Management.Deployment.Projection; using NUnit.Framework; @@ -190,7 +191,7 @@ public async Task InstallNullSoft() [Test] public async Task InstallMSI() { - if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + if (string.IsNullOrEmpty(TestIndex.MsiInstaller)) { Assert.Ignore("MSI installer not available"); } diff --git a/src/AppInstallerCLIE2ETests/Interop/InteropSetUpFixture.cs b/src/AppInstallerCLIE2ETests/Interop/InteropSetUpFixture.cs index 604843812d..6e204a589c 100644 --- a/src/AppInstallerCLIE2ETests/Interop/InteropSetUpFixture.cs +++ b/src/AppInstallerCLIE2ETests/Interop/InteropSetUpFixture.cs @@ -6,7 +6,8 @@ namespace AppInstallerCLIE2ETests.Interop { - using System; + using System; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs index 9d5648373b..a4f43adf54 100644 --- a/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/UninstallInterop.cs @@ -1,20 +1,21 @@ -// ----------------------------------------------------------------------------- -// -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// -// ----------------------------------------------------------------------------- - +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + namespace AppInstallerCLIE2ETests.Interop { using System; using System.IO; using System.Threading.Tasks; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; using Microsoft.Management.Deployment.Projection; using NUnit.Framework; - /// - /// Test uninstall interop. + /// + /// Test uninstall interop. /// [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.InProcess), Category = nameof(InstanceInitializersSource.InProcess))] [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.OutOfProcess), Category = nameof(InstanceInitializersSource.OutOfProcess))] @@ -22,19 +23,19 @@ public class UninstallInterop : BaseInterop { private string installDir; private PackageManager packageManager; - private PackageCatalogReference compositeSource; - - /// - /// Initializes a new instance of the class. - /// - /// Initializer. + private PackageCatalogReference compositeSource; + + /// + /// Initializes a new instance of the class. + /// + /// Initializer. public UninstallInterop(IInstanceInitializer initializer) : base(initializer) { } - /// - /// Set up. + /// + /// Set up. /// [SetUp] public void Init() @@ -49,11 +50,11 @@ public void Init() options.Catalogs.Add(testSource); options.CompositeSearchBehavior = CompositeSearchBehavior.AllCatalogs; this.compositeSource = this.packageManager.CreateCompositePackageCatalog(options); - } - - /// - /// Test uninstall exe. - /// + } + + /// + /// Test uninstall exe. + /// /// A representing the asynchronous unit test. [Test] public async Task UninstallTestExe() @@ -80,14 +81,14 @@ public async Task UninstallTestExe() Assert.True(TestCommon.VerifyTestExeUninstalled(this.installDir)); } - /// - /// Test uninstall msi. - /// + /// + /// Test uninstall msi. + /// /// A representing the asynchronous unit test. [Test] public async Task UninstallTestMsi() { - if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + if (string.IsNullOrEmpty(TestIndex.MsiInstaller)) { Assert.Ignore("MSI installer not available"); } @@ -113,9 +114,9 @@ public async Task UninstallTestMsi() Assert.True(TestCommon.VerifyTestMsiUninstalled(this.installDir)); } - /// - /// Test uninstall msix. - /// + /// + /// Test uninstall msix. + /// /// A representing the asynchronous unit test. [Test] public async Task UninstallTestMsix() @@ -140,14 +141,14 @@ public async Task UninstallTestMsix() Assert.True(TestCommon.VerifyTestMsixUninstalled()); } - /// - /// Test uninstall msix with machine scope. - /// + /// + /// Test uninstall msix with machine scope. + /// /// A representing the asynchronous unit test. [Test] public async Task UninstallTestMsixMachineScope() { - // TODO: Provision and Deprovision api not supported in build server. + // TODO: Provision and Deprovision api not supported in build server. Assert.Ignore(); // Find package @@ -174,9 +175,9 @@ public async Task UninstallTestMsixMachineScope() Assert.True(TestCommon.VerifyTestMsixUninstalled(true)); } - /// - /// Test uninstall portable package. - /// + /// + /// Test uninstall portable package. + /// /// A representing the asynchronous unit test. [Test] public async Task UninstallPortable() @@ -207,9 +208,9 @@ public async Task UninstallPortable() TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); } - /// - /// Test uninstall portable package with product code. - /// + /// + /// Test uninstall portable package with product code. + /// /// A representing the asynchronous unit test. [Test] public async Task UninstallPortableWithProductCode() @@ -240,9 +241,9 @@ public async Task UninstallPortableWithProductCode() TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, false); } - /// - /// Test uninstall portable package modified symlink. - /// + /// + /// Test uninstall portable package modified symlink. + /// /// A representing the asynchronous unit test. [Test] public async Task UninstallPortableModifiedSymlink() @@ -279,9 +280,9 @@ public async Task UninstallPortableModifiedSymlink() modifiedSymlinkInfo.Delete(); } - /// - /// Test uninstall not indexed. - /// + /// + /// Test uninstall not indexed. + /// /// A representing the asynchronous unit test. [Test] public async Task UninstallNotIndexed() diff --git a/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs b/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs index a8378ea3d7..9497a1000d 100644 --- a/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/UpgradeInterop.cs @@ -9,7 +9,8 @@ namespace AppInstallerCLIE2ETests.Interop using System; using System.Collections.Generic; using System.IO; - using System.Threading.Tasks; + using System.Threading.Tasks; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; using Microsoft.Management.Deployment.Projection; using NUnit.Framework; diff --git a/src/AppInstallerCLIE2ETests/ListCommand.cs b/src/AppInstallerCLIE2ETests/ListCommand.cs index 84de5d42d8..52bc0c528e 100644 --- a/src/AppInstallerCLIE2ETests/ListCommand.cs +++ b/src/AppInstallerCLIE2ETests/ListCommand.cs @@ -7,6 +7,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// @@ -84,7 +85,7 @@ public void ListWithUpgradeCode() // Installs the MSI installer using the TestMsiInstaller package. // Then tries listing the TestMsiInstallerUpgradeCode package, which should // be correlated to it by the UpgradeCode. - if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + if (string.IsNullOrEmpty(TestIndex.MsiInstaller)) { Assert.Ignore("MSI installer not available"); } diff --git a/src/AppInstallerCLIE2ETests/Pinning.cs b/src/AppInstallerCLIE2ETests/Pinning.cs index 5fb22a2244..e2964a8653 100644 --- a/src/AppInstallerCLIE2ETests/Pinning.cs +++ b/src/AppInstallerCLIE2ETests/Pinning.cs @@ -7,8 +7,9 @@ namespace AppInstallerCLIE2ETests { using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; - using static AppInstallerCLIE2ETests.TestCommon; + using static AppInstallerCLIE2ETests.Helpers.TestCommon; /// /// Test upgrading pinned packages. diff --git a/src/AppInstallerCLIE2ETests/PowerShell/PowerShellHost.cs b/src/AppInstallerCLIE2ETests/PowerShell/PowerShellHost.cs index 486cf9e296..a40955cdd8 100644 --- a/src/AppInstallerCLIE2ETests/PowerShell/PowerShellHost.cs +++ b/src/AppInstallerCLIE2ETests/PowerShell/PowerShellHost.cs @@ -10,6 +10,7 @@ namespace AppInstallerCLIE2ETests.PowerShell using System.Collections; using System.Management.Automation; using System.Management.Automation.Runspaces; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.PowerShell; using NUnit.Framework; @@ -31,7 +32,7 @@ public PowerShellHost() initialSessionState.ExecutionPolicy = ExecutionPolicy.Unrestricted; initialSessionState.ImportPSModule(new string[] { - TestCommon.PowerShellModulePath, + TestSetup.Parameters.PowerShellModuleManifestPath, }); this.runspace = RunspaceFactory.CreateRunspace(initialSessionState); diff --git a/src/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs b/src/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs index 58593466e7..516972cba7 100644 --- a/src/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs +++ b/src/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs @@ -11,6 +11,7 @@ namespace AppInstallerCLIE2ETests.PowerShell using System.Diagnostics; using System.Linq; using System.Management.Automation; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/README.md b/src/AppInstallerCLIE2ETests/README.md index a6417e5918..4c4d3e9852 100644 --- a/src/AppInstallerCLIE2ETests/README.md +++ b/src/AppInstallerCLIE2ETests/README.md @@ -1,50 +1,13 @@ # How to Run End-To-End Tests for Windows Package Manager Client +Most of the tests require having the local test source added into winget. The local test source must be hosted in a localhost web server. -## Step 1: Launch Localhost Web Server -In order to run any of the E2E tests, you will need to first launch the LocalhostWebServer executable. This program serves static test files from a given directory path through a HTTPS local loopback server in order to maintain a closed and controlled repository for resources used for testing purposes. - -### Parameters - -The executable has 3 mandatory parameters and 1 optional parameter: -|Parameter Name | Mandatory/Optional | Description | -|--|--|--| -| **StaticFileRoot** | Mandatory | Path to serve static root directory. If the directory path does not exist, a new directory will be created for you. | -| **CertPath** | Mandatory | Path to HTTPS Developer Certificate. A self signed developer certificate will need to be created in order to verify localhost https. | -| **CertPassword** | Mandatory | HTTPS Developer Certificate Password | -| **Port** | Optional | Port number [Default Port Number: 5001] | - -### How to create and trust an ASP.NET Core HTTPS Development Certificate -Windows Package Manager Client (WinGet.exe) requires new sources added to the WinGet repositories be securely accessed through HTTPS. Therefore, in order to verify the LocalhostWebServer, you will need to create a self-signed development certificate to verify the localhost address. - -- Open command prompt in administrator mode -- Run **dotnet dev-certs https --trust** in the command line -- Open up **certmgr** (search Manage User Certificates in Windows search bar) -- Locate the newly created localhost certificate in the Personal/Certificates folder with a friendly name of "ASP.NET Core HTTPS development certificate" -- Right click on the certificate --> All Tasks --> Export.. -- Click Yes to export the private key -- Export file using Personal Information Exchange (.pfx) file format -- Create and confirm password using SHA256 encryption (any password will work, just make sure to remember it for later) -- Save HTTPS development certificate and refer its certificate path and password when launching the Localhost Webserver -### How to run LocalhostWebServer.exe -The executable can most likely be found in this path: **\src\x86\Release\LocalhostWebServer** - -The command line call to run the executable needs to follow the format: - - LocalhostWebServer.exe StaticFileRoot= CertPath= CertPassword= - -Therefore to run the executable in the command line, simply change into the directory that contains **LocalhostWebServer.exe** and run the executable with the corresponding parameter values. Here is an example: (Don't forget to modify the path to match your own local computer) - - cd C:\Users\MSFT\source\repos\winget-cli\src\AnyCPU\Debug\LocalhostWebServer - - LocalhostWebServer.exe StaticFileRoot=C:\Users\MSFT\AppData\Local\Temp\TestLocalIndex CertPath=C:\Users\MSFT\Temp\HTTPSDevCert.pfx CertPassword=password - - -## 2. Prepare Test.runsettings file - The E2E tests are built on the nunit testing framework and rely on this test.runsettings file located here: **D:\Src\WinGet\Client\src\AppInstallerCLIE2ETests\Test.runsettings**. These parameters are used by the tests at runtime and need to be configured before running any of the E2E tests. - After populating the parameters in the Test.runsettings file, make sure to configure the run settings to point to the file. This can be done by opening **Test Explorer - Settings - Configure Run Settings point to file: D:\Src\WinGet\Client\src\AppInstallerCLIE2ETests\Test.runsettings** +## Run locally +The E2E tests are built on the nunit testing framework and can be configured with a Test.runsettings file. The project has its own default parameters but typically you will want to expand it by modifying **src\AppInstallerCLIE2ETests\Test.runsettings** with the parameters that you want and set it up by opening **Test Explorer - Settings - Configure Run Settings point to file: src\AppInstallerCLIE2ETests\Test.runsettings** +If your tests uses the test source see the [LocalhostWebServer](#LocalhostWebServer) and [WinGetSourceCreator](#WinGetSourceCreator) sections. +### Run settings. |Parameter| Description | |--|--| | PackagedContext | Indicates if the test should be run under packaged context | @@ -56,46 +19,31 @@ Therefore to run the executable in the command line, simply change into the dire | StaticFileRootPath | Path to the set of static test files that will be served as the source for testing purposes. This path should be identical to the one provided to the LocalHostWebServer| | MsixTestInstallerPath | The MSIX (or APPX) Installer executable under test. | | ExeTestInstallerPath |The Exe Installer executable under test. | +| MsiTestInstallerPath | The MSI Installer executable under test. | | PackageCertificatePath | Signing Certificate Path used to certify test index source package | | PowerShellModulePath | Path to the PowerShell module manifest file under test | +| PowerShellModulePath | The local server cert file | +| SkipTestSource | I solemnly swear the test won't use the local test source or the source is already set up. | -#### Example of Test.runsettings format: - - - - - - - - - - - - - - - - - #### Example of Test.runsettings with completed parameters: -Make sure to replace **MSFT** with your own user name. Modifying this example with the correct path to each test run parameter for your own local computer should be sufficient to successfully run the E2E tests once all steps are completed. - - - - - - - - - - - - - - - - - +Assuming you clone winget-cli in c:\dev, the localhost web server is running in c:\dev\TestLocalIndex (without creating the test source) and you built x64 Debug, this should cover most of the tests. + + + + + + + + + + + + + + + + +The easiest way to generate AppInstallerTestMsixInstaller.msix is by running makeappx pack for c:\dev\winget-cli\src\AppInstallerTestMsixInstaller\bin\x64\Debug. #### Log Files After running the E2E Tests, the logs can be found in the following paths: @@ -103,3 +51,124 @@ After running the E2E Tests, the logs can be found in the following paths: - **%LOCALAPPDATA%\Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\DiagOutputDir** - **%LOCALAPPDATA%\E2ETestLogs** - **%TEMP%\WinGet\defaultState** + +## LocalhostWebServer +The src\Tool\LocalhostWebServer project generates an executable that serves static test files from a given directory path through a HTTPS local loopback server in order to maintain a closed and controlled repository for resources used for testing purposes. + +### Start localhost web server + +The localhost web server needs to be running for the duration of the tests. The easiest way to run it is to use src\Tool\LocalhostWebServer\Run-LocalhostWebServer.ps1 in a different PowerShell session. + +|Parameter | Type | Description | +|--|--|--| +| **BuildRoot** | Mandatory | The output path of the LocalhostWebServer project. Normally something like \src\\\LocalhostWebServer +| **StaticFileRoot** | Mandatory | Path to serve static root directory. If the directory path does not exist, a new directory will be created for you. | +| **CertPath** | Mandatory | Path to HTTPS Developer Certificate. A self signed developer certificate will need to be created in order to verify localhost https. | +| **CertPassword** | Mandatory | HTTPS Developer Certificate Password | +| **Port** | Optional | Port number [Default Port Number: 5001] | +| **OutCertFile** | Optional | The exported certificate used | +| **LocalSourceJson** | Optional | The local source definition. If set generates the source. | + +### How to create and trust an ASP.NET Core HTTPS Development Certificate +Windows Package Manager Client (WinGet.exe) requires new sources added to the WinGet repositories be securely accessed through HTTPS. Therefore, in order to verify the LocalhostWebServer, you will need to create a self-signed development certificate to verify the localhost address. + +- Open command prompt in administrator mode +- Run **dotnet dev-certs https --trust** in the command line +- Open up **certmgr** (search Manage User Certificates in Windows search bar) +- Locate the newly created localhost certificate in the Personal/Certificates folder with a friendly name of "ASP.NET Core HTTPS development certificate" +- Right click on the certificate --> All Tasks --> Export.. +- Click Yes to export the private key +- Export file using Personal Information Exchange (.pfx) file format +- Create and confirm password using SHA256 encryption (any password will work, just make sure to remember it for later) +- Save HTTPS development certificate and refer its certificate path and password when launching the Localhost Webserver + +## WinGetSourceCreator +The src\WinGetSourceCreator is a project that helps generate a new winget source for local development. It is consumed by IndexCreationTool, LocalhostWebServer and AppInstallerCLIE2ETests projects. It supports: +- Prepare installers by signing them and placing then in the working directory and computing their hashes. For msix, signature hash is also supported. +- Generate zip installers. +- Generate the signed source.msix and index.db. + +LocalSource is the object that contains the definition of the source. A json serialized version of it is the input for IndexCreationTool and LocalhostWebServer. + +Example: +``` +{ + # If running E2E this is must be the StaticFileRoot used for the localhost web server + "WorkingDirectory": "c:/dev/temp/TestLocalIndex", + + # The appx manifest to generate the source.msix file. + "AppxManifest": "c:/dev/winget-cli/src/AppInstallerCLIE2ETests/TestData/Package/AppxManifest.xml", + + # A list of directories or files to copy. If a directory, it copies all the *.yaml files preserving subdirectories. + "LocalManifests": [ + "c:/dev/winget-cli/src/x86/Release/AppInstallerCLIE2ETests/TestData/Manifests" + ], + + # The signature to use. + "Signature": { + "CertFile": "cert.pfx", + "Password": "1324", + + # If set it will modify the Package Identity Publisher in the AppxManifest.xml + "Publisher": "CN:ThousandSunny" + } + + # Installers that are already present in the machine, by default the installers will be signed using their Signature + # property if set or the top level one. + "LocalInstallers": [ + { + "Type": "exe", + "Input": "c:/dev/winget-cli/src/x64/Debug/AppInstallerTestExeInstaller\AppInstallerTestExeInstaller.exe", + + # Name of the installer to be copied and signed if needed. + "Name": "AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe", + + # The token in the manifests for this installer. This will be replaces at copy manifests time. + "HashToken": "" + + # Overrides top level one. + "Signature": { + "CertFile": "cert2.pfx", + "Password": "2345", + } + }, + { + "Type": "msi", + "Input": "c:/dev/winget-cli/src/AppInstallerCLIE2ETests/TestData/AppInstallerTestMsiInstaller.msi", + "Name": "AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi", + "HashToken": "" + + # Don't sign this. + "SkipSignature": true + }, + { + "Type": "msix", + "Input": "D:/dev/temp/AppInstallerTestMsixInstaller.msix", + "Name": "AppInstallerTestMsixInstaller/AppInstallerTestMsixInstaller.msix", + "HashToken": "", + + # Only supported by where type is msix. Package must be signed, either already signed or signed when copied. + "SignatureToken": "", + } + ], + + # These are installers that are generated on the go. Currently only zip is supported. + "DynamicInstallers": [ + { + # Zip installers are never signed. + "Type": "zip", + + # List of files to zip. Does not preserve subdirectories. + "Input": [ + "D:/dev/temp/TestLocalIndex/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe", + "D:/dev/temp/TestLocalIndex/AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi", + "D:/dev/temp/TestLocalIndex/AppInstallerTestMsixInstaller/AppInstallerTestMsixInstaller.msix" + ], + + "Name": "AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip", + + "HashToken": "" + } + ], +} +``` diff --git a/src/AppInstallerCLIE2ETests/RunCommandException.cs b/src/AppInstallerCLIE2ETests/RunCommandException.cs index bf93487a39..cff2477ed0 100644 --- a/src/AppInstallerCLIE2ETests/RunCommandException.cs +++ b/src/AppInstallerCLIE2ETests/RunCommandException.cs @@ -7,7 +7,7 @@ namespace AppInstallerCLIE2ETests { using System; - using static AppInstallerCLIE2ETests.TestCommon; + using static AppInstallerCLIE2ETests.Helpers.TestCommon; /// /// An exception that occurred when running a command. diff --git a/src/AppInstallerCLIE2ETests/SearchCommand.cs b/src/AppInstallerCLIE2ETests/SearchCommand.cs index 1c304220b5..c030f7d166 100644 --- a/src/AppInstallerCLIE2ETests/SearchCommand.cs +++ b/src/AppInstallerCLIE2ETests/SearchCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/SetUpFixture.cs b/src/AppInstallerCLIE2ETests/SetUpFixture.cs index f063f4fb87..14b5fdaf59 100644 --- a/src/AppInstallerCLIE2ETests/SetUpFixture.cs +++ b/src/AppInstallerCLIE2ETests/SetUpFixture.cs @@ -6,8 +6,7 @@ namespace AppInstallerCLIE2ETests { - using System; - using System.IO; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Win32; using NUnit.Framework; @@ -28,107 +27,39 @@ public class SetUpFixture [OneTimeSetUp] public void Setup() { - if (TestContext.Parameters.Count == 0) + var testParams = TestSetup.Parameters; + + if (testParams.IsDefault) { // If no parameters are provided, use defaults that work locally. // This allows the user to assume responsibility for setup. - TestCommon.PackagedContext = true; - TestCommon.VerboseLogging = true; - TestCommon.AICLIPath = "WinGetDev.exe"; - TestCommon.StaticFileRootPath = Path.GetTempPath(); - TestCommon.SettingsJsonFilePath = WinGetSettingsHelper.GetUserSettingsPath(); - WinGetSettingsHelper.InitializeWingetSettings(); shouldDoAnyTeardown = false; - - return; - } - - // Read TestParameters and set runtime variables - TestCommon.PackagedContext = TestContext.Parameters.Exists(Constants.PackagedContextParameter) && - TestContext.Parameters.Get(Constants.PackagedContextParameter).Equals("true", StringComparison.OrdinalIgnoreCase); - - TestCommon.VerboseLogging = TestContext.Parameters.Exists(Constants.VerboseLoggingParameter) && - TestContext.Parameters.Get(Constants.VerboseLoggingParameter).Equals("true", StringComparison.OrdinalIgnoreCase); - - TestCommon.LooseFileRegistration = TestContext.Parameters.Exists(Constants.LooseFileRegistrationParameter) && - TestContext.Parameters.Get(Constants.LooseFileRegistrationParameter).Equals("true", StringComparison.OrdinalIgnoreCase); - - TestCommon.InvokeCommandInDesktopPackage = TestContext.Parameters.Exists(Constants.InvokeCommandInDesktopPackageParameter) && - TestContext.Parameters.Get(Constants.InvokeCommandInDesktopPackageParameter).Equals("true", StringComparison.OrdinalIgnoreCase); - - if (TestContext.Parameters.Exists(Constants.AICLIPathParameter)) - { - TestCommon.AICLIPath = TestContext.Parameters.Get(Constants.AICLIPathParameter); - } - else - { - if (TestCommon.PackagedContext) - { - // For packaged context, default to AppExecutionAlias - TestCommon.AICLIPath = "WinGetDev.exe"; - } - else - { - TestCommon.AICLIPath = TestCommon.GetTestFile("winget.exe"); - } - } - - if (TestContext.Parameters.Exists(Constants.AICLIPackagePathParameter)) - { - TestCommon.AICLIPackagePath = TestContext.Parameters.Get(Constants.AICLIPackagePathParameter); } else { - TestCommon.AICLIPackagePath = TestCommon.GetTestFile("AppInstallerCLIPackage.appxbundle"); - } - - if (TestCommon.LooseFileRegistration && TestCommon.InvokeCommandInDesktopPackage) - { - TestCommon.AICLIPath = Path.Combine(TestCommon.AICLIPackagePath, TestCommon.AICLIPath); - } - - shouldDisableDevModeOnExit = this.EnableDevMode(true); + shouldDisableDevModeOnExit = this.EnableDevMode(true); - shouldRevertDefaultFileTypeRiskOnExit = this.DecreaseFileTypeRisk(".exe;.msi", false); + shouldRevertDefaultFileTypeRiskOnExit = this.DecreaseFileTypeRisk(".exe;.msi", false); - Assert.True(TestCommon.RunCommand("certutil.exe", "-addstore -f \"TRUSTEDPEOPLE\" " + TestCommon.GetTestDataFile(Constants.AppInstallerTestCert)), "Add AppInstallerTestCert"); + Assert.True(TestCommon.RunCommand("certutil.exe", "-addstore -f \"TRUSTEDPEOPLE\" " + TestCommon.GetTestDataFile(Constants.AppInstallerTestCert)), "Add AppInstallerTestCert"); - if (TestCommon.PackagedContext) - { - if (TestCommon.LooseFileRegistration) + if (testParams.PackagedContext) { - Assert.True(TestCommon.InstallMsixRegister(TestCommon.AICLIPackagePath), $"InstallMsixRegister : {TestCommon.AICLIPackagePath}"); - } - else - { - Assert.True(TestCommon.InstallMsix(TestCommon.AICLIPackagePath), $"InstallMsix : {TestCommon.AICLIPackagePath}"); + if (testParams.LooseFileRegistration) + { + Assert.True(TestCommon.InstallMsixRegister(testParams.AICLIPackagePath), $"InstallMsixRegister : {testParams.AICLIPackagePath}"); + } + else + { + Assert.True(TestCommon.InstallMsix(testParams.AICLIPackagePath), $"InstallMsix : {testParams.AICLIPackagePath}"); + } } } - if (TestContext.Parameters.Exists(Constants.StaticFileRootPathParameter)) + if (!testParams.SkipTestSource) { - TestCommon.StaticFileRootPath = TestContext.Parameters.Get(Constants.StaticFileRootPathParameter); + TestIndex.GenerateE2ESource(); } - else - { - TestCommon.StaticFileRootPath = Path.GetTempPath(); - } - - if (TestContext.Parameters.Exists(Constants.PackageCertificatePathParameter)) - { - TestCommon.PackageCertificatePath = TestContext.Parameters.Get(Constants.PackageCertificatePathParameter); - } - - if (TestContext.Parameters.Exists(Constants.PowerShellModulePathParameter)) - { - TestCommon.PowerShellModulePath = TestContext.Parameters.Get(Constants.PowerShellModulePathParameter); - } - - this.ReadTestInstallerPaths(); - - TestIndexSetup.GenerateTestDirectory(); - - TestCommon.SettingsJsonFilePath = WinGetSettingsHelper.GetUserSettingsPath(); WinGetSettingsHelper.InitializeWingetSettings(); } @@ -155,7 +86,7 @@ public void TearDown() TestCommon.PublishE2ETestLogs(); - if (TestCommon.PackagedContext) + if (TestSetup.Parameters.PackagedContext) { TestCommon.RemoveMsix(Constants.AICLIPackageName); } @@ -215,26 +146,5 @@ private bool DecreaseFileTypeRisk(string fileTypes, bool revert) return true; } } - - private void ReadTestInstallerPaths() - { - if (TestContext.Parameters.Exists(Constants.ExeInstallerPathParameter) - && File.Exists(TestContext.Parameters.Get(Constants.ExeInstallerPathParameter))) - { - TestCommon.ExeInstallerPath = TestContext.Parameters.Get(Constants.ExeInstallerPathParameter); - } - - if (TestContext.Parameters.Exists(Constants.MsiInstallerPathParameter) - && File.Exists(TestContext.Parameters.Get(Constants.MsiInstallerPathParameter))) - { - TestCommon.MsiInstallerPath = TestContext.Parameters.Get(Constants.MsiInstallerPathParameter); - } - - if (TestContext.Parameters.Exists(Constants.MsixInstallerPathParameter) - && File.Exists(TestContext.Parameters.Get(Constants.MsixInstallerPathParameter))) - { - TestCommon.MsixInstallerPath = TestContext.Parameters.Get(Constants.MsixInstallerPathParameter); - } - } } } diff --git a/src/AppInstallerCLIE2ETests/ShowCommand.cs b/src/AppInstallerCLIE2ETests/ShowCommand.cs index 2959628e98..7fee03b1de 100644 --- a/src/AppInstallerCLIE2ETests/ShowCommand.cs +++ b/src/AppInstallerCLIE2ETests/ShowCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs index 4a07bff316..ff034f18e8 100644 --- a/src/AppInstallerCLIE2ETests/SourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/Test.runsettings b/src/AppInstallerCLIE2ETests/Test.runsettings index 3109faab93..6f6079e24e 100644 --- a/src/AppInstallerCLIE2ETests/Test.runsettings +++ b/src/AppInstallerCLIE2ETests/Test.runsettings @@ -4,35 +4,39 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Package/AppxManifest.xml b/src/AppInstallerCLIE2ETests/TestData/Package/AppxManifest.xml index b0ad5141a4..42852f1281 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Package/AppxManifest.xml +++ b/src/AppInstallerCLIE2ETests/TestData/Package/AppxManifest.xml @@ -9,10 +9,8 @@ Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" Version="2020.805.713.335" /> - - - WinGet Source + WinGet E2E Source Microsoft Corporation Assets\AppPackageStoreLogo.png @@ -23,10 +21,10 @@ - diff --git a/src/AppInstallerCLIE2ETests/TestData/localsource.json b/src/AppInstallerCLIE2ETests/TestData/localsource.json new file mode 100644 index 0000000000..adb672f0fc --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/localsource.json @@ -0,0 +1,43 @@ +{ + "AppxManifest": "%BUILD_SOURCESDIRECTORY%/src/AppInstallerCLIE2ETests/TestData/Package/AppxManifest.xml", + "WorkingDirectory": "%AGENT_TEMPDIRECTORY%/TestLocalIndex", + "LocalManifests": [ + "%BUILD_SOURCESDIRECTORY%/src/AppInstallerCLIE2ETests/TestData/Manifests" + ], + "LocalInstallers": [ + { + "Type": "exe", + "Name": "AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe", + "Input": "%BUILDOUTDIR%/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe", + "HashToken": "" + }, + { + "Type": "msi", + "Name": "AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi", + "Input": "%BUILD_SOURCESDIRECTORY%/src/AppInstallerCLIE2ETests/TestData/AppInstallerTestMsiInstaller.msi", + "HashToken": "" + }, + { + "Type": "msix", + "Name": "AppInstallerTestMsixInstaller/AppInstallerTestMsixInstaller.msix", + "Input": "%BUILD_ARTIFACTSTAGINGDIRECTORY%/AppInstallerTestMsixInstaller.msix", + "HashToken": "", + "SignatureToken": "" + } + ], + "DynamicInstallers": [ + { + "Type": "zip", + "Name": "AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip", + "Input": [ + "%AGENT_TEMPDIRECTORY%/TestLocalIndex/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe", + "%AGENT_TEMPDIRECTORY%/TestLocalIndex/AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi", + "%AGENT_TEMPDIRECTORY%/TestLocalIndex/AppInstallerTestMsixInstaller/AppInstallerTestMsixInstaller.msix" + ], + "HashToken": "" + } + ], + "Signature": { + "CertFile": "%APPINSTALLERTEST_SECUREFILEPATH%" + } +} diff --git a/src/AppInstallerCLIE2ETests/TestHashHelper.cs b/src/AppInstallerCLIE2ETests/TestHashHelper.cs deleted file mode 100644 index f1137c5cc4..0000000000 --- a/src/AppInstallerCLIE2ETests/TestHashHelper.cs +++ /dev/null @@ -1,210 +0,0 @@ -// ----------------------------------------------------------------------------- -// -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// -// ----------------------------------------------------------------------------- - -namespace AppInstallerCLIE2ETests -{ - using System; - using System.IO; - using System.Security.Cryptography; - using Microsoft.Msix.Utils.ProcessRunner; - - /// - /// TestHashHelper. - /// - public class TestHashHelper - { - /// - /// Gets or sets the exe installer hash value. - /// - public static string ExeInstallerHashValue { get; set; } - - /// - /// Gets or sets the msi installer hash value. - /// - public static string MsiInstallerHashValue { get; set; } - - /// - /// Gets or sets the msix installer hash value. - /// - public static string MsixInstallerHashValue { get; set; } - - /// - /// Gets or sets the zip installer hash value. - /// - public static string ZipInstallerHashValue { get; set; } - - /// - /// Gets or sets the signature hash value. - /// - public static string SignatureHashValue { get; set; } - - /// - /// Sets the hash of the installers. - /// - public static void HashInstallers() - { - if (!string.IsNullOrEmpty(TestCommon.ExeInstallerPath)) - { - ExeInstallerHashValue = HashFile(TestCommon.ExeInstallerPath); - } - - if (!string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) - { - MsiInstallerHashValue = HashFile(TestCommon.MsiInstallerPath); - } - - if (!string.IsNullOrEmpty(TestCommon.MsixInstallerPath)) - { - MsixInstallerHashValue = HashFile(TestCommon.MsixInstallerPath); - SignatureHashValue = HashSignatureFromMSIX(TestCommon.MsixInstallerPath); - } - - if (!string.IsNullOrEmpty(TestCommon.ZipInstallerPath)) - { - ZipInstallerHashValue = HashFile(TestCommon.ZipInstallerPath); - } - } - - /// - /// Iterates through all manifest files in a directory and replaces the hash token with the - /// corresponding installer hash token. - /// - /// Path to manifest directory. - public static void ReplaceManifestHashToken(string pathToManifestDir) - { - var dir = new DirectoryInfo(pathToManifestDir); - FileInfo[] files = dir.GetFiles(); - - foreach (FileInfo file in files) - { - string text = File.ReadAllText(file.FullName); - - if (text.Contains("")) - { - text = text.Replace("", ExeInstallerHashValue); - File.WriteAllText(file.FullName, text); - } - - if (text.Contains("")) - { - text = text.Replace("", MsiInstallerHashValue); - File.WriteAllText(file.FullName, text); - } - - if (text.Contains("")) - { - text = text.Replace("", MsixInstallerHashValue); - - if (text.Contains("")) - { - text = text.Replace("", SignatureHashValue); - } - - File.WriteAllText(file.FullName, text); - } - - if (text.Contains("")) - { - text = text.Replace("", ZipInstallerHashValue); - File.WriteAllText(file.FullName, text); - } - } - } - - /// - /// Gets hash of the AppxSignature.p7x file in the msix. - /// - /// Package file path. - /// Hash of signature file. - public static string HashSignatureFromMSIX(string packageFilePath) - { - // Obtain MakeAppX Executable Path - string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; - string makeappxExecutable = Path.Combine(pathToSDK, "makeappx.exe"); - - // Generate temp path to unpack MSIX package - FileInfo fileInfo = new FileInfo(packageFilePath); - string packageName = Path.GetFileNameWithoutExtension(fileInfo.Name); - string tempPath = Path.GetTempPath(); - string extractedPackageDest = Path.Combine(tempPath, packageName); - - // Delete existing extracted package directories to avoid stalling MakeAppX command - if (Directory.Exists(extractedPackageDest)) - { - TestIndexSetup.DeleteDirectoryContents(Directory.CreateDirectory(extractedPackageDest)); - Directory.Delete(extractedPackageDest); - } - - TestIndexSetup.RunCommand(makeappxExecutable, $"unpack /nv /p {packageFilePath} /d {extractedPackageDest}"); - - string packageSignaturePath = Path.Combine(extractedPackageDest, "AppxSignature.p7x"); - return HashFile(packageSignaturePath); - } - - /// - /// Gets the hash of the specified file. - /// - /// File path. - /// Hash of file. - public static string HashFile(string filePath) - { - FileInfo file; - - try - { - file = new FileInfo(filePath); - } - catch (FileNotFoundException e) - { - Console.WriteLine($"File Not Found: {e.Message}"); - throw; - } - - string hash = string.Empty; - - using (SHA256 mySHA256 = SHA256.Create()) - { - try - { - FileStream fileStream = file.Open(FileMode.Open); - fileStream.Position = 0; - byte[] hashValue = mySHA256.ComputeHash(fileStream); - hash = ConvertHashByteToString(hashValue); - fileStream.Close(); - } - catch (IOException e) - { - Console.WriteLine($"I/O Exception: {e.Message}"); - throw; - } - catch (UnauthorizedAccessException e) - { - Console.WriteLine($"Access Exception: {e.Message}"); - throw; - } - } - - return hash; - } - - /// - /// Converts the byte hash into its string format. - /// - /// Hash. - /// Hash as string. - public static string ConvertHashByteToString(byte[] array) - { - string hashValue = string.Empty; - - for (int i = 0; i < array.Length; i++) - { - hashValue = hashValue + $"{array[i]:X2}"; - } - - return hashValue; - } - } -} diff --git a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs deleted file mode 100644 index 65574991d2..0000000000 --- a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs +++ /dev/null @@ -1,289 +0,0 @@ -// ----------------------------------------------------------------------------- -// -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// -// ----------------------------------------------------------------------------- - -namespace AppInstallerCLIE2ETests -{ - using System; - using System.Diagnostics; - using System.IO; - using System.IO.Compression; - using Microsoft.Msix.Utils.ProcessRunner; - - /// - /// Test index setup. - /// - public class TestIndexSetup - { - private const string TestDataName = "TestData"; - private const string ManifestsName = "Manifests"; - private const string PackageName = "Package"; - private const string PublicName = "Public"; - - /// - /// Generates the Local Test Index to be served by the Localhost Web Server. - /// 1. Copies TestData to a StaticFileRootPath set in Test.runsettings file - /// 2. Copies and signs installer files (EXE or MSIX) - /// 3. Hashes installer Files - /// 4. Replaces manifests with corresponding hash values - /// 5. Generates a source package for TestData using makeappx/signtool. - /// - public static void GenerateTestDirectory() - { - SetupLocalTestDirectory(TestCommon.StaticFileRootPath); - - if (!string.IsNullOrEmpty(TestCommon.ExeInstallerPath)) - { - CopyExeInstallerToTestDirectory(); - } - - if (!string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) - { - CopyMsiInstallerToTestDirectory(); - } - - if (!string.IsNullOrEmpty(TestCommon.MsixInstallerPath)) - { - CopyMsixInstallerToTestDirectory(); - } - - CreateZipInstallerInTestDirectory(); - - TestHashHelper.HashInstallers(); - - string manifestDirectoryPath = Path.Combine(TestCommon.StaticFileRootPath, ManifestsName); - TestHashHelper.ReplaceManifestHashToken(manifestDirectoryPath); - - SetupSourcePackage(); - } - - /// - /// Sign a file using signtool.exe . - /// - /// File to sign. - public static void SignFile(string filePath) - { - string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; - string signtoolExecutable = Path.Combine(pathToSDK, "signtool.exe"); - RunCommand(signtoolExecutable, $"sign /a /fd sha256 /f {TestCommon.PackageCertificatePath} {filePath}"); - } - - /// - /// Deletes the contents of a given directory. - /// - /// Directory info. - public static void DeleteDirectoryContents(DirectoryInfo directory) - { - foreach (FileInfo file in directory.GetFiles()) - { - // Leave the server certificate file if present - if (file.Name.ToLower() != Constants.TestSourceServerCertificateFileName) - { - try - { - file.Delete(); - } - catch - { - // Just ignore errors in this setup step... - } - } - } - - foreach (DirectoryInfo dir in directory.GetDirectories()) - { - try - { - dir.Delete(true); - } - catch - { - // Just ignore errors in this setup step... - } - } - } - - /// - /// Copies the contents of a given directory from a source path to a destination path. - /// - /// Source directory name. - /// Destination directory name. - public static void CopyDirectory(string sourceDirName, string destDirName) - { - DirectoryInfo dir = new DirectoryInfo(sourceDirName); - DirectoryInfo[] dirs = dir.GetDirectories(); - - if (!Directory.Exists(destDirName)) - { - Directory.CreateDirectory(destDirName); - } - - FileInfo[] files = dir.GetFiles(); - foreach (FileInfo file in files) - { - string temppath = Path.Combine(destDirName, file.Name); - file.CopyTo(temppath, false); - } - - foreach (DirectoryInfo subdir in dirs) - { - string temppath = Path.Combine(destDirName, subdir.Name); - CopyDirectory(subdir.FullName, temppath); - } - } - - /// - /// Run a command. - /// - /// Command. - /// Arguments. - public static void RunCommand(string command, string args) - { - Process p = new Process(); - p.StartInfo = new ProcessStartInfo(command, args); - p.Start(); - p.WaitForExit(); - } - - /// - /// Run a command from a working directory. - /// - /// Command. - /// Arguments. - /// Working directory. - public static void RunCommand(string command, string args, string workingDirectory) - { - Process p = new Process(); - p.StartInfo = new ProcessStartInfo(command, args); - p.StartInfo.WorkingDirectory = workingDirectory; - p.Start(); - p.WaitForExit(); - } - - private static void SetupSourcePackage() - { - string indexDestPath = Path.Combine(TestCommon.StaticFileRootPath, PackageName, PublicName); - - DirectoryInfo parentDir = Directory.GetParent(Directory.GetCurrentDirectory()); - string indexCreationToolPath = Path.Combine(parentDir.FullName, Constants.IndexCreationTool); - string winGetUtilPath = Path.Combine(parentDir.FullName, Constants.WinGetUtil); - - // Copy WingetUtil.dll app extension to IndexCreationTool Path - File.Copy(Path.Combine(winGetUtilPath, @"WinGetUtil.dll"), Path.Combine(indexCreationToolPath, @"WinGetUtil.dll"), true); - - try - { - if (!Directory.Exists(indexDestPath)) - { - Directory.CreateDirectory(indexDestPath); - } - - // Generate Index.db file using IndexCreationTool.exe - RunCommand(Path.Combine(indexCreationToolPath, "IndexCreationTool.exe"), $"-d {TestCommon.StaticFileRootPath} -i {ManifestsName}", indexDestPath); - - string packageDir = Path.Combine(TestCommon.StaticFileRootPath, PackageName); - string indexPackageDestPath = Path.Combine(TestCommon.StaticFileRootPath, Constants.IndexPackage); - string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; - - // Package Test Source and Sign With Package Certificate - string makeappxExecutable = Path.Combine(pathToSDK, "makeappx.exe"); - RunCommand(makeappxExecutable, $"pack /nv /v /o /d {packageDir} /p {indexPackageDestPath}"); - SignFile(indexPackageDestPath); - } - catch (Exception e) - { - Console.WriteLine("Failed. Reason: " + e.Message); - } - } - - private static void CopyExeInstallerToTestDirectory() - { - // Set Exe Test Installer Path - string exeInstallerDestPath = Path.Combine(TestCommon.StaticFileRootPath, Constants.ExeInstaller); - DirectoryInfo exeInstallerDestDir = Directory.CreateDirectory(exeInstallerDestPath); - string exeInstallerFullName = Path.Combine(exeInstallerDestDir.FullName, Constants.ExeInstallerFileName); - - // Copy Exe Test Installer to Destination Path - File.Copy(TestCommon.ExeInstallerPath, exeInstallerFullName, true); - TestCommon.ExeInstallerPath = exeInstallerFullName; - - // Sign EXE Installer File - SignFile(TestCommon.ExeInstallerPath); - } - - private static void CopyMsiInstallerToTestDirectory() - { - // Set MSI Test Installer Path - string msiInstallerDestPath = Path.Combine(TestCommon.StaticFileRootPath, Constants.MsiInstaller); - DirectoryInfo msiInstallerDestDir = Directory.CreateDirectory(msiInstallerDestPath); - - // Copy MSI Test Installer to Destination Path - string msiInstallerFullName = Path.Combine(msiInstallerDestDir.FullName, Constants.MsiInstallerFileName); - - File.Copy(TestCommon.MsiInstallerPath, msiInstallerFullName, true); - TestCommon.MsiInstallerPath = msiInstallerFullName; - - // Sign MSI Installer File - SignFile(TestCommon.MsiInstallerPath); - } - - private static void CopyMsixInstallerToTestDirectory() - { - // Set Msix Test Installer Path - string msixInstallerDestPath = Path.Combine(TestCommon.StaticFileRootPath, Constants.MsixInstaller); - DirectoryInfo msixInstallerDestDir = Directory.CreateDirectory(msixInstallerDestPath); - - // Copy Msix Test Installer to Destination Path - string msixInstallerFullName = Path.Combine(msixInstallerDestDir.FullName, Constants.MsixInstallerFileName); - - File.Copy(TestCommon.MsixInstallerPath, msixInstallerFullName, true); - TestCommon.MsixInstallerPath = msixInstallerFullName; - - // Sign MSIX Installer File - SignFile(TestCommon.MsixInstallerPath); - } - - private static void CreateZipInstallerInTestDirectory() - { - DirectoryInfo zipInstallerDir = Directory.CreateDirectory(Path.Combine(TestCommon.StaticFileRootPath, Constants.ZipInstaller)); - string zipSourceDirFullPath = Directory.CreateDirectory(TestCommon.GetRandomTestDir()).FullName; - - string exeInstallerSourceDestPath = Path.Combine(zipSourceDirFullPath, Constants.ExeInstallerFileName); - string msiInstallerSourceDestPath = Path.Combine(zipSourceDirFullPath, Constants.MsiInstallerFileName); - string msixInstallerSourceDestPath = Path.Combine(zipSourceDirFullPath, Constants.MsixInstallerFileName); - - if (File.Exists(TestCommon.ExeInstallerPath)) - { - File.Copy(TestCommon.ExeInstallerPath, exeInstallerSourceDestPath, true); - } - - if (File.Exists(TestCommon.MsiInstallerPath)) - { - File.Copy(TestCommon.MsiInstallerPath, msiInstallerSourceDestPath, true); - } - - if (File.Exists(TestCommon.MsixInstallerPath)) - { - File.Copy(TestCommon.MsixInstallerPath, msixInstallerSourceDestPath, true); - } - - string destArchiveFullPath = Path.Combine(zipInstallerDir.FullName, Constants.ZipInstallerFileName); - ZipFile.CreateFromDirectory(zipSourceDirFullPath, destArchiveFullPath); - TestCommon.ZipInstallerPath = destArchiveFullPath; - } - - private static void SetupLocalTestDirectory(string staticFileRootPath) - { - DirectoryInfo staticFileRootDir = Directory.CreateDirectory(staticFileRootPath); - - DeleteDirectoryContents(staticFileRootDir); - - string currentDirectory = Environment.CurrentDirectory; - string sourcePath = Path.Combine(currentDirectory, TestDataName); - - CopyDirectory(sourcePath, TestCommon.StaticFileRootPath); - } - } -} diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs index 5b94f6f251..55d09056bd 100644 --- a/src/AppInstallerCLIE2ETests/UninstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/UninstallCommand.cs @@ -7,6 +7,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// @@ -47,7 +48,7 @@ public void UninstallTestExe() [Test] public void UninstallTestMsi() { - if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + if (string.IsNullOrEmpty(TestIndex.MsiInstaller)) { Assert.Ignore("MSI installer not available"); } diff --git a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs index ac25cef8b1..da821201f2 100644 --- a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs +++ b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs @@ -7,6 +7,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/ValidateCommand.cs b/src/AppInstallerCLIE2ETests/ValidateCommand.cs index 4218e5d4c7..db7709ce28 100644 --- a/src/AppInstallerCLIE2ETests/ValidateCommand.cs +++ b/src/AppInstallerCLIE2ETests/ValidateCommand.cs @@ -6,6 +6,7 @@ namespace AppInstallerCLIE2ETests { + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilDownload.cs b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilDownload.cs index 15605f2579..fcec745af1 100644 --- a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilDownload.cs +++ b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilDownload.cs @@ -7,7 +7,8 @@ namespace AppInstallerCLIE2ETests.WinGetUtil { using System.IO; - using System.Linq; + using System.Linq; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilInstallerMetadataCollection.cs b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilInstallerMetadataCollection.cs index ca979a5c98..fcafd33cbc 100644 --- a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilInstallerMetadataCollection.cs +++ b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilInstallerMetadataCollection.cs @@ -8,7 +8,8 @@ namespace AppInstallerCLIE2ETests.WinGetUtil { using System; using System.IO; - using System.Runtime.InteropServices; + using System.Runtime.InteropServices; + using AppInstallerCLIE2ETests.Helpers; using Newtonsoft.Json; using NUnit.Framework; diff --git a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilLog.cs b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilLog.cs index d315f13b85..e99448c06e 100644 --- a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilLog.cs +++ b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilLog.cs @@ -6,7 +6,8 @@ namespace AppInstallerCLIE2ETests.WinGetUtil { - using System.IO; + using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs index 07cc977cc3..0cb5e42b0c 100644 --- a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs +++ b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilManifest.cs @@ -7,7 +7,8 @@ namespace AppInstallerCLIE2ETests.WinGetUtil { using System; - using System.IO; + using System.IO; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilSQLiteIndex.cs b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilSQLiteIndex.cs index dde2d9f09c..ae3986e867 100644 --- a/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilSQLiteIndex.cs +++ b/src/AppInstallerCLIE2ETests/WinGetUtil/WinGetUtilSQLiteIndex.cs @@ -8,7 +8,8 @@ namespace AppInstallerCLIE2ETests.WinGetUtil { using System; using System.IO; - using System.Runtime.InteropServices; + using System.Runtime.InteropServices; + using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; /// diff --git a/src/IndexCreationTool/IndexCreationTool.csproj b/src/IndexCreationTool/IndexCreationTool.csproj index 05b04b2b03..79a4d0812b 100644 --- a/src/IndexCreationTool/IndexCreationTool.csproj +++ b/src/IndexCreationTool/IndexCreationTool.csproj @@ -7,4 +7,18 @@ x64;x86 + + + + + Content + PreserveNewest + True + + + + + + + diff --git a/src/IndexCreationTool/Program.cs b/src/IndexCreationTool/Program.cs index 0d1c34d9cb..f07ba51612 100644 --- a/src/IndexCreationTool/Program.cs +++ b/src/IndexCreationTool/Program.cs @@ -3,127 +3,49 @@ namespace IndexCreationTool { + using Microsoft.WinGetSourceCreator; using System; - using System.Collections.Generic; - using System.Diagnostics; using System.IO; - using System.Linq; + using System.Text.Json; + using System.Text.Json.Serialization; + using WinGetSourceCreator.Model; class Program { - public const string IndexName = @"index.db"; - public const string IndexPathInPackage = @"Public\index.db"; - public const string IndexPackageName = @"source.msix"; - - static void Main(string[] args) + private static void PrintUsage() { - string rootDir = string.Empty; - string appxManifestPath = string.Empty; - string certPath = string.Empty; - - // List of directories to include. By default, include all directories. - List includeDirList = new() { string.Empty }; - - for (int i = 0; i < args.Length; i++) - { - if (args[i] == "-d" && ++i < args.Length) - { - rootDir = args[i]; - } - else if (args[i] == "-i" && ++i < args.Length) - { - includeDirList = args[i].Split(",").ToList(); - } - else if (args[i] == "-m" && ++i < args.Length) - { - appxManifestPath = args[i]; - } - else if (args[i] == "-c" && ++i < args.Length) - { - certPath = args[i]; - } - } - - if (string.IsNullOrEmpty(rootDir)) - { - Console.WriteLine("Usage: IndexCreationTool.exe -d [-i ] [-m [-c ]]"); - return; - } + Console.WriteLine("Usage: IndexCreationTool.exe -f "); + } + static int Main(string[] args) + { try { - if (File.Exists(IndexName)) + string inputFile = string.Empty; + for (int i = 0; i < args.Length; i++) { - File.Delete(IndexName); - } - - using (var indexHelper = WinGetUtilWrapper.Create(IndexName)) - { - foreach (string includeDir in includeDirList) + if (args[i] == "-f" && ++i < args.Length) { - var fullPath = Path.Combine(rootDir, includeDir); - Queue filesQueue = new(Directory.EnumerateFiles(fullPath, "*.yaml", SearchOption.AllDirectories)); - - while (filesQueue.Count > 0) - { - int currentCount = filesQueue.Count; - - for (int i = 0; i < currentCount; i++) - { - string file = filesQueue.Dequeue(); - try - { - indexHelper.AddManifest(file, Path.GetRelativePath(rootDir, file)); - } - catch - { - // If adding manifest to index fails, add to queue and try again. - // This can occur if there is a package dependency that has not yet been added to the index. - filesQueue.Enqueue(file); - } - } - - if (filesQueue.Count == currentCount) - { - Console.WriteLine("Failed to add all manifests in directory to index."); - Environment.Exit(-1); - } - } + inputFile = args[i]; } - - indexHelper.PrepareForPackaging(); } - if (!string.IsNullOrEmpty(appxManifestPath)) + if (string.IsNullOrEmpty(inputFile) || !File.Exists(inputFile)) { - using (StreamWriter outputFile = new StreamWriter("MappingFile.txt")) - { - outputFile.WriteLine("[Files]"); - outputFile.WriteLine($"\"{IndexName}\" \"{IndexPathInPackage}\""); - outputFile.WriteLine($"\"{appxManifestPath}\" \"AppxManifest.xml\""); - } - RunCommand("makeappx.exe", $"pack /f MappingFile.txt /o /nv /p {IndexPackageName}"); - - if (!string.IsNullOrEmpty(certPath)) - { - RunCommand("signtool.exe", $"sign /a /fd sha256 /f {certPath} {IndexPackageName}"); - } + PrintUsage(); + throw new ArgumentException("Missing input file"); } + + WinGetLocalSource.CreateFromLocalSourceFile(inputFile); } catch (Exception e) { - Console.WriteLine("Failed. Reason: " + e.Message); + PrintUsage(); + Console.WriteLine(e.Message); + return -1; } - Environment.Exit(0); - } - - static void RunCommand(string command, string args) - { - Process p = new Process(); - p.StartInfo = new ProcessStartInfo(command, args); - p.Start(); - p.WaitForExit(); + return 0; } } -} \ No newline at end of file +} diff --git a/src/LocalhostWebServer/LocalhostWebServer.csproj b/src/LocalhostWebServer/LocalhostWebServer.csproj index 90f2342421..b4ae8ba832 100644 --- a/src/LocalhostWebServer/LocalhostWebServer.csproj +++ b/src/LocalhostWebServer/LocalhostWebServer.csproj @@ -2,8 +2,22 @@ net6.0 - $(SolutionDir)$(Platform)\$(Configuration)\LocalhostWebServer\ - x64;x86 + $(SolutionDir)$(Platform)\$(Configuration)\LocalhostWebServer\ + x64;x86 + + + + + Content + PreserveNewest + True + + + + + + + diff --git a/src/LocalhostWebServer/Program.cs b/src/LocalhostWebServer/Program.cs index fd73ebf232..46f99719b7 100644 --- a/src/LocalhostWebServer/Program.cs +++ b/src/LocalhostWebServer/Program.cs @@ -10,6 +10,10 @@ namespace LocalhostWebServer using System.IO; using Microsoft.Extensions.Configuration; using System.Security.Cryptography.X509Certificates; + using System.Text.Json.Serialization; + using System.Text.Json; + using WinGetSourceCreator.Model; + using Microsoft.WinGetSourceCreator; public class Program { @@ -22,8 +26,9 @@ static void Main(string[] args) Startup.StaticFileRoot = config.GetValue("StaticFileRoot"); Startup.CertPath = config.GetValue("CertPath"); Startup.CertPassword = config.GetValue("CertPassword"); - Startup.PutCertInRoot = config.GetValue("PutCertInRoot", false); Startup.Port = config.GetValue("Port", 5001); + Startup.OutCertFile = config.GetValue("OutCertFile"); + Startup.LocalSourceJson = config.GetValue("LocalSourceJson"); if (string.IsNullOrEmpty(Startup.StaticFileRoot) || string.IsNullOrEmpty(Startup.CertPath)) @@ -35,10 +40,26 @@ static void Main(string[] args) Directory.CreateDirectory(Startup.StaticFileRoot); - if (Startup.PutCertInRoot) + if (!string.IsNullOrEmpty(Startup.OutCertFile)) { + string parent = Path.GetDirectoryName(Startup.OutCertFile); + if (!string.IsNullOrEmpty(parent)) + { + Directory.CreateDirectory(parent); + } + X509Certificate2 serverCertificate = new X509Certificate2(Startup.CertPath, Startup.CertPassword, X509KeyStorageFlags.EphemeralKeySet); - File.WriteAllBytes(Path.Combine(Startup.StaticFileRoot, "servercert.cer"), serverCertificate.Export(X509ContentType.Cert)); + File.WriteAllBytes(Startup.OutCertFile, serverCertificate.Export(X509ContentType.Cert)); + } + + if (!string.IsNullOrEmpty(Startup.LocalSourceJson)) + { + if (!File.Exists(Startup.LocalSourceJson)) + { + throw new FileNotFoundException(Startup.LocalSourceJson); + } + + WinGetLocalSource.CreateFromLocalSourceFile(Startup.LocalSourceJson); } CreateHostBuilder(args).Build().Run(); diff --git a/src/LocalhostWebServer/Run-LocalhostWebServer.ps1 b/src/LocalhostWebServer/Run-LocalhostWebServer.ps1 index 00cd19e53a..ef243af9ed 100644 --- a/src/LocalhostWebServer/Run-LocalhostWebServer.ps1 +++ b/src/LocalhostWebServer/Run-LocalhostWebServer.ps1 @@ -9,6 +9,10 @@ Path to HTTPS Development Certificate File (pfx) .PARAMETER CertPassword Secure Password for HTTPS Certificate +.PARAMETER OutCertFile + Export cert location. +.PARAMETER LocalSourceJson + Local source json definition #> param( @@ -22,10 +26,15 @@ param( [string]$CertPath, [Parameter(Mandatory=$true)] - [string]$CertPassword + [string]$CertPassword, + + [Parameter()] + [string]$OutCertFile, + + [Parameter()] + [string]$LocalSourceJson ) cd $BuildRoot -Start-Process -FilePath "LocalhostWebServer.exe" -ArgumentList "StaticFileRoot=$StaticFileRoot CertPath=$CertPath CertPassword=$CertPassword PutCertInRoot=True" - +Start-Process -FilePath "LocalhostWebServer.exe" -ArgumentList "StaticFileRoot=$StaticFileRoot CertPath=$CertPath CertPassword=$CertPassword OutCertFile=$OutCertFile LocalSourceJson=$LocalSourceJson" diff --git a/src/LocalhostWebServer/Startup.cs b/src/LocalhostWebServer/Startup.cs index e3bb4b7723..db5706f136 100644 --- a/src/LocalhostWebServer/Startup.cs +++ b/src/LocalhostWebServer/Startup.cs @@ -21,10 +21,12 @@ public class Startup public static string CertPassword { get; set; } - public static bool PutCertInRoot { get; set; } + public static string OutCertFile { get; set; } public static int Port { get; set; } + public static string LocalSourceJson { get; set; } + public Startup(IConfiguration configuration) { Configuration = configuration; diff --git a/src/WinGetSourceCreator/Helpers.cs b/src/WinGetSourceCreator/Helpers.cs new file mode 100644 index 0000000000..77d21a5ea4 --- /dev/null +++ b/src/WinGetSourceCreator/Helpers.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.WinGetSourceCreator +{ + using global::WinGetSourceCreator.Model; + using Microsoft.Msix.Utils.ProcessRunner; + using System.Diagnostics; + using System.Xml; + + internal static class Helpers + { + public static void SignInstaller(SourceInstaller installer, Signature signature) + { + if (installer.Type == InstallerType.Msix) + { + SignMsixFile(installer.InstallerFile, signature); + } + else + { + SignFile(installer.InstallerFile, signature); + } + } + + public static void SignFile(string fileToSign, Signature signature) + { + if (!File.Exists(fileToSign)) + { + throw new FileNotFoundException(fileToSign); + } + + if (!File.Exists(signature.CertFile)) + { + throw new FileNotFoundException(signature.CertFile); + } + + string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; + string signtoolExecutable = Path.Combine(pathToSDK, "signtool.exe"); + string command = $"sign /a /fd sha256 /f {signature.CertFile} "; + if (!string.IsNullOrEmpty(signature.Password)) + { + command += $"/p {signature.Password} "; + } + command += fileToSign; + RunCommand(signtoolExecutable, command); + } + + public static void SignMsixFile(string fileToSign, Signature signature) + { + if (!File.Exists(fileToSign)) + { + throw new FileNotFoundException(fileToSign); + } + + // Modify publisher if needed. + if (signature.Publisher != null) + { + string tmpPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Unpack(fileToSign, tmpPath); + ModifyAppxManifestIdentity(Path.Combine(tmpPath, "AppxManifest.xml"), signature.Publisher); + Pack(fileToSign, tmpPath); + + try + { + Directory.Delete(tmpPath, true); + } + catch (Exception) + { + } + } + + SignFile(fileToSign, signature); + } + + public static void Unpack(string package, string outDir) + { + if (!File.Exists(package)) + { + throw new FileNotFoundException(package); + } + + string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; + string makeappxExecutable = Path.Combine(pathToSDK, "makeappx.exe"); + string args = $"unpack /nv /p {package} /d {outDir}"; + Process p = new Process + { + StartInfo = new ProcessStartInfo(makeappxExecutable, args) + }; + p.Start(); + p.WaitForExit(); + } + + public static void PackWithMappingFile(string outputPackage, string mappingFile) + { + if (!File.Exists(mappingFile)) + { + throw new FileNotFoundException(mappingFile); + } + + string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; + string makeappxExecutable = Path.Combine(pathToSDK, "makeappx.exe"); + string args = $"pack /o /nv /f {mappingFile} /p {outputPackage}"; + RunCommand(makeappxExecutable, args); + } + + public static void Pack(string outputPackage, string directoryToPack) + { + if (!Directory.Exists(directoryToPack)) + { + throw new DirectoryNotFoundException(directoryToPack); + } + + if (File.Exists(outputPackage)) + { + File.Delete(outputPackage); + } + + string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; + string makeappxExecutable = Path.Combine(pathToSDK, "makeappx.exe"); + string args = $"pack /o /d {directoryToPack} /p {outputPackage}"; + RunCommand(makeappxExecutable, args); + } + + public static void RunCommand(string command, string args, string? workingDirectory = null) + { + Process p = new() + { + StartInfo = new ProcessStartInfo(command, args) + }; + + if (workingDirectory != null) + { + p.StartInfo.WorkingDirectory = workingDirectory; + } + p.Start(); + p.WaitForExit(); + } + + // If in the future we edit more elements, this should be a nice wrapper class. + public static void ModifyAppxManifestIdentity(string manifestFile, string? identityPublisher) + { + if (!File.Exists(manifestFile)) + { + throw new FileNotFoundException(manifestFile); + } + + var xmlDoc = new XmlDocument(); + XmlNamespaceManager namespaces = new XmlNamespaceManager(xmlDoc.NameTable); + namespaces.AddNamespace("n", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); + xmlDoc.Load(manifestFile); + var identityNode = xmlDoc.SelectSingleNode("/n:Package/n:Identity", namespaces); + if (identityNode == null) + { + throw new NullReferenceException("Identity node"); + } + + if (!string.IsNullOrEmpty(identityPublisher)) + { + var attr = identityNode.Attributes?["Publisher"]; + if (attr == null) + { + throw new NullReferenceException("Publisher attribute"); + } + attr.Value = identityPublisher; + } + + xmlDoc.Save(manifestFile); + } + + // Gets the AppxSignature.p7x file. + public static string GetSignatureFileFromMsix(string packageFilePath) + { + string extractedPackageDest = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + if (Directory.Exists(extractedPackageDest)) + { + Directory.Delete(extractedPackageDest, true); + } + + Helpers.Unpack(packageFilePath, extractedPackageDest); + + return Path.Combine(extractedPackageDest, "AppxSignature.p7x"); + } + + public static void CopyDirectory(string sourceDirName, string destDirName) + { + if (!Directory.Exists(destDirName)) + { + Directory.CreateDirectory(destDirName); + } + + DirectoryInfo dir = new (sourceDirName); + DirectoryInfo[] dirs = dir.GetDirectories(); + + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string temppath = Path.Combine(destDirName, file.Name); + file.CopyTo(temppath, false); + } + + foreach (DirectoryInfo subdir in dirs) + { + string temppath = Path.Combine(destDirName, subdir.Name); + CopyDirectory(subdir.FullName, temppath); + } + } + } +} diff --git a/src/WinGetSourceCreator/ManifestTokens.cs b/src/WinGetSourceCreator/ManifestTokens.cs new file mode 100644 index 0000000000..9f70d3e595 --- /dev/null +++ b/src/WinGetSourceCreator/ManifestTokens.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.WinGetSourceCreator +{ + using System.Security.Cryptography; + + public class ManifestTokens + { + public ManifestTokens() + { + } + + public Dictionary Tokens { get; private set; } = new Dictionary(); + + public void AddHashToken(string file, string token) + { + if (!token.StartsWith("<") || !token.EndsWith(">")) + { + throw new Exception("Token should be in the form of "); + } + + var hash = HashFile(file); + this.Tokens.Add(token, hash); + } + + /// + /// Gets the hash of the specified file. + /// + /// File path. + /// Hash of file. + private static string HashFile(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException(filePath); + } + + string hash = string.Empty; + + using SHA256 mySHA256 = SHA256.Create(); + using FileStream fs = File.OpenRead(filePath); + fs.Position = 0; + byte[] hashValue = mySHA256.ComputeHash(fs); + + for (int i = 0; i < hashValue.Length; i++) + { + hash += $"{hashValue[i]:X2}"; + } + + return hash; + } + } +} diff --git a/src/WinGetSourceCreator/Model/DynamicInstaller.cs b/src/WinGetSourceCreator/Model/DynamicInstaller.cs new file mode 100644 index 0000000000..ea79d56173 --- /dev/null +++ b/src/WinGetSourceCreator/Model/DynamicInstaller.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WinGetSourceCreator.Model +{ + using Microsoft.WinGetSourceCreator; + using System.IO.Compression; + + public class DynamicInstaller : Installer + { + // Input depends on the Type. + // For zip it is the directories or files that need to included in the zip + public List Input { get; set; } = new List(); + + internal new void Validate() + { + base.Validate(); + } + + public string Create(string workingDirectory) + { + string outputFile = this.Name; + if (!Path.IsPathFullyQualified(outputFile)) + { + outputFile = Path.Combine(workingDirectory, outputFile); + } + + var parent = Path.GetDirectoryName(outputFile); + if (!string.IsNullOrEmpty(parent)) + { + Directory.CreateDirectory(parent); + } + + if (this.Type == InstallerType.Zip) + { + CreateZipInstaller(outputFile); + } + else + { + throw new NotImplementedException(); + } + + return outputFile; + } + + private void CreateZipInstaller(string outputFile) + { + var tmpPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + if (Directory.Exists(tmpPath)) + { + Directory.Delete(tmpPath, true); + } + Directory.CreateDirectory(tmpPath); + + foreach (var input in this.Input) + { + if (!Path.IsPathFullyQualified(input)) + { + throw new InvalidOperationException($"Must be a fully qualified name {input}"); + } + + if (File.Exists(input)) + { + // TODO: maybe we want to preserve the dir? + File.Copy(input, Path.Combine(tmpPath, Path.GetFileName(input)), true); + } + else if (Directory.Exists(input)) + { + Helpers.CopyDirectory(input, tmpPath); + } + else + { + throw new InvalidOperationException(input); + } + } + + ZipFile.CreateFromDirectory(tmpPath, outputFile); + + try + { + Directory.Delete(tmpPath, true); + } + catch (Exception) + { + } + } + } +} diff --git a/src/WinGetSourceCreator/Model/Installer.cs b/src/WinGetSourceCreator/Model/Installer.cs new file mode 100644 index 0000000000..e5cad447b5 --- /dev/null +++ b/src/WinGetSourceCreator/Model/Installer.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WinGetSourceCreator.Model +{ + public abstract class Installer + { + public InstallerType Type { get; set; } + + // The name of the installer when it copied or created. + public string Name { get; set; } = string.Empty; + + // The identifying token of the installer in the manifests. + public string? HashToken { get; set; } + + // An optional token relevant to the installer. + // If the installer is an msix, this is the token used for the appx signature hash. + public string? SignatureToken { get; set; } + + public Signature? Signature { get; set; } + + public bool SkipSignature { get; set; } + + protected void Validate() + { + if (string.IsNullOrEmpty(this.Name)) + { + throw new ArgumentNullException(nameof(this.Name)); + } + + if (this.Signature != null) + { + this.Signature.Validate(); + } + + if (this.Type != InstallerType.Msix && !string.IsNullOrEmpty(this.SignatureToken)) + { + throw new Exception($"{nameof(this.SignatureToken)} can only be used for MSIX"); + } + } + } +} diff --git a/src/WinGetSourceCreator/Model/InstallerType.cs b/src/WinGetSourceCreator/Model/InstallerType.cs new file mode 100644 index 0000000000..e4a505b677 --- /dev/null +++ b/src/WinGetSourceCreator/Model/InstallerType.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WinGetSourceCreator.Model +{ + public enum InstallerType + { + Msix, + Exe, + Msi, + Zip, + } +} diff --git a/src/WinGetSourceCreator/Model/LocalInstaller.cs b/src/WinGetSourceCreator/Model/LocalInstaller.cs new file mode 100644 index 0000000000..6bcfb62b40 --- /dev/null +++ b/src/WinGetSourceCreator/Model/LocalInstaller.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WinGetSourceCreator.Model +{ + public class LocalInstaller : Installer + { + // The full path of the installer. + // Gets copied to the output directory and optionally signed. + public string Input { get; set; } = string.Empty; + + internal new void Validate() + { + base.Validate(); + if (string.IsNullOrEmpty(this.Input)) + { + throw new ArgumentNullException(nameof(this.Input)); + } + + if (!File.Exists(this.Input)) + { + throw new FileNotFoundException(this.Input); + } + } + } +} diff --git a/src/WinGetSourceCreator/Model/LocalSource.cs b/src/WinGetSourceCreator/Model/LocalSource.cs new file mode 100644 index 0000000000..4cf5208fd9 --- /dev/null +++ b/src/WinGetSourceCreator/Model/LocalSource.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WinGetSourceCreator.Model +{ + public class LocalSource + { + // Full path of the input appx manifest. + // Will be used to generate the package. + public string AppxManifest { get; set; } = string.Empty; + + // The working directory where manifest will copied and referenced by the index. + public string WorkingDirectory { get; set; } = string.Empty; + + // Input manifests. + // If it is a file the manifest gets copied to the input directory. + // If it is a directory the manifests will be copied preserving the sub dirs structure. + public List LocalManifests { get; set; } = new(); + + public List? LocalInstallers { get; set; } + + public List? DynamicInstallers { get; set; } + + public Signature? Signature { get; set; } + + public void Validate() + { + if (string.IsNullOrEmpty(this.AppxManifest)) + { + throw new ArgumentNullException(nameof(this.AppxManifest)); + } + + if (string.IsNullOrEmpty(this.WorkingDirectory)) + { + throw new ArgumentNullException(nameof(this.WorkingDirectory)); + } + + if (this.LocalManifests.Count == 0) + { + throw new ArgumentException(nameof(this.LocalManifests)); + } + + if (this.LocalInstallers != null) + { + foreach (var installer in this.LocalInstallers) + { + installer.Validate(); + } + } + + if (this.DynamicInstallers != null) + { + foreach (var installer in this.DynamicInstallers) + { + installer.Validate(); + } + } + + if (this.Signature != null) + { + this.Signature.Validate(); + } + } + + public string GetIndexName() + { + return $"index.db"; + } + + public string GetSourceName() + { + return "source.msix"; + } + } +} diff --git a/src/WinGetSourceCreator/Model/Signature.cs b/src/WinGetSourceCreator/Model/Signature.cs new file mode 100644 index 0000000000..47aa5743a9 --- /dev/null +++ b/src/WinGetSourceCreator/Model/Signature.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WinGetSourceCreator.Model +{ + public class Signature + { + // Full path of the certificate used to sign the package and installers + public string CertFile { get; set; } = string.Empty; + + public string? Password { get; set; } + + // The publisher for the AppxPackage Identity Name property. + public string? Publisher { get; set; } + + internal void Validate() + { + if (string.IsNullOrEmpty(this.CertFile)) + { + throw new ArgumentNullException(nameof(this.CertFile)); + } + } + } +} diff --git a/src/WinGetSourceCreator/Model/SourceInstaller.cs b/src/WinGetSourceCreator/Model/SourceInstaller.cs new file mode 100644 index 0000000000..f614505874 --- /dev/null +++ b/src/WinGetSourceCreator/Model/SourceInstaller.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WinGetSourceCreator.Model +{ + public class SourceInstaller : Installer + { + public SourceInstaller(string workingDirectory, DynamicInstaller installer) + { + this.Initialize(installer); + this.InstallerFile = installer.Create(workingDirectory); + } + + public SourceInstaller(string workingDirectory, LocalInstaller installer) + { + this.Initialize(installer); + this.InstallerFile = Path.Combine(workingDirectory, this.Name); + var parent = Path.GetDirectoryName(this.InstallerFile); + if (!string.IsNullOrEmpty(parent)) + { + Directory.CreateDirectory(parent); + } + File.Copy(installer.Input, this.InstallerFile, true); + } + + public string InstallerFile { get; private set; } + + private void Initialize(Installer installer) + { + foreach (var installerProperty in installer.GetType().GetProperties()) + { + var toProperty = this.GetType().GetProperty(installerProperty.Name); + if (toProperty != null) + { + toProperty.SetValue(this, installerProperty.GetValue(installer)); + } + } + } + } +} diff --git a/src/WinGetSourceCreator/WinGetLocalSource.cs b/src/WinGetSourceCreator/WinGetLocalSource.cs new file mode 100644 index 0000000000..9cbe6b1e66 --- /dev/null +++ b/src/WinGetSourceCreator/WinGetLocalSource.cs @@ -0,0 +1,284 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.WinGetSourceCreator +{ + using global::WinGetSourceCreator.Model; + using System.Text.Json.Serialization; + using System.Text.Json; + using WinGetUtilInterop.Helpers; + + public class WinGetLocalSource + { + private readonly string workingDirectory; + private readonly ManifestTokens tokens; + private readonly Signature? signature; + + public static void CreateFromLocalSourceFile(string localSourceFile) + { + var content = File.ReadAllText(localSourceFile); + content = Environment.ExpandEnvironmentVariables(content); + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + + content = content.Replace("\\", "/"); + + var localSource = JsonSerializer.Deserialize(content, options); + if (localSource == null) + { + throw new Exception("Failed deserializing"); + } + + CreateLocalSource(localSource); + } + + public static void CreateLocalSource(LocalSource localSource) + { + localSource.Validate(); + + var wingetSource = new WinGetLocalSource(localSource.WorkingDirectory, localSource.Signature); + + if (localSource.LocalInstallers != null) + { + foreach (var installer in localSource.LocalInstallers) + { + wingetSource.PrepareLocalInstaller(installer); + } + } + + if (localSource.DynamicInstallers != null) + { + foreach (var installer in localSource.DynamicInstallers) + { + wingetSource.PrepareDynamicInstaller(installer); + } + } + + foreach (var localManifest in localSource.LocalManifests) + { + wingetSource.PrepareManifest(localManifest); + } + + var indexFile = wingetSource.CreateIndex(localSource.GetIndexName()); + + _ = wingetSource.CreatePackage(localSource.GetSourceName(), localSource.AppxManifest, indexFile, localSource.Signature); + } + + public WinGetLocalSource(string workingDirectory, Signature? signature) + { + this.workingDirectory = Path.GetFullPath(workingDirectory); + + if (Directory.Exists(workingDirectory)) + { + Directory.Delete(workingDirectory, true); + } + Directory.CreateDirectory(workingDirectory); + + this.tokens = new(); + this.signature = signature; + } + + public void PrepareDynamicInstaller(DynamicInstaller installer) + { + var sourceInstaller = new SourceInstaller(this.workingDirectory, installer); + PrepareInstaller(sourceInstaller); + } + + public void PrepareLocalInstaller(LocalInstaller installer) + { + var sourceInstaller = new SourceInstaller(this.workingDirectory, installer); + PrepareInstaller(sourceInstaller); + } + + public void PrepareManifest(string input) + { + if (File.Exists(input)) + { + + CopyManifestFile(input, Path.Combine(this.workingDirectory, Path.GetFileName(input))); + } + else + { + CopyManifestFiles(input, this.workingDirectory); + } + } + + public string CreateIndex(string indexName) + { + string fullPath = Path.Combine(this.workingDirectory, indexName); + using var indexHelper = WinGetUtilIndex.CreateLatestVersion(fullPath); + + Queue filesQueue = new(Directory.EnumerateFiles(this.workingDirectory, "*.yaml", SearchOption.AllDirectories)); + while (filesQueue.Count > 0) + { + int currentCount = filesQueue.Count; + + for (int i = 0; i < currentCount; i++) + { + string file = filesQueue.Dequeue(); + try + { + var rel = Path.GetRelativePath(this.workingDirectory, file); + indexHelper.AddManifest(file, rel); + } + catch + { + // If adding manifest to index fails, add to queue and try again. + // This can occur if there is a package dependency that has not yet been added to the index. + filesQueue.Enqueue(file); + } + } + + if (filesQueue.Count == currentCount) + { + throw new InvalidOperationException("Failed to add all manifests in directory to index."); + } + } + + indexHelper.PrepareForPackaging(); + + return fullPath; + } + + public string CreatePackage(string packageName, string inputAppxManifestFile, string indexPath, Signature? signature) + { + if (!File.Exists(inputAppxManifestFile)) + { + throw new FileNotFoundException(inputAppxManifestFile); + } + + if (!File.Exists(indexPath)) + { + throw new FileNotFoundException(indexPath); + } + + string appxManifestFile = Path.Combine(this.workingDirectory, "AppxManifest.xml"); + File.Copy(inputAppxManifestFile, appxManifestFile); + + if (signature != null && signature.Publisher != null) + { + Helpers.ModifyAppxManifestIdentity(appxManifestFile, signature.Publisher); + } + + string mappingFile = Path.Combine(this.workingDirectory, "MappingFile.txt"); + + { + using StreamWriter outputFile = new(mappingFile); + outputFile.WriteLine("[Files]"); + outputFile.WriteLine($"\"{indexPath}\" \"Public\\{Path.GetFileName(indexPath)}\""); + outputFile.WriteLine($"\"{appxManifestFile}\" \"AppxManifest.xml\""); + } + + string outputPackage = Path.Combine(this.workingDirectory, packageName); + Helpers.PackWithMappingFile(outputPackage, mappingFile); + + if (signature != null) + { + Helpers.SignFile(outputPackage, signature); + } + + return outputPackage; + } + + // Copies all .yaml files + private void CopyManifestFiles(string sourceDir, string destDir) + { + DirectoryInfo dir = new DirectoryInfo(sourceDir); + DirectoryInfo[] dirs = dir.GetDirectories(); + + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + if (file.Extension == ".yaml") + { + CopyManifestFile(file.FullName, Path.Combine(destDir, file.Name)); + } + } + + foreach (DirectoryInfo subdir in dirs) + { + CopyManifestFiles(subdir.FullName, Path.Combine(destDir, subdir.Name)); + } + } + + // Copies a file and replaces any token found. + private void CopyManifestFile(string sourceFile, string destinationFile) + { + if (!File.Exists(sourceFile)) + { + throw new FileNotFoundException(sourceFile); + } + + var content = File.ReadAllText(sourceFile); + + foreach (var token in this.tokens.Tokens) + { + if (content.Contains(token.Key)) + { + content = content.Replace(token.Key, token.Value); + } + } + + File.WriteAllText(destinationFile, content); + } + + private void PrepareInstaller(SourceInstaller installer) + { + // Sign installer if needed. + if (!installer.SkipSignature) + { + var sig = this.GetSignature(installer); + if (sig != null) + { + Helpers.SignInstaller(installer, sig); + } + } + + // Process hash token if needed. + if (!string.IsNullOrEmpty(installer.HashToken)) + { + this.tokens.AddHashToken(installer.InstallerFile, installer.HashToken); + } + + // Extra steps. + // An msix can include the signature token. + if (installer.Type == InstallerType.Msix) + { + if (!string.IsNullOrEmpty(installer.SignatureToken)) + { + var signatureFilePath = Helpers.GetSignatureFileFromMsix(installer.InstallerFile); + this.tokens.AddHashToken(signatureFilePath, installer.SignatureToken); + + try + { + var dir = Path.GetDirectoryName(signatureFilePath); + if (!string.IsNullOrEmpty(dir)) + { + Directory.Delete(dir, true); + } + } + catch (Exception) + { + } + } + } + } + + private Signature? GetSignature(Installer installer) + { + if (installer.Type == InstallerType.Zip) + { + return null; + } + + return installer.Signature == null ? this.signature : installer.Signature; + } + } +} diff --git a/src/WinGetSourceCreator/WinGetSourceCreator.csproj b/src/WinGetSourceCreator/WinGetSourceCreator.csproj new file mode 100644 index 0000000000..c5fa565e94 --- /dev/null +++ b/src/WinGetSourceCreator/WinGetSourceCreator.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + $(SolutionDir)$(Platform)\$(Configuration)\WinGetSourceCreator\ + enable + enable + + + + + + + + + + + diff --git a/src/WinGetUtilInterop/Exceptions/WinGetUtilIndexException.cs b/src/WinGetUtilInterop/Exceptions/WinGetUtilIndexException.cs new file mode 100644 index 0000000000..4a92e2b446 --- /dev/null +++ b/src/WinGetUtilInterop/Exceptions/WinGetUtilIndexException.cs @@ -0,0 +1,48 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- + +namespace WinGetUtilInterop.Exceptions +{ + using System; + + public class WinGetUtilIndexException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public WinGetUtilIndexException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Message. + public WinGetUtilIndexException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Inner exception. + public WinGetUtilIndexException(Exception inner) + : base(string.Empty, inner) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Message. + /// Inner exception. + public WinGetUtilIndexException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/IndexCreationTool/WinGetUtilWrapper.cs b/src/WinGetUtilInterop/Helpers/WinGetUtilIndex.cs similarity index 58% rename from src/IndexCreationTool/WinGetUtilWrapper.cs rename to src/WinGetUtilInterop/Helpers/WinGetUtilIndex.cs index 32d6e18b3e..7bc5bbb067 100644 --- a/src/IndexCreationTool/WinGetUtilWrapper.cs +++ b/src/WinGetUtilInterop/Helpers/WinGetUtilIndex.cs @@ -1,30 +1,34 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- -namespace IndexCreationTool +namespace WinGetUtilInterop.Helpers { using System; using System.Runtime.InteropServices; + using Microsoft.WinGetUtil.Interfaces; + using WinGetUtilInterop.Exceptions; - /// - /// Wrapper class around WinGetUtil index modifier native implementation. - /// - internal class WinGetUtilWrapper : IDisposable + public class WinGetUtilIndex : IWinGetUtilIndex { + private const string WinGetUtilDll = "WinGetUtil.dll"; + private const uint IndexLatestVersion = unchecked((uint)-1); + /// - /// Dll name. + /// WinGet Index latest version. /// - public const string DllName = @"WinGetUtil.dll"; - - private const uint LatestVersion = unchecked((uint)-1); + public const uint WinGetIndexLatestVersion = unchecked((uint)-1); - private IntPtr indexHandle; + private readonly IntPtr indexHandle; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Handle of the index. - private WinGetUtilWrapper(IntPtr indexHandle) + /// Logging Context. + private WinGetUtilIndex(IntPtr indexHandle) { this.indexHandle = indexHandle; } @@ -33,76 +37,74 @@ private WinGetUtilWrapper(IntPtr indexHandle) /// Creates a new index file in the specified path. /// /// Index file to create. - /// Instance of WinGetUtilWrapper. - public static WinGetUtilWrapper Create(string indexFile) + /// Major version. + /// Minor version. + /// Logging Context. + /// Instance of IWinGetUtilSQLiteIndex. + public static IWinGetUtilIndex Create(string indexFile, uint majorVersion, uint minorVersion) { try { WinGetSQLiteIndexCreate( indexFile, - WinGetUtilWrapper.LatestVersion, - WinGetUtilWrapper.LatestVersion, + majorVersion, + minorVersion, out IntPtr index); - return new WinGetUtilWrapper(index); + return new WinGetUtilIndex(index); } catch (Exception e) { - Console.WriteLine($"Error to create {indexFile}. {Environment.NewLine}{e.ToString()}"); - throw; + throw new WinGetUtilIndexException(e); } } /// - /// Open the index. + /// Creates a new index file in the specified path. + /// + /// Index file to create. + /// Instance of IWinGetUtilSQLiteIndex. + public static IWinGetUtilIndex CreateLatestVersion(string indexFile) + { + return Create(indexFile, IndexLatestVersion, IndexLatestVersion); + } + + /// + /// Open the index file. /// /// Index file to open. - /// Instance of WinGetUtilWrapper. - public static WinGetUtilWrapper Open(string indexFile) + /// Instance of IWinGetUtilSQLiteIndex. + public static IWinGetUtilIndex Open(string indexFile) { try { WinGetSQLiteIndexOpen(indexFile, out IntPtr index); - return new WinGetUtilWrapper(index); + return new WinGetUtilIndex(index); } catch (Exception e) { - Console.WriteLine($"Error to open {indexFile}. {Environment.NewLine}{e.ToString()}"); - throw; + throw new WinGetUtilIndexException(e); } } - /// - /// Adds manifest to index. - /// - /// Manifest to add. - /// Path of the manifest in the repository. + /// public void AddManifest(string manifestPath, string relativePath) { try { - Console.WriteLine($"Adding manifest {manifestPath} on index file."); WinGetSQLiteIndexAddManifest(this.indexHandle, manifestPath, relativePath); return; } catch (Exception e) { - Console.WriteLine($"Error to add manifest {manifestPath} with relative path {relativePath}. {Environment.NewLine}{e.ToString()}"); - throw; + throw new WinGetUtilIndexException(e); } } - /// - /// Updates manifest in the index. - /// - /// Path to manifest to modify. - /// Path of the manifest in the repository. - /// True if index was modified. + /// public bool UpdateManifest(string manifestPath, string relativePath) { try { - Console.WriteLine($"Updating manifest {manifestPath} on index file."); - // For now, modifying a manifest implies that the file didn't got moved in the repository. So only // contents of the file are modified. However, in the future we might support moving which requires // oldManifestPath, oldRelativePath, newManifestPath and oldManifestPath. @@ -111,28 +113,15 @@ public bool UpdateManifest(string manifestPath, string relativePath) manifestPath, relativePath, out bool indexModified); - - if (!indexModified) - { - // This means that some of the attributes that don't get indexed, like Description, where - // were modified, so the index doesn't got updated. - Console.WriteLine($"Manifest {manifestPath} didn't result in a modification to the index."); - } - return indexModified; } catch (Exception e) { - Console.WriteLine($"Error to update manifest {manifestPath} with relative path {relativePath}. {Environment.NewLine}{e.ToString()}"); - throw; + throw new WinGetUtilIndexException(e); } } - /// - /// Delete manifest from index. - /// - /// Path to manifest to modify. - /// Path of the manifest in the repository. + /// public void RemoveManifest(string manifestPath, string relativePath) { try @@ -142,14 +131,11 @@ public void RemoveManifest(string manifestPath, string relativePath) } catch (Exception e) { - Console.WriteLine($"Error to remove manifest {manifestPath} with relative path {relativePath}. {Environment.NewLine}{e.ToString()}"); - throw; + throw new WinGetUtilIndexException(e); } } - /// - /// Wrapper for WinGetSQLiteIndexPrepareForPackaging. - /// + /// public void PrepareForPackaging() { try @@ -159,11 +145,30 @@ public void PrepareForPackaging() } catch (Exception e) { - Console.WriteLine($"Error to prepare for packaging. {Environment.NewLine}{e.ToString()}"); - throw; + throw new WinGetUtilIndexException(e); } } + /// + public bool IsIndexConsistent() + { + try + { + WinGetSQLiteIndexCheckConsistency(this.indexHandle, out bool indexModified); + return indexModified; + } + catch (Exception e) + { + throw new WinGetUtilIndexException(e); + } + } + + /// + public IntPtr GetIndexHandle() + { + return this.indexHandle; + } + /// /// Dispose method. /// @@ -174,14 +179,14 @@ public void Dispose() } /// - /// Dispose method to dispose the Git desktop process runner. + /// Dispose method to free the sqlite index handle. /// /// Bool value indicating if Dispose is being run. - protected void Dispose(bool disposing) + public void Dispose(bool disposing) { if (disposing) { - if (this.indexHandle != IntPtr.Zero) + if (this.indexHandle != null) { WinGetSQLiteIndexClose(this.indexHandle); } @@ -196,7 +201,7 @@ protected void Dispose(bool disposing) /// Minor version. /// Out handle of the index. /// HRESULT. - [DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + [DllImport(WinGetUtilDll, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexCreate(string filePath, uint majorVersion, uint minorVersion, out IntPtr index); /// @@ -205,7 +210,7 @@ protected void Dispose(bool disposing) /// File path of index. /// Out handle of the index. /// HRESULT. - [DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + [DllImport(WinGetUtilDll, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexOpen(string filePath, out IntPtr index); /// @@ -213,7 +218,7 @@ protected void Dispose(bool disposing) /// /// Handle of the index. /// HRESULT. - [DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + [DllImport(WinGetUtilDll, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexClose(IntPtr index); /// @@ -224,7 +229,7 @@ protected void Dispose(bool disposing) /// Manifest to add. /// Path of the manifest in the container. /// HRESULT. - [DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + [DllImport(WinGetUtilDll, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexAddManifest(IntPtr index, string manifestPath, string relativePath); /// @@ -236,7 +241,7 @@ protected void Dispose(bool disposing) /// Old relative path in the container. /// Out bool if the index is modified. /// HRESULT. - [DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + [DllImport(WinGetUtilDll, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexUpdateManifest( IntPtr index, string manifestPath, @@ -250,7 +255,7 @@ private static extern IntPtr WinGetSQLiteIndexUpdateManifest( /// Manifest path to remove. /// Relative path in the container. /// HRESULT. - [DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + [DllImport(WinGetUtilDll, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexRemoveManifest(IntPtr index, string manifestPath, string relativePath); /// @@ -258,7 +263,16 @@ private static extern IntPtr WinGetSQLiteIndexUpdateManifest( /// /// Index handle. /// HRESULT. - [DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + [DllImport(WinGetUtilDll, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexPrepareForPackaging(IntPtr index); + + /// + /// Checks the index for consistency, ensuring that at a minimum all referenced rows actually exist. + /// + /// Index handle. + /// Does the consistency check succeeded. + /// HRESULT. + [DllImport(WinGetUtilDll, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + private static extern IntPtr WinGetSQLiteIndexCheckConsistency(IntPtr index, [MarshalAs(UnmanagedType.U1)] out bool succeeded); } -} \ No newline at end of file +} diff --git a/src/WinGetUtilInterop/Interfaces/IWinGetUtilIndex.cs b/src/WinGetUtilInterop/Interfaces/IWinGetUtilIndex.cs new file mode 100644 index 0000000000..f73920191c --- /dev/null +++ b/src/WinGetUtilInterop/Interfaces/IWinGetUtilIndex.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Interfaces +{ + using System; + + public interface IWinGetUtilIndex : IDisposable + { + /// + /// Adds manifest to index. + /// + /// Manifest to add. + /// Path of the manifest in the repository. + void AddManifest(string manifestPath, string relativePath); + + /// + /// Updates manifest in the index. + /// + /// Path to manifest to modify. + /// Path of the manifest in the repository. + /// True if index was modified. + bool UpdateManifest(string manifestPath, string relativePath); + + /// + /// Delete manifest from index. + /// + /// Path to manifest to modify. + /// Path of the manifest in the repository. + void RemoveManifest(string manifestPath, string relativePath); + + /// + /// Wrapper for WinGetSQLiteIndexPrepareForPackaging. + /// + void PrepareForPackaging(); + + /// + /// Checks the index for consistency, ensuring that at a minimum all referenced rows actually exist. + /// + /// Is index consistent. + bool IsIndexConsistent(); + + /// + /// Gets the managed index handle. It is used in additional manifest validation that requires an index. + /// + /// The managed index handle. + IntPtr GetIndexHandle(); + } +} diff --git a/src/WinGetUtilInterop/WinGetUtilInterop.csproj b/src/WinGetUtilInterop/WinGetUtilInterop.csproj index 65cfbc4ddc..773d3e6fdb 100644 --- a/src/WinGetUtilInterop/WinGetUtilInterop.csproj +++ b/src/WinGetUtilInterop/WinGetUtilInterop.csproj @@ -5,7 +5,6 @@ - diff --git a/templates/e2e-setup.yml b/templates/e2e-setup.yml new file mode 100644 index 0000000000..efff240da3 --- /dev/null +++ b/templates/e2e-setup.yml @@ -0,0 +1,38 @@ +# Configures local test source and local PowerShell repository. +parameters: +- name: source + type: string +- name: buildOutDir + type: string + +steps: + - task: DownloadSecureFile@1 + name: AppInstallerTest + displayName: 'Download Source Package Certificate' + inputs: + secureFile: 'AppInstallerTest.pfx' + + - task: DownloadSecureFile@1 + name: HTTPSDevCert + displayName: 'Download Kestrel Certificate' + inputs: + secureFile: 'HTTPSDevCertV2.pfx' + + - task: PowerShell@2 + displayName: Install Root Certificate + inputs: + filePath: '${{ parameters.source }}\src\LocalhostWebServer\InstallDevCert.ps1' + arguments: '-pfxpath $(HTTPSDevCert.secureFilePath) -password microsoft' + + - task: PowerShell@2 + displayName: Launch LocalhostWebServer + inputs: + filePath: '${{ parameters.source }}\src\LocalhostWebServer\Run-LocalhostWebServer.ps1' + arguments: '-BuildRoot ${{ parameters.buildOutDir }}\LocalhostWebServer -StaticFileRoot $(Agent.TempDirectory)\TestLocalIndex -CertPath $(HTTPSDevCert.secureFilePath) -CertPassword microsoft -OutCertFile $(Agent.TempDirectory)\servercert.cer -LocalSourceJson ${{ parameters.source }}\src\AppInstallerCLIE2ETests\TestData\localsource.json' + + - task: PowerShell@2 + displayName: Setup Local PS Repository + inputs: + filePath: '${{ parameters.source }}\src\AppInstallerCLIE2ETests\TestData\Configuration\Init-TestRepository.ps1' + arguments: '-Force' + pwsh: true diff --git a/templates/e2e-test.template.yml b/templates/e2e-test.template.yml index 228a3283e5..add441827f 100644 --- a/templates/e2e-test.template.yml +++ b/templates/e2e-test.template.yml @@ -5,14 +5,11 @@ parameters: type: boolean - name: filter type: string -- name: comTrace - type: boolean - default: false steps: - task: CmdLine@2 displayName: Start COM trace for ${{ parameters.title }} - condition: and(succeededOrFailed(), ${{ parameters.comTrace }}) + condition: and(succeededOrFailed(), eq(variables['System.debug'], true)) inputs: script: 'wpr -start $(Build.SourcesDirectory)\tools\COMTrace\ComTrace.wprp -filemode' @@ -23,7 +20,6 @@ steps: testSelector: 'testAssemblies' testAssemblyVer2: '$(buildOutDir)\AppInstallerCLIE2ETests\AppInstallerCLIE2ETests.dll' testFiltercriteria: ${{ parameters.filter }} - runSettingsFile: '$(buildOutDir)\AppInstallerCLIE2ETests\Test.runsettings' ${{ if eq(parameters.isPackaged, true) }}: overrideTestrunParameters: '-PackagedContext true -AICLIPackagePath $(packageLayoutDir) @@ -31,24 +27,20 @@ steps: -LooseFileRegistration true -InvokeCommandInDesktopPackage true -StaticFileRootPath $(Agent.TempDirectory)\TestLocalIndex - -MsiTestInstallerPath $(System.DefaultWorkingDirectory)\src\AppInstallerCLIE2ETests\TestData\AppInstallerTestMsiInstaller.msi - -MsixTestInstallerPath $(Build.ArtifactStagingDirectory)\AppInstallerTestMsixInstaller.msix - -ExeTestInstallerPath $(buildOutDir)\AppInstallerTestExeInstaller\AppInstallerTestExeInstaller.exe - -PackageCertificatePath $(AppInstallerTest.secureFilePath) - -PowerShellModulePath $(buildOutDir)\PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.psd1' + -PowerShellModulePath $(buildOutDir)\PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.psd1 + -LocalServerCertPath $(Agent.TempDirectory)\servercert.cer + -SkipTestSource true' ${{ else }}: overrideTestrunParameters: '-PackagedContext false -AICLIPath $(packageLayoutDir)\AppInstallerCLI\winget.exe -InvokeCommandInDesktopPackage false -StaticFileRootPath $(Agent.TempDirectory)\TestLocalIndex - -MsiTestInstallerPath $(System.DefaultWorkingDirectory)\src\AppInstallerCLIE2ETests\TestData\AppInstallerTestMsiInstaller.msi - -MsixTestInstallerPath $(Build.ArtifactStagingDirectory)\AppInstallerTestMsixInstaller.msix - -ExeTestInstallerPath $(buildOutDir)\AppInstallerTestExeInstaller\AppInstallerTestExeInstaller.exe - -PackageCertificatePath $(AppInstallerTest.secureFilePath) - -PowerShellModulePath $(buildOutDir)\PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.psd1' + -PowerShellModulePath $(buildOutDir)\PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.psd1 + -LocalServerCertPath $(Agent.TempDirectory)\servercert.cer + -SkipTestSource true' - task: CmdLine@2 displayName: Complete COM trace for ${{ parameters.title }} - condition: and(succeededOrFailed(), ${{ parameters.comTrace }}) + condition: and(succeededOrFailed(), eq(variables['System.debug'], true)) inputs: script: 'wpr -stop "$(artifactsDir)\ComTrace - ${{ parameters.title }}.etl"'