diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 2839f08318..3f53a4db6d 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -160,6 +160,7 @@ BUILDARCH BUILDNUMBER buildtransitive builttoroam +BUNDLEINFO BVal BValue byapp @@ -863,6 +864,7 @@ LOCKTYPE LOGFONT LOGFONTW logon +LOGMSG LOGPIXELSX LOGPIXELSY LOn @@ -1040,6 +1042,7 @@ MWBEx MYICON NAMECHANGE namespaceanddescendants +Namotion nao NCACTIVATE ncc @@ -1077,6 +1080,7 @@ NEWPLUSSHELLEXTENSIONWIN newrow nicksnettravels NIF +NJson NLog NLSTEXT NMAKE diff --git a/.gitignore b/.gitignore index 8859e53742..ed3f80a4ec 100644 --- a/.gitignore +++ b/.gitignore @@ -350,7 +350,9 @@ src/common/Telemetry/*.etl # Generated installer file for Monaco source files. /installer/PowerToysSetup/MonacoSRC.wxs +/installer/PowerToysSetup/DscResources.wxs /installer/PowerToysSetupVNext/MonacoSRC.wxs +/installer/PowerToysSetupVNext/DscResources.wxs # MSBuildCache /MSBuildCacheLogs/ diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 9cb1fcb7d5..4a12d57e81 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -55,7 +55,6 @@ "PowerToys.Awake.exe", "PowerToys.Awake.dll", - "PowerToys.FancyZonesEditor.exe", "PowerToys.FancyZonesEditor.dll", "PowerToys.FancyZonesEditorCommon.dll", @@ -230,7 +229,10 @@ "PowerToys.CmdPalModuleInterface.dll", "CmdPalKeyboardService.dll", - "*Microsoft.CmdPal.UI_*.msix" + "*Microsoft.CmdPal.UI_*.msix", + + "PowerToys.DSC.dll", + "PowerToys.DSC.exe" ], "SigningInfo": { "Operations": [ @@ -297,6 +299,9 @@ "msvcp140_1_app.dll", "msvcp140_2_app.dll", "msvcp140_app.dll", + "Namotion.Reflection.dll", + "NJsonSchema.Annotations.dll", + "NJsonSchema.dll", "vcamp140_app.dll", "vccorlib140_app.dll", "vcomp140_app.dll", diff --git a/.pipelines/generateDscManifests.ps1 b/.pipelines/generateDscManifests.ps1 new file mode 100644 index 0000000000..109610e62e --- /dev/null +++ b/.pipelines/generateDscManifests.ps1 @@ -0,0 +1,88 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$BuildPlatform, + + [Parameter(Mandatory = $true)] + [string]$BuildConfiguration, + + [Parameter()] + [string]$RepoRoot = (Get-Location).Path +) + +$ErrorActionPreference = 'Stop' + +function Resolve-PlatformDirectory { + param( + [string]$Root, + [string]$Platform + ) + + $normalized = $Platform.Trim() + $candidates = @() + $candidates += Join-Path $Root $normalized + $candidates += Join-Path $Root ($normalized.ToUpperInvariant()) + $candidates += Join-Path $Root ($normalized.ToLowerInvariant()) + $candidates = $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return $candidate + } + } + + return $candidates[0] +} + +Write-Host "Repo root: $RepoRoot" +Write-Host "Requested build platform: $BuildPlatform" +Write-Host "Requested configuration: $BuildConfiguration" + +# Always use x64 PowerToys.DSC.exe since CI/CD machines are x64 +$exePlatform = 'x64' +$exeRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $exePlatform +$exeOutputDir = Join-Path $exeRoot $BuildConfiguration +$exePath = Join-Path $exeOutputDir 'PowerToys.DSC.exe' + +Write-Host "Using x64 PowerToys.DSC.exe to generate DSC manifests for $BuildPlatform build" + +if (-not (Test-Path $exePath)) { + throw "PowerToys.DSC.exe not found at '$exePath'. Make sure it has been built first." +} + +Write-Host "Using PowerToys.DSC.exe at '$exePath'." + +# Output DSC manifests to the target build platform directory (x64, ARM64, etc.) +$outputRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $BuildPlatform +if (-not (Test-Path $outputRoot)) { + Write-Host "Creating missing platform output root at '$outputRoot'." + New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null +} + +$outputDir = Join-Path $outputRoot $BuildConfiguration +if (-not (Test-Path $outputDir)) { + Write-Host "Creating missing configuration output directory at '$outputDir'." + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null +} + +Write-Host "DSC manifests will be generated to: '$outputDir'" + +Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'." +Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force + +$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir) +Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')" +& $exePath @arguments +if ($LASTEXITCODE -ne 0) { + throw "PowerToys.DSC.exe exited with code $LASTEXITCODE" +} + +$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop +if ($generatedFiles.Count -eq 0) { + throw "No DSC manifest files were generated in '$outputDir'." +} + +Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):" +foreach ($file in $generatedFiles) { + Write-Host " - $($file.FullName)" +} diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml index 89ee99b3e0..6994c7a199 100644 --- a/.pipelines/v2/templates/job-build-project.yml +++ b/.pipelines/v2/templates/job-build-project.yml @@ -271,6 +271,23 @@ jobs: env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) + # Build PowerToys.DSC.exe for ARM64 (x64 uses existing binary from previous build) + - task: VSBuild@1 + displayName: Build PowerToys.DSC.exe (x64 for generating manifests) + condition: ne(variables['BuildPlatform'], 'x64') + inputs: + solution: src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj + msbuildArgs: /t:Build /m /restore + platform: x64 + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + + # Generate DSC manifests using PowerToys.DSC.exe + - pwsh: |- + & '.pipelines/generateDscManifests.ps1' -BuildPlatform '$(BuildPlatform)' -BuildConfiguration '$(BuildConfiguration)' -RepoRoot '$(Build.SourcesDirectory)' + displayName: Generate DSC manifests + - task: CopyFiles@2 displayName: Stage SDK/build inputs: diff --git a/Directory.Packages.props b/Directory.Packages.props index 08bf6febee..eabda4151d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -67,6 +67,7 @@ + diff --git a/NOTICE.md b/NOTICE.md index fc9f9b9696..1998ea805a 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1521,6 +1521,7 @@ SOFTWARE. - ModernWpfUI - Moq - MSTest +- NJsonSchema - NLog - NLog.Extensions.Logging - NLog.Schema diff --git a/PowerToys.sln b/PowerToys.sln index 20ac18607e..4e5524c757 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -797,6 +797,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v3", "v3", "{9605B84E-FAC4-477B-B9EC-0753177EE6A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC", "src\dsc\v3\PowerToys.DSC\PowerToys.DSC.csproj", "{94CDC147-6137-45E9-AEDE-17FF809607C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC.UnitTests", "src\dsc\v3\PowerToys.DSC.UnitTests\PowerToys.DSC.UnitTests.csproj", "{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}" @@ -2891,6 +2897,22 @@ Global {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Build.0 = Release|ARM64 {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.ActiveCfg = Release|x64 {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Build.0 = Release|x64 + {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.Build.0 = Debug|ARM64 + {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.ActiveCfg = Debug|x64 + {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.Build.0 = Debug|x64 + {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.ActiveCfg = Release|ARM64 + {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.Build.0 = Release|ARM64 + {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.ActiveCfg = Release|x64 + {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.Build.0 = Release|x64 + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.Build.0 = Debug|ARM64 + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.ActiveCfg = Debug|x64 + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.Build.0 = Debug|x64 + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.ActiveCfg = Release|ARM64 + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.Build.0 = Release|ARM64 + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.ActiveCfg = Release|x64 + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.Build.0 = Release|x64 {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64 {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64 {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64 @@ -3238,6 +3260,9 @@ Global {00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B} {66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1} + {9605B84E-FAC4-477B-B9EC-0753177EE6A8} = {557C4636-D7E1-4838-A504-7D19B725EE95} + {94CDC147-6137-45E9-AEDE-17FF809607C0} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8} + {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8} {E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} diff --git a/doc/dsc/Settings.md b/doc/dsc/Settings.md new file mode 100644 index 0000000000..24fab1e2dd --- /dev/null +++ b/doc/dsc/Settings.md @@ -0,0 +1,83 @@ +# Settings resource +Manage the settings for PowerToys modules + +## Commands + +### ✨ Modules +List all the modules supported by the settings resource. +```shell +PS C:\> PowerToys.DSC.exe modules --resource 'settings' +AdvancedPaste +AlwaysOnTop +App +Awake +ColorPicker +CropAndLock +EnvironmentVariables +FancyZones +FileLocksmith +FindMyMouse +Hosts +ImageResizer +KeyboardManager +MeasureTool +MouseHighlighter +MouseJump +MousePointerCrosshairs +Peek +PowerAccent +PowerOCR +PowerRename +RegistryPreview +ShortcutGuide +Workspaces +ZoomIt +``` + +### 📄 Get +Get the settings for a specific module. +```shell +PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables +{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}} +``` + +### 🖨️ Export +Export the settings for a specific module. + +ℹ️ Settings resource Get and Export operation output states are identical. +```shell +PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables +{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}} +``` + +### 📝 Set +Set the settings for a specific module. This command will update the settings to the specified values. +```shell +PS C:\> PowerToys.DSC.exe set --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}' +{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}} +["settings"] +``` + +### 🧪 Test +Test the settings for a specific module. This command will check if the current settings match the desired state. +```shell +PS C:\> PowerToys.DSC.exe test --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000002-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}' +{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"},"_inDesiredState":false} +["settings"] +``` + +### 🛠️ Schema +Generates the JSON schema for the settings resource of a specific module. +```shell +PS C:\> PowerToys.DSC.exe schema --resource 'settings' --module Awake +{"$schema":"http://json-schema.org/draft-04/schema#","title":"SettingsResourceObjectOfAwakeSettings","type":"object","additionalProperties":false,"required":["settings"],"properties":{"_inDesiredState":{"type":["boolean","null"],"description":"Indicates whether an instance is in the desired state"},"settings":{"description":"The settings content for the module."}}} +PS E:\src\powertoys> PowerToys.DSC.exe schema --resource 'settings' --module Awake | Format-Json +``` + +### 📦 Manifest +Generates a manifest dsc resource JSON file for the specified module. +- If the module is not specified, it will generate a manifest for all modules. +- If the output directory is not specified, it will print the manifest to the console. +```shell +PS C:\> PowerToys.DSC.exe manifest --resource settings --module 'Awake' --outputDir "C:\manifests" +``` \ No newline at end of file diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj index 6a28fbc896..4a391eb901 100644 --- a/installer/PowerToysSetup/PowerToysInstaller.wixproj +++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj @@ -16,6 +16,7 @@ SET PTRoot=$(SolutionDir)\.. call "..\..\..\publish.cmd" x64 ) call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" +call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)" @@ -26,6 +27,7 @@ SET PTRoot=$(SolutionDir)\.. call "..\..\..\publish.cmd" arm64 ) call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" +call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)" @@ -121,6 +123,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil + diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 77ffad8483..c7f9d3bda4 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -75,6 +75,7 @@ + @@ -324,7 +325,6 @@ BinaryKey="PTCustomActions" DllEntry="UninstallDSCModuleCA" /> - + + + + + + +"@ + Set-Content -Path $dscWxsFile -Value $wxsContent + exit 0 +} + +Write-Host "Found $($dscFiles.Count) DSC manifest file(s)" + +$wxsContent = @" + + + + +"@ + +$componentRefs = @() +foreach ($file in $dscFiles) { + $componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_') + $fileId = $componentId + "_File" + $guid = [System.Guid]::NewGuid().ToString().ToUpper() + $componentRefs += $componentId + + $wxsContent += @" + + + + + + + +"@ +} + +$wxsContent += @" + + + + + +"@ + +foreach ($componentId in $componentRefs) { + $wxsContent += @" + + +"@ +} + +$wxsContent += @" + + + + +"@ + +Set-Content -Path $dscWxsFile -Value $wxsContent +Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'" diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp index 8c3ad76448..815731c161 100644 --- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp @@ -3,6 +3,7 @@ #include "RcResource.h" #include #include +#include #include "../../src/common/logger/logger.h" #include "../../src/common/utils/gpo.h" @@ -232,7 +233,9 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall) auto action = [&commandLine](HANDLE userToken) { - STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL}; + STARTUPINFO startupInfo = { 0 }; + startupInfo.cb = sizeof(STARTUPINFO); + startupInfo.wShowWindow = SW_SHOWNORMAL; PROCESS_INFORMATION processInformation; PVOID lpEnvironment = NULL; @@ -271,7 +274,9 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall) } else { - STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL}; + STARTUPINFO startupInfo = { 0 }; + startupInfo.cb = sizeof(STARTUPINFO); + startupInfo.wShowWindow = SW_SHOWNORMAL; PROCESS_INFORMATION processInformation; @@ -424,7 +429,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall) const auto modulesPath = baseModulesPath / L"Microsoft.PowerToys.Configure" / (get_product_version(false) + L".0"); std::error_code errorCode; - fs::create_directories(modulesPath, errorCode); + std::filesystem::create_directories(modulesPath, errorCode); if (errorCode) { hr = E_FAIL; @@ -433,7 +438,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall) for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME}) { - fs::copy_file(fs::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, fs::copy_options::overwrite_existing, errorCode); + std::filesystem::copy_file(std::filesystem::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, std::filesystem::copy_options::overwrite_existing, errorCode); if (errorCode) { @@ -481,7 +486,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall) for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME}) { - fs::remove(versionedModulePath / filename, errorCode); + std::filesystem::remove(versionedModulePath / filename, errorCode); if (errorCode) { @@ -492,7 +497,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall) for (const auto *modulePath : {&versionedModulePath, &powerToysModulePath}) { - fs::remove(*modulePath, errorCode); + std::filesystem::remove(*modulePath, errorCode); if (errorCode) { @@ -1375,6 +1380,120 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) return WcaFinalize(er); } +UINT __stdcall SetBundleInstallLocationCA(MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + + // Declare all variables at the beginning to avoid goto issues + std::wstring customActionData; + std::wstring installationFolder; + std::wstring bundleUpgradeCode; + std::wstring installScope; + bool isPerUser = false; + size_t pos1 = std::wstring::npos; + size_t pos2 = std::wstring::npos; + std::vector keysToTry; + + hr = WcaInitialize(hInstall, "SetBundleInstallLocationCA"); + ExitOnFailure(hr, "Failed to initialize"); + + // Parse CustomActionData: "installFolder;upgradeCode;installScope" + hr = getInstallFolder(hInstall, customActionData); + ExitOnFailure(hr, "Failed to get CustomActionData."); + + pos1 = customActionData.find(L';'); + if (pos1 == std::wstring::npos) + { + hr = E_INVALIDARG; + ExitOnFailure(hr, "Invalid CustomActionData format - missing first semicolon"); + } + + pos2 = customActionData.find(L';', pos1 + 1); + if (pos2 == std::wstring::npos) + { + hr = E_INVALIDARG; + ExitOnFailure(hr, "Invalid CustomActionData format - missing second semicolon"); + } + + installationFolder = customActionData.substr(0, pos1); + bundleUpgradeCode = customActionData.substr(pos1 + 1, pos2 - pos1 - 1); + installScope = customActionData.substr(pos2 + 1); + + isPerUser = (installScope == L"perUser"); + + // Use the appropriate registry based on install scope + HKEY targetKey = isPerUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE; + const wchar_t* keyName = isPerUser ? L"HKCU" : L"HKLM"; + + WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Searching for Bundle in %ls registry", keyName); + + HKEY uninstallKey; + LONG openResult = RegOpenKeyExW(targetKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", 0, KEY_READ | KEY_ENUMERATE_SUB_KEYS, &uninstallKey); + if (openResult != ERROR_SUCCESS) + { + WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to open uninstall key, error: %ld", openResult); + goto LExit; + } + + DWORD index = 0; + wchar_t subKeyName[256]; + DWORD subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t); + + while (RegEnumKeyExW(uninstallKey, index, subKeyName, &subKeyNameSize, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) + { + HKEY productKey; + if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ | KEY_WRITE, &productKey) == ERROR_SUCCESS) + { + wchar_t upgradeCode[256]; + DWORD upgradeCodeSize = sizeof(upgradeCode); + DWORD valueType; + + if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, &valueType, + reinterpret_cast(upgradeCode), &upgradeCodeSize) == ERROR_SUCCESS) + { + // Remove brackets from registry upgradeCode for comparison (bundleUpgradeCode doesn't have brackets) + std::wstring regUpgradeCode = upgradeCode; + if (!regUpgradeCode.empty() && regUpgradeCode.front() == L'{' && regUpgradeCode.back() == L'}') + { + regUpgradeCode = regUpgradeCode.substr(1, regUpgradeCode.length() - 2); + } + + if (_wcsicmp(regUpgradeCode.c_str(), bundleUpgradeCode.c_str()) == 0) + { + // Found matching Bundle, set InstallLocation + LONG setResult = RegSetValueExW(productKey, L"InstallLocation", 0, REG_SZ, + reinterpret_cast(installationFolder.c_str()), + static_cast((installationFolder.length() + 1) * sizeof(wchar_t))); + + if (setResult == ERROR_SUCCESS) + { + WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: InstallLocation set successfully"); + } + else + { + WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to set InstallLocation, error: %ld", setResult); + } + + RegCloseKey(productKey); + RegCloseKey(uninstallKey); + goto LExit; + } + } + RegCloseKey(productKey); + } + + index++; + subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t); + } + + RegCloseKey(uninstallKey); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + void initSystemLogger() { static std::once_flag initLoggerFlag; diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def index 39efc9ff70..931a555953 100644 --- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def +++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def @@ -32,4 +32,4 @@ EXPORTS CleanFileLocksmithRuntimeRegistryCA CleanPowerRenameRuntimeRegistryCA CleanNewPlusRuntimeRegistryCA - \ No newline at end of file + SetBundleInstallLocationCA diff --git a/installer/PowerToysSetupVNext/Core.wxs b/installer/PowerToysSetupVNext/Core.wxs index d3f992d82e..f7da6162f9 100644 --- a/installer/PowerToysSetupVNext/Core.wxs +++ b/installer/PowerToysSetupVNext/Core.wxs @@ -9,6 +9,25 @@ + + + + + + + + + + + + + + + + + + + @@ -109,6 +128,11 @@ + + + + + diff --git a/installer/PowerToysSetupVNext/PowerToys.wxs b/installer/PowerToysSetupVNext/PowerToys.wxs index 19906089bf..64f6f35c5e 100644 --- a/installer/PowerToysSetupVNext/PowerToys.wxs +++ b/installer/PowerToysSetupVNext/PowerToys.wxs @@ -28,6 +28,9 @@ + + + @@ -58,6 +61,7 @@ + diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj index 0cb9118b91..a1b89ec1a9 100644 --- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj +++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj @@ -14,6 +14,7 @@ SET PTRoot=$(SolutionDir)\.. call "..\..\..\publish.cmd" x64 ) call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2" +call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)" @@ -24,6 +25,7 @@ SET PTRoot=$(SolutionDir)\.. call "..\..\..\publish.cmd" arm64 ) call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2" +call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)" @@ -115,6 +117,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil + diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs index e343897d5d..30831548dd 100644 --- a/installer/PowerToysSetupVNext/Product.wxs +++ b/installer/PowerToysSetupVNext/Product.wxs @@ -62,6 +62,7 @@ + @@ -69,8 +70,8 @@ - - + + @@ -117,6 +118,8 @@ + + @@ -160,6 +163,9 @@ + + + @@ -244,6 +250,8 @@ + + diff --git a/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1 b/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1 new file mode 100644 index 0000000000..14172db0bc --- /dev/null +++ b/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1 @@ -0,0 +1,102 @@ +[CmdletBinding()] +Param( + [Parameter(Mandatory = $True)] + [string]$dscWxsFile, + [Parameter(Mandatory = $True)] + [string]$Platform, + [Parameter(Mandatory = $True)] + [string]$Configuration +) + +$ErrorActionPreference = 'Stop' + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Find build output directory +$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration" + +if (-not (Test-Path $buildOutputDir)) { + Write-Error "Build output directory not found: '$buildOutputDir'" + exit 1 +} + +# Find all DSC manifest JSON files +$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File + +if (-not $dscFiles) { + Write-Warning "No DSC manifest files found in '$buildOutputDir'" + # Create empty component group + $wxsContent = @" + + + + + + + + + +"@ + Set-Content -Path $dscWxsFile -Value $wxsContent + exit 0 +} + +Write-Host "Found $($dscFiles.Count) DSC manifest file(s)" + +# Generate WiX fragment +$wxsContent = @" + + + + + + +"@ + +$componentRefs = @() + +foreach ($file in $dscFiles) { + $componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_') + $fileId = $componentId + "_File" + $guid = [System.Guid]::NewGuid().ToString().ToUpper() + + $componentRefs += $componentId + + $wxsContent += @" + + + + + + + +"@ +} + +$wxsContent += @" + + + + + + +"@ + +foreach ($componentId in $componentRefs) { + $wxsContent += @" + + +"@ +} + +$wxsContent += @" + + + + +"@ + +# Write the WiX file +Set-Content -Path $dscWxsFile -Value $wxsContent + +Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'" \ No newline at end of file diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/BaseDscTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/BaseDscTest.cs new file mode 100644 index 0000000000..2eda4bdac5 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/BaseDscTest.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Globalization; +using System.IO; +using System.Resources; +using PowerToys.DSC.UnitTests.Models; + +namespace PowerToys.DSC.UnitTests; + +public class BaseDscTest +{ + private readonly ResourceManager _resourceManager; + + public BaseDscTest() + { + _resourceManager = new ResourceManager("PowerToys.DSC.Properties.Resources", typeof(PowerToys.DSC.Program).Assembly); + } + + /// + /// Returns the string resource for the given name, formatted with the provided arguments. + /// + /// The name of the resource string. + /// The arguments to format the resource string with. + /// + public string GetResourceString(string name, params string[] args) + { + return string.Format(CultureInfo.InvariantCulture, _resourceManager.GetString(name, CultureInfo.InvariantCulture), args); + } + + /// + /// Execute a dsc command with the provided arguments. + /// + /// + /// + /// + protected DscExecuteResult ExecuteDscCommand(params string[] args) + where T : Command, new() + { + var originalOut = Console.Out; + var originalErr = Console.Error; + + var outSw = new StringWriter(); + var errSw = new StringWriter(); + + try + { + Console.SetOut(outSw); + Console.SetError(errSw); + + var executeResult = new T().Invoke(args); + var output = outSw.ToString(); + var errorOutput = errSw.ToString(); + return new(executeResult == 0, output, errorOutput); + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); + outSw.Dispose(); + errSw.Dispose(); + } + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/CommandTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/CommandTest.cs new file mode 100644 index 0000000000..0941c03fdf --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/CommandTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PowerToys.DSC.Commands; +using PowerToys.DSC.DSCResources; + +namespace PowerToys.DSC.UnitTests; + +[TestClass] +public sealed class CommandTest : BaseDscTest +{ + [TestMethod] + public void GetResource_Found_Success() + { + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName); + + // Assert + Assert.IsTrue(result.Success); + } + + [TestMethod] + public void GetResource_NotFound_Fail() + { + // Arrange + var availableResources = string.Join(", ", BaseCommand.AvailableResources); + + // Act + var result = ExecuteDscCommand("--resource", "ResourceNotFound"); + + // Assert + Assert.IsFalse(result.Success); + Assert.Contains(GetResourceString("InvalidResourceNameError", availableResources), result.Error); + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/Models/DscExecuteResult.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/Models/DscExecuteResult.cs new file mode 100644 index 0000000000..7bf79f1041 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/Models/DscExecuteResult.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using PowerToys.DSC.Models; + +namespace PowerToys.DSC.UnitTests.Models; + +/// +/// Result of executing a DSC command. +/// +public class DscExecuteResult +{ + /// + /// Initializes a new instance of the class. + /// + /// Value indicating whether the command execution was successful. + /// Output stream content. + /// Error stream content. + public DscExecuteResult(bool success, string output, string error) + { + Success = success; + Output = output; + Error = error; + } + + /// + /// Gets a value indicating whether the command execution was successful. + /// + public bool Success { get; } + + /// + /// Gets the output stream content of the operation. + /// + public string Output { get; } + + /// + /// Gets the error stream content of the operation. + /// + public string Error { get; } + + /// + /// Gets the messages from the error stream. + /// + /// List of messages with their levels. + public List<(DscMessageLevel Level, string Message)> Messages() + { + var lines = Error.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries); + return lines.SelectMany(line => + { + var map = JsonSerializer.Deserialize>(line); + return map.Select(v => (GetMessageLevel(v.Key), v.Value)).ToList(); + }).ToList(); + } + + /// + /// Gets the output as state. + /// + /// State. + public T OutputState() + { + var lines = Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries); + Debug.Assert(lines.Length == 1, "Output should contain exactly one line."); + return JsonSerializer.Deserialize(lines[0]); + } + + /// + /// Gets the output as state and diff. + /// + /// State and diff. + public (T State, List Diff) OutputStateAndDiff() + { + var lines = Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries); + Debug.Assert(lines.Length == 2, "Output should contain exactly two lines."); + var obj = JsonSerializer.Deserialize(lines[0]); + var diff = JsonSerializer.Deserialize>(lines[1]); + return (obj, diff); + } + + /// + /// Gets the message level from a string representation. + /// + /// The string representation of the message level. + /// The level as . + /// Thrown when the level is unknown. + private DscMessageLevel GetMessageLevel(string level) + { + return level switch + { + "error" => DscMessageLevel.Error, + "warn" => DscMessageLevel.Warning, + "info" => DscMessageLevel.Info, + "debug" => DscMessageLevel.Debug, + "trace" => DscMessageLevel.Trace, + _ => throw new ArgumentOutOfRangeException(nameof(level), level, "Unknown message level"), + }; + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/PowerToys.DSC.UnitTests.csproj b/src/dsc/v3/PowerToys.DSC.UnitTests/PowerToys.DSC.UnitTests.csproj new file mode 100644 index 0000000000..d7a8c8c2f8 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/PowerToys.DSC.UnitTests.csproj @@ -0,0 +1,17 @@ + + + + + + false + ..\..\..\..\$(Configuration)\$(Platform)\tests\PowerToys.DSC.Tests\ + + + + + + + + + + diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs new file mode 100644 index 0000000000..deae2eb832 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace PowerToys.DSC.UnitTests.SettingsResourceTests; + +[TestClass] +public sealed class SettingsResourceAdvancedPasteModuleTest : SettingsResourceModuleTest +{ + public SettingsResourceAdvancedPasteModuleTest() + : base(nameof(ModuleType.AdvancedPaste)) + { + } + + protected override Action GetSettingsModifier() + { + return s => + { + s.Properties.ShowCustomPreview = !s.Properties.ShowCustomPreview; + s.Properties.CloseAfterLosingFocus = !s.Properties.CloseAfterLosingFocus; + s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled; + s.Properties.AdvancedPasteUIShortcut = new HotkeySettings + { + Key = "mock", + Alt = true, + }; + }; + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAlwaysOnTopModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAlwaysOnTopModuleTest.cs new file mode 100644 index 0000000000..5aeb10b27e --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAlwaysOnTopModuleTest.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace PowerToys.DSC.UnitTests.SettingsResourceTests; + +[TestClass] +public sealed class SettingsResourceAlwaysOnTopModuleTest : SettingsResourceModuleTest +{ + public SettingsResourceAlwaysOnTopModuleTest() + : base(nameof(ModuleType.AlwaysOnTop)) + { + } + + protected override Action GetSettingsModifier() + { + return s => + { + s.Properties.RoundCornersEnabled.Value = !s.Properties.RoundCornersEnabled.Value; + s.Properties.FrameEnabled.Value = !s.Properties.FrameEnabled.Value; + }; + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAppModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAppModuleTest.cs new file mode 100644 index 0000000000..b49563e100 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAppModuleTest.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PowerToys.DSC.DSCResources; + +namespace PowerToys.DSC.UnitTests.SettingsResourceTests; + +[TestClass] +public sealed class SettingsResourceAppModuleTest : SettingsResourceModuleTest +{ + public SettingsResourceAppModuleTest() + : base(SettingsResource.AppModule) + { + } + + protected override Action GetSettingsModifier() + { + return s => + { + s.Startup = !s.Startup; + s.ShowSysTrayIcon = !s.ShowSysTrayIcon; + s.Enabled.Awake = !s.Enabled.Awake; + s.Enabled.ColorPicker = !s.Enabled.ColorPicker; + }; + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAwakeModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAwakeModuleTest.cs new file mode 100644 index 0000000000..bd5e60c371 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAwakeModuleTest.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace PowerToys.DSC.UnitTests.SettingsResourceTests; + +[TestClass] +public sealed class SettingsResourceAwakeModuleTest : SettingsResourceModuleTest +{ + public SettingsResourceAwakeModuleTest() + : base(nameof(ModuleType.Awake)) + { + } + + protected override Action GetSettingsModifier() + { + return s => + { + s.Properties.ExpirationDateTime = DateTimeOffset.MinValue; + s.Properties.IntervalHours = DefaultSettings.Properties.IntervalHours + 1; + s.Properties.IntervalMinutes = DefaultSettings.Properties.IntervalMinutes + 1; + s.Properties.Mode = s.Properties.Mode == AwakeMode.PASSIVE ? AwakeMode.TIMED : AwakeMode.PASSIVE; + s.Properties.KeepDisplayOn = !s.Properties.KeepDisplayOn; + s.Properties.CustomTrayTimes = new Dictionary + { + { "08:00", 1 }, + { "12:00", 2 }, + { "16:00", 3 }, + }; + }; + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceColorPickerModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceColorPickerModuleTest.cs new file mode 100644 index 0000000000..175b74623c --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceColorPickerModuleTest.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace PowerToys.DSC.UnitTests.SettingsResourceTests; + +[TestClass] +public sealed class SettingsResourceColorPickerModuleTest : SettingsResourceModuleTest +{ + public SettingsResourceColorPickerModuleTest() + : base(nameof(ModuleType.ColorPicker)) + { + } + + protected override Action GetSettingsModifier() + { + return s => + { + s.Properties.ShowColorName = !s.Properties.ShowColorName; + s.Properties.ColorHistoryLimit = s.Properties.ColorHistoryLimit == 0 ? 10 : 0; + }; + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCommandTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCommandTest.cs new file mode 100644 index 0000000000..5333f5a832 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCommandTest.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using ManagedCommon; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PowerToys.DSC.Commands; +using PowerToys.DSC.DSCResources; +using PowerToys.DSC.Models; + +namespace PowerToys.DSC.UnitTests.SettingsResourceTests; + +[TestClass] +public sealed class SettingsResourceCommandTest : BaseDscTest +{ + [TestMethod] + public void Modules_ListAllSupportedModules() + { + // Arrange + var expectedModules = new List() + { + SettingsResource.AppModule, + nameof(ModuleType.AdvancedPaste), + nameof(ModuleType.AlwaysOnTop), + nameof(ModuleType.Awake), + nameof(ModuleType.ColorPicker), + nameof(ModuleType.CropAndLock), + nameof(ModuleType.EnvironmentVariables), + nameof(ModuleType.FancyZones), + nameof(ModuleType.FileLocksmith), + nameof(ModuleType.FindMyMouse), + nameof(ModuleType.Hosts), + nameof(ModuleType.ImageResizer), + nameof(ModuleType.KeyboardManager), + nameof(ModuleType.MouseHighlighter), + nameof(ModuleType.MouseJump), + nameof(ModuleType.MousePointerCrosshairs), + nameof(ModuleType.Peek), + nameof(ModuleType.PowerRename), + nameof(ModuleType.PowerAccent), + nameof(ModuleType.RegistryPreview), + nameof(ModuleType.MeasureTool), + nameof(ModuleType.ShortcutGuide), + nameof(ModuleType.PowerOCR), + nameof(ModuleType.Workspaces), + nameof(ModuleType.ZoomIt), + }; + + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName); + + // Assert + Assert.IsTrue(result.Success); + Assert.AreEqual(string.Join(Environment.NewLine, expectedModules.Order()), result.Output.Trim()); + } + + [TestMethod] + public void Set_EmptyInput_Fail() + { + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", "Awake"); + var messages = result.Messages(); + + // Assert + Assert.IsFalse(result.Success); + Assert.AreEqual(1, messages.Count); + Assert.AreEqual(DscMessageLevel.Error, messages[0].Level); + Assert.AreEqual(GetResourceString("InputEmptyOrNullError"), messages[0].Message); + } + + [TestMethod] + public void Test_EmptyInput_Fail() + { + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", "Awake"); + var messages = result.Messages(); + + // Assert + Assert.IsFalse(result.Success); + Assert.AreEqual(1, messages.Count); + Assert.AreEqual(DscMessageLevel.Error, messages[0].Level); + Assert.AreEqual(GetResourceString("InputEmptyOrNullError"), messages[0].Message); + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCropAndLockModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCropAndLockModuleTest.cs new file mode 100644 index 0000000000..516a5fac86 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCropAndLockModuleTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace PowerToys.DSC.UnitTests.SettingsResourceTests; + +[TestClass] +public sealed class SettingsResourceCropAndLockModuleTest : SettingsResourceModuleTest +{ + public SettingsResourceCropAndLockModuleTest() + : base(nameof(ModuleType.CropAndLock)) + { + } + + protected override Action GetSettingsModifier() + { + return s => + { + s.Properties.ThumbnailHotkey = new KeyboardKeysProperty() + { + Value = new HotkeySettings + { + Key = "mock", + Alt = true, + }, + }; + }; + } +} diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs new file mode 100644 index 0000000000..ad7eb1d200 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PowerToys.DSC.Commands; +using PowerToys.DSC.DSCResources; +using PowerToys.DSC.Models.ResourceObjects; + +namespace PowerToys.DSC.UnitTests.SettingsResourceTests; + +public abstract class SettingsResourceModuleTest : BaseDscTest + where TSettingsConfig : ISettingsConfig, new() +{ + private readonly SettingsUtils _settingsUtils = new(); + private TSettingsConfig _originalSettings; + + protected TSettingsConfig DefaultSettings => new(); + + protected string Module { get; } + + protected List DiffSettings { get; } = [SettingsResourceObject.SettingsJsonPropertyName]; + + protected List DiffEmpty { get; } = []; + + public SettingsResourceModuleTest(string module) + { + Module = module; + } + + [TestInitialize] + public void TestInitialize() + { + _originalSettings = GetSettings(); + ResetSettingsToDefaultValues(); + } + + [TestCleanup] + public void TestCleanup() + { + SaveSettings(_originalSettings); + } + + [TestMethod] + public void Get_Success() + { + // Arrange + var settingsBeforeExecute = GetSettings(); + + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module); + var state = result.OutputState>(); + + // Assert + Assert.IsTrue(result.Success); + AssertSettingsAreEqual(settingsBeforeExecute, GetSettings()); + AssertStateAndSettingsAreEqual(settingsBeforeExecute, state); + } + + [TestMethod] + public void Export_Success() + { + // Arrange + var settingsBeforeExecute = GetSettings(); + + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module); + var state = result.OutputState>(); + + // Assert + Assert.IsTrue(result.Success); + AssertSettingsAreEqual(settingsBeforeExecute, GetSettings()); + AssertStateAndSettingsAreEqual(settingsBeforeExecute, state); + } + + [TestMethod] + public void SetWithDiff_Success() + { + // Arrange + var settingsModifier = GetSettingsModifier(); + var input = CreateInputResourceObject(settingsModifier); + + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input); + var (state, diff) = result.OutputStateAndDiff>(); + + // Assert + Assert.IsTrue(result.Success); + AssertSettingsHasChanged(settingsModifier); + AssertStateAndSettingsAreEqual(GetSettings(), state); + CollectionAssert.AreEqual(DiffSettings, diff); + } + + [TestMethod] + public void SetWithoutDiff_Success() + { + // Arrange + var settingsModifier = GetSettingsModifier(); + UpdateSettings(settingsModifier); + var settingsBeforeExecute = GetSettings(); + var input = CreateInputResourceObject(settingsModifier); + + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input); + var (state, diff) = result.OutputStateAndDiff>(); + + // Assert + Assert.IsTrue(result.Success); + AssertSettingsAreEqual(settingsBeforeExecute, GetSettings()); + AssertStateAndSettingsAreEqual(settingsBeforeExecute, state); + CollectionAssert.AreEqual(DiffEmpty, diff); + } + + [TestMethod] + public void TestWithDiff_Success() + { + // Arrange + var settingsModifier = GetSettingsModifier(); + var settingsBeforeExecute = GetSettings(); + var input = CreateInputResourceObject(settingsModifier); + + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input); + var (state, diff) = result.OutputStateAndDiff>(); + + // Assert + Assert.IsTrue(result.Success); + AssertSettingsAreEqual(settingsBeforeExecute, GetSettings()); + AssertStateAndSettingsAreEqual(settingsBeforeExecute, state); + CollectionAssert.AreEqual(DiffSettings, diff); + Assert.IsFalse(state.InDesiredState); + } + + [TestMethod] + public void TestWithoutDiff_Success() + { + // Arrange + var settingsModifier = GetSettingsModifier(); + UpdateSettings(settingsModifier); + var settingsBeforeExecute = GetSettings(); + var input = CreateInputResourceObject(settingsModifier); + + // Act + var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input); + var (state, diff) = result.OutputStateAndDiff>(); + + // Assert + Assert.IsTrue(result.Success); + AssertSettingsAreEqual(settingsBeforeExecute, GetSettings()); + AssertStateAndSettingsAreEqual(settingsBeforeExecute, state); + CollectionAssert.AreEqual(DiffEmpty, diff); + Assert.IsTrue(state.InDesiredState); + } + + /// + /// Gets the settings modifier action for the specific settings configuration. + /// + /// An action that modifies the settings configuration. + protected abstract Action GetSettingsModifier(); + + /// + /// Resets the settings to default values. + /// + private void ResetSettingsToDefaultValues() + { + SaveSettings(DefaultSettings); + } + + /// + /// Get the settings for the specified module. + /// + /// An instance of the settings type with the current configuration. + private TSettingsConfig GetSettings() + { + return _settingsUtils.GetSettingsOrDefault(DefaultSettings.GetModuleName()); + } + + /// + /// Saves the settings for the specified module. + /// + /// Settings to save. + private void SaveSettings(TSettingsConfig settings) + { + _settingsUtils.SaveSettings(JsonSerializer.Serialize(settings), DefaultSettings.GetModuleName()); + } + + /// + /// Create the resource object for the operation. + /// + /// Settings to include in the resource object. + /// A JSON string representing the resource object. + private string CreateResourceObject(TSettingsConfig settings) + { + var resourceObject = new SettingsResourceObject + { + Settings = settings, + }; + return JsonSerializer.Serialize(resourceObject); + } + + private string CreateInputResourceObject(Action settingsModifier) + { + var settings = DefaultSettings; + settingsModifier(settings); + return CreateResourceObject(settings); + } + + /// + /// Create the response for the Get operation. + /// + /// A JSON string representing the response. + private string CreateGetResponse() + { + return CreateResourceObject(GetSettings()); + } + + /// + /// Asserts that the state and settings are equal. + /// + /// Settings manifest to compare against. + /// Output state to compare. + private void AssertStateAndSettingsAreEqual(TSettingsConfig settings, SettingsResourceObject state) + { + AssertSettingsAreEqual(settings, state.Settings); + } + + /// + /// Asserts that two settings manifests are equal. + /// + /// Expected settings. + /// Actual settings. + private void AssertSettingsAreEqual(TSettingsConfig expected, TSettingsConfig actual) + { + var expectedJson = JsonSerializer.SerializeToNode(expected) as JsonObject; + var actualJson = JsonSerializer.SerializeToNode(actual) as JsonObject; + Assert.IsTrue(JsonNode.DeepEquals(expectedJson, actualJson)); + } + + /// + /// Asserts that the current settings have changed. + /// + /// Action to prepare the default settings. + private void AssertSettingsHasChanged(Action action) + { + var currentSettings = GetSettings(); + var defaultSettings = DefaultSettings; + action(defaultSettings); + AssertSettingsAreEqual(defaultSettings, currentSettings); + } + + /// + /// Updates the settings. + /// + /// Action to modify the settings. + private void UpdateSettings(Action action) + { + var settings = GetSettings(); + action(settings); + SaveSettings(settings); + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Commands/BaseCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/BaseCommand.cs new file mode 100644 index 0000000000..d8cfaaefc6 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Commands/BaseCommand.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.CommandLine.IO; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using PowerToys.DSC.DSCResources; +using PowerToys.DSC.Options; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Commands; + +/// +/// Base class for all DSC commands. +/// +public abstract class BaseCommand : Command +{ + private static readonly CompositeFormat ModuleNotSupportedByResource = CompositeFormat.Parse(Resources.ModuleNotSupportedByResource); + + // Shared options for all commands + private readonly ModuleOption _moduleOption; + private readonly ResourceOption _resourceOption; + private readonly InputOption _inputOption; + + // The dictionary of available resources and their factories. + private static readonly Dictionary> _resourceFactories = new() + { + { SettingsResource.ResourceName, module => new SettingsResource(module) }, + + // Add other resources here + }; + + /// + /// Gets the list of available DSC resources that can be used with the command. + /// + public static List AvailableResources => [.._resourceFactories.Keys]; + + /// + /// Gets the DSC resource to be used by the command. + /// + protected BaseResource? Resource { get; private set; } + + /// + /// Gets the input JSON provided by the user. + /// + protected string? Input { get; private set; } + + /// + /// Gets the PowerToys module to be used by the command. + /// + protected string? Module { get; private set; } + + public BaseCommand(string name, string description) + : base(name, description) + { + // Register the common options for all commands + _moduleOption = new ModuleOption(); + AddOption(_moduleOption); + + _resourceOption = new ResourceOption(AvailableResources); + AddOption(_resourceOption); + + _inputOption = new InputOption(); + AddOption(_inputOption); + + // Register the command handler + this.SetHandler(CommandHandler); + } + + /// + /// Handles the command invocation. + /// + /// The invocation context containing the parsed command options. + public void CommandHandler(InvocationContext context) + { + Input = context.ParseResult.GetValueForOption(_inputOption); + Module = context.ParseResult.GetValueForOption(_moduleOption); + Resource = ResolvedResource(context); + + // Validate the module against the resource's supported modules + var supportedModules = Resource.GetSupportedModules(); + if (!string.IsNullOrEmpty(Module) && !supportedModules.Contains(Module)) + { + var errorMessage = string.Format(CultureInfo.InvariantCulture, ModuleNotSupportedByResource, Module, Resource.Name); + context.Console.Error.WriteLine(errorMessage); + context.ExitCode = 1; + return; + } + + // Continue with the command handler logic + CommandHandlerInternal(context); + } + + /// + /// Handles the command logic internally. + /// + /// Invocation context containing the parsed command options. + public abstract void CommandHandlerInternal(InvocationContext context); + + /// + /// Resolves the resource from the provided resource name in the context. + /// + /// Invocation context containing the parsed command options. + /// The resolved instance. + private BaseResource ResolvedResource(InvocationContext context) + { + // Resource option has already been validated before the command + // handler is invoked. + var resourceName = context.ParseResult.GetValueForOption(_resourceOption); + Debug.Assert(!string.IsNullOrEmpty(resourceName), "Resource name must not be null or empty."); + Debug.Assert(_resourceFactories.ContainsKey(resourceName), $"Resource '{resourceName}' is not registered."); + return _resourceFactories[resourceName](Module); + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Commands/ExportCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/ExportCommand.cs new file mode 100644 index 0000000000..e8001fd0bd --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Commands/ExportCommand.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine.Invocation; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Commands; + +/// +/// Command to export all state instances. +/// +public sealed class ExportCommand : BaseCommand +{ + public ExportCommand() + : base("export", Resources.ExportCommandDescription) + { + } + + /// + public override void CommandHandlerInternal(InvocationContext context) + { + context.ExitCode = Resource!.ExportState(Input) ? 0 : 1; + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Commands/GetCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/GetCommand.cs new file mode 100644 index 0000000000..a5fed7bc73 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Commands/GetCommand.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine.Invocation; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Commands; + +/// +/// Command to get the resource state. +/// +public sealed class GetCommand : BaseCommand +{ + public GetCommand() + : base("get", Resources.GetCommandDescription) + { + } + + /// + public override void CommandHandlerInternal(InvocationContext context) + { + context.ExitCode = Resource!.GetState(Input) ? 0 : 1; + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Commands/ManifestCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/ManifestCommand.cs new file mode 100644 index 0000000000..da3c637137 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Commands/ManifestCommand.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine.Invocation; +using PowerToys.DSC.Options; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Commands; + +/// +/// Command to get the manifest of the DSC resource. +/// +public sealed class ManifestCommand : BaseCommand +{ + /// + /// Option to specify the output directory for the manifest. + /// + private readonly OutputDirectoryOption _outputDirectoryOption; + + public ManifestCommand() + : base("manifest", Resources.ManifestCommandDescription) + { + _outputDirectoryOption = new OutputDirectoryOption(); + AddOption(_outputDirectoryOption); + } + + /// + public override void CommandHandlerInternal(InvocationContext context) + { + var outputDir = context.ParseResult.GetValueForOption(_outputDirectoryOption); + context.ExitCode = Resource!.Manifest(outputDir) ? 0 : 1; + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Commands/ModulesCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/ModulesCommand.cs new file mode 100644 index 0000000000..9eb60659df --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Commands/ModulesCommand.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Diagnostics; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Commands; + +/// +/// Command to get all supported modules for a specific resource. +/// +/// +/// This class is primarily used for debugging purposes and for build scripts. +/// +public sealed class ModulesCommand : BaseCommand +{ + public ModulesCommand() + : base("modules", Resources.ModulesCommandDescription) + { + } + + /// + public override void CommandHandlerInternal(InvocationContext context) + { + // Module is optional, if not provided, all supported modules for the + // resource will be printed. If provided, it must be one of the + // supported modules since it has been validated before this command is + // executed. + if (!string.IsNullOrEmpty(Module)) + { + Debug.Assert(Resource!.GetSupportedModules().Contains(Module), "Module must be present in the list of supported modules."); + context.Console.WriteLine(Module); + } + else + { + // Print the supported modules for the specified resource + foreach (var module in Resource!.GetSupportedModules()) + { + context.Console.WriteLine(module); + } + } + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Commands/SchemaCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/SchemaCommand.cs new file mode 100644 index 0000000000..f7fbfc2448 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Commands/SchemaCommand.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine.Invocation; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Commands; + +/// +/// Command to output the schema of the resource. +/// +public sealed class SchemaCommand : BaseCommand +{ + public SchemaCommand() + : base("schema", Resources.SchemaCommandDescription) + { + } + + /// + public override void CommandHandlerInternal(InvocationContext context) + { + context.ExitCode = Resource!.Schema() ? 0 : 1; + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Commands/SetCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/SetCommand.cs new file mode 100644 index 0000000000..f76c24a0a8 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Commands/SetCommand.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine.Invocation; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Commands; + +/// +/// Command to set the resource state. +/// +public sealed class SetCommand : BaseCommand +{ + public SetCommand() + : base("set", Resources.SetCommandDescription) + { + } + + /// + public override void CommandHandlerInternal(InvocationContext context) + { + context.ExitCode = Resource!.SetState(Input) ? 0 : 1; + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Commands/TestCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/TestCommand.cs new file mode 100644 index 0000000000..fcdd83342e --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Commands/TestCommand.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine.Invocation; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Commands; + +/// +/// Command to test the resource state. +/// +public sealed class TestCommand : BaseCommand +{ + public TestCommand() + : base("test", Resources.TestCommandDescription) + { + } + + /// + public override void CommandHandlerInternal(InvocationContext context) + { + context.ExitCode = Resource!.TestState(Input) ? 0 : 1; + } +} diff --git a/src/dsc/v3/PowerToys.DSC/DSCResources/BaseResource.cs b/src/dsc/v3/PowerToys.DSC/DSCResources/BaseResource.cs new file mode 100644 index 0000000000..51d265cff7 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/DSCResources/BaseResource.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; +using PowerToys.DSC.Models; + +namespace PowerToys.DSC.DSCResources; + +/// +/// Base class for all DSC resources. +/// +public abstract class BaseResource +{ + /// + /// Gets the name of the resource. + /// + public string Name { get; } + + /// + /// Gets the module being used by the resource, if provided. + /// + public string? Module { get; } + + public BaseResource(string name, string? module) + { + Name = name; + Module = module; + } + + /// + /// Calls the get method on the resource. + /// + /// The input string, if any. + /// True if the operation was successful; otherwise false. + public abstract bool GetState(string? input); + + /// + /// Calls the set method on the resource. + /// + /// The input string, if any. + /// True if the operation was successful; otherwise false. + public abstract bool SetState(string? input); + + /// + /// Calls the test method on the resource. + /// + /// The input string, if any. + /// True if the operation was successful; otherwise false. + public abstract bool TestState(string? input); + + /// + /// Calls the export method on the resource. + /// + /// The input string, if any. + /// True if the operation was successful; otherwise false. + public abstract bool ExportState(string? input); + + /// + /// Calls the schema method on the resource. + /// + /// True if the operation was successful; otherwise false. + public abstract bool Schema(); + + /// + /// Generates a DSC resource JSON manifest for the resource. If the + /// outputDir is not provided, the manifest will be printed to the console. + /// + /// The directory where the manifest should be + /// saved. If null, the manifest will be printed to the console. + /// True if the manifest was successfully generated and saved,otherwise false. + public abstract bool Manifest(string? outputDir); + + /// + /// Gets the list of supported modules for the resource. + /// + /// Gets a list of supported modules. + public abstract IList GetSupportedModules(); + + /// + /// Writes a JSON output line to the console. + /// + /// The JSON output to write. + protected void WriteJsonOutputLine(JsonNode output) + { + var json = output.ToJsonString(new() { WriteIndented = false }); + WriteJsonOutputLine(json); + } + + /// + /// Writes a JSON output line to the console. + /// + /// The JSON output to write. + protected void WriteJsonOutputLine(string output) + { + Console.WriteLine(output); + } + + /// + /// Writes a message output line to the console with the specified message level. + /// + /// The level of the message. + /// The message to write. + protected void WriteMessageOutputLine(DscMessageLevel level, string message) + { + var messageObj = new Dictionary + { + [GetMessageLevel(level)] = message, + }; + var messageJson = System.Text.Json.JsonSerializer.Serialize(messageObj); + Console.Error.WriteLine(messageJson); + } + + /// + /// Gets the message level as a string based on the provided dsc message level enum value. + /// + /// The dsc message level. + /// A string representation of the message level. + /// Thrown when the provided message level is not recognized. + private static string GetMessageLevel(DscMessageLevel level) + { + return level switch + { + DscMessageLevel.Error => "error", + DscMessageLevel.Warning => "warn", + DscMessageLevel.Info => "info", + DscMessageLevel.Debug => "debug", + DscMessageLevel.Trace => "trace", + _ => throw new ArgumentOutOfRangeException(nameof(level), level, null), + }; + } +} diff --git a/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs b/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs new file mode 100644 index 0000000000..5f69b20227 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using PowerToys.DSC.Models; +using PowerToys.DSC.Models.FunctionData; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.DSCResources; + +/// +/// Represents the DSC resource for managing PowerToys settings. +/// +public sealed class SettingsResource : BaseResource +{ + private static readonly CompositeFormat FailedToWriteManifests = CompositeFormat.Parse(Resources.FailedToWriteManifests); + + public const string AppModule = "App"; + public const string ResourceName = "settings"; + + private readonly Dictionary> _moduleFunctionData; + + public string ModuleOrDefault => string.IsNullOrEmpty(Module) ? AppModule : Module; + + public SettingsResource(string? module) + : base(ResourceName, module) + { + _moduleFunctionData = new() + { + { AppModule, CreateModuleFunctionData }, + { nameof(ModuleType.AdvancedPaste), CreateModuleFunctionData }, + { nameof(ModuleType.AlwaysOnTop), CreateModuleFunctionData }, + { nameof(ModuleType.Awake), CreateModuleFunctionData }, + { nameof(ModuleType.ColorPicker), CreateModuleFunctionData }, + { nameof(ModuleType.CropAndLock), CreateModuleFunctionData }, + { nameof(ModuleType.EnvironmentVariables), CreateModuleFunctionData }, + { nameof(ModuleType.FancyZones), CreateModuleFunctionData }, + { nameof(ModuleType.FileLocksmith), CreateModuleFunctionData }, + { nameof(ModuleType.FindMyMouse), CreateModuleFunctionData }, + { nameof(ModuleType.Hosts), CreateModuleFunctionData }, + { nameof(ModuleType.ImageResizer), CreateModuleFunctionData }, + { nameof(ModuleType.KeyboardManager), CreateModuleFunctionData }, + { nameof(ModuleType.MouseHighlighter), CreateModuleFunctionData }, + { nameof(ModuleType.MouseJump), CreateModuleFunctionData }, + { nameof(ModuleType.MousePointerCrosshairs), CreateModuleFunctionData }, + { nameof(ModuleType.Peek), CreateModuleFunctionData }, + { nameof(ModuleType.PowerRename), CreateModuleFunctionData }, + { nameof(ModuleType.PowerAccent), CreateModuleFunctionData }, + { nameof(ModuleType.RegistryPreview), CreateModuleFunctionData }, + { nameof(ModuleType.MeasureTool), CreateModuleFunctionData }, + { nameof(ModuleType.ShortcutGuide), CreateModuleFunctionData }, + { nameof(ModuleType.PowerOCR), CreateModuleFunctionData }, + { nameof(ModuleType.Workspaces), CreateModuleFunctionData }, + { nameof(ModuleType.ZoomIt), CreateModuleFunctionData }, + + // The following modules are not currently supported: + // - MouseWithoutBorders Contains sensitive configuration values, making export/import potentially insecure. + // - PowerLauncher Uses absolute file paths in its settings, which are not portable across systems. + // - NewPlus Uses absolute file paths in its settings, which are not portable across systems. + }; + } + + /// + public override bool ExportState(string? input) + { + var data = CreateFunctionData(); + data.GetState(); + WriteJsonOutputLine(data.Output.ToJson()); + return true; + } + + /// + public override bool GetState(string? input) + { + return ExportState(input); + } + + /// + public override bool SetState(string? input) + { + if (string.IsNullOrEmpty(input)) + { + WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError); + return false; + } + + var data = CreateFunctionData(input); + data.GetState(); + + // Capture the diff before updating the output + var diff = data.GetDiffJson(); + + // Only call Set if the desired state is different from the current state + if (!data.TestState()) + { + var inputSettings = data.Input.SettingsInternal; + data.Output.SettingsInternal = inputSettings; + data.SetState(); + } + + WriteJsonOutputLine(data.Output.ToJson()); + WriteJsonOutputLine(diff); + return true; + } + + /// + public override bool TestState(string? input) + { + if (string.IsNullOrEmpty(input)) + { + WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError); + return false; + } + + var data = CreateFunctionData(input); + data.GetState(); + data.Output.InDesiredState = data.TestState(); + + WriteJsonOutputLine(data.Output.ToJson()); + WriteJsonOutputLine(data.GetDiffJson()); + return true; + } + + /// + public override bool Schema() + { + var data = CreateFunctionData(); + WriteJsonOutputLine(data.Schema()); + return true; + } + + /// + /// + /// If an output directory is specified, write the manifests to files, + /// otherwise output them to the console. + /// + public override bool Manifest(string? outputDir) + { + var manifests = GenerateManifests(); + + if (!string.IsNullOrEmpty(outputDir)) + { + try + { + foreach (var (name, manifest) in manifests) + { + File.WriteAllText(Path.Combine(outputDir, $"microsoft.powertoys.{name}.settings.dsc.resource.json"), manifest); + } + } + catch (Exception ex) + { + var errorMessage = string.Format(CultureInfo.InvariantCulture, FailedToWriteManifests, outputDir, ex.Message); + WriteMessageOutputLine(DscMessageLevel.Error, errorMessage); + return false; + } + } + else + { + foreach (var (_, manifest) in manifests) + { + WriteJsonOutputLine(manifest); + } + } + + return true; + } + + /// + /// Generates manifests for the specified module or all supported modules + /// if no module is specified. + /// + /// A list of tuples containing the module name and its corresponding manifest JSON. + private List<(string Name, string Manifest)> GenerateManifests() + { + List<(string Name, string Manifest)> manifests = []; + if (!string.IsNullOrEmpty(Module)) + { + manifests.Add((Module, GenerateManifest(Module))); + } + else + { + foreach (var module in GetSupportedModules()) + { + manifests.Add((module, GenerateManifest(module))); + } + } + + return manifests; + } + + /// + /// Generate a DSC resource JSON manifest for the specified module. + /// + /// The name of the module for which to generate the manifest. + /// A JSON string representing the DSC resource manifest. + private string GenerateManifest(string module) + { + // Note: The description is not localized because the generated + // manifest file will be part of the package + return new DscManifest($"{module}Settings", "0.1.0") + .AddDescription($"Allows management of {module} settings state via the DSC v3 command line interface protocol.") + .AddStdinMethod("export", ["export", "--module", module, "--resource", "settings"]) + .AddStdinMethod("get", ["get", "--module", module, "--resource", "settings"]) + .AddJsonInputMethod("set", "--input", ["set", "--module", module, "--resource", "settings"], implementsPretest: true, stateAndDiff: true) + .AddJsonInputMethod("test", "--input", ["test", "--module", module, "--resource", "settings"], stateAndDiff: true) + .AddCommandMethod("schema", ["schema", "--module", module, "--resource", "settings"]) + .ToJson(); + } + + /// + public override IList GetSupportedModules() + { + return [.. _moduleFunctionData.Keys.Order()]; + } + + /// + /// Creates the function data for the specified module or the default module if none is specified. + /// + /// The input string, if any. + /// An instance of for the specified module. + public ISettingsFunctionData CreateFunctionData(string? input = null) + { + Debug.Assert(_moduleFunctionData.ContainsKey(ModuleOrDefault), "Module should be supported by the resource."); + return _moduleFunctionData[ModuleOrDefault](input); + } + + /// + /// Creates the function data for a specific settings configuration type. + /// + /// The type of settings configuration to create function data for. + /// The input string, if any. + /// An instance of for the specified settings configuration type. + private ISettingsFunctionData CreateModuleFunctionData(string? input) + where TSettingsConfig : ISettingsConfig, new() + { + return new SettingsFunctionData(input); + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs b/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs new file mode 100644 index 0000000000..dcb6abf4a1 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace PowerToys.DSC.Models; + +/// +/// Class for building a DSC manifest for PowerToys resources. +/// +public sealed class DscManifest +{ + private const string Schema = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json"; + private const string Executable = @"PowerToys.DSC.exe"; + + private readonly string _type; + private readonly string _version; + private readonly JsonObject _manifest; + + public DscManifest(string type, string version) + { + _type = type; + _version = version; + _manifest = new JsonObject + { + ["$schema"] = Schema, + ["type"] = $"Microsoft.PowerToys/{_type}", + ["version"] = _version, + ["tags"] = new JsonArray("PowerToys"), + }; + } + + /// + /// Adds a description to the manifest. + /// + /// The description to add. + /// Returns the current instance of . + public DscManifest AddDescription(string description) + { + _manifest["description"] = description; + return this; + } + + /// + /// Adds a method to the manifest with the specified executable and arguments. + /// + /// The name of the method to add. + /// The input argument for the method + /// The list of arguments for the method. + /// Whether the method implements a pretest. + /// Whether the method returns state and diff. + /// Returns the current instance of . + public DscManifest AddJsonInputMethod(string method, string inputArg, List args, bool? implementsPretest = null, bool? stateAndDiff = null) + { + var argsJson = CreateJsonArray(args); + argsJson.Add(new JsonObject + { + ["jsonInputArg"] = inputArg, + ["mandatory"] = true, + }); + var methodObject = AddMethod(argsJson, implementsPretest, stateAndDiff); + _manifest[method] = methodObject; + return this; + } + + /// + /// Adds a method to the manifest that reads from standard input (stdin). + /// + /// The name of the method to add. + /// The list of arguments for the method. + /// Whether the method implements a pretest. + /// Whether the method returns state and diff. + /// Returns the current instance of . + public DscManifest AddStdinMethod(string method, List args, bool? implementsPretest = null, bool? stateAndDiff = null) + { + var argsJson = CreateJsonArray(args); + var methodObject = AddMethod(argsJson, implementsPretest, stateAndDiff); + methodObject["input"] = "stdin"; + _manifest[method] = methodObject; + return this; + } + + /// + /// Adds a command method to the manifest. + /// + /// The name of the method to add. + /// The list of arguments for the method. + /// Returns the current instance of . + public DscManifest AddCommandMethod(string method, List args) + { + _manifest[method] = new JsonObject + { + ["command"] = AddMethod(CreateJsonArray(args)), + }; + return this; + } + + /// + /// Gets the JSON representation of the manifest. + /// + /// Returns the JSON string of the manifest. + public string ToJson() + { + return _manifest.ToJsonString(new() { WriteIndented = true }); + } + + /// + /// Add a method to the manifest with the specified arguments. + /// + /// The list of arguments for the method. + /// Whether the method implements a pretest. + /// Whether the method returns state and diff. + /// Returns the method object. + private JsonObject AddMethod(JsonArray args, bool? implementsPretest = null, bool? stateAndDiff = null) + { + var methodObject = new JsonObject + { + ["executable"] = Executable, + ["args"] = args, + }; + + if (implementsPretest.HasValue) + { + methodObject["implementsPretest"] = implementsPretest.Value; + } + + if (stateAndDiff.HasValue) + { + methodObject["return"] = stateAndDiff.Value ? "stateAndDiff" : "state"; + } + + return methodObject; + } + + /// + /// Creates a JSON array from a list of strings. + /// + /// The list of strings to convert. + /// Returns the JSON array. + private JsonArray CreateJsonArray(List args) + { + var jsonArray = new JsonArray(); + foreach (var arg in args) + { + jsonArray.Add(arg); + } + + return jsonArray; + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Models/DscMessageLevel.cs b/src/dsc/v3/PowerToys.DSC/Models/DscMessageLevel.cs new file mode 100644 index 0000000000..9c5b12b3c0 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Models/DscMessageLevel.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace PowerToys.DSC.Models; + +/// +/// Specifies the severity level of a message. +/// +public enum DscMessageLevel +{ + /// + /// Represents an error message. + /// + Error, + + /// + /// Represents a warning message. + /// + Warning, + + /// + /// Represents an informational message. + /// + Info, + + /// + /// Represents a debug message. + /// + Debug, + + /// + /// Represents a trace message. + /// + Trace, +} diff --git a/src/dsc/v3/PowerToys.DSC/Models/FunctionData/BaseFunctionData.cs b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/BaseFunctionData.cs new file mode 100644 index 0000000000..4456beed82 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/BaseFunctionData.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Newtonsoft.Json; +using NJsonSchema.Generation; +using PowerToys.DSC.Models.ResourceObjects; + +namespace PowerToys.DSC.Models.FunctionData; + +/// +/// Base class for function data objects. +/// +public class BaseFunctionData +{ + /// + /// Generates a JSON schema for the specified resource object type. + /// + /// The type of the resource object. + /// A JSON schema string. + protected static string GenerateSchema() + where T : BaseResourceObject + { + var settings = new SystemTextJsonSchemaGeneratorSettings() + { + FlattenInheritanceHierarchy = true, + SerializerOptions = + { + IgnoreReadOnlyFields = true, + }, + }; + var generator = new JsonSchemaGenerator(settings); + var schema = generator.Generate(typeof(T)); + return schema.ToJson(Formatting.None); + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Models/FunctionData/ISettingsFunctionData.cs b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/ISettingsFunctionData.cs new file mode 100644 index 0000000000..7cf02d1c74 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/ISettingsFunctionData.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Nodes; +using PowerToys.DSC.Models.ResourceObjects; + +namespace PowerToys.DSC.Models.FunctionData; + +/// +/// Interface for function data related to settings. +/// +public interface ISettingsFunctionData +{ + /// + /// Gets the input settings resource object. + /// + public ISettingsResourceObject Input { get; } + + /// + /// Gets the output settings resource object. + /// + public ISettingsResourceObject Output { get; } + + /// + /// Gets the current settings. + /// + public void GetState(); + + /// + /// Sets the current settings. + /// + public void SetState(); + + /// + /// Tests if the current settings and the desired state are valid. + /// + /// True if the current settings match the desired state; otherwise false. + public bool TestState(); + + /// + /// Gets the difference between the current settings and the desired state in JSON format. + /// + /// A JSON array representing the differences. + public JsonArray GetDiffJson(); + + /// + /// Gets the schema for the settings resource object. + /// + /// + public string Schema(); +} diff --git a/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs new file mode 100644 index 0000000000..7fcce03d33 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using PowerToys.DSC.Models.ResourceObjects; + +namespace PowerToys.DSC.Models.FunctionData; + +/// +/// Represents function data for the settings DSC resource. +/// +/// The module settings configuration type. +public sealed class SettingsFunctionData : BaseFunctionData, ISettingsFunctionData + where TSettingsConfig : ISettingsConfig, new() +{ + private static readonly SettingsUtils _settingsUtils = new(); + private static readonly TSettingsConfig _settingsConfig = new(); + + private readonly SettingsResourceObject _input; + private readonly SettingsResourceObject _output; + + /// + public ISettingsResourceObject Input => _input; + + /// + public ISettingsResourceObject Output => _output; + + public SettingsFunctionData(string? input = null) + { + _output = new(); + _input = string.IsNullOrEmpty(input) ? new() : JsonSerializer.Deserialize>(input) ?? new(); + } + + /// + public void GetState() + { + _output.Settings = GetSettings(); + } + + /// + public void SetState() + { + Debug.Assert(_output.Settings != null, "Output settings should not be null"); + SaveSettings(_output.Settings); + } + + /// + public bool TestState() + { + var input = JsonSerializer.SerializeToNode(_input.Settings); + var output = JsonSerializer.SerializeToNode(_output.Settings); + return JsonNode.DeepEquals(input, output); + } + + /// + public JsonArray GetDiffJson() + { + var diff = new JsonArray(); + if (!TestState()) + { + diff.Add(SettingsResourceObject.SettingsJsonPropertyName); + } + + return diff; + } + + /// + public string Schema() + { + return GenerateSchema>(); + } + + /// + /// Gets the settings configuration from the settings utils for a specific module. + /// + /// The settings configuration for the module. + private static TSettingsConfig GetSettings() + { + return _settingsUtils.GetSettingsOrDefault(_settingsConfig.GetModuleName()); + } + + /// + /// Saves the settings configuration to the settings utils for a specific module. + /// + /// Settings of a specific module + private static void SaveSettings(TSettingsConfig settings) + { + var inputJson = JsonSerializer.Serialize(settings); + _settingsUtils.SaveSettings(inputJson, _settingsConfig.GetModuleName()); + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/BaseResourceObject.cs b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/BaseResourceObject.cs new file mode 100644 index 0000000000..d6e3e08dcc --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/BaseResourceObject.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace PowerToys.DSC.Models.ResourceObjects; + +/// +/// Base class for all resource objects. +/// +public class BaseResourceObject +{ + private readonly JsonSerializerOptions _options; + + public BaseResourceObject() + { + _options = new() + { + WriteIndented = false, + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + }; + } + + /// + /// Gets or sets whether an instance is in the desired state. + /// + [JsonPropertyName("_inDesiredState")] + [Description("Indicates whether an instance is in the desired state")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? InDesiredState { get; set; } + + /// + /// Generates a JSON representation of the resource object. + /// + /// + public JsonNode ToJson() + { + return JsonSerializer.SerializeToNode(this, GetType(), _options) ?? new JsonObject(); + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/ISettingsResourceObject.cs b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/ISettingsResourceObject.cs new file mode 100644 index 0000000000..85c9c7eadc --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/ISettingsResourceObject.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Nodes; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace PowerToys.DSC.Models.ResourceObjects; + +/// +/// Interface for settings resource objects. +/// +public interface ISettingsResourceObject +{ + /// + /// Gets or sets the settings configuration. + /// + public ISettingsConfig SettingsInternal { get; set; } + + /// + /// Gets or sets whether an instance is in the desired state. + /// + public bool? InDesiredState { get; set; } + + /// + /// Generates a JSON representation of the resource object. + /// + /// String representation of the resource object in JSON format. + public JsonNode ToJson(); +} diff --git a/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/SettingsResourceObject`1.cs b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/SettingsResourceObject`1.cs new file mode 100644 index 0000000000..d5017336ed --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/SettingsResourceObject`1.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using NJsonSchema.Annotations; + +namespace PowerToys.DSC.Models.ResourceObjects; + +/// +/// Represents a settings resource object for a module's settings configuration. +/// +/// The type of the settings configuration. +public sealed class SettingsResourceObject : BaseResourceObject, ISettingsResourceObject + where TSettingsConfig : ISettingsConfig, new() +{ + public const string SettingsJsonPropertyName = "settings"; + + /// + /// Gets or sets the settings content for the module. + /// + [JsonPropertyName(SettingsJsonPropertyName)] + [Required] + [Description("The settings content for the module.")] + [JsonSchemaType(typeof(object))] + public TSettingsConfig Settings { get; set; } = new(); + + /// + [JsonIgnore] + public ISettingsConfig SettingsInternal { get => Settings; set => Settings = (TSettingsConfig)value; } +} diff --git a/src/dsc/v3/PowerToys.DSC/Options/InputOption.cs b/src/dsc/v3/PowerToys.DSC/Options/InputOption.cs new file mode 100644 index 0000000000..048c50a2df --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Options/InputOption.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Globalization; +using System.Text; +using System.Text.Json; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Options; + +/// +/// Represents an option for specifying JSON input for the dsc command. +/// +public sealed class InputOption : Option +{ + private static readonly CompositeFormat InvalidJsonInputError = CompositeFormat.Parse(Resources.InvalidJsonInputError); + + public InputOption() + : base("--input", Resources.InputOptionDescription) + { + AddValidator(OptionValidator); + } + + /// + /// Validates the JSON input provided to the option. + /// + /// The option result to validate. + private void OptionValidator(OptionResult result) + { + var value = result.GetValueOrDefault() ?? string.Empty; + if (string.IsNullOrEmpty(value)) + { + result.ErrorMessage = Resources.InputEmptyOrNullError; + } + else + { + try + { + JsonDocument.Parse(value); + } + catch (Exception e) + { + result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidJsonInputError, e.Message); + } + } + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Options/ModuleOption.cs b/src/dsc/v3/PowerToys.DSC/Options/ModuleOption.cs new file mode 100644 index 0000000000..a5273c2cb0 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Options/ModuleOption.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Options; + +/// +/// Represents an option for specifying the module name for the dsc command. +/// +public sealed class ModuleOption : Option +{ + public ModuleOption() + : base("--module", Resources.ModuleOptionDescription) + { + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Options/OutputDirectoryOption.cs b/src/dsc/v3/PowerToys.DSC/Options/OutputDirectoryOption.cs new file mode 100644 index 0000000000..7de1af64b7 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Options/OutputDirectoryOption.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Globalization; +using System.IO; +using System.Text; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Options; + +/// +/// Represents an option for specifying the output directory for the dsc command. +/// +public sealed class OutputDirectoryOption : Option +{ + private static readonly CompositeFormat InvalidOutputDirectoryError = CompositeFormat.Parse(Resources.InvalidOutputDirectoryError); + + public OutputDirectoryOption() + : base("--outputDir", Resources.OutputDirectoryOptionDescription) + { + AddValidator(OptionValidator); + } + + /// + /// Validates the output directory option. + /// + /// The option result to validate. + private void OptionValidator(OptionResult result) + { + var value = result.GetValueOrDefault() ?? string.Empty; + if (string.IsNullOrEmpty(value)) + { + result.ErrorMessage = Resources.OutputDirectoryEmptyOrNullError; + } + else if (!Directory.Exists(value)) + { + result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidOutputDirectoryError, value); + } + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Options/ResourceOption.cs b/src/dsc/v3/PowerToys.DSC/Options/ResourceOption.cs new file mode 100644 index 0000000000..cfce5dbfc7 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Options/ResourceOption.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Globalization; +using System.Text; +using PowerToys.DSC.Properties; + +namespace PowerToys.DSC.Options; + +/// +/// Represents an option for specifying the resource name for the dsc command. +/// +public sealed class ResourceOption : Option +{ + private static readonly CompositeFormat InvalidResourceNameError = CompositeFormat.Parse(Resources.InvalidResourceNameError); + + private readonly IList _resources = []; + + public ResourceOption(IList resources) + : base("--resource", Resources.ResourceOptionDescription) + { + _resources = resources; + IsRequired = true; + AddValidator(OptionValidator); + } + + /// + /// Validates the resource option to ensure that the specified resource name is valid. + /// + /// The option result to validate. + private void OptionValidator(OptionResult result) + { + var value = result.GetValueOrDefault() ?? string.Empty; + if (!_resources.Contains(value)) + { + result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidResourceNameError, string.Join(", ", _resources)); + } + } +} diff --git a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj new file mode 100644 index 0000000000..230cd4556b --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj @@ -0,0 +1,48 @@ + + + + + + + Exe + ..\..\..\..\$(Platform)\$(Configuration) + false + false + PowerToys.DSC + PowerToys DSC + PowerToys.DSC + enable + + true + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + \ No newline at end of file diff --git a/src/dsc/v3/PowerToys.DSC/Program.cs b/src/dsc/v3/PowerToys.DSC/Program.cs new file mode 100644 index 0000000000..09a22b64d6 --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Threading.Tasks; +using PowerToys.DSC.Commands; + +namespace PowerToys.DSC; + +/// +/// Main entry point for the PowerToys Desired State Configuration CLI application. +/// +public class Program +{ + public static async Task Main(string[] args) + { + var rootCommand = new RootCommand(Properties.Resources.PowerToysDSC); + rootCommand.AddCommand(new GetCommand()); + rootCommand.AddCommand(new SetCommand()); + rootCommand.AddCommand(new ExportCommand()); + rootCommand.AddCommand(new TestCommand()); + rootCommand.AddCommand(new SchemaCommand()); + rootCommand.AddCommand(new ManifestCommand()); + rootCommand.AddCommand(new ModulesCommand()); + return await rootCommand.InvokeAsync(args); + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Properties/Resources.Designer.cs b/src/dsc/v3/PowerToys.DSC/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..4089d98c6b --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Properties/Resources.Designer.cs @@ -0,0 +1,234 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PowerToys.DSC.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PowerToys.DSC.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Get all state instances. + /// + internal static string ExportCommandDescription { + get { + return ResourceManager.GetString("ExportCommandDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to write manifests to directory '{0}': {1}. + /// + internal static string FailedToWriteManifests { + get { + return ResourceManager.GetString("FailedToWriteManifests", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get the resource state. + /// + internal static string GetCommandDescription { + get { + return ResourceManager.GetString("GetCommandDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input cannot be empty or null. + /// + internal static string InputEmptyOrNullError { + get { + return ResourceManager.GetString("InputEmptyOrNullError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The JSON input. + /// + internal static string InputOptionDescription { + get { + return ResourceManager.GetString("InputOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid JSON input: {0}. + /// + internal static string InvalidJsonInputError { + get { + return ResourceManager.GetString("InvalidJsonInputError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid output directory: {0}. + /// + internal static string InvalidOutputDirectoryError { + get { + return ResourceManager.GetString("InvalidOutputDirectoryError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid resource name. Valid resource names are: {0}. + /// + internal static string InvalidResourceNameError { + get { + return ResourceManager.GetString("InvalidResourceNameError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get the manifest of the dsc resource. + /// + internal static string ManifestCommandDescription { + get { + return ResourceManager.GetString("ManifestCommandDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Module '{0}' is not supported for the resource {1}. Use the 'module' command to list available modules.. + /// + internal static string ModuleNotSupportedByResource { + get { + return ResourceManager.GetString("ModuleNotSupportedByResource", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The module name. + /// + internal static string ModuleOptionDescription { + get { + return ResourceManager.GetString("ModuleOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get all supported modules for a specific resource. + /// + internal static string ModulesCommandDescription { + get { + return ResourceManager.GetString("ModulesCommandDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output directory cannot be empty or null. + /// + internal static string OutputDirectoryEmptyOrNullError { + get { + return ResourceManager.GetString("OutputDirectoryEmptyOrNullError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The output directory. + /// + internal static string OutputDirectoryOptionDescription { + get { + return ResourceManager.GetString("OutputDirectoryOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PowerToys Desired State Configuration commands. + /// + internal static string PowerToysDSC { + get { + return ResourceManager.GetString("PowerToysDSC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The resource name. + /// + internal static string ResourceOptionDescription { + get { + return ResourceManager.GetString("ResourceOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Outputs schema of the resource. + /// + internal static string SchemaCommandDescription { + get { + return ResourceManager.GetString("SchemaCommandDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set the resource state. + /// + internal static string SetCommandDescription { + get { + return ResourceManager.GetString("SetCommandDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Test the resource state. + /// + internal static string TestCommandDescription { + get { + return ResourceManager.GetString("TestCommandDescription", resourceCulture); + } + } + } +} diff --git a/src/dsc/v3/PowerToys.DSC/Properties/Resources.resx b/src/dsc/v3/PowerToys.DSC/Properties/Resources.resx new file mode 100644 index 0000000000..2648d6501b --- /dev/null +++ b/src/dsc/v3/PowerToys.DSC/Properties/Resources.resx @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + PowerToys Desired State Configuration commands + {Locked="PowerToys Desired State Configuration"} + + + Module '{0}' is not supported for the resource {1}. Use the 'module' command to list available modules. + {Locked="'module'","{0}","{1}"} + + + Get all state instances + + + Get the resource state + + + Get the manifest of the dsc resource + + + Get all supported modules for a specific resource + + + Outputs schema of the resource + + + Set the resource state + + + Test the resource state + + + Input cannot be empty or null + + + Failed to write manifests to directory '{0}': {1} + {Locked="{0}","{1}"} + + + The JSON input + + + The module name + + + The output directory + + + The resource name + + + Invalid JSON input: {0} + {Locked="{0}"} + + + Output directory cannot be empty or null + + + Invalid output directory: {0} + {Locked="{0}"} + + + Invalid resource name. Valid resource names are: {0} + {Locked="{0}"} + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj index 255ded7abd..34e37eafb2 100644 --- a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj +++ b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj @@ -48,7 +48,6 @@ - Create @@ -66,6 +65,7 @@ + @@ -82,4 +82,7 @@ + + + \ No newline at end of file diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1 index 2229be63ae..f601146577 100644 --- a/tools/build/build-installer.ps1 +++ b/tools/build/build-installer.ps1 @@ -124,6 +124,20 @@ else { Write-Warning "[SIGN] No .msix files found in $msixSearchRoot" } +# Generate DSC manifest files +Write-Host '[DSC] Generating DSC manifest files...' +$dscScriptPath = Join-Path $repoRoot '.\tools\build\generate-dsc-manifests.ps1' +if (Test-Path $dscScriptPath) { + & $dscScriptPath -BuildPlatform $Platform -BuildConfiguration $Configuration -RepoRoot $repoRoot + if ($LASTEXITCODE -ne 0) { + Write-Error "DSC manifest generation failed with exit code $LASTEXITCODE" + exit 1 + } + Write-Host '[DSC] DSC manifest files generated successfully' +} else { + Write-Warning "[DSC] DSC manifest generator script not found at: $dscScriptPath" +} + RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' $commonArgs $Platform $Configuration RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' $commonArgs $Platform $Configuration diff --git a/tools/build/generate-dsc-manifests.ps1 b/tools/build/generate-dsc-manifests.ps1 new file mode 100644 index 0000000000..cb730ddd4a --- /dev/null +++ b/tools/build/generate-dsc-manifests.ps1 @@ -0,0 +1,116 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$BuildPlatform, + + [Parameter(Mandatory = $true)] + [string]$BuildConfiguration, + + [Parameter()] + [string]$RepoRoot = (Get-Location).Path, + + [switch]$ForceRebuildExecutable +) + +$ErrorActionPreference = 'Stop' + +function Resolve-PlatformDirectory { + param( + [string]$Root, + [string]$Platform + ) + + $normalized = $Platform.Trim() + $candidates = @() + $candidates += Join-Path $Root $normalized + $candidates += Join-Path $Root ($normalized.ToUpperInvariant()) + $candidates += Join-Path $Root ($normalized.ToLowerInvariant()) + $candidates = $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return $candidate + } + } + + return $candidates[0] +} + +Write-Host "Repo root: $RepoRoot" +Write-Host "Requested build platform: $BuildPlatform" +Write-Host "Requested configuration: $BuildConfiguration" + +# Always use x64 PowerToys.DSC.exe since CI/CD machines are x64 +$exePlatform = 'x64' +$exeRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $exePlatform +$exeOutputDir = Join-Path $exeRoot $BuildConfiguration +$exePath = Join-Path $exeOutputDir 'PowerToys.DSC.exe' + +Write-Host "Using x64 PowerToys.DSC.exe to generate DSC manifests for $BuildPlatform build" + +if ($ForceRebuildExecutable -or -not (Test-Path $exePath)) { + Write-Host "PowerToys.DSC.exe not found at '$exePath'. Building x64 binary..." + + $msbuild = Get-Command msbuild.exe -ErrorAction SilentlyContinue + if ($null -eq $msbuild) { + throw "msbuild.exe was not found on the PATH." + } + + $projectPath = Join-Path $RepoRoot 'src\dsc\v3\PowerToys.DSC\PowerToys.DSC.csproj' + $msbuildArgs = @( + $projectPath, + '/t:Build', + '/m', + "/p:Configuration=$BuildConfiguration", + "/p:Platform=x64", + '/restore' + ) + + & $msbuild.Path @msbuildArgs + $msbuildExitCode = $LASTEXITCODE + + if ($msbuildExitCode -ne 0) { + throw "msbuild build failed with exit code $msbuildExitCode" + } + + if (-not (Test-Path $exePath)) { + throw "Expected PowerToys.DSC.exe at '$exePath' after build but it was not found." + } +} else { + Write-Host "Using existing PowerToys.DSC.exe at '$exePath'." +} + +# Output DSC manifests to the target build platform directory (x64, ARM64, etc.) +$outputRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $BuildPlatform +if (-not (Test-Path $outputRoot)) { + Write-Host "Creating missing platform output root at '$outputRoot'." + New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null +} + +$outputDir = Join-Path $outputRoot $BuildConfiguration +if (-not (Test-Path $outputDir)) { + Write-Host "Creating missing configuration output directory at '$outputDir'." + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null +} + +Write-Host "DSC manifests will be generated to: '$outputDir'" + +Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'." +Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force + +$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir) +Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')" +& $exePath @arguments +if ($LASTEXITCODE -ne 0) { + throw "PowerToys.DSC.exe exited with code $LASTEXITCODE" +} + +$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop +if ($generatedFiles.Count -eq 0) { + throw "No DSC manifest files were generated in '$outputDir'." +} + +Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):" +foreach ($file in $generatedFiles) { + Write-Host " - $($file.FullName)" +}