diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index c40e78f144..bfdd04c118 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -11,11 +11,11 @@ body: options: - label: I have searched for my issue and have not found a work-in-progress/duplicate/resolved issue. required: true - - label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/marticliment/WingetUI/releases/). + - label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/Devolutions/UniGetUI/releases/). required: true - - label: I have checked the [FAQ](https://github.com/marticliment/WingetUI#frequently-asked-questions) section for solutions. + - label: I have checked the [FAQ](https://github.com/Devolutions/UniGetUI#frequently-asked-questions) section for solutions. required: true - - label: This issue is about a bug (if it is not, please use the [correct template](https://github.com/marticliment/WingetUI/issues/new/choose)). + - label: This issue is about a bug (if it is not, please use the [correct template](https://github.com/Devolutions/UniGetUI/issues/new/choose)). required: true - type: input attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bb3107571f..33af5dba3b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -11,5 +11,5 @@ contact_links: url: https://marticliment.com/contact about: Please use only if the issue (for example, a vulnerability) cannot be posted publicly - name: 💬 Having doubts or questions? - url: https://github.com/marticliment/UniGetUI/discussions/new/choose + url: https://github.com/Devolutions/UniGetUI/discussions/new/choose about: Create a discussion and get help from other members of the community diff --git a/.github/ISSUE_TEMPLATE/enhancement-improvement.yml b/.github/ISSUE_TEMPLATE/enhancement-improvement.yml index a6ac9693ff..78592816d0 100644 --- a/.github/ISSUE_TEMPLATE/enhancement-improvement.yml +++ b/.github/ISSUE_TEMPLATE/enhancement-improvement.yml @@ -11,9 +11,9 @@ body: options: - label: I have searched for my feature proposal and have not found a work-in-progress/duplicate/resolved/discarded issue. required: true - - label: This improvement refers to an existing feature. If you want to suggest a new feature, please use [this template](https://github.com/marticliment/WingetUI/issues/new?assignees=marticliment&labels=new-feature&projects=&template=feature-request.yml&title=%5BFEATURE+REQUEST%5D+%28Enter+your+description+here%29). + - label: This improvement refers to an existing feature. If you want to suggest a new feature, please use [this template](https://github.com/Devolutions/UniGetUI/issues/new?assignees=marticliment&labels=new-feature&projects=&template=feature-request.yml&title=%5BFEATURE+REQUEST%5D+%28Enter+your+description+here%29). required: true - - label: This improvement is not a bug. If you want to report a bug, please use [this template](https://github.com/marticliment/WingetUI/issues/new?assignees=marticliment&labels=bug&projects=&template=bug-issue.yml&title=%5BBUG%5D+%28Enter+your+description+here%29). + - label: This improvement is not a bug. If you want to report a bug, please use [this template](https://github.com/Devolutions/UniGetUI/issues/new?assignees=marticliment&labels=bug&projects=&template=bug-issue.yml&title=%5BBUG%5D+%28Enter+your+description+here%29). required: true - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index e36adb6080..9271ddaa56 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -11,7 +11,7 @@ body: options: - label: I have searched for my feature proposal and have not found a work-in-progress/duplicate/resolved/discarded issue. required: true - - label: This proposal is a completely new feature. If you want to suggest an improvement or an enhancement, please use [this template](https://github.com/marticliment/WingetUI/issues/new?assignees=marticliment&labels=enhancement&projects=&template=enhancement-improvement.yml&title=%5BENHANCEMENT%5D+%28Enter+your+description+here%29). + - label: This proposal is a completely new feature. If you want to suggest an improvement or an enhancement, please use [this template](https://github.com/Devolutions/UniGetUI/issues/new?assignees=marticliment&labels=enhancement&projects=&template=enhancement-improvement.yml&title=%5BENHANCEMENT%5D+%28Enter+your+description+here%29). required: true - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/hard-crash.yml b/.github/ISSUE_TEMPLATE/hard-crash.yml index 2358a0c525..f6f2fc4d0b 100644 --- a/.github/ISSUE_TEMPLATE/hard-crash.yml +++ b/.github/ISSUE_TEMPLATE/hard-crash.yml @@ -13,7 +13,7 @@ body: required: true - label: I have tried reinstalling UniGetUI. required: true - - label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/marticliment/WingetUI/releases/). + - label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/Devolutions/UniGetUI/releases/). required: true - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/widgets-issue.yml b/.github/ISSUE_TEMPLATE/widgets-issue.yml index 83b9db571d..083f7f34ad 100644 --- a/.github/ISSUE_TEMPLATE/widgets-issue.yml +++ b/.github/ISSUE_TEMPLATE/widgets-issue.yml @@ -11,11 +11,11 @@ body: options: - label: I have searched for my issue and have not found a work-in-progress/duplicate/resolved issue. required: true - - label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/marticliment/WingetUI/releases/). + - label: I have tested that this issue has not been fixed in the latest [(beta or stable) release](https://github.com/Devolutions/UniGetUI/releases/). required: true - - label: I have checked the [FAQ](https://github.com/marticliment/WingetUI#frequently-asked-questions) section for solutions. + - label: I have checked the [FAQ](https://github.com/Devolutions/UniGetUI#frequently-asked-questions) section for solutions. required: true - - label: This issue is about a bug (if it is not, please use the [correct template](https://github.com/marticliment/WingetUI/issues/new/choose)). + - label: This issue is about a bug (if it is not, please use the [correct template](https://github.com/Devolutions/UniGetUI/issues/new/choose)). required: true - type: input attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 776c21156e..974c236845 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ -- [ ] **I have read the [contributing guidelines](https://github.com/marticliment/WingetUI/blob/main/CONTRIBUTING.md#coding), and I agree with the [Code of Conduct](https://github.com/marticliment/WingetUI/blob/main/CODE_OF_CONDUCT.md)**. -- [ ] **Have you checked that there aren't other open [pull requests](https://github.com/marticliment/WingetUI/pulls) for the same changes?** +- [ ] **I have read the [contributing guidelines](https://github.com/Devolutions/UniGetUI/blob/main/CONTRIBUTING.md#coding), and I agree with the [Code of Conduct](https://github.com/Devolutions/UniGetUI/blob/main/CODE_OF_CONDUCT.md)**. +- [ ] **Have you checked that there aren't other open [pull requests](https://github.com/Devolutions/UniGetUI/pulls) for the same changes?** - [ ] **Have you tested that the committed code can be executed without errors?** - [ ] **This PR is not composed of garbage changes used to farm GitHub activity to enter potential Crypto AirDrops.** Any user suspected of farming GitHub activity with crypto purposes will get banned. Submitting broken code wastes the contributors' time, who have to spend their free time reviewing, fixing, and testing code that does not even compile breaks other features, or does not introduce any useful changes. I appreciate your understanding. diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000000..b03b66a1d0 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,354 @@ +name: Build and Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 3.3.7)' + default: "latest" + required: true + draft-release: + description: 'Create the GitHub Release as a draft' + required: true + type: boolean + default: false + skip-publish: + description: 'Skip publishing to GitHub Releases' + required: true + type: boolean + default: false + dry-run: + description: 'Dry run (simulate without publishing)' + required: true + type: boolean + default: true + +jobs: + preflight: + name: Preflight + runs-on: ubuntu-latest + outputs: + package-env: ${{ steps.info.outputs.package-env }} + package-version: ${{ steps.info.outputs.package-version }} + draft-release: ${{ steps.info.outputs.draft-release }} + skip-publish: ${{ steps.info.outputs.skip-publish }} + dry-run: ${{ steps.info.outputs.dry-run }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve build parameters + id: info + shell: pwsh + run: | + $IsProductionBranch = @('main', 'master') -contains '${{ github.ref_name }}' + + try { $DraftRelease = [System.Boolean]::Parse('${{ inputs.draft-release }}') } catch { $DraftRelease = $false } + try { $SkipPublish = [System.Boolean]::Parse('${{ inputs.skip-publish }}') } catch { $SkipPublish = $false } + try { $DryRun = [System.Boolean]::Parse('${{ inputs.dry-run }}') } catch { $DryRun = $true } + + $PackageEnv = if ($IsProductionBranch) { + "publish-prod" + } else { + "publish-test" + } + + if (-Not $IsProductionBranch) { + $DryRun = $true # force dry run when not on main/master branch + } + + if (-Not $SkipPublish -And $PackageEnv -ne 'publish-prod') { + $DryRun = $true # force dry run when publishing outside production environment + } + + $PackageVersion = '${{ inputs.version }}' + if ([string]::IsNullOrEmpty($PackageVersion) -or $PackageVersion -eq 'latest') { + # Read version from SharedAssemblyInfo.cs + $match = Select-String -Path 'src/SharedAssemblyInfo.cs' -Pattern 'AssemblyInformationalVersion\("([^"]+)"\)' + if ($match) { + $PackageVersion = $match.Matches[0].Groups[1].Value + } else { + $PackageVersion = (Get-Date -Format "yyyy.M.d") + ".0" + } + } + + echo "package-env=$PackageEnv" >> $Env:GITHUB_OUTPUT + echo "package-version=$PackageVersion" >> $Env:GITHUB_OUTPUT + echo "draft-release=$($DraftRelease.ToString().ToLower())" >> $Env:GITHUB_OUTPUT + echo "skip-publish=$($SkipPublish.ToString().ToLower())" >> $Env:GITHUB_OUTPUT + echo "dry-run=$($DryRun.ToString().ToLower())" >> $Env:GITHUB_OUTPUT + + echo "::notice::Environment: $PackageEnv" + echo "::notice::Version: $PackageVersion" + echo "::notice::DraftRelease: $DraftRelease" + echo "::notice::DryRun: $DryRun" + + build: + name: Build & Sign (${{ matrix.platform }}) + runs-on: windows-latest + needs: [preflight] + environment: ${{ needs.preflight.outputs.package-env }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + platform: [x64, arm64] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Windows SDK UAP platform + shell: pwsh + run: | + # CsWinRT in WindowsPackageManager.Interop requires UAP 10.0.19041.0 platform metadata + $VsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $VsInstaller = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe" + $InstallPath = & $VsWhere -latest -property installationPath + & $VsInstaller modify --installPath $InstallPath ` + --add Microsoft.VisualStudio.Component.UWP.Support ` + --add Microsoft.VisualStudio.Component.Windows10SDK.19041 ` + --quiet --norestart --nocache | Out-Default + Write-Host "Windows SDK UAP platform installed" + + - name: Install .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.0.x + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install Inno Setup + shell: pwsh + run: | + choco install innosetup -y --no-progress + echo "C:\Program Files (x86)\Inno Setup 6" >> $Env:GITHUB_PATH + + - name: Install code-signing tools + shell: pwsh + run: | + dotnet tool install --global AzureSignTool + Install-Module -Name Devolutions.Authenticode -Force + + # Trust test code-signing CA + $TestCertsUrl = "https://raw.githubusercontent.com/Devolutions/devolutions-authenticode/master/data/certs" + Invoke-WebRequest -Uri "$TestCertsUrl/authenticode-test-ca.crt" -OutFile ".\authenticode-test-ca.crt" + Import-Certificate -FilePath ".\authenticode-test-ca.crt" -CertStoreLocation "cert:\LocalMachine\Root" + Remove-Item ".\authenticode-test-ca.crt" -ErrorAction SilentlyContinue | Out-Null + + - name: Set version + shell: pwsh + run: | + $PackageVersion = '${{ needs.preflight.outputs.package-version }}' + .\scripts\set-version.ps1 -Version $PackageVersion + + - name: Fetch WinGet CLI bundle + shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + $Platform = '${{ matrix.platform }}' + .\scripts\fetch-winget-cli.ps1 -Architectures @($Platform) -Force + + - name: Restore dependencies + working-directory: src + run: dotnet restore + + - name: Run tests + working-directory: src + shell: pwsh + run: | + # Retry once to handle flaky tests (e.g. TaskRecyclerTests uses Random) + dotnet test --no-restore --verbosity q --nologo + if ($LASTEXITCODE -ne 0) { + Write-Host "::warning::First test run failed, retrying..." + dotnet test --no-restore --verbosity q --nologo + if ($LASTEXITCODE -ne 0) { exit 1 } + } + + - name: Publish + shell: pwsh + run: | + $Platform = '${{ matrix.platform }}' + dotnet publish src/UniGetUI/UniGetUI.csproj /noLogo /p:Configuration=Release /p:Platform=$Platform -v m + if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed" } + + # Stage binaries + $PublishDir = "src/UniGetUI/bin/$Platform/Release/net8.0-windows10.0.26100.0/win-$Platform/publish" + if (Test-Path "unigetui_bin") { Remove-Item "unigetui_bin" -Recurse -Force } + New-Item "unigetui_bin" -ItemType Directory | Out-Null + Get-ChildItem $PublishDir | Move-Item -Destination "unigetui_bin" -Force + + # Backward-compat alias + Copy-Item "unigetui_bin/UniGetUI.exe" "unigetui_bin/WingetUI.exe" -Force + + - name: Code-sign binaries + shell: pwsh + run: | + $ListPath = Join-Path $PWD "signing-files.txt" + $files = Get-ChildItem "unigetui_bin" -Recurse -Include "*.exe", "*.dll" | Where-Object { + (Get-AuthenticodeSignature $_.FullName).Status -eq "NotSigned" + } + $files.FullName | Set-Content $ListPath + Write-Host "Signing list contains $($files.Count) files." + + .\scripts\sign.ps1 ` + -FileListPath $ListPath ` + -AzureTenantId '${{ secrets.AZURE_TENANT_ID }}' ` + -KeyVaultUrl '${{ secrets.CODE_SIGNING_KEYVAULT_URL }}' ` + -ClientId '${{ secrets.CODE_SIGNING_CLIENT_ID }}' ` + -ClientSecret '${{ secrets.CODE_SIGNING_CLIENT_SECRET }}' ` + -CertificateName '${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}' ` + -TimestampServer '${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}' + + - name: Generate integrity tree + shell: pwsh + run: .\scripts\generate-integrity-tree.ps1 -Path $PWD/unigetui_bin -MinOutput + + - name: Build installer + shell: pwsh + run: | + $Platform = '${{ matrix.platform }}' + $OutputDir = Join-Path $PWD "output" + New-Item $OutputDir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + + # Configure Inno Setup to use AzureSignTool + $IssPath = "UniGetUI.iss" + + # Build the installer (signing of the installer itself happens in the next step) + # Temporarily remove SignTool line so ISCC doesn't try to sign during build + $issContent = Get-Content $IssPath -Raw + $issContentNoSign = $issContent -Replace '(?m)^SignTool=.*$', '; SignTool=azsign (disabled for CI, signed separately)' + $issContentNoSign = $issContentNoSign -Replace '(?m)^SignedUninstaller=yes', 'SignedUninstaller=no' + Set-Content $IssPath $issContentNoSign -NoNewline + + $InstallerBaseName = "UniGetUI.Installer.$Platform" + & ISCC.exe $IssPath /F"$InstallerBaseName" /O"$OutputDir" + if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed with exit code $LASTEXITCODE" } + + # Restore original ISS content + Set-Content $IssPath $issContent -NoNewline + + - name: Stage output + shell: pwsh + run: | + $Platform = '${{ matrix.platform }}' + New-Item "output" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + + # Zip + Compress-Archive -Path "unigetui_bin/*" -DestinationPath "output/UniGetUI.$Platform.zip" -CompressionLevel Optimal + + # Installer is created in output during the previous step + + - name: Code-sign installer + shell: pwsh + run: | + $Platform = '${{ matrix.platform }}' + .\scripts\sign.ps1 ` + -InstallerPath "output/UniGetUI.Installer.$Platform.exe" ` + -AzureTenantId '${{ secrets.AZURE_TENANT_ID }}' ` + -KeyVaultUrl '${{ secrets.CODE_SIGNING_KEYVAULT_URL }}' ` + -ClientId '${{ secrets.CODE_SIGNING_CLIENT_ID }}' ` + -ClientSecret '${{ secrets.CODE_SIGNING_CLIENT_SECRET }}' ` + -CertificateName '${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}' ` + -TimestampServer '${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}' + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: UniGetUI-release-${{ matrix.platform }} + path: output/* + + - name: Cleanup + if: always() + shell: pwsh + run: | + Remove-Item "unigetui_bin" -Recurse -Force -ErrorAction SilentlyContinue + + publish: + name: Publish GitHub Release + runs-on: ubuntu-latest + needs: [preflight, build] + if: ${{ fromJSON(needs.preflight.outputs.skip-publish) == false }} + environment: ${{ needs.preflight.outputs.package-env }} + permissions: + contents: write + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: output + + - name: Add legacy installer filename + shell: pwsh + working-directory: output + run: | + $InstallerFiles = Get-ChildItem -Path . -Recurse -File -Filter "UniGetUI.Installer.x64.exe" + if (-not $InstallerFiles) { + throw "Could not find UniGetUI.Installer.x64.exe in downloaded artifacts" + } + + $InstallerFiles | ForEach-Object { + $LegacyInstallerPath = Join-Path $_.DirectoryName "UniGetUI.Installer.exe" + Copy-Item -Path $_.FullName -Destination $LegacyInstallerPath -Force + Write-Host "Created legacy installer alias: $LegacyInstallerPath" + } + + - name: Generate consolidated checksums + shell: pwsh + working-directory: output + run: | + $ChecksumFile = Join-Path $PWD "checksums.txt" + $ChecksumLines = Get-ChildItem -Path . -Recurse -File | Where-Object { + $_.Name -notmatch '^checksums(\..+)?\.txt$' + } | Sort-Object Name | ForEach-Object { + $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash + "$hash $($_.Name)" + } + + Set-Content -Path $ChecksumFile -Value $ChecksumLines -Encoding utf8NoBOM + + echo "::group::checksums" + Get-Content $ChecksumFile + echo "::endgroup::" + + - name: Create GitHub Release + shell: pwsh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: output + run: | + $PackageVersion = '${{ needs.preflight.outputs.package-version }}' + $DraftRelease = [System.Boolean]::Parse('${{ needs.preflight.outputs.draft-release }}') + $DryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry-run }}') + + echo "::group::checksums" + Get-Content "./checksums.txt" + echo "::endgroup::" + + $ReleaseTag = "v$PackageVersion" + $ReleaseTitle = "UniGetUI v${PackageVersion}" + $Repository = $Env:GITHUB_REPOSITORY + $DraftArg = if ($DraftRelease) { '--draft' } else { $null } + + $Files = Get-ChildItem -Path . -Recurse -File | Where-Object { + $_.Name -eq 'checksums.txt' -or $_.Name -notmatch '^checksums\..+\.txt$' + } + + if ($DryRun) { + Write-Host "Dry Run: skipping GitHub release creation!" + Write-Host "Would create release $ReleaseTag with title '$ReleaseTitle' (draft=$DraftRelease)" + $Files | ForEach-Object { Write-Host " - $($_.FullName)" } + } else { + if ($DraftArg) { + & gh release create $ReleaseTag --repo $Repository --title $ReleaseTitle $DraftArg $Files.FullName + } else { + & gh release create $ReleaseTag --repo $Repository --title $ReleaseTitle $Files.FullName + } + } diff --git a/.gitignore b/.gitignore index d128df2c9c..8cab82eb78 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ src/ExternalLibraries.*/obj src/ExternalLibraries.*/out src/ExternalLibraries.*/outpublish +src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_*/ + src/.vs src/.vscode new-names.docx diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..300450b028 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,91 @@ +# UniGetUI ΓÇô Copilot Instructions + +## Project Overview + +UniGetUI is a WinUI 3 desktop app (C#/.NET 8, Windows App SDK) providing a GUI for CLI package managers (WinGet, Scoop, Chocolatey, Pip, Npm, .NET Tool, PowerShell Gallery, Cargo, Vcpkg). Solution lives in `src/UniGetUI.sln`. + +## Architecture + +The codebase follows a **layered, modular structure** with ~40 projects: + +- **`UniGetUI/`** ΓÇô WinUI 3 entry point, XAML pages, controls, and app shell (`EntryPoint.cs`, `MainWindow.xaml`) +- **`UniGetUI.Core.*`** ΓÇô Shared infrastructure: `Logger`, `Settings`, `Tools` (includes `CoreTools.Translate()`), `IconEngine`, `LanguageEngine` +- **`UniGetUI.PackageEngine.Interfaces`** ΓÇô Contracts: `IPackageManager`, `IPackage`, `IManagerSource`, `IPackageDetails` +- **`UniGetUI.PackageEngine.PackageManagerClasses`** ΓÇô Base implementations: `PackageManager` (abstract), `Package`, helpers (`BasePkgDetailsHelper`, `BasePkgOperationHelper`, `BaseSourceHelper`) +- **`UniGetUI.PackageEngine.Managers.*`** ΓÇô Concrete manager implementations (one project per manager: `WinGet`, `Scoop`, `Chocolatey`, `Pip`, `Npm`, etc.) +- **`UniGetUI.PackageEngine.Operations`** ΓÇô Install/update/uninstall operation orchestration +- **`UniGetUI.Interface.*`** ΓÇô Enums, telemetry, background API + +## Adding a New Package Manager + +Each manager extends `PackageManager` and must override three abstract methods: + +```csharp +protected override IReadOnlyList FindPackages_UnSafe(string query); +protected override IReadOnlyList GetAvailableUpdates_UnSafe(); +protected override IReadOnlyList GetInstalledPackages_UnSafe(); +``` + +Each manager also provides three helper classes (in a `Helpers/` subfolder): +- `*PkgDetailsHelper` extends `BasePkgDetailsHelper` ΓÇô overrides `GetDetails_UnSafe`, `GetInstallableVersions_UnSafe`, `GetIcon_UnSafe`, etc. +- `*PkgOperationHelper` extends `BasePkgOperationHelper` ΓÇô overrides `_getOperationParameters`, `_getOperationResult` +- `*SourceHelper` extends `BaseSourceHelper` ΓÇô overrides `GetSources_UnSafe`, `GetAddSourceParameters`, etc. + +The constructor sets `Capabilities`, `Properties`, and wires the helpers. See `src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs` as a clean reference implementation. + +## Build & Test + +```shell +# Restore & test (from src/) +dotnet restore +dotnet test --verbosity q --nologo + +# Publish release build +dotnet publish src/UniGetUI/UniGetUI.csproj /p:Configuration=Release /p:Platform=x64 + +# Full release (runs version script, tests, publish, installer) +build_release.cmd +``` + +- Target framework: `net8.0-windows10.0.26100.0` (min `10.0.19041`) +- Build generates secrets via `src/UniGetUI/Services/generate-secrets.ps1` and integrity tree via `scripts/generate_integrity_tree.py` +- Self-contained, publish-trimmed (partial), Windows App SDK self-contained +- Tests use **xUnit** (`[Fact]`, `Assert.*`) + +## Key Patterns & Conventions + +### Settings +File-based settings via `Settings.Get(Settings.K.*)` / `Settings.Set(Settings.K.*, value)` and `Settings.GetValue(Settings.K.*)` / `Settings.SetValue(Settings.K.*, value)`. Setting keys are defined in the `Settings.K` enum in `SettingsEngine_Names.cs`. Boolean settings are stored as file existence; string settings as file content. + +### Logging +Use `Logger.Info()`, `Logger.Warn()`, `Logger.Error()`, `Logger.Debug()`, `Logger.ImportantInfo()` from `UniGetUI.Core.Logging`. Accepts both `string` and `Exception` parameters. + +### Localization +Use `CoreTools.Translate("text")` for all user-facing strings. Parameterized: `CoreTools.Translate("{0} packages found", count)`. In XAML, use the `TranslatedTextBlock` control. Translation files are managed externally via Tolgee; Python scripts in `scripts/` handle download and verification. + +### Naming +- Types, methods, properties: **PascalCase** +- Private fields: `__doubleUnderscore` or `_singleUnderscore` prefix +- Internal unsafe methods: suffix `_UnSafe` (e.g., `FindPackages_UnSafe`) +- Nullable enabled globally; `LangVersion` is `latest` +- Code style enforced in build (`EnforceCodeStyleInBuild=true`) + +### Manager conventions +- `FALSE_PACKAGE_NAMES`, `FALSE_PACKAGE_IDS`, `FALSE_PACKAGE_VERSIONS` static arrays filter CLI parsing noise +- Manager initialization flows through `Initialize()` ΓåÆ `_loadManagerExecutableFile()` ΓåÆ `_loadManagerVersion()` ΓåÆ `_performExtraLoadingSteps()` +- Operations that may fail return `OperationVeredict` (note: intentional misspelling used throughout codebase) + +## Key Files + +| Purpose | Path | +|---|---| +| Solution | `src/UniGetUI.sln` | +| Shared build props | `src/Directory.Build.props` | +| Version info | `src/SharedAssemblyInfo.cs` | +| Manager interface | `src/UniGetUI.PAckageEngine.Interfaces/IPackageManager.cs` | +| Base manager class | `src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs` | +| Package class | `src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs` | +| Settings engine | `src/UniGetUI.Core.Settings/SettingsEngine.cs` | +| Setting keys | `src/UniGetUI.Core.Settings/SettingsEngine_Names.cs` | +| Logger | `src/UniGetUI.Core.Logger/Logger.cs` | +| CI test workflow | `.github/workflows/dotnet-test.yml` | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2174dd8490..4b907cc984 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ This repository **respects** people, regardless of their race, gender, religion, Before reading: All of the rules below are guidelines, which means that they should be followed when possible. Please do not take them literally. ## Discussions: - - This is the place to post any questions/doubts regarding UniGetUI. Issues and feature requests should be posted in the [issues section](https://github.com/marticliment/UniGetUI/issues). + - This is the place to post any questions/doubts regarding UniGetUI. Issues and feature requests should be posted in the [issues section](https://github.com/Devolutions/UniGetUI/issues). ## Issues and feature requests: diff --git a/InstallerExtras/MsiCreator/README.md b/InstallerExtras/MsiCreator/README.md index 6efa64c8c3..138e0407ff 100644 --- a/InstallerExtras/MsiCreator/README.md +++ b/InstallerExtras/MsiCreator/README.md @@ -12,9 +12,9 @@ In order to obtain a .msi installer, the following guide must be followed. 1. Install [Visual Studio 2022](https://visualstudio.microsoft.com/es/downloads/) and the [Microsoft Visual Studio Installer Projects 2022 extension](https://marketplace.visualstudio.com/items?itemName=VisualStudioClient.MicrosoftVisualStudio2022InstallerProjects). 2. Clone this repository: ``` -git clone https://github.com/marticliment/UniGetUI +git clone https://github.com/Devolutions/UniGetUI ``` -3. Download from [GitHub](https://github.com/marticliment/UniGetUI/releases/) the installer version you wish to package as MSI. +3. Download from [GitHub](https://github.com/Devolutions/UniGetUI/releases/) the installer version you wish to package as MSI. 4. Move the downloaded exe installer into `InstallerExtras/MsiCreator`. Ensure that the downloaded installer name is **exactly** `UniGetUI Installer.exe` 5. Open the Solution (`MsiInstallerWrapper.sln`) with Visual Studio and build the solution. The files `UniGetUISetup.msi` and `setup.exe` will be created. You may want to delete `setup.exe`, since it will not be used. 6. (Optional) Test that the file `UniGetUISetup.msi` installs UniGetUI properly. You should be now ready to go diff --git a/README.md b/README.md index 6d3220709c..add744b335 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,56 @@ # WARNING: **wingetuicom** and **unigetuicom** are fake websites hosted by a third-party. please do NOT trust them
-## UniGetUI (formerly WingetUI) +## Devolutions UniGetUI -[![Downloads@latest](https://img.shields.io/github/downloads/marticliment/UniGetUI/3.2.0/total?style=for-the-badge)](https://github.com/marticliment/UniGetUI/releases/latest/download/UniGetUI.Installer.exe) -[![Release Version Badge](https://img.shields.io/github/v/release/marticliment/UniGetUI?style=for-the-badge)](https://github.com/marticliment/UniGetUI/releases) -[![Issues Badge](https://img.shields.io/github/issues/marticliment/UniGetUI?style=for-the-badge)](https://github.com/marticliment/UniGetUI/issues) -[![Closed Issues Badge](https://img.shields.io/github/issues-closed/marticliment/UniGetUI?color=%238256d0&style=for-the-badge)](https://github.com/marticliment/UniGetUI/issues?q=is%3Aissue+is%3Aclosed)
-The main goal of this project is to create an intuitive GUI for the most common CLI package managers for Windows 10 and 11, such as [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/), [Pip](https://pypi.org/), [Npm](https://www.npmjs.com/), [.NET Tool](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install), [PowerShell Gallery](https://www.powershellgallery.com/) and more (Check out the package manager compatibility table)!. -With this app, you can easily download, install, update, and uninstall any software published on the supported package managers — and much more! +> [!IMPORTANT] +> **Major announcement:** UniGetUI has entered its next chapter with Devolutions. +> Read the [blog post](https://devolutions.net/blog/2026/03/unigetui-enters-its-next-chapter-with-devolutions/) and the [official press release](https://www.globenewswire.com/news-release/2026/03/10/3253012/0/en/Devolutions-Acquires-UniGetUI-Strengthening-Security-and-Enterprise-Readiness.html). + +[![Downloads](https://img.shields.io/github/downloads/Devolutions/UniGetUI/total?style=for-the-badge)](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.Installer.exe) +[![Release Version Badge](https://img.shields.io/github/v/release/Devolutions/UniGetUI?style=for-the-badge)](https://github.com/Devolutions/UniGetUI/releases) +[![Issues Badge](https://img.shields.io/github/issues/Devolutions/UniGetUI?style=for-the-badge)](https://github.com/Devolutions/UniGetUI/issues) +[![Closed Issues Badge](https://img.shields.io/github/issues-closed/Devolutions/UniGetUI?color=%238256d0&style=for-the-badge)](https://github.com/Devolutions/UniGetUI/issues?q=is%3Aissue+is%3Aclosed)
+UniGetUI is an intuitive GUI for the most common CLI package managers on Windows 10 and 11, including [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/), [Pip](https://pypi.org/), [Npm](https://www.npmjs.com/), [.NET Tool](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install), [PowerShell Gallery](https://www.powershellgallery.com/), and more. +With UniGetUI, you can discover, install, update, and uninstall software from multiple package managers through one interface. ![image](https://github.com/user-attachments/assets/7cb447ca-ee8b-4bce-8561-b9332fb0139a) View more screenshots [here](#screenshots) Check out the [Supported Package Managers Table](#supported-package-managers) for more details! -**Disclaimer:** This project has no connection with any supported package managers — it's completely unofficial. Be aware that I, the developer of UniGetUI, am NOT responsible for the downloaded software. Proceed with caution +**Disclaimer:** UniGetUI is not affiliated with the package managers it integrates with. Packages are provided by third parties, so review sources and publishers before installation. -![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/marticliment/WingetUI/dotnet-test.yml?branch=main&style=for-the-badge&label=Tests)
+![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Devolutions/UniGetUI/dotnet-test.yml?branch=main&style=for-the-badge&label=Tests)
> [!CAUTION] -> **The OFFICIAL website for UniGetUI is [https://www.marticliment.com/unigetui/](https://www.marticliment.com/unigetui/)**
+> **The official website for UniGetUI is [https://devolutions.net/unigetui/](https://devolutions.net/unigetui/).**
+> **The official source repository is [https://github.com/Devolutions/UniGetUI](https://github.com/Devolutions/UniGetUI).**
> **Any other website should be considered unofficial, despite what they may say.** 🔒 Found a security issue? Please report it via [our disclosure program](https://whitehub.net/programs/unigetui/) -## Support the developer +## Project stewardship + +UniGetUI was created by Martí Climent and is now maintained by Devolutions. The project remains free, open source, and MIT-licensed. Devolutions' stewardship brings long-term investment, structured governance, stronger security processes, and a roadmap for broader enterprise readiness while keeping UniGetUI standalone and community-driven. -It really does make a big difference, and is very much appreciated. Thanks :)
-[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/P5P86KKPB) +Read more in the [Devolutions announcement](https://devolutions.net/blog/2026/03/unigetui-enters-its-next-chapter-with-devolutions/) and the [official press release](https://www.globenewswire.com/news-release/2026/03/10/3253012/0/en/Devolutions-Acquires-UniGetUI-Strengthening-Security-and-Enterprise-Readiness.html). ## Table of contents - - **[UniGetUI Homepage](https://www.marticliment.com/unigetui/)** + - **[UniGetUI Homepage](https://devolutions.net/unigetui/)** - [Table of contents](#table-of-contents) - [Installation](#installation) - - [Update UniGetUI](#update-UniGetUI) - - [Support the developer](#support-the-developer) + - [Update UniGetUI](#update-unigetui) + - [Project stewardship](#project-stewardship) - [Features](#features) - [Supported Package Managers](#supported-package-managers) - - [Translating UniGetUI](#translating-UniGetUI-to-other-languages) + - [Translating UniGetUI](#translating-unigetui-to-other-languages) - [Currently supported languages](#currently-supported-languages) - [Contributors](#contributors) - [Screenshots](#screenshots) - [Frequently Asked Questions](#frequently-asked-questions) - - [Command-line Arguments](https://github.com/marticliment/UniGetUI/blob/main/cli-arguments.md) + - [Command-line Arguments](cli-arguments.md) Featured|HelloGitHub @@ -57,8 +63,8 @@ It really does make a big difference, and is very much appreciated. Thanks :)
Click here to download UniGetUI

+![GitHub Release](https://img.shields.io/github/v/release/Devolutions/UniGetUI?style=for-the-badge) +

Click here to download UniGetUI

### Install UniGetUI via WinGet: @@ -97,7 +103,7 @@ UniGetUI has a built-in autoupdater. However, it can also be updated like any ot - Manage your available updates at the touch of a button from the **Widgets pane** or from **Dev Home** pane with [Widgets for UniGetUI](https://apps.microsoft.com/detail/9NB9M5KZ8SLX)*. - The system tray icon will also show the available updates and installed packages, to efficiently update a program or remove a package from your system. - Easily customize how and where packages are installed. Select different installation options and switches for each package. Install an older version or force to install a 32 bit architecture. \[But don't worry, those options will be saved for future updates for this package*] - - Share packages with your friends to show them off that program you found. Here is an example: [Hey \@friend, Check out this program!](https://marticliment.com/unigetui/share/?pname=Google%20Chrome&pid=Google.Chrome&psource=Winget:%20winget) + - Share packages with your friends using generated package links. - Export custom lists of packages to then import them to another machine and install those packages with previously specified, custom installation parameters. Setting up machines or configuring a specific software setup has never been easier. - Backup your packages to a local file to easily recover your setup in a matter of seconds when migrating to a new machine* @@ -105,7 +111,7 @@ UniGetUI has a built-in autoupdater. However, it can also be updated like any ot **NOTE:** All package managers do support basic install, update, and uninstall processes, as well as checking for updates, finding new packages, and retrieving details from a package. - + ✅: Supported on UniGetUI
☑️: Not directly supported but can be easily achieved
@@ -114,7 +120,7 @@ UniGetUI has a built-in autoupdater. However, it can also be updated like any ot
# Translating UniGetUI to other languages -To translate UniGetUI to other languages or to update an old translation, please see [Translating UniGetUI - UniGetUI Wiki](https://github.com/marticliment/UniGetUI/wiki#translating-wingetui) for more info. +To translate UniGetUI to other languages or to update an old translation, please see [Translating UniGetUI - UniGetUI Wiki](https://github.com/Devolutions/UniGetUI/wiki#translating-wingetui) for more info. ## Currently Supported languages @@ -184,30 +190,30 @@ Last updated: Fri Feb 27 00:46:32 2026 UniGetUI wouldn't have been possible without the help of our dear contributors. From the person who fixed a typo to the person who improved half of the code, UniGetUI wouldn't be possible without them! :smile:

## Contributors: - [![My dear contributors](https://contrib.rocks/image?repo=marticliment/UniGetUI)](https://github.com/marticliment/UniGetUI/graphs/contributors)

+ [![My dear contributors](https://contrib.rocks/image?repo=Devolutions/UniGetUI)](https://github.com/Devolutions/UniGetUI/graphs/contributors)

# Screenshots -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_1.png) +![image](media/UniGetUI_1.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_2.png) +![image](media/UniGetUI_2.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_3.png) +![image](media/UniGetUI_3.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_4.png) +![image](media/UniGetUI_4.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_5.png) +![image](media/UniGetUI_5.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_6.png) +![image](media/UniGetUI_6.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_7.png) +![image](media/UniGetUI_7.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_8.png) +![image](media/UniGetUI_8.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_9.png) +![image](media/UniGetUI_9.png) -![image](https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_10.png) +![image](media/UniGetUI_10.png) # Frequently asked questions @@ -246,8 +252,8 @@ A: UniGetUI, Microsoft, and Scoop aren't responsible for the packages available Microsoft has implemented a few checks for the software available on Winget to mitigate the risks of downloading malware. Even so, it's recommended that you only download software from trusted publishers. -

Check out the Wiki for more information!

+

Check out the Wiki for more information!

## Command-line parameters: -Check out the full list of parameters [here](https://github.com/marticliment/UniGetUI/blob/main/cli-arguments.md) +Check out the full list of parameters [here](cli-arguments.md) diff --git a/UniGetUI.iss b/UniGetUI.iss index 07ce8ee53c..9ba6d22a09 100644 --- a/UniGetUI.iss +++ b/UniGetUI.iss @@ -3,8 +3,8 @@ #define MyAppVersion "3.3.7" #define MyAppName "UniGetUI" -#define MyAppPublisher "Martí Climent" -#define MyAppURL "https://github.com/marticliment/UniGetUI" +#define MyAppPublisher "Devolutions Inc." +#define MyAppURL "https://github.com/Devolutions/UniGetUI" #define MyAppExeName "UniGetUI.exe" #define public Dependency_Path_NetCoreCheck "InstallerExtras\" @@ -20,10 +20,11 @@ AppName={#MyAppName} AppVersion={#MyAppVersion} AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} -AppPublisherURL="https://www.marticliment.com/unigetui/" +AppPublisherURL="https://devolutions.net/unigetui/" AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} VersionInfoVersion=3.3.7.0 +VersionInfoProductVersion=3.3.7.0 DefaultDirName="{autopf64}\UniGetUI" DisableProgramGroupPage=yes DisableDirPage=no diff --git a/WebBasedData/screenshot-database-v2.json b/WebBasedData/screenshot-database-v2.json index 4e5a550120..8e37cc187b 100644 --- a/WebBasedData/screenshot-database-v2.json +++ b/WebBasedData/screenshot-database-v2.json @@ -44678,11 +44678,11 @@ "images": [] }, "unigetui": { - "icon": "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/icon.png", + "icon": "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/icon.png", "images": [ - "http://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_1.png", - "http://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_3.png", - "http://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_6.png" + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_1.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_3.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_6.png" ] }, "unihex": { @@ -49337,33 +49337,33 @@ ] }, "wingetui": { - "icon": "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/icon.png", + "icon": "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/icon.png", "images": [ - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_1.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_2.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_3.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_4.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_5.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_6.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_7.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_8.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_9.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_10.png" + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_1.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_2.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_3.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_4.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_5.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_6.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_7.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_8.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_9.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_10.png" ] }, "wingetuistore": { - "icon": "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/icon.png", + "icon": "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/icon.png", "images": [ - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_1.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_2.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_3.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_4.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_5.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_6.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_7.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_8.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_9.png", - "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_10.png" + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_1.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_2.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_3.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_4.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_5.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_6.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_7.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_8.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_9.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_10.png" ] }, "winginx": { @@ -50804,11 +50804,11 @@ "images": [] }, "XPFFTQ032PTPHF": { - "icon": "https://raw.githubusercontent.com/marticliment/UniGetUI/main/media/icon.png", + "icon": "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/icon.png", "images": [ - "http://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_1.png", - "http://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_3.png", - "http://raw.githubusercontent.com/marticliment/UniGetUI/main/media/UniGetUI_6.png" + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_1.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_3.png", + "https://raw.githubusercontent.com/Devolutions/UniGetUI/main/media/UniGetUI_6.png" ] }, "xpipe": { diff --git a/cli-arguments.md b/cli-arguments.md index 69928a0dd4..80cf27283d 100644 --- a/cli-arguments.md +++ b/cli-arguments.md @@ -18,8 +18,8 @@ | `--[enable\|disable]-secure-setting-for-user username key` | Enables/disables the given secure setting for the given key2 and username. Requires administrator rights. | 3.2.1+ | | `--[enable\|disable]-secure-setting key` | Enables/disables the given secure setting2 for current user. This will generate a UAC prompt | 3.2.1+ | -1. See the available list of setting keys [here](https://github.com/marticliment/UniGetUI/blob/fc98f312a72b80e14a8ac10687f4fc506a5c9cc4/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs#L5) -2. See the available list of secure settings keys [here](https://github.com/marticliment/UniGetUI/blob/fc98f312a72b80e14a8ac10687f4fc506a5c9cc4/src/UniGetUI.Core.SecureSettings/SecureSettings.cs#L10) +1. See the available list of setting keys [here](https://github.com/Devolutions/UniGetUI/blob/fc98f312a72b80e14a8ac10687f4fc506a5c9cc4/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs#L5) +2. See the available list of secure settings keys [here](https://github.com/Devolutions/UniGetUI/blob/fc98f312a72b80e14a8ac10687f4fc506a5c9cc4/src/UniGetUI.Core.SecureSettings/SecureSettings.cs#L10) \*After modifying the settings, you must ensure that any running instance of UniGetUI is restarted for the changes to take effect diff --git a/scripts/apply_versions.py b/scripts/apply_versions.py index 9ab530a8ab..faf4955e35 100644 --- a/scripts/apply_versions.py +++ b/scripts/apply_versions.py @@ -62,6 +62,7 @@ def fileReplaceLinesWith(filename: str, list: dict[str, str], encoding="utf-8"): fileReplaceLinesWith("UniGetUI.iss", { "#define MyAppVersion": f" \"{versionName}\"\n", "VersionInfoVersion=": f"{versionISS}\n", + "VersionInfoProductVersion=": f"{versionISS}\n", }, encoding="utf-8-sig") IS_BETA = (input("Is this a beta release? [y/N]: ").lower().strip() == "y") diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000000..a81c080053 --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,136 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Builds UniGetUI, produces the published output, and packages artifacts. + +.PARAMETER Configuration + Build configuration (Debug or Release). Default: Release. + +.PARAMETER Platform + Target platform. Default: x64. + +.PARAMETER OutputPath + Directory for final packaged artifacts (zip, installer). Default: ./output + +.PARAMETER SkipTests + Skip running dotnet test before build. + +.PARAMETER SkipInstaller + Skip building the Inno Setup installer. + +.PARAMETER Version + Version string to stamp into the build (e.g. "3.3.7"). If not provided, + the current version from SharedAssemblyInfo.cs is used. +#> + +[CmdletBinding()] +param( + [string] $Configuration = "Release", + [string] $Platform = "x64", + [string] $OutputPath = (Join-Path $PSScriptRoot ".." "output"), + [switch] $SkipTests, + [switch] $SkipInstaller, + [string] $Version +) + +$ErrorActionPreference = 'Stop' + +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$SrcDir = Join-Path $RepoRoot "src" +$PublishProject = Join-Path $SrcDir "UniGetUI" "UniGetUI.csproj" +$BinDir = Join-Path $RepoRoot "unigetui_bin" +$TargetFramework = "net8.0-windows10.0.26100.0" +$PublishDir = Join-Path $SrcDir "UniGetUI" "bin" $Platform $Configuration $TargetFramework "win-$Platform" "publish" + +# --- Version stamping --- +if ($Version) { + Write-Host "Stamping version: $Version" + & (Join-Path $PSScriptRoot "set-version.ps1") -Version $Version +} + +# --- Read version from SharedAssemblyInfo.cs --- +$AssemblyInfoPath = Join-Path $SrcDir "SharedAssemblyInfo.cs" +$VersionMatch = Select-String -Path $AssemblyInfoPath -Pattern 'AssemblyInformationalVersion\("([^"]+)"\)' +$PackageVersion = if ($VersionMatch) { $VersionMatch.Matches[0].Groups[1].Value } else { "0.0.0" } +Write-Host "Building UniGetUI version: $PackageVersion" + +# --- Test --- +if (-not $SkipTests) { + Write-Host "`n=== Running tests ===" -ForegroundColor Cyan + dotnet test (Join-Path $SrcDir "UniGetUI.sln") --verbosity q --nologo + if ($LASTEXITCODE -ne 0) { + throw "Tests failed with exit code $LASTEXITCODE" + } +} + +# --- Build / Publish --- +Write-Host "`n=== Publishing $Configuration|$Platform ===" -ForegroundColor Cyan +dotnet clean (Join-Path $SrcDir "UniGetUI.sln") -v m --nologo + +# --- Fetch winget-cli payload --- +Write-Host "`n=== Fetching winget-cli ($Platform) ===" -ForegroundColor Cyan +& (Join-Path $PSScriptRoot "fetch-winget-cli.ps1") -Architectures @($Platform) -Force + +dotnet publish $PublishProject /noLogo /p:Configuration=$Configuration /p:Platform=$Platform -v m +if ($LASTEXITCODE -ne 0) { + throw "dotnet publish failed with exit code $LASTEXITCODE" +} + +# --- Stage binaries --- +if (Test-Path $BinDir) { Remove-Item $BinDir -Recurse -Force } +New-Item $BinDir -ItemType Directory | Out-Null +# Move published output into unigetui_bin +Get-ChildItem $PublishDir | Move-Item -Destination $BinDir -Force + +# WingetUI.exe alias for backward compat +Copy-Item (Join-Path $BinDir "UniGetUI.exe") (Join-Path $BinDir "WingetUI.exe") -Force + +# --- Integrity tree --- +Write-Host "`n=== Generating integrity tree ===" -ForegroundColor Cyan +& (Join-Path $PSScriptRoot "generate-integrity-tree.ps1") -Path $BinDir -MinOutput + +# --- Package output --- +if (Test-Path $OutputPath) { Remove-Item $OutputPath -Recurse -Force } +New-Item $OutputPath -ItemType Directory | Out-Null + +$ZipPath = Join-Path $OutputPath "UniGetUI.$Platform.zip" +Write-Host "`n=== Creating zip: $ZipPath ===" -ForegroundColor Cyan +Compress-Archive -Path (Join-Path $BinDir "*") -DestinationPath $ZipPath -CompressionLevel Optimal + +# --- Installer (Inno Setup) --- +if (-not $SkipInstaller) { + $IsccPath = $null + # Search common install locations + foreach ($candidate in @( + "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", + "$env:ProgramFiles\Inno Setup 6\ISCC.exe", + "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" + )) { + if (Test-Path $candidate) { $IsccPath = $candidate; break } + } + + if ($IsccPath) { + Write-Host "`n=== Building installer ===" -ForegroundColor Cyan + $InstallerBaseName = "UniGetUI.Installer.$Platform" + & $IsccPath (Join-Path $RepoRoot "UniGetUI.iss") /F"$InstallerBaseName" /O"$OutputPath" + if ($LASTEXITCODE -ne 0) { + throw "Inno Setup failed with exit code $LASTEXITCODE" + } + } else { + Write-Warning "Inno Setup 6 (ISCC.exe) not found — skipping installer build." + } +} + +# --- Checksums --- +Write-Host "`n=== Checksums ===" -ForegroundColor Cyan +$ChecksumFile = Join-Path $OutputPath "checksums.$Platform.txt" +Get-ChildItem $OutputPath -File | Where-Object { $_.Name -notlike "checksums.*.txt" } | ForEach-Object { + $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash + "$hash $($_.Name)" | Tee-Object -FilePath $ChecksumFile -Append +} + +# --- Cleanup --- +if (Test-Path $BinDir) { Remove-Item $BinDir -Recurse -Force } + +Write-Host "`n=== Build complete ===" -ForegroundColor Green +Write-Host "Artifacts in: $OutputPath" diff --git a/scripts/fetch-winget-cli.ps1 b/scripts/fetch-winget-cli.ps1 new file mode 100644 index 0000000000..bef2a5e7b1 --- /dev/null +++ b/scripts/fetch-winget-cli.ps1 @@ -0,0 +1,241 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Downloads and extracts the WinGet CLI bundle for the requested architectures. + +.PARAMETER Version + Winget-cli release tag (e.g. "v1.12.460"). Default: v1.12.470. + +.PARAMETER Architectures + Architectures to extract: x64, arm64, x86. Default: x64, arm64. + +.PARAMETER DestinationRoot + Root directory that will contain winget-cli_ folders. + +.PARAMETER Force + Overwrite existing winget-cli_ folders. +#> + +[CmdletBinding()] +param( + [string] $Version = "v1.12.470", + [string[]] $Architectures = @("x64", "arm64"), + [string] $DestinationRoot = (Join-Path $PSScriptRoot ".." "src" "UniGetUI.PackageEngine.Managers.WinGet"), + [string] $UpstreamRepo = "marticliment/UniGetUI", + [string] $UpstreamRef = "main", + [string] $UpstreamReferencePath = "src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64", + [string] $GitHubToken = "", + [switch] $Force +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +$Headers = @{ + "User-Agent" = "UniGetUI-build" + "Accept" = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" +} + +if ([string]::IsNullOrWhiteSpace($GitHubToken)) { + $GitHubToken = $env:GITHUB_TOKEN +} +if ([string]::IsNullOrWhiteSpace($GitHubToken)) { + $GitHubToken = $env:GH_TOKEN +} + +if (-not [string]::IsNullOrWhiteSpace($GitHubToken)) { + $Headers["Authorization"] = "Bearer $GitHubToken" + Write-Host "Using authenticated GitHub API requests." +} +else { + Write-Warning "No GitHub token found (GITHUB_TOKEN/GH_TOKEN). Requests may be rate-limited." +} + +$X64OnlyReferenceFiles = @( + "AppInstallerBackgroundTasks.dll" +) + +function Get-ReleaseInfo { + param([string] $Tag) + + if ([string]::IsNullOrWhiteSpace($Tag) -or $Tag -eq "latest") { + $url = "https://api.github.com/repos/microsoft/winget-cli/releases/latest" + } else { + if (-not $Tag.StartsWith("v")) { $Tag = "v$Tag" } + $url = "https://api.github.com/repos/microsoft/winget-cli/releases/tags/$Tag" + } + + return Invoke-RestMethod -Uri $url -Headers $Headers +} + +function Find-AssetUrl { + param( + [object] $Release, + [string] $AssetName + ) + + $asset = $Release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 + if (-not $asset) { + throw "Release asset '$AssetName' not found." + } + + return $asset.browser_download_url +} + +function Get-UpstreamReferenceFiles { + param( + [string] $Repository, + [string] $Ref, + [string] $DirectoryPath + ) + + $apiUrl = "https://api.github.com/repos/${Repository}/contents/${DirectoryPath}?ref=${Ref}" + $items = Invoke-RestMethod -Uri $apiUrl -Headers $Headers + + if (-not $items) { + throw "Upstream directory '$DirectoryPath' from '$Repository@$Ref' returned no entries." + } + + $files = @( + $items | + Where-Object { $_.type -eq "file" } | + Select-Object -ExpandProperty name | + Sort-Object -Unique + ) + + if ($files.Count -eq 0) { + throw "Upstream directory '$DirectoryPath' from '$Repository@$Ref' contains no files." + } + + return $files +} + +function Resolve-UpstreamReferencePathForArchitecture { + param( + [string] $BasePath, + [string] $Architecture + ) + + $archKey = $Architecture.ToLowerInvariant() + + if ($BasePath -match 'winget-cli_[^/\\]+$') { + return ($BasePath -replace 'winget-cli_[^/\\]+$', "winget-cli_$archKey") + } + + return $BasePath +} + +$release = Get-ReleaseInfo -Tag $Version +Write-Host "Using winget-cli release: $($release.tag_name)" + +$bundleUrl = Find-AssetUrl -Release $release -AssetName "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" +$depsUrl = Find-AssetUrl -Release $release -AssetName "DesktopAppInstaller_Dependencies.zip" + +$tempRoot = Join-Path $env:TEMP "winget-cli-$([Guid]::NewGuid().ToString('N'))" +$bundlePath = Join-Path $tempRoot "bundle.msixbundle" +$depsPath = Join-Path $tempRoot "deps.zip" +$bundleDir = Join-Path $tempRoot "bundle" +$depsDir = Join-Path $tempRoot "deps" + +New-Item $tempRoot -ItemType Directory | Out-Null + +Write-Host "Downloading msixbundle..." +Invoke-WebRequest -Uri $bundleUrl -OutFile $bundlePath -Headers $Headers + +Write-Host "Downloading dependencies..." +Invoke-WebRequest -Uri $depsUrl -OutFile $depsPath -Headers $Headers + +Write-Host "Extracting bundle..." +Expand-Archive -Path $bundlePath -DestinationPath $bundleDir -Force + +Write-Host "Extracting dependencies..." +Expand-Archive -Path $depsPath -DestinationPath $depsDir -Force + +foreach ($arch in $Architectures) { + $archKey = $arch.ToLowerInvariant() + $upstreamReferencePathForArch = Resolve-UpstreamReferencePathForArchitecture -BasePath $UpstreamReferencePath -Architecture $archKey + + try { + $upstreamFiles = Get-UpstreamReferenceFiles -Repository $UpstreamRepo -Ref $UpstreamRef -DirectoryPath $upstreamReferencePathForArch + } + catch { + if ($upstreamReferencePathForArch -ne $UpstreamReferencePath) { + Write-Warning "Unable to load architecture-specific upstream reference '$upstreamReferencePathForArch'. Falling back to '$UpstreamReferencePath'." + $upstreamReferencePathForArch = $UpstreamReferencePath + $upstreamFiles = Get-UpstreamReferenceFiles -Repository $UpstreamRepo -Ref $UpstreamRef -DirectoryPath $upstreamReferencePathForArch + } + else { + throw + } + } + + $expectedFiles = if ($archKey -eq 'x64') { + $upstreamFiles + } + else { + @($upstreamFiles | Where-Object { $_ -notin $X64OnlyReferenceFiles }) + } + + Write-Host "[$archKey] Using upstream reference list from $UpstreamRepo@$UpstreamRef/$upstreamReferencePathForArch ($($upstreamFiles.Count) files)" + + $msix = Get-ChildItem $bundleDir -Filter "*${archKey}*.msix" -Recurse | Select-Object -First 1 + if (-not $msix) { + throw "No msix found for architecture '$arch'." + } + + $msixDir = Join-Path $tempRoot "msix-$archKey" + Expand-Archive -Path $msix.FullName -DestinationPath $msixDir -Force + + $destDir = Join-Path $DestinationRoot "winget-cli_$archKey" + if (Test-Path $destDir) { + if ($Force) { + Remove-Item $destDir -Recurse -Force + } else { + throw "Destination folder already exists: $destDir (use -Force to overwrite)" + } + } + New-Item $destDir -ItemType Directory | Out-Null + + $depsArchDir = Join-Path $depsDir $archKey + if (-not (Test-Path $depsArchDir)) { $depsArchDir = $depsDir } + + foreach ($fileName in $expectedFiles) { + $destinationFile = Join-Path $destDir $fileName + $source = Get-ChildItem $msixDir -Recurse -Filter $fileName -File | Select-Object -First 1 + if (-not $source) { + $source = Get-ChildItem $depsArchDir -Recurse -Filter $fileName -File | Select-Object -First 1 + } + if (-not $source) { + $fallbackUrl = "https://raw.githubusercontent.com/${UpstreamRepo}/${UpstreamRef}/${upstreamReferencePathForArch}/${fileName}" + Write-Warning "Release payload missing '$fileName' for $arch. Downloading fallback from upstream reference." + try { + Invoke-WebRequest -Uri $fallbackUrl -OutFile $destinationFile -Headers $Headers + continue + } + catch { + throw "Required upstream file '$fileName' not found for $arch and fallback download failed: $fallbackUrl" + } + } + + Copy-Item $source.FullName $destinationFile -Force + } + + $copiedFiles = @( + Get-ChildItem $destDir -File | + Select-Object -ExpandProperty Name | + Sort-Object -Unique + ) + + $missingFiles = @($expectedFiles | Where-Object { $_ -notin $copiedFiles }) + $extraFiles = @($copiedFiles | Where-Object { $_ -notin $expectedFiles }) + + if ($missingFiles.Count -gt 0 -or $extraFiles.Count -gt 0) { + throw "Validation failed for $arch. Missing: $($missingFiles -join ', '). Extra: $($extraFiles -join ', ')" + } + + Write-Host "Prepared $destDir" +} + +Remove-Item $tempRoot -Recurse -Force +Write-Host "WinGet CLI bundle extracted successfully." diff --git a/scripts/generate-integrity-tree.ps1 b/scripts/generate-integrity-tree.ps1 new file mode 100644 index 0000000000..5d56abdea0 --- /dev/null +++ b/scripts/generate-integrity-tree.ps1 @@ -0,0 +1,58 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Generates an IntegrityTree.json file containing SHA256 hashes of all files + in the specified directory. Used at build time; verified at runtime by + UniGetUI.Core.Tools.IntegrityTester. + +.PARAMETER Path + The directory to scan (typically the publish/output directory). + +.PARAMETER MinOutput + Suppress per-file progress output. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory, Position = 0)] + [string] $Path, + + [switch] $MinOutput +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path $Path -PathType Container)) { + throw "The directory '$Path' does not exist." +} + +$Path = (Resolve-Path $Path).Path +$OutputFileName = 'IntegrityTree.json' +$ScriptName = [System.IO.Path]::GetFileName($PSCommandPath) + +$integrityData = [ordered]@{} + +Get-ChildItem $Path -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($Path.Length).TrimStart('\', '/') -replace '\\', '/' + + # Skip the output file itself + if ($relativePath -eq $OutputFileName) { return } + + if (-not $MinOutput) { + Write-Host " - Computing SHA256 of $relativePath..." + } + + $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower() + $integrityData[$relativePath] = $hash +} + +# Sort keys for deterministic output +$sorted = [ordered]@{} +foreach ($key in ($integrityData.Keys | Sort-Object)) { + $sorted[$key] = $integrityData[$key] +} + +$json = $sorted | ConvertTo-Json -Depth 1 +Set-Content (Join-Path $Path $OutputFileName) $json -Encoding utf8NoBOM -NoNewline + +Write-Host "Integrity tree was generated and saved to $Path/$OutputFileName" diff --git a/scripts/set-version.ps1 b/scripts/set-version.ps1 new file mode 100644 index 0000000000..6478281019 --- /dev/null +++ b/scripts/set-version.ps1 @@ -0,0 +1,97 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Stamps version information into all required source files. + CI-friendly replacement for the interactive scripts/apply_versions.py. + +.PARAMETER Version + Semantic version string, e.g. "3.3.7" or "3.4.0-beta1". + A four-part version (X.X.X.X) is derived automatically for assembly info. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $Version +) + +$ErrorActionPreference = 'Stop' + +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") + +# Derive four-part version: 3.3.7 -> 3.3.7.0, 3.4.0-beta1 -> 3.4.0.0 +$CleanVersion = ($Version -Split '-')[0] # strip prerelease tag +$Parts = $CleanVersion -Split '\.' +while ($Parts.Count -lt 4) { $Parts += '0' } +$FourPartVersion = ($Parts[0..3]) -Join '.' + +Write-Host "Version name : $Version" +Write-Host "Assembly ver : $FourPartVersion" + +# --- Bump build number --- +$BuildNumberFile = Join-Path $PSScriptRoot "BuildNumber" +$BuildNumber = 0 +if (Test-Path $BuildNumberFile) { + $BuildNumber = [int](Get-Content $BuildNumberFile -Raw).Trim() +} +$BuildNumber++ +Set-Content $BuildNumberFile $BuildNumber +Write-Host "Build number : $BuildNumber" + +# --- Helper: replace lines in a file by prefix match --- +function Set-LinesByPrefix { + param( + [string] $FilePath, + [hashtable] $Replacements, + [string] $Encoding = 'utf8BOM' + ) + + if (-not (Test-Path $FilePath)) { + Write-Warning "File not found, skipping: $FilePath" + return + } + + $lines = Get-Content $FilePath -Encoding $Encoding + $output = foreach ($line in $lines) { + $matched = $false + foreach ($prefix in $Replacements.Keys) { + if ($line.TrimStart().StartsWith($prefix)) { + $Replacements[$prefix] + $matched = $true + break + } + } + if (-not $matched) { $line } + } + $output | Set-Content $FilePath -Encoding $Encoding +} + +# --- CoreData.cs --- +Set-LinesByPrefix -FilePath (Join-Path $RepoRoot "src" "UniGetUI.Core.Data" "CoreData.cs") -Replacements @{ + 'public const string VersionName =' = " public const string VersionName = `"$Version`"; // Do not modify this line, use file scripts/apply_versions.py" + 'public const int BuildNumber =' = " public const int BuildNumber = $BuildNumber; // Do not modify this line, use file scripts/apply_versions.py" +} + +# --- SharedAssemblyInfo.cs --- +Set-LinesByPrefix -FilePath (Join-Path $RepoRoot "src" "SharedAssemblyInfo.cs") -Replacements @{ + '[assembly: AssemblyVersion("' = "[assembly: AssemblyVersion(`"$FourPartVersion`")]" + '[assembly: AssemblyFileVersion("' = "[assembly: AssemblyFileVersion(`"$FourPartVersion`")]" + '[assembly: AssemblyInformationalVersion("' = "[assembly: AssemblyInformationalVersion(`"$Version`")]" +} + +# --- UniGetUI.iss --- +Set-LinesByPrefix -FilePath (Join-Path $RepoRoot "UniGetUI.iss") -Replacements @{ + '#define MyAppVersion' = "#define MyAppVersion `"$Version`"" + 'VersionInfoVersion=' = "VersionInfoVersion=$FourPartVersion" + 'VersionInfoProductVersion=' = "VersionInfoProductVersion=$FourPartVersion" +} + +# --- app.manifest (only the assemblyIdentity version, not manifestVersion) --- +$ManifestPath = Join-Path $RepoRoot "src" "UniGetUI" "app.manifest" +if (Test-Path $ManifestPath) { + $content = Get-Content $ManifestPath -Raw -Encoding utf8BOM + $content = $content -Replace '(? + +[CmdletBinding()] +param( + [string] $BinDir, + [string] $InstallerPath, + [string] $FileListPath, + + [Parameter(Mandatory)] + [string] $AzureTenantId, + + [Parameter(Mandatory)] + [string] $KeyVaultUrl, + + [Parameter(Mandatory)] + [string] $ClientId, + + [Parameter(Mandatory)] + [string] $ClientSecret, + + [Parameter(Mandatory)] + [string] $CertificateName, + + [string] $TimestampServer = "http://timestamp.digicert.com", + + [switch] $Install +) + +$ErrorActionPreference = 'Stop' + +# --- Install tools if requested --- +if ($Install) { + Write-Host "Installing code-signing tools..." -ForegroundColor Cyan + dotnet tool install --global AzureSignTool 2>$null + Install-Module -Name Devolutions.Authenticode -Force -Scope CurrentUser 2>$null + + # Trust test code-signing CA + $TestCertsUrl = "https://raw.githubusercontent.com/Devolutions/devolutions-authenticode/master/data/certs" + $CaCertPath = Join-Path $env:TEMP "authenticode-test-ca.crt" + Invoke-WebRequest -Uri "$TestCertsUrl/authenticode-test-ca.crt" -OutFile $CaCertPath + Import-Certificate -FilePath $CaCertPath -CertStoreLocation "cert:\LocalMachine\Root" | Out-Null + Remove-Item $CaCertPath -ErrorAction SilentlyContinue + Write-Host "Code-signing tools installed." +} + +$SignParams = @( + 'sign', + '-kvt', $AzureTenantId, + '-kvu', $KeyVaultUrl, + '-kvi', $ClientId, + '-kvs', $ClientSecret, + '-kvc', $CertificateName, + '-tr', $TimestampServer, + '-v' +) + +function Invoke-BatchSign { + param( + [string[]] $Files + ) + + $Files = $Files | Where-Object { $_ -and (Test-Path $_) } + if (-not $Files -or $Files.Count -eq 0) { + Write-Warning "No files to sign." + return + } + + Write-Host "Signing $($Files.Count) files..." + AzureSignTool @SignParams $Files + if ($LASTEXITCODE -ne 0) { + throw "AzureSignTool failed with exit code $LASTEXITCODE" + } +} + +# --- Sign binaries in BinDir --- +if ($FileListPath -and (Test-Path $FileListPath)) { + Write-Host "`n=== Signing binaries from list: $FileListPath ===" -ForegroundColor Cyan + $filesToSign = Get-Content $FileListPath | Where-Object { $_ -and ($_ -notmatch '^\s*$') } + Invoke-BatchSign -Files $filesToSign +} elseif ($BinDir -and (Test-Path $BinDir)) { + Write-Host "`n=== Signing binaries in $BinDir ===" -ForegroundColor Cyan + $filesToSign = Get-ChildItem -Path $BinDir -Include @("*.exe", "*.dll") -Recurse + if ($filesToSign.Count -eq 0) { + Write-Warning "No .exe or .dll files found in $BinDir" + } else { + Invoke-BatchSign -Files ($filesToSign | ForEach-Object { $_.FullName }) + Write-Host "Binary signing complete." + } +} + +# --- Sign installer --- +if ($InstallerPath -and (Test-Path $InstallerPath)) { + Write-Host "`n=== Signing installer: $InstallerPath ===" -ForegroundColor Cyan + AzureSignTool @SignParams $InstallerPath + if ($LASTEXITCODE -ne 0) { + throw "AzureSignTool failed for installer with exit code $LASTEXITCODE" + } + Write-Host "Installer signing complete." +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 28daef943f..01b0dc8f31 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -8,8 +8,8 @@ 10.0.26100.56 8.0.407 - Martí Climent and the contributors - Martí Climent + Devolutions Inc. and the contributors + Devolutions Inc. enable diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs index 88dc064b32..4415200dfa 100644 --- a/src/SharedAssemblyInfo.cs +++ b/src/SharedAssemblyInfo.cs @@ -5,7 +5,7 @@ [assembly: AssemblyDescription("UniGetUI")] [assembly: AssemblyTitle("UniGetUI")] [assembly: AssemblyDefaultAlias("UniGetUI")] -[assembly: AssemblyCopyright("2025, Martí Climent")] +[assembly: AssemblyCopyright("Copyright 2021-2026 Devolutions Inc.")] [assembly: AssemblyVersion("3.3.7.0")] [assembly: AssemblyFileVersion("3.3.7.0")] [assembly: AssemblyInformationalVersion("3.3.7")] diff --git a/src/Solution.props b/src/Solution.props index 3894835c2f..bd968a0610 100644 --- a/src/Solution.props +++ b/src/Solution.props @@ -13,10 +13,10 @@ 3.1.0.0 3.1.0-alpha2 UniGetUI - Martí Climent and the contributors - Martí Climent + Devolutions Inc. and the contributors + Devolutions Inc. 3.1.0-alpha2 - 2025, Martí Climent + Copyright 2021-2026 Devolutions Inc. enable diff --git a/src/UniGetUI.Core.Data/Licenses.cs b/src/UniGetUI.Core.Data/Licenses.cs index 8d76341ddf..acbd048207 100644 --- a/src/UniGetUI.Core.Data/Licenses.cs +++ b/src/UniGetUI.Core.Data/Licenses.cs @@ -36,7 +36,7 @@ public static class LicenseData }; public static Dictionary LicenseURLs = new(){ - {"UniGetUI", new Uri("https://github.com/marticliment/WingetUI/blob/main/LICENSE")}, + {"UniGetUI", new Uri("https://github.com/Devolutions/UniGetUI/blob/main/LICENSE")}, // C# Libraries {"Pickers", new Uri("https://github.com/PavlikBender/Pickers/blob/master/LICENSE")}, diff --git a/src/UniGetUI.Core.IconStore/IconDatabase.cs b/src/UniGetUI.Core.IconStore/IconDatabase.cs index 946f77b618..70f9b83483 100644 --- a/src/UniGetUI.Core.IconStore/IconDatabase.cs +++ b/src/UniGetUI.Core.IconStore/IconDatabase.cs @@ -42,7 +42,7 @@ public async Task LoadIconAndScreenshotsDatabaseAsync() string IconsAndScreenshotsFile = Path.Join(CoreData.UniGetUICacheDirectory_Data, "Icon Database.json"); Uri DownloadUrl = new( - "https://github.com/marticliment/UniGetUI/raw/refs/heads/main/WebBasedData/screenshot-database-v2.json"); + "https://github.com/Devolutions/UniGetUI/raw/refs/heads/main/WebBasedData/screenshot-database-v2.json"); if (Settings.Get(Settings.K.IconDataBaseURL)) { DownloadUrl = new Uri(Settings.GetValue(Settings.K.IconDataBaseURL)); diff --git a/src/UniGetUI.Core.LanguageEngine/LanguageEngine.cs b/src/UniGetUI.Core.LanguageEngine/LanguageEngine.cs index 80d28d5b51..fba0fa5c36 100644 --- a/src/UniGetUI.Core.LanguageEngine/LanguageEngine.cs +++ b/src/UniGetUI.Core.LanguageEngine/LanguageEngine.cs @@ -141,7 +141,7 @@ public async Task DownloadUpdatedLanguageFile(string LangKey) { try { - Uri NewFile = new("https://raw.githubusercontent.com/marticliment/UniGetUI/main/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_" + LangKey + ".json"); + Uri NewFile = new("https://raw.githubusercontent.com/Devolutions/UniGetUI/main/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_" + LangKey + ".json"); HttpClient client = new(); client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); diff --git a/src/UniGetUI.Core.Tools/IntegrityTester.cs b/src/UniGetUI.Core.Tools/IntegrityTester.cs index ad840cb4b9..acb204b92a 100644 --- a/src/UniGetUI.Core.Tools/IntegrityTester.cs +++ b/src/UniGetUI.Core.Tools/IntegrityTester.cs @@ -21,15 +21,15 @@ public struct Result public Dictionary CorruptedFiles; } - private static string GetMD5(string fullPath, bool canRetry) + private static string GetSHA256(string fullPath, bool canRetry) { try { - using (var md5 = MD5.Create()) + using (var sha256 = SHA256.Create()) { using (var stream = File.OpenRead(fullPath)) { - var hashBytes = md5.ComputeHash(stream); + var hashBytes = sha256.ComputeHash(stream); return BitConverter.ToString(hashBytes).Replace("-", ""); } } @@ -39,7 +39,7 @@ private static string GetMD5(string fullPath, bool canRetry) if (canRetry) { Task.Delay(1000).GetAwaiter().GetResult(); - return GetMD5(fullPath, false); + return GetSHA256(fullPath, false); } return $"{ex.GetType()}: {ex.Message}"; @@ -97,11 +97,11 @@ public static Result CheckIntegrity(bool allowRetry = true) continue; } - var currentMd5 = GetMD5(fullPath, allowRetry).ToLower(); - if (currentMd5 != expectedHash.ToLower()) + var currentHash = GetSHA256(fullPath, allowRetry).ToLower(); + if (currentHash != expectedHash.ToLower()) { - mismatches.Add($"/{file}", new() { Expected = expectedHash, Got = currentMd5 }); - Logger.Error($"File {file} expected to have md5 {expectedHash}, but had {currentMd5} insetad"); + mismatches.Add($"/{file}", new() { Expected = expectedHash, Got = currentHash }); + Logger.Error($"File {file} expected to have sha256 {expectedHash}, but had {currentHash} instead"); } } @@ -138,7 +138,7 @@ public static string GetReadableReport(Result result) { Builder.Append("Corrupted files: "); foreach (var (file, hashes) in result.CorruptedFiles) - Builder.Append($"\n - {file} (md5 mismatch, got {hashes.Got} but expected {hashes.Expected} "); + Builder.Append($"\n - {file} (sha256 mismatch, got {hashes.Got} but expected {hashes.Expected} "); Builder.Append('\n'); } diff --git a/src/UniGetUI.PAckageEngine.Interfaces/UniGetUI.PackageEngine.Interfaces.csproj b/src/UniGetUI.PAckageEngine.Interfaces/UniGetUI.PackageEngine.Interfaces.csproj index 7307bb7ff9..9252dd20ed 100644 --- a/src/UniGetUI.PAckageEngine.Interfaces/UniGetUI.PackageEngine.Interfaces.csproj +++ b/src/UniGetUI.PAckageEngine.Interfaces/UniGetUI.PackageEngine.Interfaces.csproj @@ -12,7 +12,7 @@ 3.1.0-beta0 UniGetUI 3.1.0-beta0 - 2025, Martí Climent + Copyright 2021-2026 Devolutions Inc. diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs index 16663b9b52..a58dbb903e 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs @@ -204,7 +204,7 @@ protected override OperationVeredict _getOperationResult( if ((uintCode is 0x8A150019 or 0x80073D28) && package.OverridenOptions.RunAsAdministrator is not true) { // Installer needs to run elevated, handle autoelevation - // Code 0x80073D28 was added after https://github.com/marticliment/UniGetUI/issues/3093 + // Code 0x80073D28 was added after https://github.com/Devolutions/UniGetUI/issues/3093 package.OverridenOptions.RunAsAdministrator = true; return OperationVeredict.AutoRetry; } diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/UniGetUI.PackageEngine.Managers.WinGet.csproj b/src/UniGetUI.PackageEngine.Managers.WinGet/UniGetUI.PackageEngine.Managers.WinGet.csproj index 52d9e45af4..723b7bc9f5 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/UniGetUI.PackageEngine.Managers.WinGet.csproj +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/UniGetUI.PackageEngine.Managers.WinGet.csproj @@ -1,5 +1,21 @@ + + $(Platform) + x64 + $(MSBuildProjectDirectory)\winget-cli_$(WingetCliArchitecture) + $(WingetCliDirectory)\winget.exe + $(MSBuildProjectDirectory)\..\..\scripts\fetch-winget-cli.ps1 + + + + + + + @@ -18,52 +34,7 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs index bb62f12f04..3d58e96962 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs @@ -2,6 +2,7 @@ using System.Security.AccessControl; using System.Security.Principal; using System.Text; +using System.Runtime.InteropServices; using UniGetUI.Core.Data; using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine; @@ -32,9 +33,28 @@ public class WinGet : PackageManager public static string BundledWinGetPath = ""; + private static string GetBundledWinGetPath() + { + string folder = RuntimeInformation.ProcessArchitecture switch + { + System.Runtime.InteropServices.Architecture.Arm64 => "winget-cli_arm64", + System.Runtime.InteropServices.Architecture.X64 => "winget-cli_x64", + System.Runtime.InteropServices.Architecture.X86 => "winget-cli_x86", + _ => "winget-cli_x64" + }; + + var path = Path.Join(CoreData.UniGetUIExecutableDirectory, folder, "winget.exe"); + if (!File.Exists(path) && folder != "winget-cli_x64") + { + path = Path.Join(CoreData.UniGetUIExecutableDirectory, "winget-cli_x64", "winget.exe"); + } + + return path; + } + public WinGet() { - BundledWinGetPath = Path.Join(CoreData.UniGetUIExecutableDirectory, "winget-cli_x64", "winget.exe"); + BundledWinGetPath = GetBundledWinGetPath(); Capabilities = new ManagerCapabilities { diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/AppInstallerBackgroundTasks.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/AppInstallerBackgroundTasks.dll deleted file mode 100644 index 4124543d76..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/AppInstallerBackgroundTasks.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/Microsoft.Management.Configuration.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/Microsoft.Management.Configuration.dll deleted file mode 100644 index 8082c48533..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/Microsoft.Management.Configuration.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/Microsoft.Web.WebView2.Core.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/Microsoft.Web.WebView2.Core.dll deleted file mode 100644 index 5e813eb62e..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/Microsoft.Web.WebView2.Core.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/WindowsPackageManager.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/WindowsPackageManager.dll deleted file mode 100644 index 26f3ec7d39..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/WindowsPackageManager.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/WindowsPackageManagerServer.exe b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/WindowsPackageManagerServer.exe deleted file mode 100644 index 8463335ec2..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/WindowsPackageManagerServer.exe and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/concrt140_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/concrt140_app.dll deleted file mode 100644 index 8b05de185a..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/concrt140_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/libsmartscreenn.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/libsmartscreenn.dll deleted file mode 100644 index 15bd3c1af1..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/libsmartscreenn.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_1_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_1_app.dll deleted file mode 100644 index 7ab6f0c9b2..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_1_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_2_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_2_app.dll deleted file mode 100644 index 4fe001d088..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_2_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_app.dll deleted file mode 100644 index dc9bf5239c..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_atomic_wait_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_atomic_wait_app.dll deleted file mode 100644 index 4ceca79dc8..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/msvcp140_atomic_wait_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/resources.pri b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/resources.pri deleted file mode 100644 index 55f954dba2..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/resources.pri and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcamp140_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcamp140_app.dll deleted file mode 100644 index e21b997405..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcamp140_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vccorlib140_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vccorlib140_app.dll deleted file mode 100644 index c1c2be313e..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vccorlib140_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcomp140_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcomp140_app.dll deleted file mode 100644 index b767d90371..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcomp140_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcruntime140_1_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcruntime140_1_app.dll deleted file mode 100644 index beb8aba6ee..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcruntime140_1_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcruntime140_app.dll b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcruntime140_app.dll deleted file mode 100644 index 0c5d571c98..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/vcruntime140_app.dll and /dev/null differ diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/winget.exe b/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/winget.exe deleted file mode 100644 index 1d41f67de8..0000000000 Binary files a/src/UniGetUI.PackageEngine.Managers.WinGet/winget-cli_x64/winget.exe and /dev/null differ diff --git a/src/UniGetUI/App.xaml.cs b/src/UniGetUI/App.xaml.cs index c8d2f8f7c8..b317d2e21c 100644 --- a/src/UniGetUI/App.xaml.cs +++ b/src/UniGetUI/App.xaml.cs @@ -117,10 +117,7 @@ private static async Task LoadGSudo() Logger.Warn($"Using bundled GSudo at {CoreData.ElevatorPath} since UniGetUI Elevator is not available!"); CoreData.ElevatorPath = (await CoreTools.WhichAsync("gsudo.exe")).Item2; #else - string elevatorKind = Settings.Get(Settings.K.UseLegacyElevator) - ? "UniGetUI Elevator (Legacy).exe" - : "UniGetUI Elevator.exe"; - CoreData.ElevatorPath = Path.Join(CoreData.UniGetUIExecutableDirectory, "Assets", "Utilities", elevatorKind); + CoreData.ElevatorPath = Path.Join(CoreData.UniGetUIExecutableDirectory, "Assets", "Utilities", "UniGetUI Elevator.exe"); Logger.Debug($"Using built-in UniGetUI Elevator at {CoreData.ElevatorPath}"); #endif } diff --git a/src/UniGetUI/Assets/Utilities/UniGetUI Elevator (Legacy).exe b/src/UniGetUI/Assets/Utilities/UniGetUI Elevator (Legacy).exe deleted file mode 100644 index 292d3fc50d..0000000000 Binary files a/src/UniGetUI/Assets/Utilities/UniGetUI Elevator (Legacy).exe and /dev/null differ diff --git a/src/UniGetUI/Assets/Utilities/UniGetUI Elevator.exe b/src/UniGetUI/Assets/Utilities/UniGetUI Elevator.exe deleted file mode 100644 index 2a739b0c4f..0000000000 Binary files a/src/UniGetUI/Assets/Utilities/UniGetUI Elevator.exe and /dev/null differ diff --git a/src/UniGetUI/AutoUpdater.cs b/src/UniGetUI/AutoUpdater.cs index 51d92b6497..7544256344 100644 --- a/src/UniGetUI/AutoUpdater.cs +++ b/src/UniGetUI/AutoUpdater.cs @@ -1,6 +1,10 @@ using System.Diagnostics; using System.Globalization; +using System.Runtime.InteropServices; using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text.Json; +using Microsoft.Win32; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.Windows.AppNotifications; @@ -15,13 +19,32 @@ namespace UniGetUI; public class AutoUpdater { + private const string REGISTRY_PATH = @"Software\Devolutions\UniGetUI"; + private const string DEFAULT_PRODUCTINFO_URL = "https://devolutions.net/productinfo.json"; + private const string DEFAULT_PRODUCTINFO_KEY = "Devolutions.UniGetUI"; + + private const string REG_PRODUCTINFO_URL = "UpdaterProductInfoUrl"; + private const string REG_PRODUCTINFO_KEY = "UpdaterProductKey"; + private const string REG_ALLOW_UNSAFE_URLS = "UpdaterAllowUnsafeUrls"; + private const string REG_SKIP_HASH_VALIDATION = "UpdaterSkipHashValidation"; + private const string REG_SKIP_SIGNER_THUMBPRINT_CHECK = "UpdaterSkipSignerThumbprintCheck"; + private const string REG_DISABLE_TLS_VALIDATION = "UpdaterDisableTlsValidation"; + private const string REG_USE_LEGACY_GITHUB = "UpdaterUseLegacyGithub"; + + private static readonly string[] DEVOLUTIONS_CERT_THUMBPRINTS = + [ + "3f5202a9432d54293bdfe6f7e46adb0a6f8b3ba6", + "8db5a43bb8afe4d2ffb92da9007d8997a4cc4e13", + "50f753333811ff11f1920274afde3ffd4468b210", + ]; + public static Window Window = null!; public static InfoBar Banner = null!; //------------------------------------------------------------------------------------------------------------------ private const string STABLE_ENDPOINT = "https://www.marticliment.com/versions/unigetui/stable.ver"; private const string BETA_ENDPOINT = "https://www.marticliment.com/versions/unigetui/beta.ver"; - private const string STABLE_INSTALLER_URL = "https://github.com/marticliment/UniGetUI/releases/latest/download/UniGetUI.Installer.exe"; - private const string BETA_INSTALLER_URL = "https://github.com/marticliment/UniGetUI/releases/download/$TAG/UniGetUI.Installer.exe"; + private const string STABLE_INSTALLER_URL = "https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.Installer.exe"; + private const string BETA_INSTALLER_URL = "https://github.com/Devolutions/UniGetUI/releases/download/$TAG/UniGetUI.Installer.exe"; //------------------------------------------------------------------------------------------------------------------ public static bool ReleaseLockForAutoupdate_Notification; public static bool ReleaseLockForAutoupdate_Window; @@ -63,6 +86,7 @@ public static async Task CheckAndInstallUpdates(Window window, InfoBar ban Window = window; Banner = banner; bool WasCheckingForUpdates = true; + UpdaterOverrides updaterOverrides = LoadUpdaterOverrides(); try { @@ -74,40 +98,39 @@ public static async Task CheckAndInstallUpdates(Window window, InfoBar ban ); // Check for updates - string UpdatesEndpoint = Settings.Get(Settings.K.EnableUniGetUIBeta) ? BETA_ENDPOINT : STABLE_ENDPOINT; - string InstallerDownloadUrl = Settings.Get(Settings.K.EnableUniGetUIBeta) ? BETA_INSTALLER_URL : STABLE_INSTALLER_URL; - var (IsUpgradable, LatestVersion, InstallerHash) = await CheckForUpdates(UpdatesEndpoint); + UpdateCandidate updateCandidate = await GetUpdateCandidate(updaterOverrides); + Logger.Info($"Updater source '{updateCandidate.SourceName}' returned version {updateCandidate.VersionName} (upgradable={updateCandidate.IsUpgradable})"); - if (IsUpgradable) + if (updateCandidate.IsUpgradable) { WasCheckingForUpdates = false; - InstallerDownloadUrl = InstallerDownloadUrl.Replace("$TAG", LatestVersion); - - Logger.Info($"An update to UniGetUI version {LatestVersion} is available"); + Logger.Info($"An update to UniGetUI version {updateCandidate.VersionName} is available"); string InstallerPath = Path.Join(CoreData.UniGetUIDataDirectory, "UniGetUI Updater.exe"); if (File.Exists(InstallerPath) - && await CheckInstallerHash(InstallerPath, InstallerHash)) + && await CheckInstallerHash(InstallerPath, updateCandidate.InstallerHash, updaterOverrides) + && CheckInstallerSignerThumbprint(InstallerPath, updaterOverrides)) { Logger.Info($"A cached valid installer was found, launching update process..."); - return await PrepairToLaunchInstaller(InstallerPath, LatestVersion, AutoLaunch, ManualCheck); + return await PrepairToLaunchInstaller(InstallerPath, updateCandidate.VersionName, AutoLaunch, ManualCheck); } File.Delete(InstallerPath); ShowMessage_ThreadSafe( - CoreTools.Translate("UniGetUI version {0} is being downloaded.", LatestVersion.ToString(CultureInfo.InvariantCulture)), + CoreTools.Translate("UniGetUI version {0} is being downloaded.", updateCandidate.VersionName.ToString(CultureInfo.InvariantCulture)), CoreTools.Translate("This may take a minute or two"), InfoBarSeverity.Informational, false); // Download the installer - await DownloadInstaller(InstallerDownloadUrl, InstallerPath); + await DownloadInstaller(updateCandidate.InstallerDownloadUrl, InstallerPath, updaterOverrides); - if (await CheckInstallerHash(InstallerPath, InstallerHash)) + if (await CheckInstallerHash(InstallerPath, updateCandidate.InstallerHash, updaterOverrides) + && CheckInstallerSignerThumbprint(InstallerPath, updaterOverrides)) { Logger.Info("The downloaded installer is valid, launching update process..."); - return await PrepairToLaunchInstaller(InstallerPath, LatestVersion, AutoLaunch, ManualCheck); + return await PrepairToLaunchInstaller(InstallerPath, updateCandidate.VersionName, AutoLaunch, ManualCheck); } ShowMessage_ThreadSafe( @@ -142,41 +165,131 @@ public static async Task CheckAndInstallUpdates(Window window, InfoBar ban } } + private static async Task GetUpdateCandidate(UpdaterOverrides updaterOverrides) + { + if (updaterOverrides.UseLegacyGithub) + { + return await CheckForUpdatesFromLegacyGitHub(updaterOverrides); + } + + try + { + return await CheckForUpdatesFromProductInfo(updaterOverrides); + } + catch (Exception ex) + { + Logger.Warn("Productinfo updater source failed. Falling back to legacy GitHub updater source."); + Logger.Warn(ex); + return await CheckForUpdatesFromLegacyGitHub(updaterOverrides); + } + } + + /// + /// Default update source using Devolutions productinfo.json + /// + private static async Task CheckForUpdatesFromProductInfo(UpdaterOverrides updaterOverrides) + { + Logger.Debug($"Begin check for updates on productinfo source {updaterOverrides.ProductInfoUrl}"); + + if (!IsSourceUrlAllowed(updaterOverrides.ProductInfoUrl, updaterOverrides.AllowUnsafeUrls)) + { + throw new InvalidOperationException($"Productinfo URL is not allowed: {updaterOverrides.ProductInfoUrl}"); + } + + string productInfo; + using (HttpClient client = new(CreateHttpClientHandler(updaterOverrides))) + { + client.Timeout = TimeSpan.FromSeconds(600); + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + productInfo = await client.GetStringAsync(updaterOverrides.ProductInfoUrl); + } + + Dictionary? productInfoRoot = JsonSerializer.Deserialize>(productInfo, SerializationHelpers.DefaultOptions); + if (productInfoRoot is null || productInfoRoot.Count == 0) + { + throw new FormatException("productinfo.json content is empty or invalid"); + } + + if (!productInfoRoot.TryGetValue(updaterOverrides.ProductInfoProductKey, out ProductInfoProduct? product)) + { + throw new KeyNotFoundException($"Product '{updaterOverrides.ProductInfoProductKey}' was not found in productinfo.json"); + } + + ProductInfoChannel? channel = Settings.Get(Settings.K.EnableUniGetUIBeta) ? product.Beta : product.Current; + if (channel is null) + { + string missingChannel = Settings.Get(Settings.K.EnableUniGetUIBeta) ? "Beta" : "Current"; + throw new KeyNotFoundException($"Channel '{missingChannel}' was not found for product '{updaterOverrides.ProductInfoProductKey}'"); + } + + ProductInfoFile installerFile = SelectInstallerFile(channel.Files); + if (!IsSourceUrlAllowed(installerFile.Url, updaterOverrides.AllowUnsafeUrls)) + { + throw new InvalidOperationException($"Installer URL is not allowed: {installerFile.Url}"); + } + + Version currentVersion = ParseVersionOrFallback(CoreData.VersionName, new Version(0, 0, 0, CoreData.BuildNumber)); + Version availableVersion = ParseVersionOrFallback(channel.Version, new Version(0, 0, 0, 0)); + + bool isUpgradable = availableVersion > currentVersion; + Logger.Debug($"Productinfo check result: current={currentVersion}, available={availableVersion}, upgradable={isUpgradable}"); + + return new UpdateCandidate( + isUpgradable, + channel.Version, + installerFile.Hash, + installerFile.Url, + "ProductInfo"); + } + /// - /// Checks whether new updates are available, and returns a tuple containing: - /// - A boolean that is set to True if new updates are available - /// - The new version name - /// - The hash of the installer for the new version, as a string. + /// Legacy updater source. Kept for compatibility and manual fallback testing. /// - private static async Task<(bool, string, string)> CheckForUpdates(string endpoint) + private static async Task CheckForUpdatesFromLegacyGitHub(UpdaterOverrides updaterOverrides) { + string endpoint = Settings.Get(Settings.K.EnableUniGetUIBeta) ? BETA_ENDPOINT : STABLE_ENDPOINT; + string installerDownloadUrl = Settings.Get(Settings.K.EnableUniGetUIBeta) ? BETA_INSTALLER_URL : STABLE_INSTALLER_URL; + + Logger.Warn("Using legacy GitHub updater source due to registry override."); Logger.Debug($"Begin check for updates on endpoint {endpoint}"); - string[] UpdateResponse; - using (HttpClient client = new(CoreTools.GenericHttpClientParameters)) + + string[] updateResponse; + using (HttpClient client = new(CreateHttpClientHandler(updaterOverrides))) { client.Timeout = TimeSpan.FromSeconds(600); client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); - UpdateResponse = (await client.GetStringAsync(endpoint)).Split("////"); + updateResponse = (await client.GetStringAsync(endpoint)).Split("////"); } - if (UpdateResponse.Length >= 3) + if (updateResponse.Length >= 3) { - int LatestVersion = int.Parse(UpdateResponse[0].Replace("\n", "").Replace("\r", "").Trim()); - string InstallerHash = UpdateResponse[1].Replace("\n", "").Replace("\r", "").Trim(); - string VersionName = UpdateResponse[2].Replace("\n", "").Replace("\r", "").Trim(); - Logger.Debug($"Got response from endpoint: ({LatestVersion}, {VersionName}, {InstallerHash})"); - return (LatestVersion > CoreData.BuildNumber, VersionName, InstallerHash); + int latestVersion = int.Parse(updateResponse[0].Replace("\n", "").Replace("\r", "").Trim()); + string installerHash = updateResponse[1].Replace("\n", "").Replace("\r", "").Trim(); + string versionName = updateResponse[2].Replace("\n", "").Replace("\r", "").Trim(); + Logger.Debug($"Got response from endpoint: ({latestVersion}, {versionName}, {installerHash})"); + return new UpdateCandidate( + latestVersion > CoreData.BuildNumber, + versionName, + installerHash, + installerDownloadUrl.Replace("$TAG", versionName), + "LegacyGitHub"); } - Logger.Warn($"Received update string is {UpdateResponse[0]}"); + Logger.Warn($"Received update string is {updateResponse[0]}"); throw new FormatException("The updates file does not follow the FloatVersion////Sha256Hash////VersionName format"); - } + } /// /// Checks whether the downloaded updater matches the hash. /// - private static async Task CheckInstallerHash(string installerLocation, string expectedHash) + private static async Task CheckInstallerHash(string installerLocation, string expectedHash, UpdaterOverrides updaterOverrides) { + if (updaterOverrides.SkipHashValidation) + { + Logger.Warn("Registry override enabled: skipping updater hash validation."); + return true; + } + Logger.Debug($"Checking updater hash on location {installerLocation}"); using (FileStream stream = File.OpenRead(installerLocation)) { @@ -191,13 +304,55 @@ private static async Task CheckInstallerHash(string installerLocation, str } } + private static bool CheckInstallerSignerThumbprint(string installerLocation, UpdaterOverrides updaterOverrides) + { + if (updaterOverrides.SkipSignerThumbprintCheck) + { + Logger.Warn("Registry override enabled: skipping updater signer thumbprint validation."); + return true; + } + + try + { + X509Certificate signerCertificate = X509Certificate.CreateFromSignedFile(installerLocation); + using X509Certificate2 cert = new(signerCertificate); + + string signerThumbprint = NormalizeThumbprint(cert.Thumbprint ?? string.Empty); + if (string.IsNullOrWhiteSpace(signerThumbprint)) + { + Logger.Warn($"Could not read signer thumbprint for installer '{installerLocation}'"); + return false; + } + + if (DEVOLUTIONS_CERT_THUMBPRINTS.Contains(signerThumbprint, StringComparer.OrdinalIgnoreCase)) + { + Logger.Debug($"Installer signer thumbprint is trusted: {signerThumbprint}"); + return true; + } + + Logger.Warn($"Installer signer thumbprint is not trusted. Got: {signerThumbprint}"); + return false; + } + catch (Exception ex) + { + Logger.Warn("Could not validate installer signer thumbprint"); + Logger.Warn(ex); + return false; + } + } + /// /// Downloads the given installer to the given location /// - private static async Task DownloadInstaller(string downloadUrl, string installerLocation) + private static async Task DownloadInstaller(string downloadUrl, string installerLocation, UpdaterOverrides updaterOverrides) { + if (!IsSourceUrlAllowed(downloadUrl, updaterOverrides.AllowUnsafeUrls)) + { + throw new InvalidOperationException($"Download URL is not allowed: {downloadUrl}"); + } + Logger.Debug($"Downloading installer from {downloadUrl} to {installerLocation}"); - using (HttpClient client = new(CoreTools.GenericHttpClientParameters)) + using (HttpClient client = new(CreateHttpClientHandler(updaterOverrides))) { client.Timeout = TimeSpan.FromSeconds(600); client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); @@ -339,4 +494,209 @@ private static void ShowMessage_ThreadSafe(string Title, string Message, InfoBar } } + + private static HttpClientHandler CreateHttpClientHandler(UpdaterOverrides updaterOverrides) + { + HttpClientHandler handler = CoreTools.GenericHttpClientParameters; + if (updaterOverrides.DisableTlsValidation) + { + Logger.Warn("Registry override enabled: TLS certificate validation is disabled for updater requests."); + handler.ServerCertificateCustomValidationCallback = static (_, _, _, _) => true; + } + + return handler; + } + + private static bool IsSourceUrlAllowed(string url, bool allowUnsafeUrls) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri)) + { + return false; + } + + if (allowUnsafeUrls) + { + Logger.Warn($"Registry override enabled: allowing potentially unsafe updater URL {url}"); + return true; + } + + if (!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return uri.Host.EndsWith("devolutions.net", StringComparison.OrdinalIgnoreCase) + || uri.Host.Equals("github.com", StringComparison.OrdinalIgnoreCase) + || uri.Host.Equals("objects.githubusercontent.com", StringComparison.OrdinalIgnoreCase) + || uri.Host.Equals("release-assets.githubusercontent.com", StringComparison.OrdinalIgnoreCase) + || uri.Host.Equals("marticliment.com", StringComparison.OrdinalIgnoreCase) + || uri.Host.EndsWith("marticliment.com", StringComparison.OrdinalIgnoreCase); + } + + private static ProductInfoFile SelectInstallerFile(List files) + { + string targetArch = RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => "arm64", + Architecture.X64 => "x64", + _ => "x64" + }; + + ProductInfoFile? match = files.FirstOrDefault(file => + file.Type.Equals("exe", StringComparison.OrdinalIgnoreCase) + && file.Arch.Equals(targetArch, StringComparison.OrdinalIgnoreCase)); + + match ??= files.FirstOrDefault(file => + file.Type.Equals("exe", StringComparison.OrdinalIgnoreCase) + && file.Arch.Equals("Any", StringComparison.OrdinalIgnoreCase)); + + match ??= files.FirstOrDefault(file => + file.Type.Equals("msi", StringComparison.OrdinalIgnoreCase) + && file.Arch.Equals(targetArch, StringComparison.OrdinalIgnoreCase)); + + match ??= files.FirstOrDefault(file => + file.Type.Equals("msi", StringComparison.OrdinalIgnoreCase) + && file.Arch.Equals("Any", StringComparison.OrdinalIgnoreCase)); + + if (match is null) + { + throw new KeyNotFoundException($"No compatible installer file found in productinfo for architecture '{targetArch}'"); + } + + return match; + } + + private static Version ParseVersionOrFallback(string rawVersion, Version fallbackVersion) + { + if (Version.TryParse(rawVersion, out Version? parsed)) + { + return parsed; + } + + string sanitized = rawVersion.Trim().TrimStart('v', 'V'); + if (Version.TryParse(sanitized, out parsed)) + { + return parsed; + } + + Logger.Warn($"Could not parse version '{rawVersion}', using fallback '{fallbackVersion}'"); + return fallbackVersion; + } + + private static string NormalizeThumbprint(string thumbprint) + { + char[] normalized = thumbprint + .ToLowerInvariant() + .Where(char.IsAsciiHexDigit) + .ToArray(); + + return new string(normalized); + } + + private static UpdaterOverrides LoadUpdaterOverrides() + { + using RegistryKey? key = Registry.CurrentUser.OpenSubKey(REGISTRY_PATH); + + string productInfoUrl = GetRegistryString(key, REG_PRODUCTINFO_URL) ?? DEFAULT_PRODUCTINFO_URL; + string productInfoProductKey = GetRegistryString(key, REG_PRODUCTINFO_KEY) ?? DEFAULT_PRODUCTINFO_KEY; + + bool allowUnsafeUrls = GetRegistryBool(key, REG_ALLOW_UNSAFE_URLS); + bool skipHashValidation = GetRegistryBool(key, REG_SKIP_HASH_VALIDATION); + bool skipSignerThumbprintCheck = GetRegistryBool(key, REG_SKIP_SIGNER_THUMBPRINT_CHECK); + bool disableTlsValidation = GetRegistryBool(key, REG_DISABLE_TLS_VALIDATION); + bool useLegacyGithub = GetRegistryBool(key, REG_USE_LEGACY_GITHUB); + + if (key is not null) + { + Logger.Info($"Updater registry overrides loaded from HKCU\\{REGISTRY_PATH}"); + } + + return new UpdaterOverrides( + productInfoUrl, + productInfoProductKey, + allowUnsafeUrls, + skipHashValidation, + skipSignerThumbprintCheck, + disableTlsValidation, + useLegacyGithub); + } + + private static string? GetRegistryString(RegistryKey? key, string valueName) + { + object? value = key?.GetValue(valueName); + if (value is null) + { + return null; + } + + string? parsedValue = value.ToString(); + if (string.IsNullOrWhiteSpace(parsedValue)) + { + return null; + } + + return parsedValue.Trim(); + } + + private static bool GetRegistryBool(RegistryKey? key, string valueName) + { + object? value = key?.GetValue(valueName); + if (value is null) + { + return false; + } + + if (value is int intValue) + { + return intValue != 0; + } + + if (value is long longValue) + { + return longValue != 0; + } + + string normalized = value.ToString()?.Trim() ?? ""; + return normalized.Equals("1", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("true", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("yes", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("on", StringComparison.OrdinalIgnoreCase); + } + + private sealed record UpdateCandidate( + bool IsUpgradable, + string VersionName, + string InstallerHash, + string InstallerDownloadUrl, + string SourceName); + + private sealed record UpdaterOverrides( + string ProductInfoUrl, + string ProductInfoProductKey, + bool AllowUnsafeUrls, + bool SkipHashValidation, + bool SkipSignerThumbprintCheck, + bool DisableTlsValidation, + bool UseLegacyGithub); + + private sealed class ProductInfoProduct + { + public ProductInfoChannel? Current { get; set; } + public ProductInfoChannel? Beta { get; set; } + } + + private sealed class ProductInfoChannel + { + public string Version { get; set; } = string.Empty; + public List Files { get; set; } = []; + } + + private sealed class ProductInfoFile + { + public string Arch { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string Hash { get; set; } = string.Empty; + } + } diff --git a/src/UniGetUI/CLIHandler.cs b/src/UniGetUI/CLIHandler.cs index 308bc0ba7d..b6999209ad 100644 --- a/src/UniGetUI/CLIHandler.cs +++ b/src/UniGetUI/CLIHandler.cs @@ -38,7 +38,7 @@ private enum HRESULT public static int Help() { - var url = "https://github.com/marticliment/UniGetUI/blob/main/cli-arguments.md#unigetui-command-line-parameters"; + var url = "https://github.com/Devolutions/UniGetUI/blob/main/cli-arguments.md#unigetui-command-line-parameters"; CoreTools.Launch(url); return 0; } diff --git a/src/UniGetUI/Controls/Announcer.xaml b/src/UniGetUI/Controls/Announcer.xaml deleted file mode 100644 index b23e50cdac..0000000000 --- a/src/UniGetUI/Controls/Announcer.xaml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/UniGetUI/Controls/Announcer.xaml.cs b/src/UniGetUI/Controls/Announcer.xaml.cs deleted file mode 100644 index 85db7b9571..0000000000 --- a/src/UniGetUI/Controls/Announcer.xaml.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Documents; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Media.Imaging; -using UniGetUI.Core.Data; -using UniGetUI.Core.Logging; -using UniGetUI.Core.Tools; -using Windows.UI.Text; - -// To learn more about WinUI, the WinUI project structure, -// and more about our project templates, see: http://aka.ms/winui-project-info. - -namespace UniGetUI.Interface.Widgets -{ - public sealed partial class Announcer : UserControl - { - public Uri Url - { - get => (Uri)GetValue(UrlProperty); - set => SetValue(UrlProperty, value); - } - - private readonly DependencyProperty UrlProperty; - - private static readonly HttpClient NetClient = new(CoreTools.GenericHttpClientParameters); - public Announcer() - { - NetClient.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); - UrlProperty = DependencyProperty.Register( - nameof(UrlProperty), - typeof(Uri), - typeof(CheckboxCard), - new PropertyMetadata(default(Uri), new PropertyChangedCallback((_, _) => _ = LoadAnnouncements()))); - - InitializeComponent(); - DefaultStyleKey = typeof(Announcer); - BringIntoViewRequested += (_, _) => _ = LoadAnnouncements(); - - int i = 0; - PointerPressed += (_, _) => { if (i++ % 3 != 0) { _ = LoadAnnouncements(); } }; - - SetText(CoreTools.Translate("Fetching latest announcements, please wait...")); - _textblock.TextWrapping = TextWrapping.Wrap; - } - - public async Task LoadAnnouncements(bool retry = false) - { - try - { - Uri announcement_url = Url; - if (retry) - { - announcement_url = new Uri(Url.ToString().Replace("https://", "http://")); - } - - HttpResponseMessage response = await NetClient.GetAsync(announcement_url); - if (response.IsSuccessStatusCode) - { - string[] response_body = (await response.Content.ReadAsStringAsync()).Split("////"); - string title = response_body[0].Trim().Trim('\n').Trim(); - string body = response_body[1].Trim().Trim('\n').Trim(); - string linkId = response_body[2].Trim().Trim('\n').Trim(); - string linkName = response_body[3].Trim().Trim('\n').Trim(); - Uri imageUrl = new(response_body[4].Trim().Trim('\n').Trim()); - SetText(title, body, linkId, linkName); - SetImage(imageUrl); - } - else - { - SetText(CoreTools.Translate("Could not load announcements - HTTP status code is $CODE").Replace("$CODE", response.StatusCode.ToString())); - SetImage(new Uri("ms-appx:///Assets/Images/warn.png")); - if (!retry) - { - _ = LoadAnnouncements(true); - } - } - } - catch (Exception ex) - { - Logger.Warn("Could not load announcements"); - Logger.Warn(ex); - SetText(CoreTools.Translate("Could not load announcements - ") + ex.ToString()); - SetImage(new Uri("ms-appx:///Assets/Images/warn.png")); - } - } - - public void SetText_Safe(string title, string body, string linkId, string linkName) - { - ((MainApp)Application.Current).MainWindow.DispatcherQueue.TryEnqueue(() => - { - SetText(title, body, linkId, linkName); - }); - } - - public void SetText(string title, string body, string linkId, string linkName) - { - Paragraph paragraph = new(); - paragraph.Inlines.Add(new Run { Text = title, FontSize = 24, FontWeight = new FontWeight(700), FontFamily = new FontFamily("Segoe UI Variable Display") }); - _textblock.Blocks.Clear(); - _textblock.Blocks.Add(paragraph); - - paragraph = new(); - foreach (string line in body.Split("\n")) - { - paragraph.Inlines.Add(new Run { Text = line + " " }); - paragraph.Inlines.Add(new LineBreak()); - } - Hyperlink link = new(); - link.Inlines.Add(new Run { Text = linkName }); - link.NavigateUri = new Uri("https://marticliment.com/redirect?" + linkId); - paragraph.Inlines[^1] = link; - paragraph.Inlines.Add(new Run() { Text= "" }); - - _textblock.Blocks.Add(paragraph); - } - - public void SetText(string body) - { - Paragraph paragraph = new(); - foreach (string line in body.Split("\n")) - { - paragraph.Inlines.Add(new Run { Text = line }); - paragraph.Inlines.Add(new LineBreak()); - } - - _textblock.Blocks.Clear(); - _textblock.Blocks.Add(paragraph); - } - - public void SetImage(Uri url) - { - BitmapImage bitmapImage = new() - { - UriSource = url - }; - _image.Source = bitmapImage; - - } - } -} diff --git a/src/UniGetUI/Pages/AboutPages/AboutUniGetUI.xaml b/src/UniGetUI/Pages/AboutPages/AboutUniGetUI.xaml index d0536cd91a..893f7b1b3a 100644 --- a/src/UniGetUI/Pages/AboutPages/AboutUniGetUI.xaml +++ b/src/UniGetUI/Pages/AboutPages/AboutUniGetUI.xaml @@ -48,10 +48,10 @@ - + - + diff --git a/src/UniGetUI/Pages/AboutPages/Translators.xaml b/src/UniGetUI/Pages/AboutPages/Translators.xaml index 3b3fcbc4f0..727ead9a9e 100644 --- a/src/UniGetUI/Pages/AboutPages/Translators.xaml +++ b/src/UniGetUI/Pages/AboutPages/Translators.xaml @@ -27,7 +27,7 @@ Margin="0,4,0,4" HorizontalAlignment="Stretch" VerticalAlignment="Center" - NavigateUri="https://github.com/marticliment/WingetUI/wiki#translating-wingetui"> + NavigateUri="https://github.com/Devolutions/UniGetUI/wiki#translating-wingetui"> diff --git a/src/UniGetUI/Pages/DialogPages/ReleaseNotes.xaml.cs b/src/UniGetUI/Pages/DialogPages/ReleaseNotes.xaml.cs index 5b62401d84..ba2cd463e7 100644 --- a/src/UniGetUI/Pages/DialogPages/ReleaseNotes.xaml.cs +++ b/src/UniGetUI/Pages/DialogPages/ReleaseNotes.xaml.cs @@ -26,7 +26,7 @@ public ReleaseNotes() private async Task InitializeWebView() { await WebView.EnsureCoreWebView2Async(); - WebView.Source = new Uri("https://github.com/marticliment/WingetUI/releases/tag/" + CoreData.VersionName); + WebView.Source = new Uri("https://github.com/Devolutions/UniGetUI/releases/tag/" + CoreData.VersionName); } public void Dispose() diff --git a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Experimental.xaml b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Experimental.xaml index 5590777369..93f8c07ca4 100644 --- a/src/UniGetUI/Pages/SettingsPages/GeneralPages/Experimental.xaml +++ b/src/UniGetUI/Pages/SettingsPages/GeneralPages/Experimental.xaml @@ -55,17 +55,11 @@ - - - + diff --git a/src/UniGetUI/Pages/SettingsPages/GeneralPages/SettingsHomepage.xaml b/src/UniGetUI/Pages/SettingsPages/GeneralPages/SettingsHomepage.xaml index 92c4cf3935..6a9f804a67 100644 --- a/src/UniGetUI/Pages/SettingsPages/GeneralPages/SettingsHomepage.xaml +++ b/src/UniGetUI/Pages/SettingsPages/GeneralPages/SettingsHomepage.xaml @@ -20,16 +20,6 @@ HorizontalContentAlignment="Center" VerticalContentAlignment="Center"> - - - - - - @@ -19,16 +18,6 @@ VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"> - - - - - - - + diff --git a/src/UniGetUI/Services/GitHubBackupService.cs b/src/UniGetUI/Services/GitHubBackupService.cs index 0c589fcdd1..ea133a3cc3 100644 --- a/src/UniGetUI/Services/GitHubBackupService.cs +++ b/src/UniGetUI/Services/GitHubBackupService.cs @@ -13,7 +13,7 @@ public class GitHubBackupService private const string ReadMeContents = "" + "This special Gist is used by UniGetUI to store your package backups. \n" + "Please DO NOT EDIT the contents or the description of this gist, or unexpected behaviours may occur.\n" + - "Learn more about UniGetUI at https://github.com/marticliment/UniGetUI\n"; + "Learn more about UniGetUI at https://github.com/Devolutions/UniGetUI\n"; private readonly GitHubAuthService _authService; diff --git a/src/UniGetUI/UniGetUI.csproj b/src/UniGetUI/UniGetUI.csproj index ac10b9aa8f..1b2fb3ca50 100644 --- a/src/UniGetUI/UniGetUI.csproj +++ b/src/UniGetUI/UniGetUI.csproj @@ -20,6 +20,12 @@ true + + arm64 + x64 + $(PkgDevolutions_UniGetUI_Elevator)\runtimes\win-$(ElevatorPackageArchitecture)\native\UniGetUI Elevator.exe + + $(IntermediateOutputPath)\Generated Files\Secrets.Generated.cs @@ -33,7 +39,14 @@ - + + + + + @@ -104,6 +117,7 @@ + @@ -139,11 +153,6 @@ MSBuild:Compile - - - MSBuild:Compile - - MSBuild:Compile @@ -308,13 +317,13 @@ Always - - PreserveNewest - - + PreserveNewest + PreserveNewest + false + false - + PreserveNewest diff --git a/testing/UPDATE-TESTING.md b/testing/UPDATE-TESTING.md new file mode 100644 index 0000000000..265b2676d1 --- /dev/null +++ b/testing/UPDATE-TESTING.md @@ -0,0 +1,179 @@ +# UniGetUI Auto-Update Testing Guide (productinfo.json path) + +This guide validates the new default auto-update flow that reads from `productinfo.json`. + +If `productinfo.json` lookup fails for any reason, UniGetUI now falls back to the legacy updater logic that uses the existing version endpoint and GitHub release download URL. + +## Files used + +- `testing/productinfo.unigetui.test.json` +- Test artifacts expected by that file: + - `UniGetUI.Installer.x64.exe` + - `UniGetUI.Installer.arm64.exe` + - `UniGetUI.x64.zip` + - `UniGetUI.arm64.zip` + +## What is being tested + +- Default updater source is productinfo-based. +- Product key lookup for `Devolutions.UniGetUI`. +- Architecture-aware installer selection (`x64`/`arm64`, `exe` preferred). +- Hash validation (enabled by default). +- Test-only override behavior via registry keys under `HKCU\Software\Devolutions\UniGetUI`. + +## 1) Host the test files locally + +From repository root: + +```powershell +Push-Location testing +python -m http.server 8080 +Pop-Location +``` + +Make sure these URLs are reachable: + +- `http://127.0.0.1:8080/productinfo.unigetui.test.json` +- `http://127.0.0.1:8080/UniGetUI.Installer.x64.exe` +- `http://127.0.0.1:8080/UniGetUI.Installer.arm64.exe` + +## 2) Configure updater overrides (test mode) + +Run in PowerShell: + +```powershell +$regPath = 'HKCU:\Software\Devolutions\UniGetUI' +New-Item -Path $regPath -Force | Out-Null + +# Point updater to local productinfo +Set-ItemProperty -Path $regPath -Name 'UpdaterProductInfoUrl' -Value 'http://127.0.0.1:8080/productinfo.unigetui.test.json' + +# Product key inside productinfo JSON +Set-ItemProperty -Path $regPath -Name 'UpdaterProductKey' -Value 'Devolutions.UniGetUI' + +# Allow local http URL and local domain for package downloads +Set-ItemProperty -Path $regPath -Name 'UpdaterAllowUnsafeUrls' -Type DWord -Value 1 + +# Keep hash validation enabled for normal test pass +Set-ItemProperty -Path $regPath -Name 'UpdaterSkipHashValidation' -Type DWord -Value 0 + +# Keep signer thumbprint validation enabled for normal test pass +Set-ItemProperty -Path $regPath -Name 'UpdaterSkipSignerThumbprintCheck' -Type DWord -Value 0 + +# Keep legacy path disabled (productinfo path is default) +Set-ItemProperty -Path $regPath -Name 'UpdaterUseLegacyGithub' -Type DWord -Value 0 + +# Optional only for HTTPS cert troubleshooting in test environments +Set-ItemProperty -Path $regPath -Name 'UpdaterDisableTlsValidation' -Type DWord -Value 0 +``` + +## 3) Trigger update check in UniGetUI + +1. Launch UniGetUI. +2. Go to **Settings → General**. +3. Click **Check for updates**. + +Expected result: + +- Updater reads the local `productinfo.unigetui.test.json`. +- It picks the correct architecture `exe` installer. +- Download starts and hash is validated. +- Update banner/toast appears when update is ready. + +## 4) Negative test: hash mismatch protection + +Use one of these methods: + +- Replace installer file content but keep original hash in JSON, or +- Edit hash in JSON to an incorrect value. + +Expected result: + +- Hash validation fails. +- Update is aborted with installer authenticity error. + +## 5) Negative test: block unsafe URLs + +Set: + +```powershell +Set-ItemProperty -Path 'HKCU:\Software\Devolutions\UniGetUI' -Name 'UpdaterAllowUnsafeUrls' -Type DWord -Value 0 +``` + +Expected result with local `http://127.0.0.1` URLs: + +- Updater rejects source/download URL as unsafe. +- No installer launch. + +## 6) Optional: force legacy GitHub updater path + +```powershell +Set-ItemProperty -Path 'HKCU:\Software\Devolutions\UniGetUI' -Name 'UpdaterUseLegacyGithub' -Type DWord -Value 1 +``` + +Expected result: + +- Legacy endpoint/GitHub code path is used. +- Productinfo path is bypassed for that run. + +## 7) Fallback test: broken productinfo with successful legacy fallback + +Use one of these methods: + +- Point `UpdaterProductInfoUrl` to a missing URL, or +- Point `UpdaterProductInfoUrl` to a malformed JSON file, or +- Point `UpdaterProductKey` to a non-existent product. + +Example: + +```powershell +Set-ItemProperty -Path 'HKCU:\Software\Devolutions\UniGetUI' -Name 'UpdaterProductInfoUrl' -Value 'http://127.0.0.1:8080/does-not-exist.json' +Set-ItemProperty -Path 'HKCU:\Software\Devolutions\UniGetUI' -Name 'UpdaterUseLegacyGithub' -Type DWord -Value 0 +``` + +Expected result: + +- Productinfo check fails. +- UniGetUI logs that it is falling back to the legacy GitHub updater source. +- Legacy updater path is used automatically. +- If the legacy source has a newer version, the update flow continues normally. + +## 8) Fallback test: both sources fail + +Use a broken productinfo override and also make the legacy source unavailable in your test environment. + +Expected result: + +- Productinfo check fails first. +- UniGetUI attempts the legacy updater path. +- The updater shows the existing terminal error because neither source succeeded. + +## 9) Optional: disable signer thumbprint check (test-only) + +Use this only if your local installer is unsigned or signed with a non-Devolutions certificate. + +```powershell +Set-ItemProperty -Path 'HKCU:\Software\Devolutions\UniGetUI' -Name 'UpdaterSkipSignerThumbprintCheck' -Type DWord -Value 1 +``` + +## 10) Cleanup after testing + +Reset to default production behavior: + +```powershell +$regPath = 'HKCU:\Software\Devolutions\UniGetUI' +Remove-ItemProperty -Path $regPath -Name 'UpdaterProductInfoUrl' -ErrorAction SilentlyContinue +Remove-ItemProperty -Path $regPath -Name 'UpdaterProductKey' -ErrorAction SilentlyContinue +Remove-ItemProperty -Path $regPath -Name 'UpdaterAllowUnsafeUrls' -ErrorAction SilentlyContinue +Remove-ItemProperty -Path $regPath -Name 'UpdaterSkipHashValidation' -ErrorAction SilentlyContinue +Remove-ItemProperty -Path $regPath -Name 'UpdaterSkipSignerThumbprintCheck' -ErrorAction SilentlyContinue +Remove-ItemProperty -Path $regPath -Name 'UpdaterDisableTlsValidation' -ErrorAction SilentlyContinue +Remove-ItemProperty -Path $regPath -Name 'UpdaterUseLegacyGithub' -ErrorAction SilentlyContinue +``` + +With all override values removed, UniGetUI uses: + +- `https://devolutions.net/productinfo.json` +- product key `Devolutions.UniGetUI` +- safety checks enabled +- hash validation enabled diff --git a/testing/productinfo.unigetui.test.json b/testing/productinfo.unigetui.test.json new file mode 100644 index 0000000000..5a63686990 --- /dev/null +++ b/testing/productinfo.unigetui.test.json @@ -0,0 +1,806 @@ +{ + "RDMWindows": { + "Current": { + "Version": "2025.3.32.0", + "Date": "2026-02-26", + "Files": [ + { + "Arch": "Any", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.2025.3.32.0.exe", + "Hash": "EFA2B96FF59DFEEAE6BBA3DBC425220E447C7069F408CB48B4AA9F1AC8B1E850" + }, + { + "Arch": "Any", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.2025.3.32.0.msi", + "Hash": "62AFE25ED724CCA3EA83AAB9FD84B7D065C643722CDDCFCFF3FD5E7A27389ED4" + }, + { + "Arch": "arm64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-arm64.2025.3.32.0.exe", + "Hash": "5392753F8F960C426906C2DCC883491ADF2D4CB5ADF1D8B799FA543BCE939861" + }, + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-x64.2025.3.32.0.exe", + "Hash": "F036DFFA06971DA7A374ECA72650BABBC19EDE83C611F3270964949A79361311" + }, + { + "Arch": "arm64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-arm64.2025.3.32.0.msi", + "Hash": "24514E83B47233E48E6CCD522D1B03C67E421645E7760E4618E58FD306550441" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-x64.2025.3.32.0.msi", + "Hash": "9AACE90721C9CC7A3C6DB073EA85B836FBC57DF7A081FBE52FC7D970DC6BB8D7" + }, + { + "Arch": "Any", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManager.Bin.2025.3.32.0.zip", + "Hash": "4A68FA1065F37599CC67C8F7EA0C2F8ABF3BD9A935D11CAF16A3D5BD1693507C" + }, + { + "Arch": "arm64", + "Type": "7z", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManager.win-arm64.2025.3.32.0.7z", + "Hash": "FCC97E4D34CA8F54E1DDF6001E2E34597BAD65122B0B714B9A49249BF2CAEFBC" + }, + { + "Arch": "x64", + "Type": "7z", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManager.win-x64.2025.3.32.0.7z", + "Hash": "857C91113F12FE629692716ABE87F7204A9CF7BC677A583ED6C4C9D8D1A42CFB" + } + ] + }, + "Beta": { + "Version": "2026.1.8.0", + "Date": "2026-02-19", + "Files": [ + { + "Arch": "Any", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.2026.1.8.0.exe", + "Hash": "8A850F23D0F2D494D8601B2D8861A9764AF63EE36D620FB5F41C81EDE67E641F" + }, + { + "Arch": "Any", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.2026.1.8.0.msi", + "Hash": "4CE4F38C047D766F888F3E8128ADA4B51322D2E1A1FCB5CE9ACC0E0961522A5E" + }, + { + "Arch": "arm64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-arm64.2026.1.8.0.exe", + "Hash": "D4FF7DDA881F12ECCB2CCC443B22FE3385151AC488BE1BBC97027D18952481AA" + }, + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-x64.2026.1.8.0.exe", + "Hash": "B0E0AA4325647595AE83829794C7937D926A138C4743A2D881EC0BAEC5D98347" + }, + { + "Arch": "arm64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-arm64.2026.1.8.0.msi", + "Hash": "4C8EF2BDD7F0DD6B2811804D6B6FF566252452C3EF6354F89598C8E3FE2AD03D" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-x64.2026.1.8.0.msi", + "Hash": "DF1AD55915A54DDE0F685782843E0BABB83B5807F56488947572DD98421B1946" + }, + { + "Arch": "Any", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManager.Bin.2026.1.8.0.zip", + "Hash": "0598B5EAB915E42348101331DF1A57D8B6874F0C437821328AF6EA818CBE6FC6" + }, + { + "Arch": "arm64", + "Type": "7z", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManager.win-arm64.2026.1.8.0.7z", + "Hash": "9953C9061C90E8AB6078B7FB9647C6D033C4A34FC476515A494ACA3A3DF4DED0" + }, + { + "Arch": "x64", + "Type": "7z", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManager.win-x64.2026.1.8.0.7z", + "Hash": "FD3592CC14D6ED487023490560AD9847555B93935CB3AD9126BD7DEB93E41DCC" + } + ] + }, + "Update": { + "Version": "2025.3.32.0", + "Date": "2026-02-26", + "Files": [ + { + "Arch": "Any", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.2025.3.32.0.exe", + "Hash": "EFA2B96FF59DFEEAE6BBA3DBC425220E447C7069F408CB48B4AA9F1AC8B1E850" + }, + { + "Arch": "Any", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.2025.3.32.0.msi", + "Hash": "62AFE25ED724CCA3EA83AAB9FD84B7D065C643722CDDCFCFF3FD5E7A27389ED4" + }, + { + "Arch": "arm64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-arm64.2025.3.32.0.exe", + "Hash": "5392753F8F960C426906C2DCC883491ADF2D4CB5ADF1D8B799FA543BCE939861" + }, + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-x64.2025.3.32.0.exe", + "Hash": "F036DFFA06971DA7A374ECA72650BABBC19EDE83C611F3270964949A79361311" + }, + { + "Arch": "arm64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-arm64.2025.3.32.0.msi", + "Hash": "24514E83B47233E48E6CCD522D1B03C67E421645E7760E4618E58FD306550441" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManager.win-x64.2025.3.32.0.msi", + "Hash": "9AACE90721C9CC7A3C6DB073EA85B836FBC57DF7A081FBE52FC7D970DC6BB8D7" + }, + { + "Arch": "arm64", + "Type": "7z", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManager.win-arm64.2025.3.32.0.7z", + "Hash": "FCC97E4D34CA8F54E1DDF6001E2E34597BAD65122B0B714B9A49249BF2CAEFBC" + }, + { + "Arch": "x64", + "Type": "7z", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManager.win-x64.2025.3.32.0.7z", + "Hash": "857C91113F12FE629692716ABE87F7204A9CF7BC677A583ED6C4C9D8D1A42CFB" + } + ] + } + }, + "RDMMac": { + "Current": { + "Version": "2025.3.10.2", + "Date": "2026-02-19", + "Files": [ + { + "Arch": "universal", + "Type": "dmg", + "Url": "https://cdn.devolutions.net/download/Mac/Devolutions.RemoteDesktopManager.Mac.2025.3.10.2.dmg", + "Hash": "93460DAB1FFEB28DF4DB7F2226A2858035251EB2C0104C41D9EEBFCA29731451" + } + ] + }, + "Beta": { + "Version": "2025.3.10.2", + "Date": "2026-02-19", + "Files": [ + { + "Arch": "universal", + "Type": "dmg", + "Url": "https://cdn.devolutions.net/download/Mac/Devolutions.RemoteDesktopManager.Mac.2025.3.10.2.dmg", + "Hash": "93460DAB1FFEB28DF4DB7F2226A2858035251EB2C0104C41D9EEBFCA29731451" + } + ] + } + }, + "RDMLinux": { + "Current": { + "Version": "2025.3.2.3", + "Date": "2026-02-13", + "Files": [ + { + "Arch": "x64", + "Type": "deb", + "Url": "https://cdn.devolutions.net/download/Linux/RDM/2025.3.2.3/RemoteDesktopManager_2025.3.2.3_amd64.deb", + "Hash": "545822CD95D7EAE3E1423963B1934EFBC6EFB2E8B0E9ED2ECA0393B2D447B3EB" + }, + { + "Arch": "x64", + "Type": "rpm", + "Url": "https://cdn.devolutions.net/download/Linux/RDM/2025.3.2.3/RemoteDesktopManager_2025.3.2.3_x86_64.rpm", + "Hash": "3DEED02B1DB6DBC3B0721CF211B39AD4BE436A8FA06715A7D3C51FF055110DB4" + }, + { + "Arch": "arm64", + "Type": "deb", + "Url": "https://cdn.devolutions.net/download/Linux/RDM/2025.3.2.3/RemoteDesktopManager_2025.3.2.3_arm64.deb", + "Hash": "611B37B4A47719E9CBC917E16E3456FCA5198EB47549BBED3487713F9D28703B" + }, + { + "Arch": "arm64", + "Type": "rpm", + "Url": "https://cdn.devolutions.net/download/Linux/RDM/2025.3.2.3/RemoteDesktopManager_2025.3.2.3_aarch64.rpm", + "Hash": "BA3317B7EF80231014AB919EB54A3B2CB55B4E99E36E9A7AD34544A9C9D9DE13" + } + ] + } + }, + "RDMAgent": { + "Current": { + "Version": "2025.3.32.0", + "Date": "2026-02-26", + "Files": [ + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManagerJump.2025.3.32.0.exe", + "Hash": "95399330EBEE12F6526BE7879D832B112182E3E6EB2321236C7D71A51225A471" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManagerJump.2025.3.32.0.msi", + "Hash": "C59B020D9F10A9A21C11F5EF6CCF167FEAA29B8B55312AA090DD192A1BDC07E4" + }, + { + "Arch": "x64", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManagerJump.Bin.2025.3.32.0.zip", + "Hash": "52DB422FA1ED07E028C212D7C4D26801EBF6C6EFEA480AF6B893BE816ABF2EE2" + } + ] + }, + "Beta": { + "Version": "2026.1.8.0", + "Date": "2026-02-19", + "Files": [ + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManagerJump.2026.1.8.0.exe", + "Hash": "4FCBB551474835226DA895B55AC1641288432F28B81E67CFEBD20F37CDEAA224" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.RemoteDesktopManagerJump.2026.1.8.0.msi", + "Hash": "AD90374D7230B60DC1EAABF75E9ADF16DA7B947248557C8BFE0065212151C432" + }, + { + "Arch": "x64", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.RemoteDesktopManagerJump.Bin.2026.1.8.0.zip", + "Hash": "5A197493B43AF8434D38F3CEA9F2ACDBF4C7CC2A922D145F6656887FFD6D54B2" + } + ] + } + }, + "Agent": { + "Current": { + "Version": "2026.1.0.0", + "Date": "2026-02-27", + "Files": [ + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/DevolutionsAgent-x86_64-2026.1.0.0.msi", + "Hash": "794DB95AA3A846D0135E7233D89AE9A61400A80CAAA046BA4EBD52F341A28CD6" + }, + { + "Arch": "arm64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/DevolutionsAgent-arm64-2026.1.0.0.msi", + "Hash": "4396B8862DF5EC49E58E4E099ADB707202E0DAA730AFAAC10EACCAD428B062FA" + }, + { + "Arch": "x64", + "Type": "deb", + "Url": "https://cdn.devolutions.net/download/devolutions-agent_2026.1.0.0_amd64.deb", + "Hash": "B05A1CD591238CF2050E540F7000584AAAD1CBE3A8D5CDBAC812CBF53D6D6564" + } + ] + }, + "Beta": { + "Version": "2026.1.0.0", + "Date": "2026-02-27", + "Files": [ + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/DevolutionsAgent-x86_64-2026.1.0.0.msi", + "Hash": "794DB95AA3A846D0135E7233D89AE9A61400A80CAAA046BA4EBD52F341A28CD6" + }, + { + "Arch": "arm64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/DevolutionsAgent-arm64-2026.1.0.0.msi", + "Hash": "4396B8862DF5EC49E58E4E099ADB707202E0DAA730AFAAC10EACCAD428B062FA" + }, + { + "Arch": "x64", + "Type": "deb", + "Url": "https://cdn.devolutions.net/download/devolutions-agent_2026.1.0.0_amd64.deb", + "Hash": "B05A1CD591238CF2050E540F7000584AAAD1CBE3A8D5CDBAC812CBF53D6D6564" + } + ] + } + }, + "PowerShell": { + "Current": { + "Version": "2025.3.4.0", + "Date": "2026-02-02", + "Files": [ + { + "Arch": "Any", + "Type": "nupkg", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShell.2025.3.4.0.nupkg", + "Hash": "63FB9ADECD0AB10F1FEE187F06030225AF9FF4D526AF8469AC8BC9279483BA9C" + } + ] + } + }, + "PowerShellUniversal": { + "Current": { + "Version": "2026.1.3.0", + "Date": "2026-02-27", + "Files": [ + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.2026.1.3.0.msi", + "Hash": "E555093C06E64EA096D1162EEA08BF5FAD020D1617668DD4C8433C589D3373E9" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.Agent.2026.1.3.0.msi", + "Hash": "9CBE8D03EA2D41356BEC4A95DD257E5CAEEF539A8056A3BEED73F8C5ED12E17A" + }, + { + "Arch": "x64", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.win-x64.2026.1.3.0.zip", + "Hash": "B5B0DC1ABE87F11FF6F7B245E50BC9538FFCF7B3328521D309F36EEB7DDA0172" + }, + { + "Arch": "arm64", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.win-arm64.2026.1.3.0.zip", + "Hash": "E57F71BF173C96B8EB41317953AA8E4239EF31C5A7C1E8C870922030EA78EB47" + }, + { + "Arch": "x64", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.linux-x64.2026.1.3.0.zip", + "Hash": "4ECD52E4E80BFA73F010842EC228DEB6825A1D322C3C4105D0000AE9275A1F09" + }, + { + "Arch": "arm", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.linux-arm.2026.1.3.0.zip", + "Hash": "4E9267E04B036FD93AD156333138B37365ACC2FD533B6D9E727696902D0C1E7F" + }, + { + "Arch": "arm64", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.linux-arm64.2026.1.3.0.zip", + "Hash": "70D47AC41B41B4A71262BAAFFDAD01E275D8EC5FE8B11FD32A0128884C433779" + }, + { + "Arch": "x64", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.osx-x64.2026.1.3.0.zip", + "Hash": "9168B4EC85AE5980D50CAC0CBA2F798CB4075F7E75367919E7E0938FB1F906E7" + }, + { + "Arch": "arm64", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.osx-arm64.2026.1.3.0.zip", + "Hash": "79865486934ADA2BA527E53401CCAF61C17319346E31FA869C7F5EFBF1DC9CC0" + } + ] + } + }, + "DVLS": { + "Current": { + "Version": "2025.3.15.0", + "Date": "2026-02-09", + "Files": [ + { + "Arch": "Any", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/RDMS/DVLS.2025.3.15.0.zip", + "Hash": "1B47D61293FB601C26B7F0F38E939B9FB8EDA87CB0AE8A3BA0EDD6B783F001DD" + }, + { + "Arch": "x64", + "Type": "tar.gz", + "Url": "https://cdn.devolutions.net/download/RDMS/DVLS.2025.3.15.0.linux-x64.tar.gz", + "Hash": "4B5F44C0FFBBF297A3E61145D09AE9426ECB81B2EC87260A2282DFAF9C689523" + } + ] + }, + "Stable": { + "Version": "2025.2.22.0", + "Date": "2026-01-14", + "Files": [ + { + "Arch": "Any", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/RDMS/DVLS.2025.2.22.0.zip", + "Hash": "E1806A33CA37FB8BB332D58037AFDCD2BD60B536ED56D568920F738C43B2307F" + }, + { + "Arch": "x64", + "Type": "tar.gz", + "Url": "https://cdn.devolutions.net/download/RDMS/DVLS.2025.2.22.0.linux-x64.tar.gz", + "Hash": "5EA811FF3C1F715E53186D4AF09DE6380DF341B8F13B82753116737055EFDC89" + } + ] + }, + "Beta": { + "Version": "2026.1.3.0", + "Date": "2026-02-23", + "Files": [ + { + "Arch": "Any", + "Type": "zip", + "Url": "https://cdn.devolutions.net/download/RDMS/DVLS.2026.1.3.0.zip", + "Hash": "8C085283FFB6492F9411502B8EFD7AB7B56C766C130FEE01CAA2BCCF3CF9964D" + }, + { + "Arch": "x64", + "Type": "tar.gz", + "Url": "https://cdn.devolutions.net/download/RDMS/DVLS.2026.1.3.0.linux-x64.tar.gz", + "Hash": "0C49BE5EF9ED6C5ABB3A66BFBED917C60834943110DBFC98CEAEF1E8133CDBC6" + } + ] + } + }, + "Console": { + "Current": { + "Version": "2025.3.15.0", + "Date": "2026-02-09", + "Files": [ + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.DVLS.Console.2025.3.15.0.exe", + "Hash": "5B107771BF9B6A1BFEA86C7712C25332A9FA0A314D66446C3B6A4612A3BAA6BF" + } + ] + }, + "Beta": { + "Version": "2026.1.3.0", + "Date": "2026-02-23", + "Files": [ + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.DVLS.Console.2026.1.3.0.exe", + "Hash": "02C78B0B23A6A471C9F6CA8795F38C30C8B55FC10FFE97C2A248FBEBE2C376FB" + } + ] + } + }, + "Gateway": { + "Current": { + "Version": "2026.1.0.0", + "Date": "2026-02-27", + "Files": [ + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/DevolutionsGateway-x86_64-2026.1.0.0.msi", + "Hash": "0253C4267FB1B1699BBBC1C783C37514D1059051C5678FFEFDE7B390724A4756" + }, + { + "Arch": "x64", + "Type": "deb", + "Url": "https://cdn.devolutions.net/download/devolutions-gateway_2026.1.0.0_amd64.deb", + "Hash": "2612A9F18D9227D014CA096EDC76BA0BF34E6CBAAE773849B631FAFEA8D0DE31" + } + ] + }, + "Beta": { + "Version": "2026.1.0.0", + "Date": "2026-02-27", + "Files": [ + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/DevolutionsGateway-x86_64-2026.1.0.0.msi", + "Hash": "0253C4267FB1B1699BBBC1C783C37514D1059051C5678FFEFDE7B390724A4756" + }, + { + "Arch": "x64", + "Type": "deb", + "Url": "https://cdn.devolutions.net/download/devolutions-gateway_2026.1.0.0_amd64.deb", + "Hash": "2612A9F18D9227D014CA096EDC76BA0BF34E6CBAAE773849B631FAFEA8D0DE31" + } + ] + } + }, + "Launcher": { + "Current": { + "Version": "2025.3.32.0", + "Date": "2026-02-26", + "Files": [ + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.Devolutions.Launcher.2025.3.32.0.exe", + "Hash": "CBC70FAFA11A0B677BE01158F9E39F407DC17A1EAB413B0F428CFC913288031D" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.Devolutions.Launcher.2025.3.32.0.msi", + "Hash": "968C24593650FF5C1B9D3B8888D331769199B71C0667283921DA634F176BFDD5" + } + ] + }, + "Beta": { + "Version": "2026.1.8.0", + "Date": "2026-02-19", + "Files": [ + { + "Arch": "x64", + "Type": "exe", + "Url": "https://cdn.devolutions.net/download/Setup.Devolutions.Launcher.2026.1.8.0.exe", + "Hash": "241AA246DC15845A505F0794F5DADB19DFC37D7F4A83A6633796C18036013021" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Setup.Devolutions.Launcher.2026.1.8.0.msi", + "Hash": "FE8E895FFF609010AF2C8574DA7F22EDCE4758E5F9B76782CE8FA7076D6380D0" + } + ] + } + }, + "HubServices": { + "Current": { + "Version": "2025.3.1.1", + "Date": "2025-10-07", + "Files": [ + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Hub/Services/Setup.Devolutions.Hub.Services.2025.3.1.1.msi", + "Hash": "2802128648097C2C5BBD061ADCFCA0A509E9FDF29662619F9721C2494F7521F3" + } + ] + } + }, + "WorkspaceWindows": { + "Current": { + "Version": "2025.3.4.0", + "Date": "2025-12-03", + "Files": [ + { + "Arch": "x64", + "Type": "msix", + "Url": "https://cdn.devolutions.net/download/Devolutions.Workspace-2025.3.4.0-x64.msix", + "Hash": "445968BFDC0CC208D95BD55ED59C6E124C3E0FF0EEE2F8F101662663173A3FB6" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Devolutions.Workspace-2025.3.4.0-x64.msi", + "Hash": "9D6CABD84990E69BD774EA6E7BDDF7CCD759C133CA564098F6816AC1E8266524" + } + ] + }, + "Beta": { + "Version": "2025.3.4.0", + "Date": "2025-12-03", + "Files": [ + { + "Arch": "x64", + "Type": "msix", + "Url": "https://cdn.devolutions.net/download/Devolutions.Workspace-2025.3.4.0-x64.msix", + "Hash": "445968BFDC0CC208D95BD55ED59C6E124C3E0FF0EEE2F8F101662663173A3FB6" + }, + { + "Arch": "x64", + "Type": "msi", + "Url": "https://cdn.devolutions.net/download/Devolutions.Workspace-2025.3.4.0-x64.msi", + "Hash": "9D6CABD84990E69BD774EA6E7BDDF7CCD759C133CA564098F6816AC1E8266524" + } + ] + } + }, + "WorkspaceMac": { + "Current": { + "Version": "2025.3.4.0", + "Date": "2025-12-03", + "Files": [ + { + "Arch": "universal", + "Type": "dmg", + "Url": "https://cdn.devolutions.net/download/Mac/Workspace/2025.3.4.0/Devolutions.Workspace.2025.3.4.0.dmg", + "Hash": "B83F7DC1C47A0384B63DEE9154C76609A27A4B4C11A7AB95893C2125F78FB44F" + }, + { + "Arch": "universal", + "Type": "pkg", + "Url": "https://cdn.devolutions.net/download/Mac/Workspace/2025.3.4.0/Devolutions.Workspace.2025.3.4.0.pkg", + "Hash": "1C7D173E919D420D34208AEE9C8D389911CF4917F08E9B34659022B9028D1A3F" + } + ] + }, + "Beta": { + "Version": "2025.3.4.0", + "Date": "2025-12-03", + "Files": [ + { + "Arch": "universal", + "Type": "dmg", + "Url": "https://cdn.devolutions.net/download/Mac/Workspace/2025.3.4.0/Devolutions.Workspace.2025.3.4.0.dmg", + "Hash": "B83F7DC1C47A0384B63DEE9154C76609A27A4B4C11A7AB95893C2125F78FB44F" + }, + { + "Arch": "universal", + "Type": "pkg", + "Url": "https://cdn.devolutions.net/download/Mac/Workspace/2025.3.4.0/Devolutions.Workspace.2025.3.4.0.pkg", + "Hash": "1C7D173E919D420D34208AEE9C8D389911CF4917F08E9B34659022B9028D1A3F" + } + ] + } + }, + "WorkspaceLinux": { + "Current": { + "Version": "2025.3.4.0", + "Date": "2025-12-03", + "Files": [ + { + "Arch": "x64", + "Type": "deb", + "Url": "https://cdn.devolutions.net/download/Linux/Workspace/2025.3.4.0/workspace_2025.3.4.0_amd64.deb", + "Hash": "AACED08BDE0800A6D998683A8651DA1608D140C1C9C526C85833564C0DD980CC" + } + ] + }, + "Beta": { + "Version": "2025.3.4.0", + "Date": "2025-12-03", + "Files": [ + { + "Arch": "x64", + "Type": "deb", + "Url": "https://cdn.devolutions.net/download/Linux/Workspace/2025.3.4.0/workspace_2025.3.4.0_amd64.deb", + "Hash": "AACED08BDE0800A6D998683A8651DA1608D140C1C9C526C85833564C0DD980CC" + } + ] + } + }, + "WorkspaceFirefox": { + "Current": { + "Version": "2025.3.3.5", + "Date": "2026-01-14", + "Files": [ + { + "Arch": "Any", + "Type": "xpi", + "Url": "https://cdn.devolutions.net/download/BrowserExtension/DevolutionsWebLoginFirefox.2025.3.3.5.xpi", + "Hash": "" + } + ] + } + }, + "WorkspaceChrome": { + "Current": { + "Version": "2025.3.3.4", + "Date": "2026-01-14", + "Files": [ + { + "Arch": "Any", + "Type": "web", + "Url": "https://chrome.google.com/webstore/detail/devolutions-web-login/neimonjjffhehnojilepgfejkneaidmo?hl=en-US&gl=CA", + "Hash": "" + } + ] + } + }, + "WorkspaceEdge": { + "Current": { + "Version": "2025.3.3.4", + "Date": "2026-01-14", + "Files": [ + { + "Arch": "Any", + "Type": "web", + "Url": "https://microsoftedge.microsoft.com/addons/detail/ddloeodolhdfbohkokiflfbacbfpjahp", + "Hash": "" + } + ] + } + }, + "WorkspaceOpera": { + "Current": { + "Version": "2025.3.3.5", + "Date": "2026-01-14", + "Files": [ + { + "Arch": "Any", + "Type": "crx", + "Url": "https://cdn.devolutions.net/download/BrowserExtension/DevolutionsWebLoginOpera.2025.3.3.5.crx", + "Hash": "" + } + ] + } + }, + "WorkspaceSafari": { + "Current": { + "Version": "2025.3.0.3", + "Date": "2025-10-07", + "Files": [ + { + "Arch": "Any", + "Type": "web", + "Url": "https://apps.apple.com/us/app/devolutions-workspace/id1462282993", + "Hash": "" + } + ] + } + }, + "Devolutions.UniGetUI": { + "Current": { + "Version": "3.3.99.0", + "Date": "2026-02-27", + "Files": [ + { + "Arch": "x64", + "Type": "exe", + "Url": "http://127.0.0.1:8080/UniGetUI.Installer.x64.exe", + "Hash": "E870E68BAF732DFAD6275BC0F851D2BB8FCD2CB00DEB514459A997FCDDBFFC35" + }, + { + "Arch": "arm64", + "Type": "exe", + "Url": "http://127.0.0.1:8080/UniGetUI.Installer.arm64.exe", + "Hash": "67F89B83907851552925F16262DE6F2DB3E551A4DF6C37265C625AAC0E79AB14" + }, + { + "Arch": "x64", + "Type": "zip", + "Url": "http://127.0.0.1:8080/UniGetUI.x64.zip", + "Hash": "7CB3F3D725AC1FE4DF0835FB291E8F9611826ACF57FB3842AE374BD5B121542C" + }, + { + "Arch": "arm64", + "Type": "zip", + "Url": "http://127.0.0.1:8080/UniGetUI.arm64.zip", + "Hash": "5D4B3606BC772BCFF375226249DAF63524551BBAE7F18B1AE6110BD326680FCD" + } + ] + }, + "Beta": { + "Version": "3.4.0.0", + "Date": "2026-02-27", + "Files": [ + { + "Arch": "x64", + "Type": "exe", + "Url": "http://127.0.0.1:8080/UniGetUI.Installer.x64.exe", + "Hash": "E870E68BAF732DFAD6275BC0F851D2BB8FCD2CB00DEB514459A997FCDDBFFC35" + }, + { + "Arch": "arm64", + "Type": "exe", + "Url": "http://127.0.0.1:8080/UniGetUI.Installer.arm64.exe", + "Hash": "67F89B83907851552925F16262DE6F2DB3E551A4DF6C37265C625AAC0E79AB14" + } + ] + } + } +}