diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c20f8c7a..7434884e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -134,6 +134,9 @@ jobs: lipo -create -output macos-universal/UpdateMac macos-x64/UpdateMac macos-arm64/UpdateMac file macos-universal/UpdateMac lipo -archs macos-universal/UpdateMac + - uses: geekyeggo/delete-artifact@v5 + with: + name: rust-macos-* - name: Upload Universal Binary uses: actions/upload-artifact@v4 with: @@ -172,52 +175,75 @@ jobs: sudo add-apt-repository universe sudo apt install libfuse2 if: ${{ matrix.os == 'ubuntu-latest' }} + - name: Install squashfs-tools + run: brew install squashfs + if: ${{ matrix.os == 'macos-latest' }} - name: Install dotnet-coverage run: dotnet tool install -g dotnet-coverage - name: Build .NET run: dotnet build -c Release - - name: Wait for artifacts - shell: pwsh - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # for gh cli - run: | - # Wait 15 minutes for the artifact to become available - $artifactName = "rust-${{ matrix.os }}" - $maxAttempts = 90 - $sleepSeconds = 10 - $attempt = 0 - $workflowRunId = $env:GITHUB_RUN_ID - - Write-Host "Waiting for artifact '$artifactName' to become available in workflow run $workflowRunId..." - while ($attempt -lt $maxAttempts) { - $artifactsJson = gh api repos/$env:GITHUB_REPOSITORY/actions/runs/$workflowRunId/artifacts - $artifacts = $artifactsJson | ConvertFrom-Json - if ($artifacts.artifacts | Where-Object { $_.name -eq $artifactName }) { - Write-Host "Artifact '$artifactName' is available, continuing..." - break - } - Write-Host "Artifact not available yet. Attempt $($attempt + 1)/$maxAttempts" - Start-Sleep -Seconds $sleepSeconds - $attempt++ - } - - if ($attempt -ge $maxAttempts) { - Write-Host "Error: Artifact '$artifactName' did not become available in time." - exit 1 - } + - uses: caesay/wait-artifact-action@494939e840383463b1686ce3624a8aab059c2c8b + with: + token: ${{ secrets.GITHUB_TOKEN }} + max_wait_seconds: 900 + artifacts: rust-macos-latest,rust-windows-latest,rust-ubuntu-latest + verbose: true - name: Download Rust Artifacts uses: actions/download-artifact@v4 with: - name: rust-${{ matrix.os }} path: src/Rust/target/release + pattern: rust-* + merge-multiple: true - name: Test .NET run: dotnet test --no-build -c Release -l "console;verbosity=detailed;consoleLoggerParameters=ErrorsOnly" -l GithubActions -- RunConfiguration.CollectSourceInformation=true + - name: Upload Cross-Compile Artifacts + uses: actions/upload-artifact@v4 + with: + name: cross-${{ matrix.os }} + path: test/artifacts/* - name: Upload Coverage Artifacts uses: actions/upload-artifact@v4 with: name: coverage-dotnet-${{ matrix.os }} path: ./test/*.xml + test-cross: + strategy: + matrix: + os: [windows-latest, ubuntu-latest] + needs: [test-dotnet] + runs-on: ${{ matrix.os }} + env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Install FUSE + run: | + sudo add-apt-repository universe + sudo apt install libfuse2 + if: ${{ matrix.os == 'ubuntu-latest' }} + - name: Download Cross Artifacts + uses: actions/download-artifact@v4 + with: + path: test/artifacts + pattern: cross-* + merge-multiple: true + - name: Test Cross-Compiled Apps + env: + VELOPACK_CROSS_ARTIFACTS: true + run: dotnet test -c Release test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj --filter "FullyQualifiedName~RunCrossApp" -l "console;verbosity=detailed;consoleLoggerParameters=ErrorsOnly" -l GithubActions -- RunConfiguration.CollectSourceInformation=true + - uses: geekyeggo/delete-artifact@v5 + with: + name: cross-* + package: runs-on: ubuntu-latest needs: [build-rust-windows, build-rust-linux, build-mac-universal] @@ -227,21 +253,12 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Download Rust OSX + - name: Download Rust Artifacts uses: actions/download-artifact@v4 with: - name: rust-macos-latest - path: src/Rust/target/release - - name: Download Rust Windows - uses: actions/download-artifact@v4 - with: - name: rust-windows-latest - path: src/Rust/target/release - - name: Download Rust Linux - uses: actions/download-artifact@v4 - with: - name: rust-ubuntu-latest path: src/Rust/target/release + pattern: rust-* + merge-multiple: true - name: Build .NET run: dotnet build -c Release /p:PackRustAssets=true /p:ContinuousIntegrationBuild=true - name: Upload Package Artifacts @@ -267,6 +284,9 @@ jobs: with: directory: ./test fail_ci_if_error: true + - uses: geekyeggo/delete-artifact@v5 + with: + name: coverage-* # - name: Publish to GitHub Packages diff --git a/Velopack.sln b/Velopack.sln index f1558f2c..1a2b468a 100644 --- a/Velopack.sln +++ b/Velopack.sln @@ -54,10 +54,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VeloWpfSample", "samples\Ve EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Divergic.Logging.Xunit", "test\Divergic.Logging.Xunit\Divergic.Logging.Xunit.csproj", "{5ED2E9AF-101D-4D2D-B0B5-90A920EF692D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velopack.Packaging.HostModel", "src\Velopack.Packaging.HostModel\Velopack.Packaging.HostModel.csproj", "{E9A2620C-C638-446C-BA30-F62C05709365}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velopack.Build", "src\Velopack.Build\Velopack.Build.csproj", "{97C9B2CF-877F-4C98-A513-058784A23697}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velopack.IcoLib", "src\Velopack.IcoLib\Velopack.IcoLib.csproj", "{8A0A980A-D51C-458E-8942-00BC900FD2D0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -120,14 +120,14 @@ Global {5ED2E9AF-101D-4D2D-B0B5-90A920EF692D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5ED2E9AF-101D-4D2D-B0B5-90A920EF692D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5ED2E9AF-101D-4D2D-B0B5-90A920EF692D}.Release|Any CPU.Build.0 = Release|Any CPU - {E9A2620C-C638-446C-BA30-F62C05709365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E9A2620C-C638-446C-BA30-F62C05709365}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E9A2620C-C638-446C-BA30-F62C05709365}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E9A2620C-C638-446C-BA30-F62C05709365}.Release|Any CPU.Build.0 = Release|Any CPU {97C9B2CF-877F-4C98-A513-058784A23697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {97C9B2CF-877F-4C98-A513-058784A23697}.Debug|Any CPU.Build.0 = Debug|Any CPU {97C9B2CF-877F-4C98-A513-058784A23697}.Release|Any CPU.ActiveCfg = Release|Any CPU {97C9B2CF-877F-4C98-A513-058784A23697}.Release|Any CPU.Build.0 = Release|Any CPU + {8A0A980A-D51C-458E-8942-00BC900FD2D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A0A980A-D51C-458E-8942-00BC900FD2D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A0A980A-D51C-458E-8942-00BC900FD2D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A0A980A-D51C-458E-8942-00BC900FD2D0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ab86c120..b251166e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,7 +10,7 @@ true false $(MSBuildThisFileDirectory)SelfContained.targets - $(NoWarn);NETSDK1188 + $(NoWarn);NETSDK1188;NU5100 en $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=./ diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 57f2441c..832cb3cf 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -35,10 +35,7 @@ - - - - + diff --git a/src/Velopack.Build/PublishTask.cs b/src/Velopack.Build/PublishTask.cs index a4bf0dc3..0055b939 100644 --- a/src/Velopack.Build/PublishTask.cs +++ b/src/Velopack.Build/PublishTask.cs @@ -39,7 +39,10 @@ public class PublishTask : MSBuildAsyncTask return true; } - await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl, cancellationToken) + // todo: currently it's not possible to cross-compile for different OSes using Velopack.Build + var targetOs = VelopackRuntimeInfo.SystemOs; + + await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl, targetOs, cancellationToken) .ConfigureAwait(false); return true; diff --git a/src/Velopack.Build/Velopack.Build.csproj b/src/Velopack.Build/Velopack.Build.csproj index c8ce0537..a48e5b33 100644 --- a/src/Velopack.Build/Velopack.Build.csproj +++ b/src/Velopack.Build/Velopack.Build.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Velopack.Build/packages.lock.json b/src/Velopack.Build/packages.lock.json index 2888339f..a7949b56 100644 --- a/src/Velopack.Build/packages.lock.json +++ b/src/Velopack.Build/packages.lock.json @@ -4,13 +4,13 @@ ".NETFramework,Version=v4.7.2": { "Microsoft.Build.Utilities.Core": { "type": "Direct", - "requested": "[17.9.5, )", - "resolved": "17.9.5", - "contentHash": "H2hpVdm7cX/uGJD1HOfab3RKgD5tlnvzQkFqvsrAqGHRi0sqb2w1+hRwERFm23witCjmERnqNgiQjYks6/ds8A==", + "requested": "[17.10.4, )", + "resolved": "17.10.4", + "contentHash": "eEB/tcXkSV+nQgvoa/l53UPtn+KVtKZ8zBceDZsXVTrfE4fA+4+/olrx9W8n2tq4XiESsL9UuGJgCKzqBwQCoQ==", "dependencies": { - "Microsoft.Build.Framework": "17.9.5", + "Microsoft.Build.Framework": "17.10.4", "Microsoft.IO.Redist": "6.0.0", - "Microsoft.NET.StringTools": "17.9.5", + "Microsoft.NET.StringTools": "17.10.4", "System.Collections.Immutable": "8.0.0", "System.Configuration.ConfigurationManager": "8.0.0", "System.Memory": "4.5.5", @@ -30,8 +30,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "Riok.Mapperly": { "type": "Direct", @@ -101,8 +101,8 @@ }, "Microsoft.Build.Framework": { "type": "Transitive", - "resolved": "17.9.5", - "contentHash": "CjRmqu9Wv2fyC1d7NKOuBDXcNMI8+GiXGM6izygB+skGGu4Vf0cBcoPq7AFqZCcMpn5DtZ+y7RpaLpB2qrzanQ==", + "resolved": "17.10.4", + "contentHash": "4qXCwNOXBR1dyCzuks9SwTwFJQO/xmf2wcMislotDWJu7MN/r3xDNoU8Ae5QmKIHPaLG1xmfDkYS7qBVzxmeKw==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0" } @@ -119,8 +119,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -128,27 +128,27 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -166,8 +166,8 @@ }, "Microsoft.NET.StringTools": { "type": "Transitive", - "resolved": "17.9.5", - "contentHash": "C/oPRnjcIZBRzcpl1V06R1eEMCxOGt6mIm+8ioyblELgJEXLM8XjUPuCwljMO52VetsHw54xMcYwU8UEeHEIEg==", + "resolved": "17.10.4", + "contentHash": "wyABaqY+IHCMMSTQmcc3Ca6vbmg5BaEPgicnEgpll+4xyWZWlkQqUwafweUd9VAhBb4jqplMl6voUHQ6yfdUcg==", "dependencies": { "System.Memory": "4.5.5", "System.Runtime.CompilerServices.Unsafe": "6.0.0" @@ -185,8 +185,29 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" + }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==", + "dependencies": { + "System.Memory": "4.5.4", + "System.Threading.Tasks.Extensions": "4.5.2" + } + }, + "SixLabors.ImageSharp": { + "type": "Transitive", + "resolved": "2.1.8", + "contentHash": "ML1++vactR5xMW36wHA2nAX3vc6VpR62qYnpopQdLNGbP8BAJ/ckv1IrGIIFRUEQ0JS9EgWr1y1gEa/81f+HaA==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } }, "System.Buffers": { "type": "Transitive", @@ -261,15 +282,6 @@ "resolved": "4.5.0", "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", - "dependencies": { - "System.Collections.Immutable": "8.0.0", - "System.Memory": "4.5.5" - } - }, "System.Runtime": { "type": "Transitive", "resolved": "4.3.0", @@ -328,6 +340,14 @@ "resolved": "5.0.0", "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "6.0.0", @@ -371,31 +391,32 @@ "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Newtonsoft.Json": "[13.0.1, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" + } + }, + "velopack.icolib": { + "type": "Project", + "dependencies": { + "SixLabors.ImageSharp": "[2.1.8, )" } }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" } }, - "velopack.packaging.hostmodel": { - "type": "Project", - "dependencies": { - "System.Reflection.Metadata": "[8.0.0, )" - } - }, "velopack.packaging.unix": { "type": "Project", "dependencies": { "ELFSharp": "[2.17.3, )", + "SharpZipLib": "[1.4.2, )", "Velopack.Packaging": "[1.0.0, )" } }, @@ -404,20 +425,20 @@ "dependencies": { "AsmResolver.DotNet": "[5.5.1, )", "AsmResolver.PE.Win32Resources": "[5.5.1, )", - "Velopack.Packaging": "[1.0.0, )", - "Velopack.Packaging.HostModel": "[1.0.0, )" + "Velopack.IcoLib": "[1.1.1, )", + "Velopack.Packaging": "[1.0.0, )" } } }, "net6.0": { "Microsoft.Build.Utilities.Core": { "type": "Direct", - "requested": "[17.9.5, )", - "resolved": "17.9.5", - "contentHash": "H2hpVdm7cX/uGJD1HOfab3RKgD5tlnvzQkFqvsrAqGHRi0sqb2w1+hRwERFm23witCjmERnqNgiQjYks6/ds8A==", + "requested": "[17.10.4, )", + "resolved": "17.10.4", + "contentHash": "eEB/tcXkSV+nQgvoa/l53UPtn+KVtKZ8zBceDZsXVTrfE4fA+4+/olrx9W8n2tq4XiESsL9UuGJgCKzqBwQCoQ==", "dependencies": { - "Microsoft.Build.Framework": "17.9.5", - "Microsoft.NET.StringTools": "17.9.5", + "Microsoft.Build.Framework": "17.10.4", + "Microsoft.NET.StringTools": "17.10.4", "Microsoft.Win32.Registry": "5.0.0", "System.Collections.Immutable": "8.0.0", "System.Configuration.ConfigurationManager": "8.0.0", @@ -440,8 +461,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "Riok.Mapperly": { "type": "Direct", @@ -505,8 +526,8 @@ }, "Microsoft.Build.Framework": { "type": "Transitive", - "resolved": "17.9.5", - "contentHash": "CjRmqu9Wv2fyC1d7NKOuBDXcNMI8+GiXGM6izygB+skGGu4Vf0cBcoPq7AFqZCcMpn5DtZ+y7RpaLpB2qrzanQ==", + "resolved": "17.10.4", + "contentHash": "4qXCwNOXBR1dyCzuks9SwTwFJQO/xmf2wcMislotDWJu7MN/r3xDNoU8Ae5QmKIHPaLG1xmfDkYS7qBVzxmeKw==", "dependencies": { "Microsoft.Win32.Registry": "5.0.0", "System.Memory": "4.5.5", @@ -526,8 +547,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -535,26 +556,26 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -563,8 +584,8 @@ }, "Microsoft.NET.StringTools": { "type": "Transitive", - "resolved": "17.9.5", - "contentHash": "C/oPRnjcIZBRzcpl1V06R1eEMCxOGt6mIm+8ioyblELgJEXLM8XjUPuCwljMO52VetsHw54xMcYwU8UEeHEIEg==", + "resolved": "17.10.4", + "contentHash": "wyABaqY+IHCMMSTQmcc3Ca6vbmg5BaEPgicnEgpll+4xyWZWlkQqUwafweUd9VAhBb4jqplMl6voUHQ6yfdUcg==", "dependencies": { "System.Memory": "4.5.5", "System.Runtime.CompilerServices.Unsafe": "6.0.0" @@ -596,8 +617,8 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", @@ -697,6 +718,20 @@ "resolved": "4.3.2", "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "SixLabors.ImageSharp": { + "type": "Transitive", + "resolved": "2.1.8", + "contentHash": "ML1++vactR5xMW36wHA2nAX3vc6VpR62qYnpopQdLNGbP8BAJ/ckv1IrGIIFRUEQ0JS9EgWr1y1gEa/81f+HaA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } + }, "System.Collections": { "type": "Transitive", "resolved": "4.3.0", @@ -1219,28 +1254,32 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" + } + }, + "velopack.icolib": { + "type": "Project", + "dependencies": { + "SixLabors.ImageSharp": "[2.1.8, )" } }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" } }, - "velopack.packaging.hostmodel": { - "type": "Project" - }, "velopack.packaging.unix": { "type": "Project", "dependencies": { "ELFSharp": "[2.17.3, )", + "SharpZipLib": "[1.4.2, )", "Velopack.Packaging": "[1.0.0, )" } }, @@ -1249,8 +1288,8 @@ "dependencies": { "AsmResolver.DotNet": "[5.5.1, )", "AsmResolver.PE.Win32Resources": "[5.5.1, )", - "Velopack.Packaging": "[1.0.0, )", - "Velopack.Packaging.HostModel": "[1.0.0, )" + "Velopack.IcoLib": "[1.1.1, )", + "Velopack.Packaging": "[1.0.0, )" } } } diff --git a/src/Velopack.Deployment/GitHubRepository.cs b/src/Velopack.Deployment/GitHubRepository.cs index d7dad373..ca8cdf78 100644 --- a/src/Velopack.Deployment/GitHubRepository.cs +++ b/src/Velopack.Deployment/GitHubRepository.cs @@ -56,7 +56,7 @@ public class GitHubRepository : SourceRepository - + diff --git a/src/Velopack.Deployment/_Repository.cs b/src/Velopack.Deployment/_Repository.cs index 5fcc5931..f9f2c609 100644 --- a/src/Velopack.Deployment/_Repository.cs +++ b/src/Velopack.Deployment/_Repository.cs @@ -7,7 +7,14 @@ namespace Velopack.Deployment; public class RepositoryOptions : IOutputOptions { - public string Channel { get; set; } = ReleaseEntryHelper.GetDefaultChannel(); + private string _channel; + + public RuntimeOs TargetOs { get; set; } + + public string Channel { + get => _channel ?? ReleaseEntryHelper.GetDefaultChannel(TargetOs); + set => _channel = value; + } public DirectoryInfo ReleaseDir { get; set; } } diff --git a/src/Velopack.Deployment/packages.lock.json b/src/Velopack.Deployment/packages.lock.json index d0c58052..1875d9b3 100644 --- a/src/Velopack.Deployment/packages.lock.json +++ b/src/Velopack.Deployment/packages.lock.json @@ -4,11 +4,11 @@ "net6.0": { "AWSSDK.S3": { "type": "Direct", - "requested": "[3.7.308, )", - "resolved": "3.7.308", - "contentHash": "U4LWi1yTKVK6IFHWQc4anKPJKXj9dPN+dT1oXto4ndXXlr+DVzm9dULkquyXBjAHlr6jf339ojuYlE6t9Mk6pQ==", + "requested": "[3.7.308.8, )", + "resolved": "3.7.308.8", + "contentHash": "lUQgJsj9n/sJo1CRQFsiB2Gqh3rC21I4e5CWR1b+oYBnc9m8LIsGewctX6YQRzGzuv7Im7iIZo7XoirmYhhqEA==", "dependencies": { - "AWSSDK.Core": "[3.7.304.1, 4.0.0)" + "AWSSDK.Core": "[3.7.304.10, 4.0.0)" } }, "Azure.Storage.Blobs": { @@ -34,8 +34,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "Octokit": { "type": "Direct", @@ -45,8 +45,8 @@ }, "AWSSDK.Core": { "type": "Transitive", - "resolved": "3.7.304.1", - "contentHash": "O/gyE5ptF5zEc3QDk3JI3+AOgBfRfmg6eLH5z3x7hUPmV1Wxu0V4Fm86FSyT6czRviMmQGY0q0SVMbWRbP4vDA==" + "resolved": "3.7.304.10", + "contentHash": "0CXnPzoM+KYXODm2bvRW8eYs7ic2VLop45sphL8FBfvxHfBK/3OJgpxEE2InSdWS1Iby+6KXpz3NplJJY5+Y1A==" }, "Azure.Core": { "type": "Transitive", @@ -94,8 +94,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -103,26 +103,26 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -146,8 +146,8 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", @@ -752,16 +752,16 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" } }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" diff --git a/src/Velopack.IcoLib/Binary/BitReader.cs b/src/Velopack.IcoLib/Binary/BitReader.cs new file mode 100644 index 00000000..90fcb677 --- /dev/null +++ b/src/Velopack.IcoLib/Binary/BitReader.cs @@ -0,0 +1,61 @@ +using System; + +namespace Ico.Binary +{ + internal ref struct BitReader + { + private ByteReader reader; + private byte currentByte; + private uint currentBitsRemaining; + + public BitReader(ByteReader reader) + { + this.reader = reader; + this.currentByte = 0; + this.currentBitsRemaining = 0; + } + + public uint NextBit1() + { + return NextBit(1); + } + + public uint NextBit2() + { + return NextBit(2); + } + + public uint NextBit4() + { + return NextBit(4); + } + + public uint NextBit(uint n) + { + EnsureBits(n); + + var shift = (byte)(currentBitsRemaining - n); + var mask = (1u << (int)n) - 1; + var result = (uint)(currentByte >> shift) & mask; + + currentBitsRemaining -= n; + return result; + } + + private void EnsureBits(uint numBitsNeeded) + { + if (0 != (currentBitsRemaining % numBitsNeeded)) + { + throw new Exception("Cannot read unaligned value"); + } + + if (currentBitsRemaining >= numBitsNeeded) + { + return; + } + + currentByte = reader.NextUint8(); + currentBitsRemaining = 8; + } + } +} diff --git a/src/Velopack.IcoLib/Binary/BitWriter.cs b/src/Velopack.IcoLib/Binary/BitWriter.cs new file mode 100644 index 00000000..79b94f6f --- /dev/null +++ b/src/Velopack.IcoLib/Binary/BitWriter.cs @@ -0,0 +1,63 @@ +using System; + +namespace Ico.Binary +{ + internal class BitWriter + { + private ByteWriter writer; + private uint currentBitsRemaining; + + public BitWriter(ByteWriter writer) + { + this.writer = writer; + this.currentBitsRemaining = 0; + } + + public void AddBit1(byte value) + { + AddBits(1, value); + } + + public void AddBit2(byte value) + { + AddBits(2, value); + } + + public void AddBit4(byte value) + { + AddBits(4, value); + } + + public void AddBits(uint numBits, byte value) + { + EnsureBits(numBits); + + if (value != 0) + { + var shift = (int)(currentBitsRemaining - numBits); + + var b = writer.Data[writer.Data.Count - 1]; + b = (byte)(b | (value << shift)); + writer.Data[writer.Data.Count - 1] = b; + } + + currentBitsRemaining -= numBits; + } + + private void EnsureBits(uint numBitsNeeded) + { + if (0 != (currentBitsRemaining % numBitsNeeded)) + { + throw new Exception("Cannot write unaligned value"); + } + + if (currentBitsRemaining >= numBitsNeeded) + { + return; + } + + writer.AddUint8(0); + currentBitsRemaining = 8; + } + } +} diff --git a/src/Velopack.IcoLib/Binary/ByteOrderConverter.cs b/src/Velopack.IcoLib/Binary/ByteOrderConverter.cs new file mode 100644 index 00000000..e973e496 --- /dev/null +++ b/src/Velopack.IcoLib/Binary/ByteOrderConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ico.Binary +{ + public enum ByteOrder + { + LittleEndian, + BigEndian, + NetworkEndian = BigEndian, + } + + public static class ByteOrderConverter + { + private static bool NeedsSwap(ByteOrder endian) => BitConverter.IsLittleEndian != (endian == ByteOrder.LittleEndian); + + public static ushort To(ByteOrder endian, ushort value) => NeedsSwap(endian) ? Swap(value) : value; + + public static uint To(ByteOrder endian, uint value) => NeedsSwap(endian) ? Swap(value) : value; + + public static ulong To(ByteOrder endian, ulong value) => NeedsSwap(endian) ? Swap(value) : value; + + public static int To(ByteOrder endian, int value) => NeedsSwap(endian) ? (int)Swap((uint)value) : value; + + public static ushort Swap(ushort value) => + (ushort)((value & 0xffu) << 8 | (value & 0xff00u) >> 8); + + private static uint Swap(uint value) => + (value & 0x000000FFU) << 24 | (value & 0x0000FF00U) << 8 | + (value & 0x00FF0000U) >> 8 | (value & 0xFF000000U) >> 24; + + private static ulong Swap(ulong value) => + (value & 0x00000000000000FFUL) << 56 | (value & 0x000000000000FF00UL) << 40 | + (value & 0x0000000000FF0000UL) << 24 | (value & 0x00000000FF000000UL) << 8 | + (value & 0x000000FF00000000UL) >> 8 | (value & 0x0000FF0000000000UL) >> 24 | + (value & 0x00FF000000000000UL) >> 40 | (value & 0xFF00000000000000UL) >> 56; + } +} diff --git a/src/Velopack.IcoLib/Binary/ByteReader.cs b/src/Velopack.IcoLib/Binary/ByteReader.cs new file mode 100644 index 00000000..10602695 --- /dev/null +++ b/src/Velopack.IcoLib/Binary/ByteReader.cs @@ -0,0 +1,53 @@ +using System; + +namespace Ico.Binary +{ + public class ByteReader + { + public Memory Data { get; } + public ByteOrder Endianness { get; } + public int SeekOffset { get; set; } + + public ByteReader(Memory data, ByteOrder endianness) + { + Data = data; + Endianness = endianness; + SeekOffset = 0; + } + + public byte NextUint8() + { + return Data.Span[SeekOffset++]; + } + + public ushort NextUint16() + { + var result = BitConverter.ToUInt16(Data.Span.Slice(SeekOffset, 2).ToArray(), 0); + SeekOffset += 2; + return ByteOrderConverter.To(Endianness, result); + } + + public uint NextUint32() + { + var result = BitConverter.ToUInt32(Data.Span.Slice(SeekOffset, 4).ToArray(), 0); + SeekOffset += 4; + return ByteOrderConverter.To(Endianness, result); + } + + public int NextInt32() + { + var result = BitConverter.ToInt32(Data.Span.Slice(SeekOffset, 4).ToArray(), 0); + SeekOffset += 4; + return ByteOrderConverter.To(Endianness, result); + } + + public ulong NextUint64() + { + var result = BitConverter.ToUInt64(Data.Span.Slice(SeekOffset, 8).ToArray(), 0); + SeekOffset += 8; + return ByteOrderConverter.To(Endianness, result); + } + + public bool IsEof => SeekOffset == Data.Length; + } +} diff --git a/src/Velopack.IcoLib/Binary/ByteWriter.cs b/src/Velopack.IcoLib/Binary/ByteWriter.cs new file mode 100644 index 00000000..ed4ad892 --- /dev/null +++ b/src/Velopack.IcoLib/Binary/ByteWriter.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace Ico.Binary +{ + internal class ByteWriter + { + public List Data { get; } = new List(); + public ByteOrder Endianness { get; } + public int SeekOffset { get; set; } = 0; + + public ByteWriter(ByteOrder endianness) + { + Endianness = endianness; + } + + public void AddUint8(byte value) + { + EnsureCapacity(1); + Data[SeekOffset++] = value; + } + + public void AddUint16(ushort value) + { + EnsureCapacity(2); + value = ByteOrderConverter.To(Endianness, value); + foreach (var b in BitConverter.GetBytes(value)) + { + Data[SeekOffset++] = b; + } + } + + public void AddUint32(uint value) + { + EnsureCapacity(4); + value = ByteOrderConverter.To(Endianness, value); + foreach (var b in BitConverter.GetBytes(value)) + { + Data[SeekOffset++] = b; + } + } + + public void AddInt32(int value) + { + EnsureCapacity(4); + value = ByteOrderConverter.To(Endianness, value); + foreach (var b in BitConverter.GetBytes(value)) + { + Data[SeekOffset++] = b; + } + } + + private void EnsureCapacity(int additionalBytesNeeded) + { + while (Data.Count < SeekOffset + additionalBytesNeeded) + { + Data.Add(0); + } + } + + internal void AddBlob(byte[] blob) + { + EnsureCapacity(blob.Length); + foreach (var b in blob) + { + Data[SeekOffset++] = b; + } + } + } +} diff --git a/src/Velopack.IcoLib/Codecs/BmpDecoder.cs b/src/Velopack.IcoLib/Codecs/BmpDecoder.cs new file mode 100644 index 00000000..c681523e --- /dev/null +++ b/src/Velopack.IcoLib/Codecs/BmpDecoder.cs @@ -0,0 +1,302 @@ +using Ico.Binary; +using Ico.Model; +using Ico.Validation; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Ico.Codecs +{ + public static class BmpDecoder + { + public static void DoBitmapEntry(ByteReader reader, ParseContext context, IcoFrame source) + { + var biSize = reader.NextUint32(); + var biWidth = reader.NextInt32(); + var biHeight = reader.NextInt32(); + var biPlanes = reader.NextUint16(); + var biBitCount = reader.NextUint16(); + var biCompression = reader.NextUint32(); + var biSizeImage = reader.NextUint32(); + var biXPelsPerMeter = reader.NextInt32(); + var biYPelsPerMeter = reader.NextInt32(); + var biClrUsed = reader.NextUint32(); + var biClrImportant = reader.NextUint32(); + + if (biSize != FileFormatConstants._bitmapInfoHeaderSize) + { + throw new InvalidIcoFileException(IcoErrorCode.InvalidBitapInfoHeader_ciSize, $"BITMAPINFOHEADER.ciSize should be {FileFormatConstants._bitmapInfoHeaderSize}, was {biSize}.", context); + } + + if (biXPelsPerMeter != 0) + { + context.Reporter.WarnLine(IcoErrorCode.InvalidBitapInfoHeader_biXPelsPerMeter, $"BITMAPINFOHEADER.biXPelsPerMeter should be 0, was {biXPelsPerMeter}.", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + if (biYPelsPerMeter != 0) + { + context.Reporter.WarnLine(IcoErrorCode.InvalidBitapInfoHeader_biYPelsPerMeter, $"BITMAPINFOHEADER.biYPelsPerMeter should be 0, was {biYPelsPerMeter}.", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + if (biCompression == FileFormatConstants.BI_BITFIELDS) + { + throw new InvalidIcoFileException(IcoErrorCode.BitfieldCompressionNotSupported, $"This tool does not implement icon bitmaps that use BI_BITFIELDS compression. (The .ICO file may be okay, although it is certainly unusual.)", context); + } + + if (biCompression != FileFormatConstants.BI_RGB) + { + throw new InvalidIcoFileException(IcoErrorCode.BitmapCompressionNotSupported, $"BITMAPINFOHEADER.biCompression is unknown value ({biCompression}).", context); + } + + if (biHeight != source.Encoding.ClaimedHeight * 2) + { + context.Reporter.WarnLine(IcoErrorCode.MismatchedHeight, $"BITMAPINFOHEADER.biHeight is not exactly double ICONDIRECTORY.bHeight ({biHeight} != 2 * {source.Encoding.ClaimedHeight}).", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + if (biWidth != source.Encoding.ClaimedWidth) + { + context.Reporter.WarnLine(IcoErrorCode.MismatchedWidth, $"BITMAPINFOHEADER.biWidth is not exactly equal to ICONDIRECTORY.bWidth ({biWidth} != 2 * {source.Encoding.ClaimedWidth}).", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + var height = biHeight / 2; + var width = biWidth; + + source.Encoding.ActualHeight = (uint)height; + source.Encoding.ActualWidth = (uint)width; + source.Encoding.ActualBitDepth = biBitCount; + source.Encoding.Type = IcoEncodingType.Bitmap; + source.CookedData = new Image(width, height); + + switch (biBitCount) + { + case 1: + case 2: + case 4: + case 8: + ReadIndexedBitmap(reader, context, biBitCount, biClrUsed, height, width, source); + break; + case 16: + ReadBitmap16(reader, context, height, width, source); + break; + case 24: + ReadBitmap24(reader, context, biClrUsed, height, width, source); + break; + case 32: + ReadBitmap32(reader, context, height, width, source); + break; + default: + throw new InvalidIcoFileException(IcoErrorCode.InvalidBitapInfoHeader_biBitCount, $"BITMAPINFOHEADER.biBitCount is unknown value ({biBitCount}); expected 1, 4, 8, 16, or 32 bit depth.", context); + } + } + + + private static void ReadIndexedBitmap(ByteReader reader, ParseContext context, uint bitDepth, uint colorTableSize, int height, int width, IcoFrame source) + { + var anyReservedChannel = false; + var anyIndexOutOfBounds = false; + + if (colorTableSize == 0) + { + colorTableSize = 1u << (int)bitDepth; + } + + source.Encoding.PaletteSize = colorTableSize; + + if (colorTableSize > 1u << (int)bitDepth) + { + throw new InvalidIcoFileException(IcoErrorCode.InvalidBitapInfoHeader_biClrUsed, $"BITMAPINFOHEADER.biClrUsed is greater than 2^biBitCount (biClrUsed == {colorTableSize}, biBitCount = {bitDepth}).", context); + } + else if (colorTableSize < 1u << (int)bitDepth) + { + context.Reporter.WarnLine(IcoErrorCode.UndersizedColorTable, $"This bitmap uses a color table that is smaller than the bit depth ({colorTableSize} < 2^{bitDepth})", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + var colorTable = new Rgba32[colorTableSize]; + for (var i = 0; i < colorTableSize; i++) + { + var c = new Bgra32 + { + PackedValue = reader.NextUint32() + }; + + if (c.A != 0) + { + anyReservedChannel = true; + } + + c.A = 255; + c.ToRgba32(ref colorTable[i]); + } + + var padding = reader.SeekOffset % 4; + + for (var y = height - 1; y >= 0; y--) + { + var bits = new BitReader(reader); + + for (var x = 0; x < width; x++) + { + var colorIndex = bits.NextBit(bitDepth); + + if (colorIndex >= colorTableSize) + { + anyIndexOutOfBounds = true; + source.CookedData[x, y] = Color.Black; + } + else + { + source.CookedData[x, y] = colorTable[colorIndex]; + } + } + + while ((reader.SeekOffset % 4) != padding) + { + reader.SeekOffset += 1; + } + } + + switch (bitDepth) + { + case 1: + source.Encoding.PixelFormat = BitmapEncoding.Pixel_indexed1; + break; + case 2: + source.Encoding.PixelFormat = BitmapEncoding.Pixel_indexed2; + break; + case 4: + source.Encoding.PixelFormat = BitmapEncoding.Pixel_indexed4; + break; + case 8: + source.Encoding.PixelFormat = BitmapEncoding.Pixel_indexed8; + break; + } + + ReadBitmapMask(reader, context, height, width, source); + + if (anyReservedChannel) + { + context.Reporter.WarnLine(IcoErrorCode.NonzeroAlpha, $"Reserved Alpha channel used in color table.", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + if (anyIndexOutOfBounds) + { + context.Reporter.WarnLine(IcoErrorCode.IndexedColorOutOfBounds, $"Bitmap uses color at illegal index; pixel filled with Black color.", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + } + + private static void ReadBitmap16(ByteReader reader, ParseContext context, int height, int width, IcoFrame source) + { + for (var y = height - 1; y >= 0; y--) + { + for (var x = 0; x < width; x++) + { + var colorValue = reader.NextUint16(); + source.CookedData[x, y] = new Rgba32( + _5To8[colorValue >> 10], + _5To8[(colorValue >> 5) & 0x1f], + _5To8[colorValue & 0x1f], + 255); + } + } + + source.Encoding.PixelFormat = BitmapEncoding.Pixel_rgb15; + ReadBitmapMask(reader, context, height, width, source); + } + + private static readonly byte[] _5To8 = new byte[] + { + 0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255, + }; + + private static void ReadBitmap24(ByteReader reader, ParseContext context, uint colorTableSize, int height, int width, IcoFrame source) + { + reader.SeekOffset += (int)colorTableSize * 4; + + for (var y = height - 1; y >= 0; y--) + { + for (var x = 0; x < width; x++) + { + var b = reader.NextUint8(); + var g = reader.NextUint8(); + var r = reader.NextUint8(); + + source.CookedData[x, y] = new Rgba32(r, g, b, 255); + } + } + + source.Encoding.PixelFormat = BitmapEncoding.Pixel_rgb24; + ReadBitmapMask(reader, context, height, width, source); + } + + private static void ReadBitmap32(ByteReader reader, ParseContext context, int height, int width, IcoFrame source) + { + for (var y = height - 1; y >= 0; y--) + { + for (var x = 0; x < width; x++) + { + var colorValue = new Bgra32 { PackedValue = reader.NextUint32() }; + Rgba32 rgba32Value = source.CookedData[x, y]; // the ref keyword cannot be used on this indexer, so we need a temporary + colorValue.ToRgba32(ref rgba32Value); + source.CookedData[x, y] = rgba32Value; + } + } + + source.Encoding.PixelFormat = BmpUtil.IsAnyAlphaChannel(source.CookedData) + ? BitmapEncoding.Pixel_argb32 + : BitmapEncoding.Pixel_0rgb32; + + ReadBitmapMask(reader, context, height, width, source); + } + + private static void ReadBitmapMask(ByteReader reader, ParseContext context, int height, int width, IcoFrame source) + { + source.Mask = new bool[width, height]; + + var anyMask = false; + var anyMaskedColors = false; + + var padding = reader.SeekOffset % 4; + + for (var y = height - 1; y >= 0; y--) + { + var bits = new BitReader(reader); + + for (var x = 0; x < width; x++) + { + var mask = bits.NextBit1(); + + if (mask == 0) + { + continue; + } + + source.Mask[x, y] = true; + + anyMask = true; + + if (source.CookedData[x, y].R != 0 || source.CookedData[x, y].G != 0 || source.CookedData[x, y].B != 0) + { + anyMaskedColors = true; + } + + //source.CookedData[x, y] = new Rgba32(0, 0, 0, 0); + } + + while ((reader.SeekOffset % 4) != padding) + { + reader.SeekOffset += 1; + } + } + + if (!anyMask) + { + context.Reporter.WarnLine(IcoErrorCode.NoMaskedPixels, $"No bitmap mask.", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + if (anyMaskedColors) + { + context.Reporter.WarnLine(IcoErrorCode.MaskedPixelWithColor, $"Non-black image pixels masked out.", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + } + } +} + diff --git a/src/Velopack.IcoLib/Codecs/BmpEncoder.cs b/src/Velopack.IcoLib/Codecs/BmpEncoder.cs new file mode 100644 index 00000000..4751e812 --- /dev/null +++ b/src/Velopack.IcoLib/Codecs/BmpEncoder.cs @@ -0,0 +1,328 @@ +using Ico.Binary; +using Ico.Model; +using Ico.Validation; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ico.Codecs +{ + public static class BmpEncoder + { + public enum Dialect + { + Ico, + Bmp, + } + + public static byte[] EncodeBitmap(ParseContext context, BitmapEncoding encoding, Dialect dialect, IcoFrame source) + { + context.LastEncodeError = IcoErrorCode.NoError; + + return (BmpUtil.GetBitDepthForPixelFormat(encoding) < 16) + ? EncodeIndexedBitmap(context, encoding, dialect, source) + : EncodeRgbBitmap(source, context, encoding, dialect); + } + + private static byte[] EncodeIndexedBitmap(ParseContext context, BitmapEncoding encoding, Dialect dialect, IcoFrame source) + { + var numBits = BmpUtil.GetBitDepthForPixelFormat(encoding); + + var colorTable = BuildColorTable(1u << numBits, context, source); + if (colorTable == null) + { + context.LastEncodeError = IcoErrorCode.TooManyColorsForBitDepth; + return null; + } + + var writer = new ByteWriter(ByteOrder.LittleEndian); + + EncodeBitmapHeader(source, dialect, encoding, colorTable, writer, out var offsetToImageSize); + + var reverseTable = new Dictionary(); + for (var i = 0; i < colorTable.Length; i++) + { + if (!reverseTable.ContainsKey(colorTable[i])) + { + reverseTable.Add(colorTable[i], i); + } + } + + var offsetToData = (uint)writer.Data.Count; + var padding = writer.Data.Count % 4; + + for (var y = source.CookedData.Height - 1; y >= 0; y--) + { + var bits = new BitWriter(writer); + + for (var x = 0; x < source.CookedData.Width; x++) + { + var color = source.CookedData[x, y]; + + if (source.Mask[x, y]) + { + switch (context.MaskedImagePixelEmitOptions) + { + case StrictnessPolicy.Compliant: + color = new Rgba32(0, 0, 0, 255); + break; + case StrictnessPolicy.PreserveSource: + // Pass through whatever the original pixel was. + break; + case StrictnessPolicy.Loose: + color = colorTable.First(); + break; + } + } + + color.A = 255; + + var index = reverseTable[color]; + bits.AddBits((uint)numBits, (byte)index); + } + + while ((writer.Data.Count % 4) != padding) + { + writer.AddUint8(0); + } + } + + return FinalizeBitmap(source, encoding, dialect, writer, offsetToData, offsetToImageSize); + } + + private static byte[] EncodeRgbBitmap(IcoFrame source, ParseContext context, BitmapEncoding encoding, Dialect dialect) + { + var writer = new ByteWriter(ByteOrder.LittleEndian); + EncodeBitmapHeader(source, dialect, encoding, null, writer, out var offsetToImageSize); + + var offsetToData = (uint)writer.Data.Count; + var padding = writer.Data.Count % 4; + + for (var y = source.CookedData.Height - 1; y >= 0; y--) + { + var bits = new BitWriter(writer); + + for (var x = 0; x < source.CookedData.Width; x++) + { + var color = source.CookedData[x, y]; + + if (source.Mask[x, y]) + { + switch (context.MaskedImagePixelEmitOptions) + { + case StrictnessPolicy.Compliant: + case StrictnessPolicy.Loose: + color = new Rgba32(0, 0, 0, 0); + break; + case StrictnessPolicy.PreserveSource: + // Pass through whatever the original pixel was. + break; + } + } + + switch (encoding) + { + case BitmapEncoding.Pixel_rgb15: + var value = X8To5(color.R) << 10 | X8To5(color.G) << 5 | X8To5(color.B); + writer.AddUint16((ushort)value); + break; + case BitmapEncoding.Pixel_rgb24: + writer.AddUint8(color.B); + writer.AddUint8(color.G); + writer.AddUint8(color.R); + break; + case BitmapEncoding.Pixel_0rgb32: + writer.AddUint8(color.B); + writer.AddUint8(color.G); + writer.AddUint8(color.R); + writer.AddUint8(0); + break; + case BitmapEncoding.Pixel_argb32: + writer.AddUint8(color.B); + writer.AddUint8(color.G); + writer.AddUint8(color.R); + writer.AddUint8(color.A); + break; + } + } + + while ((writer.Data.Count % 4) != padding) + { + writer.AddUint8(0); + } + } + + return FinalizeBitmap(source, encoding, dialect, writer, offsetToData, offsetToImageSize); + } + + private static byte X8To5(uint b) + { + return (byte)(b * 32 / 256); + } + + private static void EncodeBitmapHeader(IcoFrame source, Dialect dialect, BitmapEncoding encoding, Rgba32[] colorTable, ByteWriter writer, out uint offsetToImageSize) + { + if (dialect != Dialect.Ico) + { + writer.AddUint16(FileFormatConstants._bitmapFileMagic); + writer.AddUint32(0); // Size will be filled in later + writer.AddUint32(0); // Reserved + writer.AddUint32(0); // Offset will be filled in later + } + + writer.AddUint32(FileFormatConstants._bitmapInfoHeaderSize); + writer.AddUint32((uint)source.CookedData.Width); + writer.AddUint32((uint)source.CookedData.Height * ((dialect == Dialect.Ico) ? 2u : 1u)); + writer.AddUint16(1); // biPlanes + writer.AddUint16((ushort)BmpUtil.GetBitDepthForPixelFormat(encoding)); // biBitCount + writer.AddUint32(FileFormatConstants.BI_RGB); // biCompression + offsetToImageSize = (uint)writer.SeekOffset; + writer.AddUint32(0); // biSizeImage + writer.AddUint32((dialect == Dialect.Ico) ? 0u : FileFormatConstants._72dpiInPixelsPerMeter); // biXPelsPerMeter + writer.AddUint32((dialect == Dialect.Ico) ? 0u : FileFormatConstants._72dpiInPixelsPerMeter); // biYPelsPerMeter + writer.AddUint32((uint)(colorTable?.Length ?? 0)); // biClrUsed + writer.AddUint32(0); // biClrImportant + + if (colorTable != null) + { + foreach (var color in colorTable) + { + writer.AddUint8(color.B); + writer.AddUint8(color.G); + writer.AddUint8(color.R); + writer.AddUint8(0); + } + } + + if (dialect != Dialect.Ico) + { + while (writer.Data.Count % 4 != 0) + { + writer.AddUint8(0); + } + } + } + + private static byte[] FinalizeBitmap(IcoFrame source, BitmapEncoding encoding, Dialect dialect, ByteWriter writer, uint offsetToData, uint offsetToImageSize) + { + var offsetToEndOfData = writer.SeekOffset; + + if (dialect == Dialect.Ico) + { + var padding = writer.Data.Count % 4; + + var inferMaskFromAlpha = (source.Encoding.PixelFormat == BitmapEncoding.Pixel_argb32 && encoding != BitmapEncoding.Pixel_argb32); + + for (var y = source.CookedData.Height - 1; y >= 0; y--) + { + var bits = new BitWriter(writer); + + for (var x = 0; x < source.CookedData.Width; x++) + { + var mask = inferMaskFromAlpha + ? (source.CookedData[x, y].A == 0) + : source.Mask[x, y]; + + bits.AddBit1((byte)(mask ? 1 : 0)); + } + + while ((writer.Data.Count % 4) != padding) + { + writer.AddUint8(0); + } + } + } + + if (dialect != Dialect.Ico) + { + writer.SeekOffset = 2; + writer.AddUint32((uint)writer.Data.Count); + + writer.SeekOffset = 10; + writer.AddUint32(offsetToData); + } + + writer.SeekOffset = (int)offsetToImageSize; + writer.AddUint32((uint)(offsetToEndOfData - offsetToData)); // biSizeImage + + return writer.Data.ToArray(); + } + + private static Rgba32[] BuildColorTable(uint maxColorTableSize, ParseContext context, IcoFrame source) + { + var colorTable = new Dictionary(); + + for (var y = source.CookedData.Height - 1; y >= 0; y--) + { + for (var x = 0; x < source.CookedData.Width; x++) + { + var color = source.CookedData[x, y]; + + if (source.Mask[x, y]) + { + switch (context.MaskedImagePixelEmitOptions) + { + case StrictnessPolicy.Compliant: + // Ensure an entry is added for black. + color = new Rgba32(0, 0, 0, 0); + break; + case StrictnessPolicy.PreserveSource: + // Pass through whatever the original pixel was. + break; + case StrictnessPolicy.Loose: + // Don't create a palette entry for this pixel. + continue; + } + } + + color.A = 255; + + if (colorTable.ContainsKey(color)) + { + colorTable[color] += 1; + } + else if (colorTable.Count == maxColorTableSize) + { + return null; + } + else + { + colorTable.Add(color, 1); + } + } + } + + if (colorTable.Count == 0) + { + colorTable.Add(new Rgba32(0, 0, 0, 255), 1); + } + + var table = (from c in colorTable + orderby c.Value descending + select c.Key).ToList(); + + var targetPaletteSize = 0u; + + switch (context.AllowPaletteTruncation) + { + case StrictnessPolicy.Compliant: + targetPaletteSize = maxColorTableSize; + break; + case StrictnessPolicy.PreserveSource: + targetPaletteSize = source.Encoding.PaletteSize; + break; + case StrictnessPolicy.Loose: + targetPaletteSize = (uint)table.Count; + break; + } + + while (table.Count < targetPaletteSize) + { + table.Add(new Rgba32(0, 0, 0, 255)); + } + + return table.ToArray(); + } + } +} diff --git a/src/Velopack.IcoLib/Codecs/BmpUtil.cs b/src/Velopack.IcoLib/Codecs/BmpUtil.cs new file mode 100644 index 00000000..f1bc05f2 --- /dev/null +++ b/src/Velopack.IcoLib/Codecs/BmpUtil.cs @@ -0,0 +1,236 @@ +using Ico.Model; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; + +namespace Ico.Codecs +{ + public static class BmpUtil + { + public static bool IsAlphaSignificant(IcoFrame source) + { + if (IsAnyPartialTransparency(source.CookedData)) + { + return true; + } + + return IsAnyPixel(source.CookedData, (x, y, pixel) + => IsCompletelyTransparent(pixel) != source.Mask[x, y]); + } + + public static bool IsAnyPartialTransparency(Image image) + { + return IsAnyPixel(image, (x, y, pixel) + => !IsCompletelyOpaque(pixel) && !IsCompletelyTransparent(pixel)); + } + + public static bool IsAnyAlphaChannel(Image image) + { + return IsAnyPixel(image, (x, y, pixel) + => !IsCompletelyOpaque(pixel)); + } + + public static ulong GetNumberOfDistinctColors(Image image, bool includeAlpha) + { + var colors = new HashSet(); + + if (includeAlpha) + { + ForeachPixel(image, (x, y, pixel) + => colors.Add(pixel.PackedValue)); + } + else + { + ForeachPixel(image, (x, y, pixel) + => colors.Add((uint)((pixel.R << 16) | (pixel.G << 8) | pixel.B))); + } + + return (uint)colors.Count; + } + + private class NumBitsPerChannel + { + public int R { get; set; } = 1; + public int G { get; set; } = 1; + public int B { get; set; } = 1; + public int A { get; set; } = 1; + + public bool RgbLessThan(int depth) + { + return R < depth && G < depth && B < depth; + } + } + + public static int GetMinimumColorDepthForDisplay(Image image) + { + if (IsAnyPartialTransparency(image)) + { + return 32; + } + + if (!IsAnyPixel(image, (x, y, pixel) => !IsBlack(pixel) && !IsWhite(pixel))) + { + return 1; + } + + var bpc = new NumBitsPerChannel(); + ForeachPixel(image, (x, y, pixel) => UpdateNumBitsPerChannel(pixel, bpc)); + + if (bpc.RgbLessThan(2)) + return 3 * 1; + if (bpc.RgbLessThan(3)) + return 3 * 2; + if (bpc.RgbLessThan(5)) + return 3 * 3; + + return 3 * 4; + } + + public static BitmapEncoding GetIdealBitmapEncoding(Image image, bool hasIcoMask) + { + if (IsAnyPartialTransparency(image)) + { + return BitmapEncoding.Pixel_argb32; + } + + var numColors = GetNumberOfDistinctColors(image, !hasIcoMask); + if (numColors <= 2) + return BitmapEncoding.Pixel_indexed1; + if (numColors <= 4) + return BitmapEncoding.Pixel_indexed2; + if (numColors <= 16) + return BitmapEncoding.Pixel_indexed4; + if (numColors <= 256) + return BitmapEncoding.Pixel_indexed8; + + var bpc = new NumBitsPerChannel(); + ForeachPixel(image, (x, y, pixel) => UpdateNumBitsPerChannel(pixel, bpc)); + + if (hasIcoMask || !IsAnyAlphaChannel(image)) + { + if (bpc.RgbLessThan(6)) + return BitmapEncoding.Pixel_rgb15; + return BitmapEncoding.Pixel_rgb24; + } + + return BitmapEncoding.Pixel_argb32; + } + + private static void UpdateNumBitsPerChannel(Rgba32 pixel, NumBitsPerChannel bpc) + { + bpc.R = Math.Max(bpc.R, GetMinimumColorDepth(pixel.R)); + bpc.G = Math.Max(bpc.G, GetMinimumColorDepth(pixel.G)); + bpc.B = Math.Max(bpc.B, GetMinimumColorDepth(pixel.B)); + bpc.A = Math.Max(bpc.A, GetMinimumColorDepth(pixel.A)); + } + + private static int GetMinimumColorDepth(byte channel) + { + if (channel == 255) + return 1; + + var mask = 255; + int depth; + + for (depth = 1; depth < 8; depth++) + { + if ((channel & mask) == 0) + break; + + mask >>= 1; + } + + return depth; + } + + public static bool[,] CreateMaskFromImage(Image image, bool blackIsTransparent) + { + var mask = new bool[image.Width, image.Height]; + + ForeachPixel(image, (x, y, pixel) => + { + if (IsCompletelyTransparent(pixel) || + (blackIsTransparent && IsBlack(pixel))) + { + mask[x, y] = true; + } + }); + + return mask; + } + + public static int GetBitDepthForPixelFormat(BitmapEncoding pixelFormat) + { + switch (pixelFormat) + { + case BitmapEncoding.Pixel_indexed1: + return 1; + case BitmapEncoding.Pixel_indexed2: + return 2; + case BitmapEncoding.Pixel_indexed4: + return 4; + case BitmapEncoding.Pixel_indexed8: + return 8; + case BitmapEncoding.Pixel_rgb15: + return 16; + case BitmapEncoding.Pixel_rgb24: + return 24; + case BitmapEncoding.Pixel_0rgb32: + return 32; + case BitmapEncoding.Pixel_argb32: + return 32; + } + throw new ArgumentException(nameof(pixelFormat)); + } + + private static void ForeachPixel(Image image, Action action) + { + var mask = new bool[image.Width, image.Height]; + + for (int x = 0; x < image.Width; x++) + { + for (int y = 0; y < image.Height; y++) + { + action(x, y, image[x,y]); + } + } + } + + private static bool IsAnyPixel(Image image, Func predicate) + { + var mask = new bool[image.Width, image.Height]; + + for (int x = 0; x < image.Width; x++) + { + for (int y = 0; y < image.Height; y++) + { + if (predicate(x, y, image[x, y])) + return true; + } + } + + return false; + } + + private static bool IsCompletelyTransparent(Rgba32 pixel) + { + return pixel.A == 0; + } + + private static bool IsCompletelyOpaque(Rgba32 pixel) + { + return pixel.A == 255; + } + + private static bool IsBlack(Rgba32 pixel) + { + return pixel.R == 0 && pixel.G == 0 && pixel.B == 0; + } + + private static bool IsWhite(Rgba32 pixel) + { + return pixel.R == 255 && pixel.G == 255 && pixel.B == 255; + } + } +} diff --git a/src/Velopack.IcoLib/Codecs/EncodingOptions.cs b/src/Velopack.IcoLib/Codecs/EncodingOptions.cs new file mode 100644 index 00000000..df3d93f3 --- /dev/null +++ b/src/Velopack.IcoLib/Codecs/EncodingOptions.cs @@ -0,0 +1,26 @@ +namespace Ico.Codecs +{ + public enum MaskedImagePixelEmitOptions + { + Compliant, + PreserveSource, + Loose, + } + + public enum StrictnessPolicy + { + Compliant, + PreserveSource, + Loose, + } + + public enum BestFormatPolicy + { + PreserveSource, + MinimizeStorage, + PngLargeImages, + AlwaysPng, + AlwaysBmp, + Inherited, + } +} diff --git a/src/Velopack.IcoLib/Codecs/FileFormatConstants.cs b/src/Velopack.IcoLib/Codecs/FileFormatConstants.cs new file mode 100644 index 00000000..d6865bc3 --- /dev/null +++ b/src/Velopack.IcoLib/Codecs/FileFormatConstants.cs @@ -0,0 +1,20 @@ +namespace Ico.Codecs +{ + public static class FileFormatConstants + { + public static int MaxIcoFileSize = 20 * 1024 * 1024; + internal static ushort _iconMagicHeader = 0; + internal static ushort _iconMagicType = 1; + internal static ushort _iconMaxEntries = 256; + internal static byte _iconEntryReserved = 0; + internal static ulong _pngHeader = 0x89504e470d0a1a0a; + + internal static uint _bitmapInfoHeaderSize = 40; + internal static uint BI_RGB = 0; + internal static uint BI_BITFIELDS = 3; + + internal static ushort _bitmapFileMagic = 0x4d42; // 'BM' + internal static ushort _bitmapFileHeaderSize = 14; + internal static uint _72dpiInPixelsPerMeter = 2835u; + } +} diff --git a/src/Velopack.IcoLib/Codecs/IcoDecoder.cs b/src/Velopack.IcoLib/Codecs/IcoDecoder.cs new file mode 100644 index 00000000..d60bb739 --- /dev/null +++ b/src/Velopack.IcoLib/Codecs/IcoDecoder.cs @@ -0,0 +1,110 @@ +using Ico.Binary; +using Ico.Model; +using Ico.Validation; +using System; + +namespace Ico.Codecs +{ + public static class IcoDecoder + { + public static void DoFile(byte[] data, ParseContext context, Action processFrame) + { + var reader = new ByteReader(data, ByteOrder.LittleEndian); + + var idReserved = reader.NextUint16(); + var idType = reader.NextUint16(); + var idCount = reader.NextUint16(); + + if (idReserved != FileFormatConstants._iconMagicHeader) + { + throw new InvalidIcoFileException(IcoErrorCode.InvalidIcoHeader_idReserved, $"ICONDIR.idReserved should be {FileFormatConstants._iconMagicHeader}, was {idReserved}.", context); + } + + if (idType != FileFormatConstants._iconMagicType) + { + throw new InvalidIcoFileException(IcoErrorCode.InvalidIconHeader_idType, $"ICONDIR.idType should be {FileFormatConstants._iconMagicType}, was {idType}.", context); + } + + if (idCount == 0 || idCount > FileFormatConstants._iconMaxEntries) + { + throw new InvalidIcoFileException(IcoErrorCode.TooManyFrames, $"ICONDIR.idCount is {idCount}, an implausible value for an ICO file.", context); + } + + for (var i = 0u; i < idCount; i++) + { + context.ImageDirectoryIndex = i; + var source = ProcessIcoFrame(reader, context); + processFrame(source); + } + + context.ImageDirectoryIndex = null; + } + + private static IcoFrame ProcessIcoFrame(ByteReader reader, ParseContext context) + { + var bWidth = reader.NextUint8(); + var bHeight = reader.NextUint8(); + var bColorCount = reader.NextUint8(); + var bReserved = reader.NextUint8(); + var wPlanes = reader.NextUint16(); + var wBitCount = reader.NextUint16(); + var dwBytesInRes = reader.NextUint32(); + var dwImageOffset = reader.NextUint32(); + + if (bWidth != bHeight) + { + context.Reporter.WarnLine(IcoErrorCode.NotSquare, $"Icon is not square ({bWidth}x{bHeight}).", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + if (bReserved != FileFormatConstants._iconEntryReserved) + { + throw new InvalidIcoFileException(IcoErrorCode.InvalidFrameHeader_bReserved, $"ICONDIRECTORY.bReserved should be {FileFormatConstants._iconEntryReserved}, was {bReserved}.", context); + } + + if (wPlanes > 1) + { + throw new InvalidIcoFileException(IcoErrorCode.InvalidFrameHeader_wPlanes, $"ICONDIRECTORY.wPlanes is {wPlanes}. Only single-plane bitmaps are supported.", context); + } + + if (dwBytesInRes > int.MaxValue) + { + throw new InvalidIcoFileException(IcoErrorCode.InvalidFrameHeader_dwBytesInRes, $"ICONDIRECTORY.dwBytesInRes == {dwBytesInRes}, which is unreasonably large.", context); + } + + if (dwImageOffset > int.MaxValue) + { + throw new InvalidIcoFileException(IcoErrorCode.InvalidFrameHeader_dwImageOffset, $"ICONDIRECTORY.dwImageOffset == {dwImageOffset}, which is unreasonably large.", context); + } + + var source = new IcoFrame + { + TotalDiskUsage = dwBytesInRes + /* sizeof(ICONDIRENTRY) */ 16, + + Encoding = new IcoFrameEncoding + { + ClaimedBitDepth = wBitCount, + ClaimedHeight = bHeight > 0 ? bHeight : 256u, + ClaimedWidth = bWidth > 0 ? bWidth : 256u, + }, + }; + + source.RawData = reader.Data.Slice((int)dwImageOffset, (int)dwBytesInRes).ToArray(); + var bitmapHeader = new ByteReader(source.RawData, ByteOrder.LittleEndian); + + var signature = bitmapHeader.NextUint64(); + bitmapHeader.SeekOffset = 0; + + if (PngDecoder.IsProbablyPngFile(ByteOrderConverter.To(ByteOrder.NetworkEndian, signature))) + { + PngDecoder.DoPngEntry(bitmapHeader, context, source); + } + else + { + BmpDecoder.DoBitmapEntry(bitmapHeader, context, source); + } + + return source; + } + + } +} diff --git a/src/Velopack.IcoLib/Codecs/IcoEncoder.cs b/src/Velopack.IcoLib/Codecs/IcoEncoder.cs new file mode 100644 index 00000000..0256dc9f --- /dev/null +++ b/src/Velopack.IcoLib/Codecs/IcoEncoder.cs @@ -0,0 +1,53 @@ +using Ico.Binary; +using Ico.Model; +using System.Collections.Generic; +using System.IO; + +namespace Ico.Codecs +{ + public static class IcoEncoder + { + public static void EmitIco(string outputPath, ParseContext context) + { + var writer = new ByteWriter(ByteOrder.LittleEndian); + + writer.AddUint16(FileFormatConstants._iconMagicHeader); + writer.AddUint16(FileFormatConstants._iconMagicType); + writer.AddUint16((ushort)context.GeneratedFrames.Count); + + var offsets = new Queue(); + + foreach (var frame in context.GeneratedFrames) + { + var width = frame.Encoding.ClaimedWidth; + var height = frame.Encoding.ClaimedHeight; + var bitDepth = frame.Encoding.ClaimedBitDepth; + + writer.AddUint8((byte)(width >= 256 ? 0 : width)); // bWidth + writer.AddUint8((byte)(height >= 256 ? 0 : height)); // bHeight + writer.AddUint8((byte)(bitDepth < 8 ? 1u << (int)bitDepth : 0)); // bColorCount + writer.AddUint8(0); // bReserved + writer.AddUint16(1); // wPlanes + writer.AddUint16((ushort)bitDepth); // wBitCount + writer.AddUint32((uint)frame.RawData.Length); // dwBytesInRes + + offsets.Enqueue((uint)writer.SeekOffset); + writer.AddUint32(0); // dwImageOffset (will fix later) + } + + foreach (var frame in context.GeneratedFrames) + { + var currentOffset = writer.SeekOffset; + writer.SeekOffset = (int)offsets.Dequeue(); + writer.AddUint32((uint)currentOffset); // dwImageOffset + writer.SeekOffset = currentOffset; + + writer.AddBlob(frame.RawData); + + frame.TotalDiskUsage = (uint)frame.RawData.Length + /* sizeof(ICONDIRENTRY) */ 16; + } + + File.WriteAllBytes(outputPath, writer.Data.ToArray()); + } + } +} diff --git a/src/Velopack.IcoLib/Codecs/PngDecoder.cs b/src/Velopack.IcoLib/Codecs/PngDecoder.cs new file mode 100644 index 00000000..9cc95bce --- /dev/null +++ b/src/Velopack.IcoLib/Codecs/PngDecoder.cs @@ -0,0 +1,157 @@ +using Ico.Binary; +using Ico.Model; +using Ico.Validation; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; +using System.Threading; + +namespace Ico.Codecs +{ + public static class PngDecoder + { + private const int _ihdrChunkName = 0x49484452; // "IHDR" + + public static bool IsProbablyPngFile(ulong first8Bytes) + { + return FileFormatConstants._pngHeader == first8Bytes; + } + + public static void DoPngEntry(ByteReader bitmapHeader, ParseContext context, IcoFrame source) + { + if (source.Encoding.ClaimedBitDepth != 32) + { + context.Reporter.WarnLine(IcoErrorCode.PngNot32Bit, $"PNG-encoded image with bit depth {source.Encoding.ClaimedBitDepth} (expected 32).", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + using (var stream = new MemoryStream(bitmapHeader.Data.ToArray())) + { + var decoder = new SixLabors.ImageSharp.Formats.Png.PngDecoder(); + source.CookedData = decoder.Decode(new Configuration(), stream, CancellationToken.None); + } + + source.Encoding.Type = IcoEncodingType.Png; + source.Encoding.PixelFormat = BmpUtil.IsAnyPartialTransparency(source.CookedData) ? BitmapEncoding.Pixel_argb32 : BitmapEncoding.Pixel_0rgb32; + source.Mask = GenerateMaskFromAlpha(source.CookedData); + + // Conservatively assume that the output wouldn't have used palette trimming, if it had been a bmp frame. + if (source.Encoding.ClaimedBitDepth < 16) + { + source.Encoding.PaletteSize = 1u << (int)source.Encoding.ClaimedBitDepth; + } + + var encoding = GetPngFileEncoding(bitmapHeader.Data); + if (encoding.ColorType != PngColorType.RGBA) + { + context.Reporter.WarnLine(IcoErrorCode.PngNotRGBA32, $"ICO files require the embedded PNG image to be encoded in RGBA32 format; this is {encoding.ColorType}", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + else if (encoding.BitsPerChannel != 8) + { + context.Reporter.WarnLine(IcoErrorCode.PngNotRGBA32, $"ICO files require the embedded PNG image to be encoded in RGBA32 format; this is RGBA{encoding.BitsPerChannel * 4}", context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + + uint numChannels = 0; + switch (encoding.ColorType) + { + case PngColorType.Grayscale: + numChannels = 1; + break; + case PngColorType.RGB: + numChannels = 3; + break; + case PngColorType.GrayscaleAlpha: + numChannels = 2; + break; + case PngColorType.RGBA: + numChannels = 4; + break; + case PngColorType.Palette: + default: + break; + } + + source.Encoding.ActualHeight = encoding.Height; + source.Encoding.ActualWidth = encoding.Width; + source.Encoding.ActualBitDepth = encoding.BitsPerChannel * numChannels; + } + + public static PngFileEncoding GetPngFileEncoding(Memory data) + { + var reader = new ByteReader(data, Ico.Binary.ByteOrder.NetworkEndian); + if (FileFormatConstants._pngHeader != reader.NextUint64()) + { + throw new InvalidPngFileException(IcoErrorCode.NotPng, $"Data stream does not begin with the PNG magic constant"); + } + + var chunkLength = reader.NextUint32(); + var chunkType = reader.NextUint32(); + + if (chunkType != _ihdrChunkName) + { + throw new InvalidPngFileException(IcoErrorCode.PngBadIHDR, $"PNG file should begin with IHDR chunk; found {chunkType} instead"); + } + + if (chunkLength < 13) + { + throw new InvalidPngFileException(IcoErrorCode.PngBadIHDR, $"IHDR chunk is invalid length {chunkLength}; expected at least 13 bytes"); + } + + var result = new PngFileEncoding + { + Width = reader.NextUint32(), + Height = reader.NextUint32(), + BitsPerChannel = reader.NextUint8(), + ColorType = (PngColorType)reader.NextUint8(), + }; + + if (result.Width == 0 || result.Height == 0) + { + throw new InvalidPngFileException(IcoErrorCode.PngIllegalInputDimensions, $"Illegal Width x Height of {result.Width} x {result.Height}"); + } + + switch (result.BitsPerChannel) + { + case 1: + case 2: + case 4: + case 8: + case 16: + break; + default: + throw new InvalidPngFileException(IcoErrorCode.PngIllegalInputDepth, $"Illegal bits per color channel / palette entry of {result.BitsPerChannel}"); + } + + switch (result.ColorType) + { + case PngColorType.Grayscale: + case PngColorType.RGB: + case PngColorType.Palette: + case PngColorType.GrayscaleAlpha: + case PngColorType.RGBA: + break; + default: + throw new InvalidPngFileException(IcoErrorCode.PngIllegalColorType, $"Illegal color type {result.ColorType}"); + } + + return result; + } + + private static bool[,] GenerateMaskFromAlpha(Image image) + { + var mask = new bool[image.Width, image.Height]; + + for (var x = 0; x < image.Width; x++) + { + for (var y = 0; y < image.Height; y++) + { + var alpha = image[x, y].A; + mask[x, y] = alpha == 0; + } + } + + return mask; + } + + } +} diff --git a/src/Velopack.IcoLib/Host/ExceptionWrapper.cs b/src/Velopack.IcoLib/Host/ExceptionWrapper.cs new file mode 100644 index 00000000..715bbd2c --- /dev/null +++ b/src/Velopack.IcoLib/Host/ExceptionWrapper.cs @@ -0,0 +1,40 @@ +using Ico.Model; +using Ico.Validation; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ico.Host +{ + public static class ExceptionWrapper + { + public static void Try(Action action, ParseContext context, IErrorReporter reporter) + { + try + { + action(); + } + catch (InvalidIcoFileException e) + { + var frame = (e.Context?.ImageDirectoryIndex); + + if (frame != null) + { + reporter.ErrorLine(e.ErrorCode, e.Message, context.DisplayedPath, context.ImageDirectoryIndex.Value); + } + else + { + reporter.ErrorLine(e.ErrorCode, e.Message, context.DisplayedPath); + } + } + catch (InvalidPngFileException e) when (e.ErrorCode != IcoErrorCode.NoError) + { + reporter.ErrorLine(e.ErrorCode, e.Message); + } + catch (Exception e) + { + reporter.ErrorLine(IcoErrorCode.NoError, e.ToString(), context.DisplayedPath); + } + } + } +} diff --git a/src/Velopack.IcoLib/Host/IErrorReporter.cs b/src/Velopack.IcoLib/Host/IErrorReporter.cs new file mode 100644 index 00000000..f333aeb6 --- /dev/null +++ b/src/Velopack.IcoLib/Host/IErrorReporter.cs @@ -0,0 +1,23 @@ +using Ico.Validation; + +namespace Ico.Host +{ + public interface IErrorReporter + { + void ErrorLine(IcoErrorCode errorCode, string message); + + void ErrorLine(IcoErrorCode errorCode, string message, string fileName); + + void ErrorLine(IcoErrorCode errorCode, string message, string fileName, uint frameNumber); + + void WarnLine(IcoErrorCode errorCode, string message); + + void WarnLine(IcoErrorCode errorCode, string message, string fileName); + + void WarnLine(IcoErrorCode errorCode, string message, string fileName, uint frameNumber); + + void InfoLine(string message); + + void VerboseLine(string message); + } +} diff --git a/src/Velopack.IcoLib/Model/IcoFrame.cs b/src/Velopack.IcoLib/Model/IcoFrame.cs new file mode 100644 index 00000000..6d59f93d --- /dev/null +++ b/src/Velopack.IcoLib/Model/IcoFrame.cs @@ -0,0 +1,18 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Ico.Model +{ + public class IcoFrame + { + public IcoFrameEncoding Encoding { get; set; } + + public byte[] RawData { get; set; } + + public Image CookedData { get; set; } + + public bool[,] Mask { get; set; } + + public uint TotalDiskUsage { get; set; } + } +} diff --git a/src/Velopack.IcoLib/Model/IcoFrameEncoding.cs b/src/Velopack.IcoLib/Model/IcoFrameEncoding.cs new file mode 100644 index 00000000..7136f9ae --- /dev/null +++ b/src/Velopack.IcoLib/Model/IcoFrameEncoding.cs @@ -0,0 +1,45 @@ +namespace Ico.Model +{ + public enum IcoEncodingType + { + Bitmap, + Png, + } + + public enum BitmapEncoding + { + Pixel_indexed1, + Pixel_indexed2, + Pixel_indexed4, + Pixel_indexed8, + Pixel_rgb15, + Pixel_rgb24, + Pixel_0rgb32, + Pixel_argb32, + } + + public class IcoFrameEncoding + { + public IcoEncodingType Type { get; set; } + + // In the ICO header + + public uint ClaimedBitDepth { get; set; } + + public uint ClaimedWidth { get; set; } + + public uint ClaimedHeight { get; set; } + + public uint ActualWidth { get; set; } + + public uint ActualHeight { get; set; } + + // Bitmap only + + public BitmapEncoding PixelFormat { get; set; } + + public uint ActualBitDepth { get; set; } + + public uint PaletteSize { get; set; } + } +} diff --git a/src/Velopack.IcoLib/Model/ParseContext.cs b/src/Velopack.IcoLib/Model/ParseContext.cs new file mode 100644 index 00000000..80acece8 --- /dev/null +++ b/src/Velopack.IcoLib/Model/ParseContext.cs @@ -0,0 +1,29 @@ +using Ico.Codecs; +using Ico.Host; +using Ico.Validation; +using SixLabors.ImageSharp.Formats.Png; +using System.Collections.Generic; + +namespace Ico.Model +{ + public class ParseContext + { + public string FullPath { get; set; } + + public string DisplayedPath { get; set; } + + public uint? ImageDirectoryIndex { get; set; } + + public List GeneratedFrames { get; set; } + + public PngEncoder PngEncoder { get; set; } + + public StrictnessPolicy MaskedImagePixelEmitOptions { get; set; } + + public StrictnessPolicy AllowPaletteTruncation { get; set; } + + public IErrorReporter Reporter { get; set; } + + public IcoErrorCode LastEncodeError { get; set; } + } +} diff --git a/src/Velopack.IcoLib/Model/PngFileEncoding.cs b/src/Velopack.IcoLib/Model/PngFileEncoding.cs new file mode 100644 index 00000000..ddd1e243 --- /dev/null +++ b/src/Velopack.IcoLib/Model/PngFileEncoding.cs @@ -0,0 +1,23 @@ +namespace Ico.Model +{ + public enum PngColorType + { + Grayscale = 0, + RGB = 2, + Palette = 3, + GrayscaleAlpha = 4, + RGBA = 6, + } + + public class PngFileEncoding + { + // 1, 2, 4, 8, or 16 + public uint BitsPerChannel { get; set; } + + public PngColorType ColorType { get; set; } + + public uint Width { get; set; } + + public uint Height { get; set; } + } +} diff --git a/src/Velopack.IcoLib/README.md b/src/Velopack.IcoLib/README.md new file mode 100644 index 00000000..492fe8b1 --- /dev/null +++ b/src/Velopack.IcoLib/README.md @@ -0,0 +1,4 @@ +# IcoLib +IcoLib is a library for creating, extracting, and managing ICO files. It is written in C# and is compatible with .NET Standard 2.0. + +Find the original project here https://github.com/jtippet/IcoTools \ No newline at end of file diff --git a/src/Velopack.IcoLib/Validation/IcoErrorCode.cs b/src/Velopack.IcoLib/Validation/IcoErrorCode.cs new file mode 100644 index 00000000..7defc29d --- /dev/null +++ b/src/Velopack.IcoLib/Validation/IcoErrorCode.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ico.Validation +{ + public enum IcoErrorCode + { + NoError = 0, + + // 100 - 199: invalid ICO file format + InvalidIcoHeader_idReserved = 100, + InvalidIconHeader_idType = 101, + InvalidFrameHeader_bReserved = 110, + InvalidFrameHeader_wPlanes = 111, + InvalidFrameHeader_dwBytesInRes = 112, + InvalidFrameHeader_dwImageOffset = 113, + TooManyFrames = 120, + InvalidBitapInfoHeader_ciSize = 130, + InvalidBitapInfoHeader_biXPelsPerMeter = 131, + InvalidBitapInfoHeader_biYPelsPerMeter = 132, + InvalidBitapInfoHeader_biBitCount = 133, + InvalidBitapInfoHeader_biClrUsed = 134, + + // 200 - 299: nonstandard or nonportable ICO file format + ZeroFrames = 200, + DuplicateFrameTypes = 201, + MismatchedHeight = 210, + MismatchedWidth = 211, + NonzeroAlpha = 220, + MaskedPixelWithColor = 221, + NoMaskedPixels = 222, + IndexedColorOutOfBounds = 230, + UndersizedColorTable = 231, + PngNot32Bit = 240, + PngNotRGBA32 = 241, + NotSquare = 256, + + // 300 - 399: tool limitations + FileTooLarge = 300, + BitfieldCompressionNotSupported = 310, + BitmapCompressionNotSupported = 311, + + // 400 - 499: usage or environmental error + FileExists = 403, + FileNotFound = 404, + UnsupportedCodec = 410, + UnsupportedBitmapEncoding = 411, + OnlySupportedOnBitmaps = 412, + BitmapMaskWrongDimensions = 420, + BitampMaskWrongColors = 421, + InvalidFrameIndex = 430, + TooManyColorsForBitDepth = 431, + NotPng = 440, + PngBadIHDR = 441, + PngIllegalInputDimensions = 445, + PngIllegalInputDepth = 446, + PngIllegalColorType = 447, + } +} diff --git a/src/Velopack.IcoLib/Validation/InvalidIcoFileException.cs b/src/Velopack.IcoLib/Validation/InvalidIcoFileException.cs new file mode 100644 index 00000000..6029b185 --- /dev/null +++ b/src/Velopack.IcoLib/Validation/InvalidIcoFileException.cs @@ -0,0 +1,56 @@ +using Ico.Model; +using System; +using System.Runtime.Serialization; + +namespace Ico.Validation +{ + [Serializable] + public class InvalidIcoFileException : Exception + { + public ParseContext Context { get; private set; } + + public IcoErrorCode ErrorCode { get; private set; } + + public InvalidIcoFileException() + { + } + + public InvalidIcoFileException(string message) : base(message) + { + } + + public InvalidIcoFileException(string message, Exception innerException) : base(message, innerException) + { + } + + public InvalidIcoFileException(IcoErrorCode errorCode, string message, ParseContext context) : this(message) + { + ErrorCode = errorCode; + Context = context; + } + + protected InvalidIcoFileException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public override string ToString() + { + if (Context != null) + { + if (Context.DisplayedPath != null && Context.ImageDirectoryIndex == null) + { + return base.ToString() + $"\nFile: \"{Context.DisplayedPath}\""; + } + else if (Context.DisplayedPath != null && Context.ImageDirectoryIndex == null) + { + return base.ToString() + $"\nFile: \"{Context.DisplayedPath}\""; + } + else if (Context.DisplayedPath != null && Context.ImageDirectoryIndex.HasValue) + { + return base.ToString() + $"\nFile: \"{Context.DisplayedPath}\"\nImage directory index: #{Context.ImageDirectoryIndex.Value}"; + } + } + return base.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Velopack.IcoLib/Validation/InvalidPngFileException.cs b/src/Velopack.IcoLib/Validation/InvalidPngFileException.cs new file mode 100644 index 00000000..74320e9b --- /dev/null +++ b/src/Velopack.IcoLib/Validation/InvalidPngFileException.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.Serialization; + +namespace Ico.Validation +{ + public class InvalidPngFileException : Exception + { + public IcoErrorCode ErrorCode { get; private set; } + + public InvalidPngFileException() + { + } + + public InvalidPngFileException(string message) : base(message) + { + } + + public InvalidPngFileException(IcoErrorCode errorCode, string message) : base(message) + { + ErrorCode = errorCode; + } + + public InvalidPngFileException(string message, Exception innerException) : base(message, innerException) + { + } + + protected InvalidPngFileException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + } +} diff --git a/src/Velopack.IcoLib/Velopack.IcoLib.csproj b/src/Velopack.IcoLib/Velopack.IcoLib.csproj new file mode 100644 index 00000000..87a42e91 --- /dev/null +++ b/src/Velopack.IcoLib/Velopack.IcoLib.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + Ico + 7.3 + 1.1.1 + Jeffrey Tippet + IcoTools + Copyright 2019, Jeffrey Tippet. All rights reserved. + + + + + + + diff --git a/src/Velopack.IcoLib/packages.lock.json b/src/Velopack.IcoLib/packages.lock.json new file mode 100644 index 00000000..e2d3df53 --- /dev/null +++ b/src/Velopack.IcoLib/packages.lock.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "dependencies": { + ".NETStandard,Version=v2.0": { + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Nerdbank.GitVersioning": { + "type": "Direct", + "requested": "[3.6.*, )", + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" + }, + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "SixLabors.ImageSharp": { + "type": "Direct", + "requested": "[2.1.8, )", + "resolved": "2.1.8", + "contentHash": "ML1++vactR5xMW36wHA2nAX3vc6VpR62qYnpopQdLNGbP8BAJ/ckv1IrGIIFRUEQ0JS9EgWr1y1gEa/81f+HaA==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.4.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/src/Velopack.Packaging.HostModel/AppHost/AppHostExceptions.cs b/src/Velopack.Packaging.HostModel/AppHost/AppHostExceptions.cs deleted file mode 100644 index 31719d74..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/AppHostExceptions.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// An instance of this exception is thrown when an AppHost binary update - /// fails due to known user errors. - /// - public class AppHostUpdateException : Exception - { - internal AppHostUpdateException(string message = null) - : base(message) - { - } - } - - /// - /// The application host executable cannot be customized because adding resources requires - /// that the build be performed on Windows (excluding Nano Server). - /// - public sealed class AppHostCustomizationUnsupportedOSException : AppHostUpdateException - { - internal AppHostCustomizationUnsupportedOSException() - { - } - } - - /// - /// The MachO application host executable cannot be customized because - /// it was not in the expected format - /// - public sealed class AppHostMachOFormatException : AppHostUpdateException - { - public readonly MachOFormatError Error; - - internal AppHostMachOFormatException(MachOFormatError error) - { - Error = error; - } - } - - /// - /// Unable to use the input file as application host executable because it's not a - /// Windows executable for the CUI (Console) subsystem. - /// - public sealed class AppHostNotCUIException : AppHostUpdateException - { - internal AppHostNotCUIException() - { - } - } - - /// - /// Unable to use the input file as an application host executable - /// because it's not a Windows PE file - /// - public sealed class AppHostNotPEFileException : AppHostUpdateException - { - internal AppHostNotPEFileException() - { - } - } - - /// - /// Unable to sign the apphost binary. - /// - public sealed class AppHostSigningException : AppHostUpdateException - { - public readonly int ExitCode; - - internal AppHostSigningException(int exitCode, string signingErrorMessage) - : base(signingErrorMessage) - { - } - } - - /// - /// Given app file name is longer than 1024 bytes - /// - public sealed class AppNameTooLongException : AppHostUpdateException - { - public string LongName { get; } - - internal AppNameTooLongException(string name) - { - LongName = name; - } - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/BinaryUtils.cs b/src/Velopack.Packaging.HostModel/AppHost/BinaryUtils.cs deleted file mode 100644 index 54c3ded6..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/BinaryUtils.cs +++ /dev/null @@ -1,168 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO.MemoryMappedFiles; - -namespace Microsoft.NET.HostModel.AppHost -{ - public static class BinaryUtils - { - internal static unsafe void SearchAndReplace( - MemoryMappedViewAccessor accessor, - byte[] searchPattern, - byte[] patternToReplace, - bool pad0s = true) - { - byte* pointer = null; - - try { - accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer); - byte* bytes = pointer + accessor.PointerOffset; - - int position = KMPSearch(searchPattern, bytes, accessor.Capacity); - if (position < 0) { - throw new PlaceHolderNotFoundInAppHostException(searchPattern); - } - - accessor.WriteArray( - position: position, - array: patternToReplace, - offset: 0, - count: patternToReplace.Length); - - if (pad0s) { - Pad0(searchPattern, patternToReplace, bytes, position); - } - } finally { - if (pointer != null) { - accessor.SafeMemoryMappedViewHandle.ReleasePointer(); - } - } - } - - private static unsafe void Pad0(byte[] searchPattern, byte[] patternToReplace, byte* bytes, int offset) - { - if (patternToReplace.Length < searchPattern.Length) { - for (int i = patternToReplace.Length; i < searchPattern.Length; i++) { - bytes[i + offset] = 0x0; - } - } - } - - public static unsafe void SearchAndReplace( - string filePath, - byte[] searchPattern, - byte[] patternToReplace, - bool pad0s = true) - { - using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath)) { - using (var accessor = mappedFile.CreateViewAccessor()) { - SearchAndReplace(accessor, searchPattern, patternToReplace, pad0s); - } - } - } - - public static unsafe int SearchInFile(MemoryMappedViewAccessor accessor, byte[] searchPattern) - { - var safeBuffer = accessor.SafeMemoryMappedViewHandle; - return KMPSearch(searchPattern, (byte*) safeBuffer.DangerousGetHandle(), (int) safeBuffer.ByteLength); - } - - public static unsafe int SearchInFile(string filePath, byte[] searchPattern) - { - using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath)) { - using (var accessor = mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) { - return SearchInFile(accessor, searchPattern); - } - } - } - - // See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm - private static int[] ComputeKMPFailureFunction(byte[] pattern) - { - int[] table = new int[pattern.Length]; - if (pattern.Length >= 1) { - table[0] = -1; - } - if (pattern.Length >= 2) { - table[1] = 0; - } - - int pos = 2; - int cnd = 0; - while (pos < pattern.Length) { - if (pattern[pos - 1] == pattern[cnd]) { - table[pos] = cnd + 1; - cnd++; - pos++; - } else if (cnd > 0) { - cnd = table[cnd]; - } else { - table[pos] = 0; - pos++; - } - } - return table; - } - - // See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm - private static unsafe int KMPSearch(byte[] pattern, byte* bytes, long bytesLength) - { - int m = 0; - int i = 0; - int[] table = ComputeKMPFailureFunction(pattern); - - while (m + i < bytesLength) { - if (pattern[i] == bytes[m + i]) { - if (i == pattern.Length - 1) { - return m; - } - i++; - } else { - if (table[i] > -1) { - m = m + i - table[i]; - i = table[i]; - } else { - m++; - i = 0; - } - } - } - - return -1; - } - - public static void CopyFile(string sourcePath, string destinationPath) - { - var destinationDirectory = new FileInfo(destinationPath).Directory.FullName; - if (!Directory.Exists(destinationDirectory)) { - Directory.CreateDirectory(destinationDirectory); - } - - // Copy file to destination path so it inherits the same attributes/permissions. - File.Copy(sourcePath, destinationPath, overwrite: true); - } - - internal static void WriteToStream(MemoryMappedViewAccessor sourceViewAccessor, FileStream fileStream, long length) - { - int pos = 0; - int bufSize = 16384; //16K - - byte[] buf = new byte[bufSize]; - length = Math.Min(length, sourceViewAccessor.Capacity); - do { - int bytesRequested = Math.Min((int) length - pos, bufSize); - if (bytesRequested <= 0) { - break; - } - - int bytesRead = sourceViewAccessor.ReadArray(pos, buf, 0, bytesRequested); - if (bytesRead > 0) { - fileStream.Write(buf, 0, bytesRead); - pos += bytesRead; - } - } - while (true); - } - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/ElfUtils.cs b/src/Velopack.Packaging.HostModel/AppHost/ElfUtils.cs deleted file mode 100644 index b2dce3c2..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/ElfUtils.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.AppHost -{ - internal static class ElfUtils - { - // The Linux Headers are copied from elf.h - -#pragma warning disable 0649 - private struct ElfHeader - { - private byte EI_MAG0; - private byte EI_MAG1; - private byte EI_MAG2; - private byte EI_MAG3; - - public bool IsValid() - { - return EI_MAG0 == 0x7f && - EI_MAG1 == 0x45 && - EI_MAG2 == 0x4C && - EI_MAG3 == 0x46; - } - - - } -#pragma warning restore 0649 - - public static bool IsElfImage(string filePath) - { - using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath))) - { - if (reader.BaseStream.Length < 16) // EI_NIDENT = 16 - { - return false; - } - - byte[] eIdent = reader.ReadBytes(4); - - // Check that the first four bytes are 0x7f, 'E', 'L', 'F' - return eIdent[0] == 0x7f && - eIdent[1] == 0x45 && - eIdent[2] == 0x4C && - eIdent[3] == 0x46; - } - } - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/HResultException.cs b/src/Velopack.Packaging.HostModel/AppHost/HResultException.cs deleted file mode 100644 index 1e2db783..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/HResultException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel -{ - /// - /// Represents an exception thrown because of a Win32 error - /// - public class HResultException : Exception - { - public readonly int Win32HResult; - public HResultException(int hResult) : base(hResult.ToString("X4")) - { - Win32HResult = hResult; - } - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/HostWriter.cs b/src/Velopack.Packaging.HostModel/AppHost/HostWriter.cs deleted file mode 100644 index 7a8183e0..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/HostWriter.cs +++ /dev/null @@ -1,277 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.IO.MemoryMappedFiles; -using System.Runtime.InteropServices; -using System.Text; - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// Embeds the App Name into the AppHost.exe - /// If an apphost is a single-file bundle, updates the location of the bundle headers. - /// - public static class HostWriter - { - /// - /// hash value embedded in default apphost executable in a place where the path to the app binary should be stored. - /// - private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"; - private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder); - - /// - /// Create an AppHost with embedded configuration of app binary location - /// - /// The path of Apphost template, which has the place holder - /// The destination path for desired location to place, including the file name - /// Full path to app binary or relative path to the result apphost file - /// Specify whether to set the subsystem to GUI. Only valid for PE apphosts. - /// Path to the intermediate assembly, used for copying resources to PE apphosts. - /// Sign the app binary using codesign with an anonymous certificate. - public static void CreateAppHost( - string appHostSourceFilePath, - string appHostDestinationFilePath, - string appBinaryFilePath, - bool windowsGraphicalUserInterface = false, - string assemblyToCopyResorcesFrom = null, - bool enableMacOSCodeSign = false) - { - var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath); - if (bytesToWrite.Length > 1024) - { - throw new AppNameTooLongException(appBinaryFilePath); - } - - bool appHostIsPEImage = false; - - void RewriteAppHost(MemoryMappedViewAccessor accessor) - { - // Re-write the destination apphost with the proper contents. - BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite); - - appHostIsPEImage = PEUtils.IsPEImage(accessor); - - if (windowsGraphicalUserInterface) - { - if (!appHostIsPEImage) - { - throw new AppHostNotPEFileException(); - } - - PEUtils.SetWindowsGraphicalUserInterfaceBit(accessor); - } - } - - void UpdateResources() - { - if (assemblyToCopyResorcesFrom != null && appHostIsPEImage) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && ResourceUpdater.IsSupportedOS()) - { - // Copy resources from managed dll to the apphost - new ResourceUpdater(appHostDestinationFilePath) - .AddResourcesFromPEImage(assemblyToCopyResorcesFrom) - .Update(); - } - else - { - throw new AppHostCustomizationUnsupportedOSException(); - } - } - } - - try - { - RetryUtil.RetryOnIOError(() => - { - FileStream appHostSourceStream = null; - MemoryMappedFile memoryMappedFile = null; - MemoryMappedViewAccessor memoryMappedViewAccessor = null; - try - { - // Open the source host file. - appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); - memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true); - memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite); - - // Get the size of the source app host to ensure that we don't write extra data to the destination. - // On Windows, the size of the view accessor is rounded up to the next page boundary. - long sourceAppHostLength = appHostSourceStream.Length; - - // Transform the host file in-memory. - RewriteAppHost(memoryMappedViewAccessor); - - // Save the transformed host. - using (FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create)) - { - BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, sourceAppHostLength); - - // Remove the signature from MachO hosts. - if (!appHostIsPEImage) - { - MachOUtils.RemoveSignature(fileStream); - } - } - } - finally - { - memoryMappedViewAccessor?.Dispose(); - memoryMappedFile?.Dispose(); - appHostSourceStream?.Dispose(); - } - }); - - RetryUtil.RetryOnWin32Error(UpdateResources); - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x - const int EINTR = 4; - int chmodReturnCode = 0; - - do - { - chmodReturnCode = chmod(appHostDestinationFilePath, filePermissionOctal); - } - while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR); - - if (chmodReturnCode == -1) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {appHostDestinationFilePath}."); - } - - if (enableMacOSCodeSign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable()) - { - (int exitCode, string stdErr) = HostModelUtils.RunCodesign("-s -", appHostDestinationFilePath); - if (exitCode != 0) - { - throw new AppHostSigningException(exitCode, stdErr); - } - } - } - } - catch (Exception ex) - { - // Delete the destination file so we don't leave an unmodified apphost - try - { - File.Delete(appHostDestinationFilePath); - } - catch (Exception failedToDeleteEx) - { - throw new AggregateException(ex, failedToDeleteEx); - } - - throw; - } - } - - /// - /// Set the current AppHost as a single-file bundle. - /// - /// The path of Apphost template, which has the place holder - /// The offset to the location of bundle header - public static void SetAsBundle( - string appHostPath, - long bundleHeaderOffset) - { - byte[] bundleHeaderPlaceholder = { - // 8 bytes represent the bundle header-offset - // Zero for non-bundle apphosts (default). - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 32 bytes represent the bundle signature: SHA-256 for ".net core bundle" - 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, - 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, - 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, - 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae - }; - - // Re-write the destination apphost with the proper contents. - RetryUtil.RetryOnIOError(() => - BinaryUtils.SearchAndReplace(appHostPath, - bundleHeaderPlaceholder, - BitConverter.GetBytes(bundleHeaderOffset), - pad0s: false)); - - RetryUtil.RetryOnIOError(() => - MachOUtils.AdjustHeadersForBundle(appHostPath)); - - // Memory-mapped write does not updating last write time - RetryUtil.RetryOnIOError(() => - File.SetLastWriteTimeUtc(appHostPath, DateTime.UtcNow)); - } - - /// - /// Check if the an AppHost is a single-file bundle - /// - /// The path of Apphost to check - /// True if the AppHost is a single-file bundle, false otherwise - public static void ResetBundle(string appHostFilePath) - { - byte[] bundleSignature = { - // 32 bytes represent the bundle signature: SHA-256 for ".net core bundle" - 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, - 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, - 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, - 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae - }; - - void ResetBundleHeader() - { - using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostFilePath)) - { - using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor()) - { - int position = BinaryUtils.SearchInFile(accessor, bundleSignature); - if (position == -1) - { - throw new PlaceHolderNotFoundInAppHostException(bundleSignature); - } - - accessor.WriteArray(position - sizeof(long), new byte[sizeof(long)], 0, sizeof(long)); - } - } - } - - RetryUtil.RetryOnIOError(ResetBundleHeader); - } - - public static bool IsBundle(string appHostFilePath, out long bundleHeaderOffset) - { - byte[] bundleSignature = { - // 32 bytes represent the bundle signature: SHA-256 for ".net core bundle" - 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, - 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, - 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, - 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae - }; - - long headerOffset = 0; - void FindBundleHeader() - { - using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostFilePath)) - { - using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor()) - { - int position = BinaryUtils.SearchInFile(accessor, bundleSignature); - if (position == -1) - { - throw new PlaceHolderNotFoundInAppHostException(bundleSignature); - } - - headerOffset = accessor.ReadInt64(position - sizeof(long)); - } - } - } - - RetryUtil.RetryOnIOError(FindBundleHeader); - bundleHeaderOffset = headerOffset; - - return headerOffset != 0; - } - - [DllImport("libc", SetLastError = true)] - private static extern int chmod(string pathname, int mode); - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/MachOFormatError.cs b/src/Velopack.Packaging.HostModel/AppHost/MachOFormatError.cs deleted file mode 100644 index eae935f1..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/MachOFormatError.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// Additional details about the failure with caused an AppHostMachOFormatException - /// - public enum MachOFormatError - { - Not64BitExe, // Apphost is expected to be a 64-bit MachO executable - DuplicateLinkEdit, // Only one __LINKEDIT segment is expected in the apphost - DuplicateSymtab, // Only one SYMTAB is expected in the apphost - MissingLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT - MissingSymtab, // CODE_SIGNATURE command must follow the SYMTAB command - LinkEditNotLast, // __LINKEDIT must be the last segment in the binary layout - SymtabNotInLinkEdit, // SYMTAB must within the __LINKEDIT segment! - SignNotInLinkEdit, // Signature blob must be within the __LINKEDIT segment! - SignCommandNotLast, // CODE_SIGNATURE command must be the last command - SignBlobNotLast, // Signature blob must be at the very end of the file - SignDoesntFollowSymtab, // Signature blob must immediately follow the Symtab - MemoryMapAccessFault, // Error reading the memory-mapped apphost - InvalidUTF8, // UTF8 decoding failed - SignNotRemoved, // Signature not removed from the host (while processing a single-file bundle) - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/MachOUtils.cs b/src/Velopack.Packaging.HostModel/AppHost/MachOUtils.cs deleted file mode 100644 index 30f64476..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/MachOUtils.cs +++ /dev/null @@ -1,444 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO.MemoryMappedFiles; -using System.Runtime.CompilerServices; -using System.Text; - -namespace Microsoft.NET.HostModel.AppHost -{ - internal static class MachOUtils - { - // The MachO Headers are copied from - // https://opensource.apple.com/source/cctools/cctools-870/include/mach-o/loader.h - // - // The data fields and enumerations match the structure definitions in the above file, - // and hence do not conform to C# CoreFx naming style. - - private enum Magic : uint - { - MH_MAGIC = 0xfeedface, - MH_CIGAM = 0xcefaedfe, - MH_MAGIC_64 = 0xfeedfacf, - MH_CIGAM_64 = 0xcffaedfe - } - - private enum FileType : uint - { - MH_EXECUTE = 0x2 - } - -#pragma warning disable 0649 - private struct MachHeader - { - public Magic magic; - public int cputype; - public int cpusubtype; - public FileType filetype; - public uint ncmds; - public uint sizeofcmds; - public uint flags; - public uint reserved; - - public bool Is64BitExecutable() - { - return magic == Magic.MH_MAGIC_64 && filetype == FileType.MH_EXECUTE; - } - - public bool IsValid() - { - switch (magic) - { - case Magic.MH_CIGAM: - case Magic.MH_CIGAM_64: - case Magic.MH_MAGIC: - case Magic.MH_MAGIC_64: - return true; - - default: - return false; - } - } - } - - private enum Command : uint - { - LC_SYMTAB = 0x2, - LC_SEGMENT_64 = 0x19, - LC_CODE_SIGNATURE = 0x1d, - } - - private struct LoadCommand - { - public Command cmd; - public uint cmdsize; - } - - // The linkedit_data_command contains the offsets and sizes of a blob - // of data in the __LINKEDIT segment (including LC_CODE_SIGNATURE). - private struct LinkEditDataCommand - { - public Command cmd; - public uint cmdsize; - public uint dataoff; - public uint datasize; - } - - private struct SymtabCommand - { - public uint cmd; - public uint cmdsize; - public uint symoff; - public uint nsyms; - public uint stroff; - public uint strsize; - }; - - private unsafe struct SegmentCommand64 - { - public Command cmd; - public uint cmdsize; - public fixed byte segname[16]; - public ulong vmaddr; - public ulong vmsize; - public ulong fileoff; - public ulong filesize; - public int maxprot; - public int initprot; - public uint nsects; - public uint flags; - - public string SegName - { - get - { - fixed (byte* p = segname) - { - int len = 0; - while (*(p + len) != 0 && len++ < 16) ; - - try - { - return Encoding.UTF8.GetString(p, len); - } - catch (ArgumentException) - { - throw new AppHostMachOFormatException(MachOFormatError.InvalidUTF8); - } - } - } - } - } - -#pragma warning restore 0649 - - private static void Verify(bool condition, MachOFormatError error) - { - if (!condition) - { - throw new AppHostMachOFormatException(error); - } - } - - public static bool IsMachOImage(string filePath) - { - using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath))) - { - if (reader.BaseStream.Length < 256) // Header size - { - return false; - } - - uint magic = reader.ReadUInt32(); - return Enum.IsDefined(typeof(Magic), magic); - } - } - - /// - /// This Method is a utility to remove the code-signature (if any) - /// from a MachO AppHost binary. - /// - /// The tool assumes the following layout of the executable: - /// - /// * MachoHeader (64-bit, executable, not swapped integers) - /// * LoadCommands - /// LC_SEGMENT_64 (__PAGEZERO) - /// LC_SEGMENT_64 (__TEXT) - /// LC_SEGMENT_64 (__DATA) - /// LC_SEGMENT_64 (__LINKEDIT) - /// ... - /// LC_SYMTAB - /// ... - /// LC_CODE_SIGNATURE (last) - /// - /// * ... Different Segments ... - /// - /// * The __LINKEDIT Segment (last) - /// * ... Different sections ... - /// * SYMTAB - /// * (Some alignment bytes) - /// * The Code-signature - /// - /// In order to remove the signature, the method: - /// - Removes (zeros out) the LC_CODE_SIGNATURE command - /// - Adjusts the size and count of the load commands in the header - /// - Truncates the size of the __LINKEDIT segment to the end of SYMTAB - /// - Truncates the apphost file to the end of the __LINKEDIT segment - /// - /// - /// Stream containing the AppHost - /// - /// True if - /// - The input is a MachO binary, and - /// - It is a signed binary, and - /// - The signature was successfully removed - /// False otherwise - /// - /// - /// The input is a MachO file, but doesn't match the expect format of the AppHost. - /// - public static unsafe bool RemoveSignature(FileStream stream) - { - uint signatureSize = 0; - using (var mappedFile = MemoryMappedFile.CreateFromFile(stream, - mapName: null, - capacity: 0, - MemoryMappedFileAccess.ReadWrite, - HandleInheritability.None, - leaveOpen: true)) - { - using (var accessor = mappedFile.CreateViewAccessor()) - { - byte* file = null; - try - { - accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file); - Verify(file != null, MachOFormatError.MemoryMapAccessFault); - - MachHeader* header = (MachHeader*)file; - - if (!header->IsValid()) - { - // Not a MachO file. - return false; - } - - Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe); - - file += sizeof(MachHeader); - SegmentCommand64* linkEdit = null; - SymtabCommand* symtab = null; - LinkEditDataCommand* signature = null; - - for (uint i = 0; i < header->ncmds; i++) - { - LoadCommand* command = (LoadCommand*)file; - if (command->cmd == Command.LC_SEGMENT_64) - { - SegmentCommand64* segment = (SegmentCommand64*)file; - if (segment->SegName.Equals("__LINKEDIT")) - { - Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit); - linkEdit = segment; - } - } - else if (command->cmd == Command.LC_SYMTAB) - { - Verify(symtab == null, MachOFormatError.DuplicateSymtab); - symtab = (SymtabCommand*)command; - } - else if (command->cmd == Command.LC_CODE_SIGNATURE) - { - Verify(i == header->ncmds - 1, MachOFormatError.SignCommandNotLast); - signature = (LinkEditDataCommand*)command; - break; - } - - file += command->cmdsize; - } - - if (signature != null) - { - Verify(linkEdit != null, MachOFormatError.MissingLinkEdit); - Verify(symtab != null, MachOFormatError.MissingSymtab); - - var symtabEnd = symtab->stroff + symtab->strsize; - var linkEditEnd = linkEdit->fileoff + linkEdit->filesize; - var signatureEnd = signature->dataoff + signature->datasize; - var fileEnd = (ulong)stream.Length; - - Verify(linkEditEnd == fileEnd, MachOFormatError.LinkEditNotLast); - Verify(signatureEnd == fileEnd, MachOFormatError.SignBlobNotLast); - - Verify(symtab->symoff > linkEdit->fileoff, MachOFormatError.SymtabNotInLinkEdit); - Verify(signature->dataoff > linkEdit->fileoff, MachOFormatError.SignNotInLinkEdit); - - // The signature blob immediately follows the symtab blob, - // except for a few bytes of padding. - Verify(signature->dataoff >= symtabEnd && signature->dataoff - symtabEnd < 32, MachOFormatError.SignBlobNotLast); - - // Remove the signature command - header->ncmds--; - header->sizeofcmds -= signature->cmdsize; - Unsafe.InitBlock(signature, 0, signature->cmdsize); - - // Remove the signature blob (note for truncation) - signatureSize = (uint)(fileEnd - symtabEnd); - - // Adjust the __LINKEDIT segment load command - linkEdit->filesize -= signatureSize; - - // codesign --remove-signature doesn't reset the vmsize. - // Setting the vmsize here makes the output bin-equal with the original - // unsigned apphost (and not bin-equal with a signed-unsigned-apphost). - linkEdit->vmsize = linkEdit->filesize; - } - } - finally - { - if (file != null) - { - accessor.SafeMemoryMappedViewHandle.ReleasePointer(); - } - } - } - } - - if (signatureSize != 0) - { - // The signature was removed, update the file length - stream.SetLength(stream.Length - signatureSize); - return true; - } - - return false; - } - - /// - /// This Method is a utility to adjust the apphost MachO-header - /// to include the bytes added by the single-file bundler at the end of the file. - /// - /// The tool assumes the following layout of the executable - /// - /// * MachoHeader (64-bit, executable, not swapped integers) - /// * LoadCommands - /// LC_SEGMENT_64 (__PAGEZERO) - /// LC_SEGMENT_64 (__TEXT) - /// LC_SEGMENT_64 (__DATA) - /// LC_SEGMENT_64 (__LINKEDIT) - /// ... - /// LC_SYMTAB - /// - /// * ... Different Segments - /// - /// * The __LINKEDIT Segment (last) - /// * ... Different sections ... - /// * SYMTAB (last) - /// - /// The MAC codesign tool places several restrictions on the layout - /// * The __LINKEDIT segment must be the last one - /// * The __LINKEDIT segment must cover the end of the file - /// * All bytes in the __LINKEDIT segment are used by other linkage commands - /// (ex: symbol/string table, dynamic load information etc) - /// - /// In order to circumvent these restrictions, we: - /// * Extend the __LINKEDIT segment to include the bundle-data - /// * Extend the string table to include all the bundle-data - /// (that is, the bundle-data appear as strings to the loader/codesign tool). - /// - /// This method has certain limitations: - /// * The bytes for the bundler may be unnecessarily loaded at startup - /// * Tools that process the string table may be confused (?) - /// * The string table size is limited to 4GB. Bundles larger than that size - /// cannot be accomodated by this utility. - /// - /// - /// Path to the AppHost - /// - /// True if - /// - The input is a MachO binary, and - /// - The additional bytes were successfully accomodated within the MachO segments. - /// False otherwise - /// - /// - /// The input is a MachO file, but doesn't match the expect format of the AppHost. - /// - public static unsafe bool AdjustHeadersForBundle(string filePath) - { - ulong fileLength = (ulong)new FileInfo(filePath).Length; - using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath)) - { - using (var accessor = mappedFile.CreateViewAccessor()) - { - byte* file = null; - try - { - accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file); - Verify(file != null, MachOFormatError.MemoryMapAccessFault); - - MachHeader* header = (MachHeader*)file; - - if (!header->IsValid()) - { - // Not a MachO file. - return false; - } - - Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe); - - file += sizeof(MachHeader); - SegmentCommand64* linkEdit = null; - SymtabCommand* symtab = null; - LinkEditDataCommand* signature = null; - - for (uint i = 0; i < header->ncmds; i++) - { - LoadCommand* command = (LoadCommand*)file; - if (command->cmd == Command.LC_SEGMENT_64) - { - SegmentCommand64* segment = (SegmentCommand64*)file; - if (segment->SegName.Equals("__LINKEDIT")) - { - Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit); - linkEdit = segment; - } - } - else if (command->cmd == Command.LC_SYMTAB) - { - Verify(symtab == null, MachOFormatError.DuplicateSymtab); - symtab = (SymtabCommand*)command; - } - - file += command->cmdsize; - } - - Verify(linkEdit != null, MachOFormatError.MissingLinkEdit); - Verify(symtab != null, MachOFormatError.MissingSymtab); - - // Update the string table to include bundle-data - ulong newStringTableSize = fileLength - symtab->stroff; - if (newStringTableSize > uint.MaxValue) - { - // Too big, too bad; - return false; - } - symtab->strsize = (uint)newStringTableSize; - - // Update the __LINKEDIT segment to include bundle-data - linkEdit->filesize = fileLength - linkEdit->fileoff; - linkEdit->vmsize = linkEdit->filesize; - } - finally - { - if (file != null) - { - accessor.SafeMemoryMappedViewHandle.ReleasePointer(); - } - } - } - } - - return true; - } - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/PEUtils.cs b/src/Velopack.Packaging.HostModel/AppHost/PEUtils.cs deleted file mode 100644 index ffc68f8c..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/PEUtils.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO.MemoryMappedFiles; - -namespace Microsoft.NET.HostModel.AppHost -{ - public static class PEUtils - { - /// - /// The first two bytes of a PE file are a constant signature. - /// - private const ushort PEFileSignature = 0x5A4D; - - /// - /// The offset of the PE header pointer in the DOS header. - /// - private const int PEHeaderPointerOffset = 0x3C; - - /// - /// The offset of the Subsystem field in the PE header. - /// - private const int SubsystemOffset = 0x5C; - - /// - /// The value of the sybsystem field which indicates Windows GUI (Graphical UI) - /// - private const ushort WindowsGUISubsystem = 0x2; - - /// - /// The value of the subsystem field which indicates Windows CUI (Console) - /// - private const ushort WindowsCUISubsystem = 0x3; - - /// - /// Check whether the apphost file is a windows PE image by looking at the first few bytes. - /// - /// The memory accessor which has the apphost file opened. - /// true if the accessor represents a PE image, false otherwise. - internal static unsafe bool IsPEImage(MemoryMappedViewAccessor accessor) - { - byte* pointer = null; - - try - { - accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer); - byte* bytes = pointer + accessor.PointerOffset; - - // https://en.wikipedia.org/wiki/Portable_Executable - // Validate that we're looking at Windows PE file - if (((ushort*)bytes)[0] != PEFileSignature || accessor.Capacity < PEHeaderPointerOffset + sizeof(uint)) - { - return false; - } - return true; - } - finally - { - if (pointer != null) - { - accessor.SafeMemoryMappedViewHandle.ReleasePointer(); - } - } - } - - public static bool IsPEImage(string filePath) - { - using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath))) - { - if (reader.BaseStream.Length < PEHeaderPointerOffset + sizeof(uint)) - { - return false; - } - - ushort signature = reader.ReadUInt16(); - return signature == PEFileSignature; - } - } - - /// - /// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file. - /// - /// The memory accessor which has the apphost file opened. - internal static unsafe void SetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor) - { - byte* pointer = null; - - try - { - accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer); - byte* bytes = pointer + accessor.PointerOffset; - - // https://en.wikipedia.org/wiki/Portable_Executable - uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0]; - - if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort)) - { - throw new AppHostNotPEFileException(); - } - - ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset)); - - // https://docs.microsoft.com/windows/desktop/Debug/pe-format#windows-subsystem - // The subsystem of the prebuilt apphost should be set to CUI - if (subsystem[0] != WindowsCUISubsystem) - { - throw new AppHostNotCUIException(); - } - - // Set the subsystem to GUI - subsystem[0] = WindowsGUISubsystem; - } - finally - { - if (pointer != null) - { - accessor.SafeMemoryMappedViewHandle.ReleasePointer(); - } - } - } - - public static unsafe void SetWindowsGraphicalUserInterfaceBit(string filePath) - { - using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath)) - { - using (var accessor = mappedFile.CreateViewAccessor()) - { - SetWindowsGraphicalUserInterfaceBit(accessor); - } - } - } - - /// - /// This method will return the subsystem CUI/GUI value. The apphost file should be a windows PE file. - /// - /// The memory accessor which has the apphost file opened. - internal static unsafe ushort GetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor) - { - byte* pointer = null; - - try - { - accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer); - byte* bytes = pointer + accessor.PointerOffset; - - // https://en.wikipedia.org/wiki/Portable_Executable - uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0]; - - if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort)) - { - throw new AppHostNotPEFileException(); - } - - ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset)); - - return subsystem[0]; - } - finally - { - if (pointer != null) - { - accessor.SafeMemoryMappedViewHandle.ReleasePointer(); - } - } - } - - public static unsafe ushort GetWindowsGraphicalUserInterfaceBit(string filePath) - { - using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath)) - { - using (var accessor = mappedFile.CreateViewAccessor()) - { - return GetWindowsGraphicalUserInterfaceBit(accessor); - } - } - } - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs b/src/Velopack.Packaging.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs deleted file mode 100644 index 8c93866b..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// Unable to use input file as a valid application host executable, as it does not contain - /// the expected placeholder byte sequence. - /// - public class PlaceHolderNotFoundInAppHostException : AppHostUpdateException - { - public byte[] MissingPattern { get; } - public PlaceHolderNotFoundInAppHostException(byte[] pattern) - { - MissingPattern = pattern; - } - } -} diff --git a/src/Velopack.Packaging.HostModel/AppHost/RetryUtil.cs b/src/Velopack.Packaging.HostModel/AppHost/RetryUtil.cs deleted file mode 100644 index 6bb57049..00000000 --- a/src/Velopack.Packaging.HostModel/AppHost/RetryUtil.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel -{ - /// - /// HostModel library implements several services for updating the AppHost DLL. - /// These updates involve multiple file open/close operations. - /// An Antivirus scanner may intercept in-between and lock the file, - /// causing the operations to fail with IO-Error. - /// So, the operations are retried a few times on failures such as - /// - IOException - /// - Failure with Win32 errors indicating file-lock - /// - public static class RetryUtil - { - public const int NumberOfRetries = 500; - public const int NumMilliSecondsToWait = 100; - - public static void RetryOnIOError(Action func) - { - for (int i = 1; i <= NumberOfRetries; i++) - { - try - { - func(); - break; - } - catch (IOException) when (i < NumberOfRetries) - { - Thread.Sleep(NumMilliSecondsToWait); - } - } - } - - public static void RetryOnWin32Error(Action func) - { - static bool IsKnownIrrecoverableError(int hresult) - { - // Error codes are defined in winerror.h - // The error code is stored in the lowest 16 bits of the HResult - - switch (hresult & 0xffff) - { - case 0x00000001: // ERROR_INVALID_FUNCTION - case 0x00000002: // ERROR_FILE_NOT_FOUND - case 0x00000003: // ERROR_PATH_NOT_FOUND - case 0x00000006: // ERROR_INVALID_HANDLE - case 0x00000008: // ERROR_NOT_ENOUGH_MEMORY - case 0x0000000B: // ERROR_BAD_FORMAT - case 0x0000000E: // ERROR_OUTOFMEMORY - case 0x0000000F: // ERROR_INVALID_DRIVE - case 0x00000012: // ERROR_NO_MORE_FILES - case 0x00000035: // ERROR_BAD_NETPATH - case 0x00000057: // ERROR_INVALID_PARAMETER - case 0x00000071: // ERROR_NO_MORE_SEARCH_HANDLES - case 0x00000072: // ERROR_INVALID_TARGET_HANDLE - case 0x00000078: // ERROR_CALL_NOT_IMPLEMENTED - case 0x0000007B: // ERROR_INVALID_NAME - case 0x0000007C: // ERROR_INVALID_LEVEL - case 0x0000007D: // ERROR_NO_VOLUME_LABEL - case 0x0000009A: // ERROR_LABEL_TOO_LONG - case 0x000000A0: // ERROR_BAD_ARGUMENTS - case 0x000000A1: // ERROR_BAD_PATHNAME - case 0x000000CE: // ERROR_FILENAME_EXCED_RANGE - case 0x000000DF: // ERROR_FILE_TOO_LARGE - case 0x000003ED: // ERROR_UNRECOGNIZED_VOLUME - case 0x000003EE: // ERROR_FILE_INVALID - case 0x00000651: // ERROR_DEVICE_REMOVED - return true; - - default: - return false; - } - } - - for (int i = 1; i <= NumberOfRetries; i++) - { - try - { - func(); - break; - } - catch (HResultException hrex) - when (i < NumberOfRetries && !IsKnownIrrecoverableError(hrex.Win32HResult)) - { - Thread.Sleep(NumMilliSecondsToWait); - } - } - } - } -} diff --git a/src/Velopack.Packaging.HostModel/Bundle/BundleOptions.cs b/src/Velopack.Packaging.HostModel/Bundle/BundleOptions.cs deleted file mode 100644 index 8eb34115..00000000 --- a/src/Velopack.Packaging.HostModel/Bundle/BundleOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.Bundle -{ - /// - /// BundleOptions: Optional settings for configuring the type of files - /// included in the single file bundle. - /// - [Flags] - public enum BundleOptions - { - None = 0, - BundleNativeBinaries = 1, - BundleOtherFiles = 2, - BundleSymbolFiles = 4, - BundleAllContent = BundleNativeBinaries | BundleOtherFiles, - EnableCompression = 8, - }; -} diff --git a/src/Velopack.Packaging.HostModel/Bundle/Bundler.cs b/src/Velopack.Packaging.HostModel/Bundle/Bundler.cs deleted file mode 100644 index 5a10e427..00000000 --- a/src/Velopack.Packaging.HostModel/Bundle/Bundler.cs +++ /dev/null @@ -1,373 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.IO.Compression; -using System.Reflection.PortableExecutable; -using System.Runtime.InteropServices; -using Microsoft.NET.HostModel.AppHost; - -namespace Microsoft.NET.HostModel.Bundle -{ - /// - /// Bundler: Functionality to embed the managed app and its dependencies - /// into the host native binary. - /// - public class Bundler - { - public const uint BundlerMajorVersion = 6; - public const uint BundlerMinorVersion = 0; - public readonly Manifest BundleManifest; - - private readonly string _hostName; - private readonly string _outputDir; - private readonly string _depsJson; - private readonly string _runtimeConfigJson; - private readonly string _runtimeConfigDevJson; - - private readonly Trace _tracer; - private readonly TargetInfo _target; - private readonly BundleOptions _options; - private readonly bool _macosCodesign; - - public Bundler(string hostName, - string outputDir, - BundleOptions options = BundleOptions.None, - OSPlatform? targetOS = null, - Architecture? targetArch = null, - Version targetFrameworkVersion = null, - bool diagnosticOutput = false, - string appAssemblyName = null, - bool macosCodesign = true) - { - _tracer = new Trace(diagnosticOutput); - - _hostName = hostName; - _outputDir = Path.GetFullPath(string.IsNullOrEmpty(outputDir) ? Environment.CurrentDirectory : outputDir); - _target = new TargetInfo(targetOS, targetArch, targetFrameworkVersion); - - if (_target.BundleMajorVersion < 6 && - (options & BundleOptions.EnableCompression) != 0) - { - throw new ArgumentException("Compression requires framework version 6.0 or above", nameof(options)); - } - - appAssemblyName ??= _target.GetAssemblyName(hostName); - _depsJson = appAssemblyName + ".deps.json"; - _runtimeConfigJson = appAssemblyName + ".runtimeconfig.json"; - _runtimeConfigDevJson = appAssemblyName + ".runtimeconfig.dev.json"; - - BundleManifest = new Manifest(_target.BundleMajorVersion, netcoreapp3CompatMode: options.HasFlag(BundleOptions.BundleAllContent)); - _options = _target.DefaultOptions | options; - _macosCodesign = macosCodesign; - } - - private bool ShouldCompress(FileType type) - { - if (!_options.HasFlag(BundleOptions.EnableCompression)) - { - return false; - } - - switch (type) - { - case FileType.DepsJson: - case FileType.RuntimeConfigJson: - return false; - - default: - return true; - } - } - - /// - /// Embed 'file' into 'bundle' - /// - /// - /// startOffset: offset of the start 'file' within 'bundle' - /// compressedSize: size of the compressed data, if entry was compressed, otherwise 0 - /// - private (long startOffset, long compressedSize) AddToBundle(Stream bundle, Stream file, FileType type) - { - long startOffset = bundle.Position; - if (ShouldCompress(type)) - { - long fileLength = file.Length; - file.Position = 0; - - // We use DeflateStream here. - // It uses GZip algorithm, but with a trivial header that does not contain file info. - using (DeflateStream compressionStream = new DeflateStream(bundle, CompressionLevel.Optimal, leaveOpen: true)) - { - file.CopyTo(compressionStream); - } - - long compressedSize = bundle.Position - startOffset; - if (compressedSize < fileLength * 0.75) - { - return (startOffset, compressedSize); - } - - // compression rate was not good enough - // roll back the bundle offset and let the uncompressed code path take care of the entry. - bundle.Seek(startOffset, SeekOrigin.Begin); - } - - if (type == FileType.Assembly) - { - long misalignment = (bundle.Position % _target.AssemblyAlignment); - - if (misalignment != 0) - { - long padding = _target.AssemblyAlignment - misalignment; - bundle.Position += padding; - } - } - - file.Position = 0; - startOffset = bundle.Position; - file.CopyTo(bundle); - - return (startOffset, 0); - } - - private bool IsHost(string fileRelativePath) - { - return fileRelativePath.Equals(_hostName); - } - - private bool ShouldIgnore(string fileRelativePath) - { - return fileRelativePath.Equals(_runtimeConfigDevJson); - } - - private bool ShouldExclude(FileType type, string relativePath) - { - switch (type) - { - case FileType.Assembly: - case FileType.DepsJson: - case FileType.RuntimeConfigJson: - return false; - - case FileType.NativeBinary: - return !_options.HasFlag(BundleOptions.BundleNativeBinaries) || _target.ShouldExclude(relativePath); - - case FileType.Symbols: - return !_options.HasFlag(BundleOptions.BundleSymbolFiles); - - case FileType.Unknown: - return !_options.HasFlag(BundleOptions.BundleOtherFiles); - - default: - Debug.Assert(false); - return false; - } - } - - private bool IsAssembly(string path, out bool isPE) - { - isPE = false; - - using (FileStream file = File.OpenRead(path)) - { - try - { - PEReader peReader = new PEReader(file); - CorHeader corHeader = peReader.PEHeaders.CorHeader; - - isPE = true; // If peReader.PEHeaders doesn't throw, it is a valid PEImage - return corHeader != null; - } - catch (BadImageFormatException) - { - } - } - - return false; - } - - private FileType InferType(FileSpec fileSpec) - { - if (fileSpec.BundleRelativePath.Equals(_depsJson)) - { - return FileType.DepsJson; - } - - if (fileSpec.BundleRelativePath.Equals(_runtimeConfigJson)) - { - return FileType.RuntimeConfigJson; - } - - if (Path.GetExtension(fileSpec.BundleRelativePath).ToLowerInvariant().Equals(".pdb")) - { - return FileType.Symbols; - } - - bool isPE; - if (IsAssembly(fileSpec.SourcePath, out isPE)) - { - return FileType.Assembly; - } - - bool isNativeBinary = _target.IsWindows ? isPE : _target.IsNativeBinary(fileSpec.SourcePath); - - if (isNativeBinary) - { - return FileType.NativeBinary; - } - - return FileType.Unknown; - } - - /// - /// Generate a bundle, given the specification of embedded files - /// - /// - /// An enumeration FileSpecs for the files to be embedded. - /// - /// Files in fileSpecs that are not bundled within the single file bundle, - /// and should be published as separate files are marked as "IsExcluded" by this method. - /// This doesn't include unbundled files that should be dropped, and not publised as output. - /// - /// - /// The full path the the generated bundle file - /// - /// - /// ArgumentException if input is invalid - /// IOExceptions and ArgumentExceptions from callees flow to the caller. - /// - public string GenerateBundle(IReadOnlyList fileSpecs) - { - _tracer.Log($"Bundler Version: {BundlerMajorVersion}.{BundlerMinorVersion}"); - _tracer.Log($"Bundle Version: {BundleManifest.BundleVersion}"); - _tracer.Log($"Target Runtime: {_target}"); - _tracer.Log($"Bundler Options: {_options}"); - - if (fileSpecs.Any(x => !x.IsValid())) - { - throw new ArgumentException("Invalid input specification: Found entry with empty source-path or bundle-relative-path."); - } - - string hostSource; - try - { - hostSource = fileSpecs.Where(x => x.BundleRelativePath.Equals(_hostName)).Single().SourcePath; - } - catch (InvalidOperationException) - { - throw new ArgumentException("Invalid input specification: Must specify the host binary"); - } - - string bundlePath = Path.Combine(_outputDir, _hostName); - if (File.Exists(bundlePath)) - { - _tracer.Log($"Ovewriting existing File {bundlePath}"); - } - - BinaryUtils.CopyFile(hostSource, bundlePath); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable()) - { - RemoveCodesignIfNecessary(bundlePath); - } - - // Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app - // We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems - // and vice versa for Windows). So it's safer to do case sensitive comparison everywhere. - var relativePathToSpec = new Dictionary(StringComparer.Ordinal); - - long headerOffset = 0; - using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath))) - { - Stream bundle = writer.BaseStream; - bundle.Position = bundle.Length; - - foreach (var fileSpec in fileSpecs) - { - string relativePath = fileSpec.BundleRelativePath; - - if (IsHost(relativePath)) - { - continue; - } - - if (ShouldIgnore(relativePath)) - { - _tracer.Log($"Ignore: {relativePath}"); - continue; - } - - FileType type = InferType(fileSpec); - - if (ShouldExclude(type, relativePath)) - { - _tracer.Log($"Exclude [{type}]: {relativePath}"); - fileSpec.Excluded = true; - continue; - } - - if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec)) - { - if (!string.Equals(fileSpec.SourcePath, existingFileSpec.SourcePath, StringComparison.Ordinal)) - { - throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'"); - } - - // Exact duplicate - intentionally skip and don't include a second copy in the bundle - continue; - } - else - { - relativePathToSpec.Add(fileSpec.BundleRelativePath, fileSpec); - } - - using (FileStream file = File.OpenRead(fileSpec.SourcePath)) - { - FileType targetType = _target.TargetSpecificFileType(type); - (long startOffset, long compressedSize) = AddToBundle(bundle, file, targetType); - FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, _target.BundleMajorVersion); - _tracer.Log($"Embed: {entry}"); - } - } - - // Write the bundle manifest - headerOffset = BundleManifest.Write(writer); - _tracer.Log($"Header Offset={headerOffset}"); - _tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}"); - _tracer.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}"); - } - - HostWriter.SetAsBundle(bundlePath, headerOffset); - - // Sign the bundle if requested - if (_macosCodesign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable()) - { - var (exitCode, stdErr) = HostModelUtils.RunCodesign("-s -", bundlePath); - if (exitCode != 0) - { - throw new InvalidOperationException($"Failed to codesign '{bundlePath}': {stdErr}"); - } - } - - return bundlePath; - - // Remove mac code signature if applied before bundling - static void RemoveCodesignIfNecessary(string bundlePath) - { - Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)); - Debug.Assert(HostModelUtils.IsCodesignAvailable()); - - // `codesign -v` returns 0 if app is signed - if (HostModelUtils.RunCodesign("-v", bundlePath).ExitCode == 0) - { - var (exitCode, stdErr) = HostModelUtils.RunCodesign("--remove-signature", bundlePath); - if (exitCode != 0) - { - throw new InvalidOperationException($"Removing codesign from '{bundlePath}' failed: {stdErr}"); - } - } - } - } - } -} diff --git a/src/Velopack.Packaging.HostModel/Bundle/FileEntry.cs b/src/Velopack.Packaging.HostModel/Bundle/FileEntry.cs deleted file mode 100644 index 74053529..00000000 --- a/src/Velopack.Packaging.HostModel/Bundle/FileEntry.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.Bundle -{ - /// - /// FileEntry: Records information about embedded files. - /// - /// The bundle manifest records the following meta-data for each - /// file embedded in the bundle: - /// * Type (1 byte) - /// * NameLength (7-bit extension encoding, typically 1 byte) - /// * Name ("NameLength" Bytes) - /// * Offset (Int64) - /// * Size (Int64) - /// === present only in bundle version 3+ - /// * CompressedSize (Int64) 0 indicates No Compression - /// - public class FileEntry - { - public readonly uint BundleMajorVersion; - - public readonly long Offset; - public readonly long Size; - public readonly long CompressedSize; - public readonly FileType Type; - public readonly string RelativePath; // Path of an embedded file, relative to the Bundle source-directory. - - public const char DirectorySeparatorChar = '/'; - - public FileEntry(FileType fileType, string relativePath, long offset, long size, long compressedSize, uint bundleMajorVersion) - { - BundleMajorVersion = bundleMajorVersion; - Type = fileType; - RelativePath = relativePath.Replace('\\', DirectorySeparatorChar); - Offset = offset; - Size = size; - CompressedSize = compressedSize; - } - - public void Write(BinaryWriter writer) - { - writer.Write(Offset); - writer.Write(Size); - // compression is used only in version 6.0+ - if (BundleMajorVersion >= 6) - { - writer.Write(CompressedSize); - } - writer.Write((byte)Type); - writer.Write(RelativePath); - } - - public override string ToString() => $"{RelativePath} [{Type}] @{Offset} Sz={Size} CompressedSz={CompressedSize}"; - } -} diff --git a/src/Velopack.Packaging.HostModel/Bundle/FileSpec.cs b/src/Velopack.Packaging.HostModel/Bundle/FileSpec.cs deleted file mode 100644 index 14119e9e..00000000 --- a/src/Velopack.Packaging.HostModel/Bundle/FileSpec.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.Bundle -{ - /// - /// Information about files to embed into the Bundle (input to the Bundler). - /// - /// SourcePath: path to the file to be bundled at compile time - /// BundleRelativePath: path where the file is expected at run time, - /// relative to the app DLL. - /// - public class FileSpec - { - public readonly string SourcePath; - public readonly string BundleRelativePath; - public bool Excluded; - - public FileSpec(string sourcePath, string bundleRelativePath) - { - SourcePath = sourcePath; - BundleRelativePath = bundleRelativePath; - Excluded = false; - } - - public bool IsValid() - { - return !string.IsNullOrWhiteSpace(SourcePath) && - !string.IsNullOrWhiteSpace(BundleRelativePath); - } - - public override string ToString() => $"SourcePath: {SourcePath}, RelativePath: {BundleRelativePath} {(Excluded ? "[Excluded]" : "")}"; - } -} diff --git a/src/Velopack.Packaging.HostModel/Bundle/FileType.cs b/src/Velopack.Packaging.HostModel/Bundle/FileType.cs deleted file mode 100644 index 3369d314..00000000 --- a/src/Velopack.Packaging.HostModel/Bundle/FileType.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.Bundle -{ - /// - /// FileType: Identifies the type of file embedded into the bundle. - /// - /// The bundler differentiates a few kinds of files via the manifest, - /// with respect to the way in which they'll be used by the runtime. - /// - public enum FileType : byte - { - Unknown, // Type not determined. - Assembly, // IL and R2R Assemblies - NativeBinary, // NativeBinaries - DepsJson, // .deps.json configuration file - RuntimeConfigJson, // .runtimeconfig.json configuration file - Symbols // PDB Files - }; -} diff --git a/src/Velopack.Packaging.HostModel/Bundle/Manifest.cs b/src/Velopack.Packaging.HostModel/Bundle/Manifest.cs deleted file mode 100644 index f4f74bf0..00000000 --- a/src/Velopack.Packaging.HostModel/Bundle/Manifest.cs +++ /dev/null @@ -1,173 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Security.Cryptography; - -namespace Microsoft.NET.HostModel.Bundle -{ - /// - /// BundleManifest is a description of the contents of a bundle file. - /// This class handles creation and consumption of bundle-manifests. - /// - /// Here is the description of the Bundle Layout: - /// _______________________________________________ - /// AppHost - /// - /// - /// ------------Embedded Files --------------------- - /// The embedded files including the app, its - /// configuration files, dependencies, and - /// possibly the runtime. - /// - /// - /// - /// - /// - /// - /// - /// ------------ Bundle Header ------------- - /// MajorVersion - /// MinorVersion - /// NumEmbeddedFiles - /// ExtractionID - /// DepsJson Location [Version 2+] - /// Offset - /// Size - /// RuntimeConfigJson Location [Version 2+] - /// Offset - /// Size - /// Flags [Version 2+] - /// - - - - - - Manifest Entries - - - - - - - - - - - - /// Series of FileEntries (for each embedded file) - /// [File Type, Name, Offset, Size information] - /// - /// - /// - /// _________________________________________________ - /// - public class Manifest - { - // NetcoreApp3CompatMode flag is set on a .net5 app, - // which chooses to build single-file apps in .netcore3.x compat mode, - // by constructing the bundler with BundleAllConent option. - // This mode is expected to be deprecated in future versions of .NET. - [Flags] - private enum HeaderFlags : ulong - { - None = 0, - NetcoreApp3CompatMode = 1 - } - - // Bundle ID is a string that is used to uniquely - // identify this bundle. It is choosen to be compatible - // with path-names so that the AppHost can use it in - // extraction path. - public string BundleID { get; private set; } - //Same as Path.GetRandomFileName - private const int BundleIdLength = 12; - private SHA256 bundleHash = SHA256.Create(); - public readonly uint BundleMajorVersion; - // The Minor version is currently unused, and is always zero - public const uint BundleMinorVersion = 0; - private FileEntry DepsJsonEntry; - private FileEntry RuntimeConfigJsonEntry; - private HeaderFlags Flags; - public List Files; - public string BundleVersion => $"{BundleMajorVersion}.{BundleMinorVersion}"; - - public Manifest(uint bundleMajorVersion, bool netcoreapp3CompatMode = false) - { - BundleMajorVersion = bundleMajorVersion; - Files = new List(); - Flags = (netcoreapp3CompatMode) ? HeaderFlags.NetcoreApp3CompatMode : HeaderFlags.None; - } - - public FileEntry AddEntry(FileType type, FileStream fileContent, string relativePath, long offset, long compressedSize, uint bundleMajorVersion) - { - if (bundleHash == null) - { - throw new InvalidOperationException("It is forbidden to change Manifest state after it was written or BundleId was obtained."); - } - - FileEntry entry = new FileEntry(type, relativePath, offset, fileContent.Length, compressedSize, bundleMajorVersion); - Files.Add(entry); - - fileContent.Position = 0; - byte[] hashBytes = ComputeSha256Hash(fileContent); - bundleHash.TransformBlock(hashBytes, 0, hashBytes.Length, hashBytes, 0); - - switch (entry.Type) - { - case FileType.DepsJson: - DepsJsonEntry = entry; - break; - case FileType.RuntimeConfigJson: - RuntimeConfigJsonEntry = entry; - break; - - case FileType.Assembly: - break; - - default: - break; - } - - return entry; - } - - private static byte[] ComputeSha256Hash(Stream stream) - { - using (SHA256 sha = SHA256.Create()) - { - return sha.ComputeHash(stream); - } - } - - private string GenerateDeterministicId() - { - bundleHash.TransformFinalBlock(Array.Empty(), 0, 0); - byte[] manifestHash = bundleHash.Hash; - bundleHash.Dispose(); - bundleHash = null; - - return Convert.ToBase64String(manifestHash).Substring(BundleIdLength).Replace('/', '_'); - } - - public long Write(BinaryWriter writer) - { - BundleID = BundleID ?? GenerateDeterministicId(); - - long startOffset = writer.BaseStream.Position; - - // Write the bundle header - writer.Write(BundleMajorVersion); - writer.Write(BundleMinorVersion); - writer.Write(Files.Count); - writer.Write(BundleID); - - if (BundleMajorVersion >= 2) - { - writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Offset : 0); - writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Size : 0); - - writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Offset : 0); - writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Size : 0); - - writer.Write((ulong)Flags); - } - - // Write the manifest entries - foreach (FileEntry entry in Files) - { - entry.Write(writer); - } - - return startOffset; - } - - public bool Contains(string relativePath) - { - return Files.Any(entry => relativePath.Equals(entry.RelativePath)); - } - } -} diff --git a/src/Velopack.Packaging.HostModel/Bundle/TargetInfo.cs b/src/Velopack.Packaging.HostModel/Bundle/TargetInfo.cs deleted file mode 100644 index 1342dc4e..00000000 --- a/src/Velopack.Packaging.HostModel/Bundle/TargetInfo.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.NET.HostModel.AppHost; -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace Microsoft.NET.HostModel.Bundle -{ - /// - /// TargetInfo: Information about the target for which the single-file bundle is built. - /// - /// Currently the TargetInfo only tracks: - /// - the target operating system - /// - the target architecture - /// - the target framework - /// - the default options for this target - /// - the assembly alignment for this target - /// - - public class TargetInfo - { - public readonly OSPlatform OS; - public readonly Architecture Arch; - public readonly Version FrameworkVersion; - public readonly uint BundleMajorVersion; - public readonly BundleOptions DefaultOptions; - public readonly int AssemblyAlignment; - - public TargetInfo(OSPlatform? os, Architecture? arch, Version targetFrameworkVersion) - { - OS = os ?? HostOS; - Arch = arch ?? RuntimeInformation.OSArchitecture; - FrameworkVersion = targetFrameworkVersion ?? net60; - - Debug.Assert(IsLinux || IsOSX || IsWindows); - - if (FrameworkVersion.CompareTo(net60) >= 0) - { - BundleMajorVersion = 6u; - DefaultOptions = BundleOptions.None; - } - else if (FrameworkVersion.CompareTo(net50) >= 0) - { - BundleMajorVersion = 2u; - DefaultOptions = BundleOptions.None; - } - else if (FrameworkVersion.Major == 3 && (FrameworkVersion.Minor == 0 || FrameworkVersion.Minor == 1)) - { - BundleMajorVersion = 1u; - DefaultOptions = BundleOptions.BundleAllContent; - } - else - { - throw new ArgumentException($"Invalid input: Unsupported Target Framework Version {targetFrameworkVersion}"); - } - - if (IsLinux && Arch == Architecture.Arm64) - { - // We align assemblies in the bundle at 4K so that we can use mmap on Linux without changing the page alignment of ARM64 R2R code. - // This is only necessary for R2R assemblies, but we do it for all assemblies for simplicity. - // See https://github.com/dotnet/runtime/issues/41832. - AssemblyAlignment = 4096; - } - else - { - // Otherwise, assemblies are 16 bytes aligned, so that their sections can be memory-mapped cache aligned. - AssemblyAlignment = 16; - } - } - - public bool IsNativeBinary(string filePath) - { - return IsLinux ? ElfUtils.IsElfImage(filePath) : IsOSX ? MachOUtils.IsMachOImage(filePath) : PEUtils.IsPEImage(filePath); - } - - public string GetAssemblyName(string hostName) - { - // This logic to calculate assembly name from hostName should be removed (and probably moved to test helpers) - // once the SDK in the correct assembly name. - return (IsWindows ? Path.GetFileNameWithoutExtension(hostName) : hostName); - } - - public override string ToString() - { - string os = IsWindows ? "win" : IsLinux ? "linux" : "osx"; - string arch = Arch.ToString().ToLowerInvariant(); - return $"OS: {os} Arch: {arch} FrameworkVersion: {FrameworkVersion}"; - } - - private static OSPlatform HostOS => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OSPlatform.Linux : - RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OSPlatform.OSX : OSPlatform.Windows; - - public bool IsLinux => OS.Equals(OSPlatform.Linux); - public bool IsOSX => OS.Equals(OSPlatform.OSX); - public bool IsWindows => OS.Equals(OSPlatform.Windows); - - // The .net core 3 apphost doesn't care about semantics of FileType -- all files are extracted at startup. - // However, the apphost checks that the FileType value is within expected bounds, so set it to the first enumeration. - public FileType TargetSpecificFileType(FileType fileType) => (BundleMajorVersion == 1) ? FileType.Unknown : fileType; - - // In .net core 3.x, bundle processing happens within the AppHost. - // Therefore HostFxr and HostPolicy can be bundled within the single-file app. - // In .net 5, bundle processing happens in HostFxr and HostPolicy libraries. - // Therefore, these libraries themselves cannot be bundled into the single-file app. - // This problem is mitigated by statically linking these host components with the AppHost. - // https://github.com/dotnet/runtime/issues/32823 - public bool ShouldExclude(string relativePath) => - (FrameworkVersion.Major != 3) && (relativePath.Equals(HostFxr) || relativePath.Equals(HostPolicy)); - - private readonly Version net60 = new Version(6, 0); - private readonly Version net50 = new Version(5, 0); - private string HostFxr => IsWindows ? "hostfxr.dll" : IsLinux ? "libhostfxr.so" : "libhostfxr.dylib"; - private string HostPolicy => IsWindows ? "hostpolicy.dll" : IsLinux ? "libhostpolicy.so" : "libhostpolicy.dylib"; - - - } -} diff --git a/src/Velopack.Packaging.HostModel/Bundle/Trace.cs b/src/Velopack.Packaging.HostModel/Bundle/Trace.cs deleted file mode 100644 index a531e702..00000000 --- a/src/Velopack.Packaging.HostModel/Bundle/Trace.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.NET.HostModel.Bundle -{ - /// - /// Tracing utilities for diagnostic output - /// - public class Trace - { - private readonly bool Verbose; - - public Trace(bool verbose) - { - Verbose = verbose; - } - - public void Log(string fmt, params object[] args) - { - if (Verbose) - { - Console.WriteLine("LOG: " + fmt, args); - } - } - - public void Error(string type, string message) - { - Console.Error.WriteLine($"ERROR: {message}"); - } - } -} diff --git a/src/Velopack.Packaging.HostModel/HostModelUtils.cs b/src/Velopack.Packaging.HostModel/HostModelUtils.cs deleted file mode 100644 index a4eafb9a..00000000 --- a/src/Velopack.Packaging.HostModel/HostModelUtils.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace Microsoft.NET.HostModel -{ - internal static class HostModelUtils - { - private const string CodesignPath = @"/usr/bin/codesign"; - - public static bool IsCodesignAvailable() => File.Exists(CodesignPath); - - public static (int ExitCode, string StdErr) RunCodesign(string args, string appHostPath) - { - Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)); - Debug.Assert(IsCodesignAvailable()); - - var psi = new ProcessStartInfo() { - Arguments = $"{args} \"{appHostPath}\"", - FileName = CodesignPath, - RedirectStandardError = true, - UseShellExecute = false, - }; - - using (var p = Process.Start(psi)) { - p.WaitForExit(); - return (p.ExitCode, p.StandardError.ReadToEnd()); - } - } - } -} \ No newline at end of file diff --git a/src/Velopack.Packaging.HostModel/README.md b/src/Velopack.Packaging.HostModel/README.md deleted file mode 100644 index f473c6c8..00000000 --- a/src/Velopack.Packaging.HostModel/README.md +++ /dev/null @@ -1,19 +0,0 @@ -Host Model -=================================== - -HostModel is a library used by the [SDK](https://github.com/dotnet/sdk) to perform certain transformations on host executables. The main services implemented in HostModel are: - -* AppHost rewriter: Embeds the App Name into the AppHost executable. On Windows, also copies resources from App.dll to the AppHost. -* ComHost rewriter: Creates a ComHost with an embedded CLSIDMap file to map CLSIDs to .NET Classes. - -* Single-file bundler: Embeds an application and its dependencies into the AppHost, to publish a single executable, as described [here](https://github.com/dotnet/designs/blob/master/accepted/2020/single-file/design.md). - -The HostModel library is in the Runtime repo because: - -* The implementations of the host and HostModel are closely related, which facilitates easy development, update, and testing. -* Separating the HostModel implementation from SDK repo repo aligns with code ownership, and facilitates maintenance. - -The build targets/tasks that use the HostModel library are in the SDK repo because: - -* This facilitates the MSBuild tasks to be multi-targeted. -* It helps generate localized error messages, since SDK repo has the localization infrastructure. diff --git a/src/Velopack.Packaging.HostModel/ResourceUpdater.Squirrel.cs b/src/Velopack.Packaging.HostModel/ResourceUpdater.Squirrel.cs deleted file mode 100644 index 0b59075f..00000000 --- a/src/Velopack.Packaging.HostModel/ResourceUpdater.Squirrel.cs +++ /dev/null @@ -1,49 +0,0 @@ -// If updating HostModel, mark the ResourceUpdater.cs class as partial so these functions can get mixed in - -namespace Microsoft.NET.HostModel -{ - public partial class ResourceUpdater - { - public ResourceUpdater(string peFile, bool bDeleteExistingResources) - { - hUpdate = Kernel32.BeginUpdateResource(peFile, bDeleteExistingResources); - if (hUpdate.IsInvalid) { - ThrowExceptionForLastWin32Error(); - } - } - - public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName, ushort langId) - { - if (hUpdate.IsInvalid) { - ThrowExceptionForInvalidUpdate(); - } - - if (!IsIntResource(lpName)) { - throw new ArgumentException("AddResource can only be used with integer resource names"); - } - - if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, langId, data, (uint) data.Length)) { - ThrowExceptionForLastWin32Error(); - } - - return this; - } - - //public ResourceUpdater ClearResource(string lpType, IntPtr lpName, ushort langId) - //{ - // if (hUpdate.IsInvalid) { - // ThrowExceptionForInvalidUpdate(); - // } - - // if (!IsIntResource(lpName)) { - // throw new ArgumentException("AddResource can only be used with integer resource names"); - // } - - // if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, langId, null, 0)) { - // ThrowExceptionForLastWin32Error(); - // } - - // return this; - //} - } -} diff --git a/src/Velopack.Packaging.HostModel/ResourceUpdater.cs b/src/Velopack.Packaging.HostModel/ResourceUpdater.cs deleted file mode 100644 index 1b8a7387..00000000 --- a/src/Velopack.Packaging.HostModel/ResourceUpdater.cs +++ /dev/null @@ -1,458 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace Microsoft.NET.HostModel -{ - /// - /// Provides methods for modifying the embedded native resources - /// in a PE image. It currently only works on Windows, because it - /// requires various kernel32 APIs. - /// - public partial class ResourceUpdater : IDisposable - { - private sealed class Kernel32 - { - // - // Native methods for updating resources - // - - [DllImport(nameof(Kernel32), CharSet = CharSet.Unicode, SetLastError = true)] - public static extern SafeUpdateHandle BeginUpdateResource(string pFileName, - [MarshalAs(UnmanagedType.Bool)] bool bDeleteExistingResources); - - // Update a resource with data from an IntPtr - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool UpdateResource(SafeUpdateHandle hUpdate, - IntPtr lpType, - IntPtr lpName, - ushort wLanguage, - IntPtr lpData, - uint cbData); - - // Update a resource with data from a managed byte[] - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool UpdateResource(SafeUpdateHandle hUpdate, - IntPtr lpType, - IntPtr lpName, - ushort wLanguage, - [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 5)] byte[] lpData, - uint cbData); - - // Update a resource with data from a managed byte[] - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool UpdateResource(SafeUpdateHandle hUpdate, - string lpType, - IntPtr lpName, - ushort wLanguage, - [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 5)] byte[] lpData, - uint cbData); - - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool EndUpdateResource(SafeUpdateHandle hUpdate, - bool fDiscard); - - // The IntPtr version of this dllimport is used in the - // SafeHandle implementation - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool EndUpdateResource(IntPtr hUpdate, - bool fDiscard); - - public const ushort LangID_LangNeutral_SublangNeutral = 0; - - // - // Native methods used to read resources from a PE file - // - - // Loading and freeing PE files - - public enum LoadLibraryFlags : uint - { - LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040, - LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020 - } - - [DllImport(nameof(Kernel32), CharSet = CharSet.Unicode, SetLastError = true)] - public static extern IntPtr LoadLibraryEx(string lpFileName, - IntPtr hReservedNull, - LoadLibraryFlags dwFlags); - - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool FreeLibrary(IntPtr hModule); - - // Enumerating resources - - public delegate bool EnumResTypeProc(IntPtr hModule, - IntPtr lpType, - IntPtr lParam); - - public delegate bool EnumResNameProc(IntPtr hModule, - IntPtr lpType, - IntPtr lpName, - IntPtr lParam); - - public delegate bool EnumResLangProc(IntPtr hModule, - IntPtr lpType, - IntPtr lpName, - ushort wLang, - IntPtr lParam); - - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool EnumResourceTypes(IntPtr hModule, - EnumResTypeProc lpEnumFunc, - IntPtr lParam); - - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool EnumResourceNames(IntPtr hModule, - IntPtr lpType, - EnumResNameProc lpEnumFunc, - IntPtr lParam); - - [DllImport(nameof(Kernel32), SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool EnumResourceLanguages(IntPtr hModule, - IntPtr lpType, - IntPtr lpName, - EnumResLangProc lpEnumFunc, - IntPtr lParam); - - public const int UserStoppedResourceEnumerationHRESULT = unchecked((int) 0x80073B02); - public const int ResourceDataNotFoundHRESULT = unchecked((int) 0x80070714); - - // Querying and loading resources - - [DllImport(nameof(Kernel32), SetLastError = true)] - public static extern IntPtr FindResourceEx(IntPtr hModule, - IntPtr lpType, - IntPtr lpName, - ushort wLanguage); - - [DllImport(nameof(Kernel32), SetLastError = true)] - public static extern IntPtr LoadResource(IntPtr hModule, - IntPtr hResInfo); - - [DllImport(nameof(Kernel32))] // does not call SetLastError - public static extern IntPtr LockResource(IntPtr hResData); - - [DllImport(nameof(Kernel32), SetLastError = true)] - public static extern uint SizeofResource(IntPtr hModule, - IntPtr hResInfo); - - public const int ERROR_CALL_NOT_IMPLEMENTED = 0x78; - } - - /// - /// Holds the update handle returned by BeginUpdateResource. - /// Normally, native resources for the update handle are - /// released by a call to ResourceUpdater.Update(). In case - /// this doesn't happen, the SafeUpdateHandle will release the - /// native resources for the update handle without updating - /// the target file. - /// - private sealed class SafeUpdateHandle : SafeHandle - { - public SafeUpdateHandle() : base(IntPtr.Zero, true) - { - } - - public override bool IsInvalid => handle == IntPtr.Zero; - - protected override bool ReleaseHandle() - { - // discard pending updates without writing them - return Kernel32.EndUpdateResource(handle, true); - } - } - - /// - /// Holds the native handle for the resource update. - /// - private readonly SafeUpdateHandle hUpdate; - - /// - /// Determines if the ResourceUpdater is supported by the current operating system. - /// Some versions of Windows, such as Nano Server, do not support the needed APIs. - /// - public static bool IsSupportedOS() - { -#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return false; - } -#endif - - try { - // On Nano Server 1709+, `BeginUpdateResource` is exported but returns a null handle with a zero error - // Try to call `BeginUpdateResource` with an invalid parameter; the error should be non-zero if supported - // On Nano Server 20213, `BeginUpdateResource` fails with ERROR_CALL_NOT_IMPLEMENTED - using (var handle = Kernel32.BeginUpdateResource("", false)) { - int lastWin32Error = Marshal.GetLastWin32Error(); - - if (handle.IsInvalid && (lastWin32Error == 0 || lastWin32Error == Kernel32.ERROR_CALL_NOT_IMPLEMENTED)) { - return false; - } - } - } catch (EntryPointNotFoundException) { - // BeginUpdateResource isn't exported from Kernel32 - return false; - } - - return true; - } - - /// - /// Create a resource updater for the given PE file. This will - /// acquire a native resource update handle for the file, - /// preparing it for updates. Resources can be added to this - /// updater, which will queue them for update. The target PE - /// file will not be modified until Update() is called, after - /// which the ResourceUpdater can not be used for further - /// updates. - /// - public ResourceUpdater(string peFile) - { - hUpdate = Kernel32.BeginUpdateResource(peFile, false); - if (hUpdate.IsInvalid) { - ThrowExceptionForLastWin32Error(); - } - } - - /// - /// Add all resources from a source PE file. It is assumed - /// that the input is a valid PE file. If it is not, an - /// exception will be thrown. This will not modify the target - /// until Update() is called. - /// Throws an InvalidOperationException if Update() was already called. - /// - public ResourceUpdater AddResourcesFromPEImage(string peFile) - { - if (hUpdate.IsInvalid) { - ThrowExceptionForInvalidUpdate(); - } - - // Using both flags lets the OS loader decide how to load - // it most efficiently. Either mode will prevent other - // processes from modifying the module while it is loaded. - IntPtr hModule = Kernel32.LoadLibraryEx(peFile, IntPtr.Zero, - Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE | - Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_IMAGE_RESOURCE); - if (hModule == IntPtr.Zero) { - ThrowExceptionForLastWin32Error(); - } - - var enumTypesCallback = new Kernel32.EnumResTypeProc(EnumAndUpdateTypesCallback); - var errorInfo = new EnumResourcesErrorInfo(); - GCHandle errorInfoHandle = GCHandle.Alloc(errorInfo); - var errorInfoPtr = GCHandle.ToIntPtr(errorInfoHandle); - - try { - if (!Kernel32.EnumResourceTypes(hModule, enumTypesCallback, errorInfoPtr)) { - if (Marshal.GetHRForLastWin32Error() != Kernel32.ResourceDataNotFoundHRESULT) { - CaptureEnumResourcesErrorInfo(errorInfoPtr); - errorInfo.ThrowException(); - } - } - } finally { - errorInfoHandle.Free(); - - if (!Kernel32.FreeLibrary(hModule)) { - ThrowExceptionForLastWin32Error(); - } - } - - return this; - } - - internal static bool IsIntResource(IntPtr lpType) - { - return ((uint) lpType >> 16) == 0; - } - - /// - /// Add a language-neutral integer resource from a byte[] with - /// a particular type and name. This will not modify the - /// target until Update() is called. - /// Throws an InvalidOperationException if Update() was already called. - /// - public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName) - { - if (hUpdate.IsInvalid) { - ThrowExceptionForInvalidUpdate(); - } - - if (!IsIntResource(lpType) || !IsIntResource(lpName)) { - throw new ArgumentException("AddResource can only be used with integer resource types"); - } - - if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint) data.Length)) { - ThrowExceptionForLastWin32Error(); - } - - return this; - } - - /// - /// Add a language-neutral integer resource from a byte[] with - /// a particular type and name. This will not modify the - /// target until Update() is called. - /// Throws an InvalidOperationException if Update() was already called. - /// - public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName) - { - if (hUpdate.IsInvalid) { - ThrowExceptionForInvalidUpdate(); - } - - if (!IsIntResource(lpName)) { - throw new ArgumentException("AddResource can only be used with integer resource names"); - } - - if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint) data.Length)) { - ThrowExceptionForLastWin32Error(); - } - - return this; - } - - /// - /// Write the pending resource updates to the target PE - /// file. After this, the ResourceUpdater no longer maintains - /// an update handle, and can not be used for further updates. - /// Throws an InvalidOperationException if Update() was already called. - /// - public void Update() - { - if (hUpdate.IsInvalid) { - ThrowExceptionForInvalidUpdate(); - } - - try { - if (!Kernel32.EndUpdateResource(hUpdate, false)) { - ThrowExceptionForLastWin32Error(); - } - } finally { - hUpdate.SetHandleAsInvalid(); - } - } - - private bool EnumAndUpdateTypesCallback(IntPtr hModule, IntPtr lpType, IntPtr lParam) - { - var enumNamesCallback = new Kernel32.EnumResNameProc(EnumAndUpdateNamesCallback); - if (!Kernel32.EnumResourceNames(hModule, lpType, enumNamesCallback, lParam)) { - CaptureEnumResourcesErrorInfo(lParam); - return false; - } - return true; - } - - private bool EnumAndUpdateNamesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, IntPtr lParam) - { - var enumLanguagesCallback = new Kernel32.EnumResLangProc(EnumAndUpdateLanguagesCallback); - if (!Kernel32.EnumResourceLanguages(hModule, lpType, lpName, enumLanguagesCallback, lParam)) { - CaptureEnumResourcesErrorInfo(lParam); - return false; - } - return true; - } - - private bool EnumAndUpdateLanguagesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLang, IntPtr lParam) - { - IntPtr hResource = Kernel32.FindResourceEx(hModule, lpType, lpName, wLang); - if (hResource == IntPtr.Zero) { - CaptureEnumResourcesErrorInfo(lParam); - return false; - } - - // hResourceLoaded is just a handle to the resource, which - // can be used to get the resource data - IntPtr hResourceLoaded = Kernel32.LoadResource(hModule, hResource); - if (hResourceLoaded == IntPtr.Zero) { - CaptureEnumResourcesErrorInfo(lParam); - return false; - } - - // This doesn't actually lock memory. It just retrieves a - // pointer to the resource data. The pointer is valid - // until the module is unloaded. - IntPtr lpResourceData = Kernel32.LockResource(hResourceLoaded); - if (lpResourceData == IntPtr.Zero) { - ((EnumResourcesErrorInfo) GCHandle.FromIntPtr(lParam).Target).failedToLockResource = true; - } - - if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, wLang, lpResourceData, Kernel32.SizeofResource(hModule, hResource))) { - CaptureEnumResourcesErrorInfo(lParam); - return false; - } - - return true; - } - - private class EnumResourcesErrorInfo - { - public int hResult; - public bool failedToLockResource; - - public void ThrowException() - { - if (failedToLockResource) { - Debug.Assert(hResult == 0); - throw new ResourceNotAvailableException("Failed to lock resource"); - } - - Debug.Assert(hResult != 0); - throw new Win32Exception(hResult); - } - } - - private static void CaptureEnumResourcesErrorInfo(IntPtr errorInfoPtr) - { - int hResult = Marshal.GetHRForLastWin32Error(); - if (hResult != Kernel32.UserStoppedResourceEnumerationHRESULT) { - GCHandle errorInfoHandle = GCHandle.FromIntPtr(errorInfoPtr); - var errorInfo = (EnumResourcesErrorInfo) errorInfoHandle.Target; - errorInfo.hResult = hResult; - } - } - - private class ResourceNotAvailableException : Exception - { - public ResourceNotAvailableException(string message) : base(message) - { - } - } - - private static void ThrowExceptionForLastWin32Error() - { - throw new Win32Exception(Marshal.GetHRForLastWin32Error()); - } - - private static void ThrowExceptionForInvalidUpdate() - { - throw new InvalidOperationException("Update handle is invalid. This instance may not be used for further updates"); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public void Dispose(bool disposing) - { - if (disposing) { - hUpdate.Dispose(); - } - } - } -} diff --git a/src/Velopack.Packaging.HostModel/Velopack.Packaging.HostModel.csproj b/src/Velopack.Packaging.HostModel/Velopack.Packaging.HostModel.csproj deleted file mode 100644 index 054b9c4b..00000000 --- a/src/Velopack.Packaging.HostModel/Velopack.Packaging.HostModel.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net472;net6.0 - enable - $(NoWarn);CA2007;CS8002;IDE0161 - true - Microsoft.NET.HostModel - - - - - - - diff --git a/src/Velopack.Packaging.HostModel/packages.lock.json b/src/Velopack.Packaging.HostModel/packages.lock.json deleted file mode 100644 index 4210d9a2..00000000 --- a/src/Velopack.Packaging.HostModel/packages.lock.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETFramework,Version=v4.7.2": { - "Microsoft.SourceLink.GitHub": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" - } - }, - "Nerdbank.GitVersioning": { - "type": "Direct", - "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" - }, - "System.Reflection.Metadata": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", - "dependencies": { - "System.Collections.Immutable": "8.0.0", - "System.Memory": "4.5.5" - } - }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" - }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", - "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.5", - "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.5.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - } - }, - "net6.0": { - "Microsoft.SourceLink.GitHub": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" - } - }, - "Nerdbank.GitVersioning": { - "type": "Direct", - "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" - }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" - }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" - } - } - } -} \ No newline at end of file diff --git a/src/Velopack.Packaging.Unix/AppImageTool.cs b/src/Velopack.Packaging.Unix/AppImageTool.cs index 38053df6..b18bd107 100644 --- a/src/Velopack.Packaging.Unix/AppImageTool.cs +++ b/src/Velopack.Packaging.Unix/AppImageTool.cs @@ -1,30 +1,138 @@ -using System.Runtime.Versioning; +using ICSharpCode.SharpZipLib.Tar; using Microsoft.Extensions.Logging; +using Velopack.Compression; namespace Velopack.Packaging.Unix; public class AppImageTool { - [SupportedOSPlatform("linux")] public static void CreateLinuxAppImage(string appDir, string outputFile, RuntimeCpu machine, ILogger logger) { - var tool = HelperFile.AppImageToolX64; - - string arch = machine switch { - RuntimeCpu.x86 => "i386", - RuntimeCpu.x64 => "x86_64", - RuntimeCpu.arm64 => "arm_aarch64", + string runtime = machine switch { + RuntimeCpu.x86 => HelperFile.AppImageRuntimeX86, + RuntimeCpu.x64 => HelperFile.AppImageRuntimeX64, + RuntimeCpu.arm64 => HelperFile.AppImageRuntimeArm64, _ => throw new ArgumentOutOfRangeException(nameof(machine), machine, null) }; - var envVar = new Dictionary() { - { "ARCH", arch } - }; - - logger.Info("About to create .AppImage for architecture: " + arch); + string tmpSquashFile = outputFile + ".tmpfs"; + string tmpTarFile = outputFile + ".tmptar"; - Chmod.ChmodFileAsExecutable(tool); - Exe.InvokeAndThrowIfNonZero(tool, new[] { appDir, outputFile }, null, envVar); - Chmod.ChmodFileAsExecutable(outputFile); + try { + if (VelopackRuntimeInfo.IsWindows) { + // to workaround a permissions limitation of gensquashfs.exe + // we need to create a tar archive of the AppDir, setting Linux permissions in the tar header + // and then use tar2sqfs.exe to create the squashfs filesystem + logger.Info("Compressing AppDir into tar and setting file permissions"); + using (var outStream = File.Create(tmpTarFile)) + using (var tarArchive = TarArchive.CreateOutputTarArchive(outStream)) { + tarArchive.RootPath = Path.GetFullPath(appDir); + void AddDirectoryToTar(TarArchive tarArchive, DirectoryInfo dir) + { + var directories = dir.GetDirectories(); + foreach (var directory in directories) { + AddDirectoryToTar(tarArchive, directory); + } + + var filenames = dir.GetFiles(); + foreach (var filename in filenames) { + var tarEntry = TarEntry.CreateEntryFromFile(filename.FullName); + tarEntry.TarHeader.Magic = "ustar"; + tarEntry.TarHeader.Version = "00"; + tarEntry.TarHeader.ModTime = EasyZip.ZipFormatMinDate; + tarEntry.TarHeader.Mode = Convert.ToInt32("755", 8); + tarArchive.WriteEntry(tarEntry, true); + } + } + AddDirectoryToTar(tarArchive, new DirectoryInfo(appDir)); + } + + logger.Info("Converting tar into squashfs filesystem"); + var tool = HelperFile.FindHelperFile("squashfs-tools\\tar2sqfs.exe"); + logger.Debug(Exe.RunHostedCommand($"\"{tool}\" \"{tmpSquashFile}\" < \"{tmpTarFile}\"")); + } else { + Exe.AssertSystemBinaryExists("mksquashfs", "sudo apt install squashfs-tools", "brew install squashfs"); + var tool = "mksquashfs"; + List args = + [ + appDir, + tmpSquashFile, + "-comp", + "xz", + "-root-owned", + "-noappend", + "-Xdict-size", + "100%", + "-b", + "16384", + "-mkfs-time", + "0", + ]; + logger.Info("Compressing AppDir into squashfs filesystem"); + logger.Debug(Exe.InvokeAndThrowIfNonZero(tool, args, null)); + } + + logger.Info($"Creating AppImage with {Path.GetFileName(runtime)} runtime"); + File.Copy(runtime, outputFile, true); + + using var outputfs = File.Open(outputFile, FileMode.Append); + using var squashfs = File.OpenRead(tmpSquashFile); + squashfs.CopyTo(outputfs); + + Chmod.ChmodFileAsExecutable(outputFile); + } finally { + Utility.DeleteFileOrDirectoryHard(tmpSquashFile); + Utility.DeleteFileOrDirectoryHard(tmpTarFile); + } + } + + public static void CreateLinuxAppImageOld(string appDir, string outputFile, RuntimeCpu machine, ILogger logger) + { + string runtime = machine switch { + RuntimeCpu.x86 => HelperFile.AppImageRuntimeX86, + RuntimeCpu.x64 => HelperFile.AppImageRuntimeX64, + RuntimeCpu.arm64 => HelperFile.AppImageRuntimeArm64, + _ => throw new ArgumentOutOfRangeException(nameof(machine), machine, null) + }; + + string tool = HelperFile.GetMkSquashFsPath(); + List args = new(); + + string tmpPath = outputFile + ".tmpfs"; + if (VelopackRuntimeInfo.IsWindows) { + args.Add("--all-root"); + args.Add("--pack-dir"); + args.Add(appDir); + args.Add(tmpPath); + } else { + args.Add(appDir); + args.Add(tmpPath); + args.Add("-comp"); + args.Add("xz"); + args.Add("-root-owned"); + args.Add("-noappend"); + args.Add("-Xdict-size"); + args.Add("100%"); + args.Add("-b"); + args.Add("16384"); + args.Add("-mkfs-time"); + args.Add("0"); + } + + try { + logger.Info("Compressing AppDir into squashfs filesystem"); + Exe.InvokeAndThrowIfNonZero(tool, args, null); + + logger.Info($"Creating AppImage with {Path.GetFileName(runtime)} runtime"); + File.Copy(runtime, outputFile, true); + + using var outputfs = File.Open(outputFile, FileMode.Append); + using var squashfs = File.OpenRead(tmpPath); + squashfs.CopyTo(outputfs); + + Chmod.ChmodFileAsExecutable(outputFile); + } finally { + Utility.DeleteFileOrDirectoryHard(tmpPath); + } } } diff --git a/src/Velopack.Packaging.Unix/BinDetect.cs b/src/Velopack.Packaging.Unix/BinDetect.cs new file mode 100644 index 00000000..369e3c71 --- /dev/null +++ b/src/Velopack.Packaging.Unix/BinDetect.cs @@ -0,0 +1,46 @@ +namespace Velopack.Packaging; + +public class BinDetect +{ + private enum MagicMachO : uint + { + MH_MAGIC = 0xfeedface, + MH_CIGAM = 0xcefaedfe, + MH_MAGIC_64 = 0xfeedfacf, + MH_CIGAM_64 = 0xcffaedfe + } + + public static bool IsMachOImage(string filePath) + { + using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath))) { + if (reader.BaseStream.Length < 256) // Header size + return false; + + uint magic = reader.ReadUInt32(); + return Enum.IsDefined(typeof(MagicMachO), magic); + } + } + + // First four bytes of valid ELF, as defined in https://github.com/torvalds/linux/blob/aae703b/include/uapi/linux/elf.h + // 0x7f (DEL), 'E', 'L', 'F' + private static ReadOnlySpan ElfMagic => "\u007f"u8 + "ELF"u8; + + public static bool IsElfImage(string filePath) + { + using FileStream fileStream = File.OpenRead(filePath); + using BinaryReader reader = new(fileStream); + + if (reader.BaseStream.Length < 16) // EI_NIDENT = 16 + { + return false; + } + + byte[] eIdent = reader.ReadBytes(4); + + return + eIdent[0] == ElfMagic[0] && + eIdent[1] == ElfMagic[1] && + eIdent[2] == ElfMagic[2] && + eIdent[3] == ElfMagic[3]; + } +} \ No newline at end of file diff --git a/src/Velopack.Packaging.Unix/Chmod.cs b/src/Velopack.Packaging.Unix/Chmod.cs index 79f91461..6ec899da 100644 --- a/src/Velopack.Packaging.Unix/Chmod.cs +++ b/src/Velopack.Packaging.Unix/Chmod.cs @@ -4,8 +4,6 @@ using System.Runtime.Versioning; namespace Velopack.Packaging.Unix; -[SupportedOSPlatform("linux")] -[SupportedOSPlatform("macos")] public class Chmod { private const string OSX_CSTD_LIB = "libSystem.dylib"; diff --git a/src/Velopack.Packaging.Unix/Commands/LinuxPackCommandRunner.cs b/src/Velopack.Packaging.Unix/Commands/LinuxPackCommandRunner.cs index e827add3..f8ab224c 100644 --- a/src/Velopack.Packaging.Unix/Commands/LinuxPackCommandRunner.cs +++ b/src/Velopack.Packaging.Unix/Commands/LinuxPackCommandRunner.cs @@ -1,11 +1,9 @@ -using System.Runtime.Versioning; -using ELFSharp.ELF; +using ELFSharp.ELF; using Microsoft.Extensions.Logging; using Velopack.Packaging.Abstractions; namespace Velopack.Packaging.Unix.Commands; -[SupportedOSPlatform("linux")] public class LinuxPackCommandRunner : PackageBuilder { protected string PortablePackagePath { get; set; } @@ -21,36 +19,34 @@ public class LinuxPackCommandRunner : PackageBuilder var bin = dir.CreateSubdirectory("usr").CreateSubdirectory("bin"); if (Options.PackIsAppDir) { - Log.Info("Using provided .AppDir, will skip building new one."); + Log.Info("Using provided AppDir, will skip building new one."); CopyFiles(new DirectoryInfo(Options.PackDirectory), dir, progress, true); } else { - Log.Info("Building new .AppDir"); + Log.Info("Building automatic AppDir from pack directory"); var appRunPath = Path.Combine(dir.FullName, "AppRun"); // app icon - var icon = Options.Icon ?? HelperFile.GetDefaultAppIcon(); + var icon = Options.Icon ?? HelperFile.GetDefaultAppIcon(RuntimeOs.Linux); var iconFilename = Options.PackId + Path.GetExtension(icon); File.Copy(icon, Path.Combine(dir.FullName, iconFilename), true); - var categories = String.IsNullOrWhiteSpace(Options.Categories) - ? "Utility" + var categories = String.IsNullOrWhiteSpace(Options.Categories) + ? "Utility" : Options.Categories.TrimEnd(';'); File.WriteAllText(appRunPath, $$""" #!/bin/sh - if [ ! -z "$APPIMAGE" ] && [ ! -z "$APPDIR" ]; then MD5=$(echo -n "file://$APPIMAGE" | md5sum | cut -d' ' -f1) - cp "$APPDIR/{{iconFilename}}" "$HOME/.cache/thumbnails/normal/$MD5.png" - cp "$APPDIR/{{iconFilename}}" "$HOME/.cache/thumbnails/large/$MD5.png" - xdg-icon-resource forceupdate + cp "$APPDIR/{{iconFilename}}" "$HOME/.cache/thumbnails/normal/$MD5.png" >/dev/null 2>&1 + cp "$APPDIR/{{iconFilename}}" "$HOME/.cache/thumbnails/large/$MD5.png" >/dev/null 2>&1 + xdg-icon-resource forceupdate >/dev/null 2>&1 fi - HERE="$(dirname "$(readlink -f "${0}")")" export PATH="${HERE}"/usr/bin/:"${PATH}" EXEC=$(grep -e '^Exec=.*' "${HERE}"/*.desktop | head -n 1 | cut -d "=" -f 2 | cut -d " " -f 1 | sed 's/\\s/ /g') exec "${EXEC}" "$@" -"""); +""".Replace("\r", "")); Chmod.ChmodFileAsExecutable(appRunPath); var mainExeName = Options.EntryExecutableName ?? Options.PackId; @@ -71,7 +67,7 @@ Icon={Options.PackId} Exec={mainExeName} StartupWMClass={Options.PackId} Categories={categories}; -"""); +""".Replace("\r", "")); // copy existing app files CopyFiles(new DirectoryInfo(packDir), bin, progress, true); @@ -79,7 +75,7 @@ Categories={categories}; // velopack required files File.WriteAllText(Path.Combine(bin.FullName, "sq.version"), GenerateNuspecContent()); - File.Copy(HelperFile.GetUpdatePath(), Path.Combine(bin.FullName, "UpdateNix"), true); + File.Copy(HelperFile.GetUpdatePath(RuntimeOs.Linux), Path.Combine(bin.FullName, "UpdateNix"), true); progress(100); return Task.FromResult(dir.FullName); } diff --git a/src/Velopack.Packaging.Unix/Commands/OsxBundleCommandRunner.cs b/src/Velopack.Packaging.Unix/Commands/OsxBundleCommandRunner.cs index 900c53f2..3fba6abc 100644 --- a/src/Velopack.Packaging.Unix/Commands/OsxBundleCommandRunner.cs +++ b/src/Velopack.Packaging.Unix/Commands/OsxBundleCommandRunner.cs @@ -25,7 +25,7 @@ public class OsxBundleCommandRunner : ICommand public string Bundle(OsxBundleOptions options) { - var icon = options.Icon ?? HelperFile.GetDefaultAppIcon(); + var icon = options.Icon ?? HelperFile.GetDefaultAppIcon(RuntimeOs.OSX); var packId = options.PackId; var packDirectory = options.PackDirectory; var packVersion = options.PackVersion; @@ -41,7 +41,7 @@ public class OsxBundleCommandRunner : ICommand throw new UserInfoException($"--exeName '{mainExePath}' does not exist."); } - if (!MachO.IsMachOImage(mainExePath)) { + if (!BinDetect.IsMachOImage(mainExePath)) { throw new UserInfoException($"--exeName '{mainExePath}' is not a mach-o executable."); } diff --git a/src/Velopack.Packaging.Unix/Commands/OsxPackCommandRunner.cs b/src/Velopack.Packaging.Unix/Commands/OsxPackCommandRunner.cs index 4dbdca77..aee11b5e 100644 --- a/src/Velopack.Packaging.Unix/Commands/OsxPackCommandRunner.cs +++ b/src/Velopack.Packaging.Unix/Commands/OsxPackCommandRunner.cs @@ -32,10 +32,10 @@ public class OsxPackCommandRunner : PackageBuilder var structure = new OsxStructureBuilder(dir.FullName); var macosdir = structure.MacosDirectory; File.WriteAllText(Path.Combine(macosdir, "sq.version"), GenerateNuspecContent()); - File.Copy(HelperFile.GetUpdatePath(), Path.Combine(macosdir, "UpdateMac"), true); + File.Copy(HelperFile.GetUpdatePath(RuntimeOs.OSX), Path.Combine(macosdir, "UpdateMac"), true); foreach (var f in Directory.GetFiles(macosdir, "*", SearchOption.AllDirectories)) { - if (MachO.IsMachOImage(f)) { + if (BinDetect.IsMachOImage(f)) { Log.Debug(f + " is a mach-o binary, chmod as executable."); Chmod.ChmodFileAsExecutable(f); } diff --git a/src/Velopack.Packaging.Unix/Velopack.Packaging.Unix.csproj b/src/Velopack.Packaging.Unix/Velopack.Packaging.Unix.csproj index c5b9686b..6fd5560c 100644 --- a/src/Velopack.Packaging.Unix/Velopack.Packaging.Unix.csproj +++ b/src/Velopack.Packaging.Unix/Velopack.Packaging.Unix.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Velopack.Packaging.Unix/packages.lock.json b/src/Velopack.Packaging.Unix/packages.lock.json index a5cf7e0f..9eeca3a2 100644 --- a/src/Velopack.Packaging.Unix/packages.lock.json +++ b/src/Velopack.Packaging.Unix/packages.lock.json @@ -21,8 +21,18 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" + }, + "SharpZipLib": { + "type": "Direct", + "requested": "[1.4.2, )", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==", + "dependencies": { + "System.Memory": "4.5.4", + "System.Threading.Tasks.Extensions": "4.5.2" + } }, "Markdig": { "type": "Transitive", @@ -52,8 +62,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -61,27 +71,27 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -100,8 +110,8 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "System.Buffers": { "type": "Transitive", @@ -233,16 +243,16 @@ "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Newtonsoft.Json": "[13.0.1, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" } }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" @@ -269,8 +279,14 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" + }, + "SharpZipLib": { + "type": "Direct", + "requested": "[1.4.2, )", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" }, "Markdig": { "type": "Transitive", @@ -294,8 +310,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -303,26 +319,26 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -346,8 +362,8 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", @@ -909,16 +925,16 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" } }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" diff --git a/src/Velopack.Packaging.Windows/CodeSign.cs b/src/Velopack.Packaging.Windows/CodeSign.cs index a794d133..515c8b2d 100644 --- a/src/Velopack.Packaging.Windows/CodeSign.cs +++ b/src/Velopack.Packaging.Windows/CodeSign.cs @@ -1,12 +1,10 @@ using System.Diagnostics; -using System.Runtime.Versioning; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Velopack.Packaging.Exceptions; namespace Velopack.Packaging.Windows; -[SupportedOSPlatform("windows")] public class CodeSign { public ILogger Log { get; } @@ -88,7 +86,11 @@ public class CodeSign if (signAsTemplate) { command = signArguments.Replace("{{file}}", filesToSignStr); } else { - command = $"\"{HelperFile.SignToolPath}\" sign {signArguments} {filesToSignStr}"; + if (VelopackRuntimeInfo.IsWindows) { + command = $"\"{HelperFile.SignToolPath}\" sign {signArguments} {filesToSignStr}"; + } else { + throw new PlatformNotSupportedException("signtool.exe does not work on non-Windows platforms."); + } } RunSigningCommand(command, rootDir, signLogFile); @@ -107,22 +109,29 @@ public class CodeSign // about how the dotnet tool host works prevents signtool from being able to open a token password // prompt, meaning signing fails for those with an HSM. - string args = $"/S /C \"{command} >> \"{signLogFile}\" 2>&1\""; + var fileName = "cmd.exe"; + var args = $"/S /C \"{command} >> \"{signLogFile}\" 2>&1\""; + + if (!VelopackRuntimeInfo.IsWindows) { + fileName = "/bin/bash"; + string escapedCommand = command.Replace("'", "'\\''"); + args = $"-c '{escapedCommand} >> \"{signLogFile}\" 2>&1'"; + } var psi = new ProcessStartInfo { - FileName = "cmd.exe", + FileName = fileName, Arguments = args, UseShellExecute = false, WorkingDirectory = workDir, CreateNoWindow = true, }; - var process = Process.Start(psi); + using var process = Process.Start(psi); process.WaitForExit(); if (process.ExitCode != 0) { - var cmdWithPasswordHidden = "cmd.exe " + new Regex(@"\/p\s+?[^\s]+").Replace(command, "/p ********"); - Log.Debug($"Signing command failed: {cmdWithPasswordHidden}"); + var cmdWithPasswordHidden = fileName + " " + new Regex(@"\/p\s+?[^\s]+").Replace(args, "/p ********"); + Log.Debug($"Signing command failed - {Environment.NewLine} {cmdWithPasswordHidden}"); var output = File.Exists(signLogFile) ? File.ReadAllText(signLogFile).Trim() : "No output file was created."; throw new UserInfoException( $"Signing command failed. Specify --verbose argument to print signing command." + Environment.NewLine + diff --git a/src/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs b/src/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs index 059594dc..830dec8d 100644 --- a/src/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs +++ b/src/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs @@ -1,5 +1,4 @@ -using System.Runtime.Versioning; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Velopack.Compression; using Velopack.NuGet; using Velopack.Packaging.Abstractions; @@ -8,7 +7,6 @@ using Velopack.Windows; namespace Velopack.Packaging.Windows.Commands; -[SupportedOSPlatform("windows")] public class WindowsPackCommandRunner : PackageBuilder { public WindowsPackCommandRunner(ILogger logger, IFancyConsole console) @@ -48,7 +46,7 @@ public class WindowsPackCommandRunner : PackageBuilder packDir = dir.FullName; var updatePath = Path.Combine(TempDir.FullName, "Update.exe"); - File.Copy(HelperFile.GetUpdatePath(), updatePath, true); + File.Copy(HelperFile.GetUpdatePath(RuntimeOs.Windows), updatePath, true); // check for and delete clickonce manifest var clickonceManifests = Directory.EnumerateFiles(packDir, "*.application") @@ -65,10 +63,10 @@ public class WindowsPackCommandRunner : PackageBuilder } // update icon for Update.exe if requested - if (Options.Icon != null && VelopackRuntimeInfo.IsWindows) { - Rcedit.SetExeIcon(updatePath, Options.Icon); - } else if (Options.Icon != null) { - Log.Warn("Unable to set icon for Update.exe (only supported on windows)."); + if (Options.Icon != null) { + var editor = new ResourceEdit(updatePath, Log); + editor.SetExeIcon(Options.Icon); + editor.Commit(); } File.Copy(updatePath, Path.Combine(packDir, "Squirrel.exe"), true); @@ -176,11 +174,14 @@ public class WindowsPackCommandRunner : PackageBuilder var bundledZip = new ZipPackage(releasePkg); Utility.Retry(() => File.Copy(HelperFile.SetupPath, targetSetupExe, true)); progress(10); - if (VelopackRuntimeInfo.IsWindows) { - Rcedit.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledZip, Options.Icon); - } else { - Log.Warn("Unable to set PE Version on Setup.exe (only supported on windows)"); + + var editor = new ResourceEdit(targetSetupExe, Log); + editor.SetVersionInfo(bundledZip); + if (Options.Icon != null) { + editor.SetExeIcon(Options.Icon); } + editor.Commit(); + progress(25); Log.Debug($"Creating Setup bundle"); SetupBundle.CreatePackageBundle(targetSetupExe, releasePkg); @@ -231,15 +232,9 @@ public class WindowsPackCommandRunner : PackageBuilder try { Utility.Retry(() => File.Copy(HelperFile.StubExecutablePath, targetStubPath, true)); - Utility.Retry(() => { - if (VelopackRuntimeInfo.IsWindows) { - using var writer = new Microsoft.NET.HostModel.ResourceUpdater(targetStubPath, true); - writer.AddResourcesFromPEImage(exeToCopy); - writer.Update(); - } else { - Log.Warn($"Cannot set resources/icon for {targetStubPath} (only supported on windows)."); - } - }); + var edit = new ResourceEdit(targetStubPath, Log); + edit.CopyResourcesFrom(exeToCopy); + edit.Commit(); } catch (Exception ex) { Log.Error(ex, $"Error creating StubExecutable and copying resources for '{exeToCopy}'. This stub may or may not work properly."); } diff --git a/src/Velopack.Packaging.Windows/IcoExtract.cs b/src/Velopack.Packaging.Windows/IcoExtract.cs new file mode 100644 index 00000000..3e9baf99 --- /dev/null +++ b/src/Velopack.Packaging.Windows/IcoExtract.cs @@ -0,0 +1,111 @@ +using System.Diagnostics.CodeAnalysis; +using Ico.Codecs; +using Ico.Host; +using Ico.Model; +using Ico.Validation; +using Microsoft.Extensions.Logging; + +namespace Velopack.Packaging.Windows; + +public class IcoExtract +{ + private readonly ILogger _logger; + private readonly List _frames = new(); + + public IcoExtract(ILogger logger) + { + _logger = logger; + } + + public List ExtractFrames(FileInfo file) + { + var reporter = new IcoILoggerReporter(_logger); + var context = new ParseContext { + DisplayedPath = file.Name, + FullPath = file.FullName, + GeneratedFrames = new List(), + Reporter = reporter, + PngEncoder = new SixLabors.ImageSharp.Formats.Png.PngEncoder { + CompressionLevel = SixLabors.ImageSharp.Formats.Png.PngCompressionLevel.Level9, + ColorType = SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha, + }, + AllowPaletteTruncation = StrictnessPolicy.Loose, + MaskedImagePixelEmitOptions = StrictnessPolicy.Loose, + }; + + _frames.Clear(); + ExceptionWrapper.Try(() => DoExtractFile(context), context, reporter); + return _frames; + } + + private void DoExtractFile(ParseContext context) + { + var length = new FileInfo(context.FullPath).Length; + var data = File.ReadAllBytes(context.FullPath); + IcoDecoder.DoFile(data, context, _frames.Add); + } + + #region IErrorReporter + + [ExcludeFromCodeCoverage] + private class IcoILoggerReporter : IErrorReporter + { + private readonly ILogger _logger; + + public IcoILoggerReporter(ILogger logger) + { + _logger = logger; + } + + void IErrorReporter.ErrorLine(IcoErrorCode errorCode, string message) + { + _logger.LogDebug($"Error{GenerateCode(errorCode)}: {message}"); + } + + void IErrorReporter.ErrorLine(IcoErrorCode errorCode, string message, string fileName) + { + _logger.LogDebug($"{fileName}: Error{GenerateCode(errorCode)}: {message}"); + } + + void IErrorReporter.ErrorLine(IcoErrorCode errorCode, string message, string fileName, uint frameNumber) + { + _logger.LogDebug($"{fileName}({frameNumber + 1}): Error{GenerateCode(errorCode)}: {message}"); + } + + void IErrorReporter.InfoLine(string message) + { + _logger.LogDebug(message); + } + + void IErrorReporter.VerboseLine(string message) + { + _logger.LogDebug(message); + } + + void IErrorReporter.WarnLine(IcoErrorCode errorCode, string message) + { + _logger.LogDebug($"Warning{GenerateCode(errorCode)}: {message}"); + } + + void IErrorReporter.WarnLine(IcoErrorCode errorCode, string message, string fileName) + { + _logger.LogDebug($"{fileName}: Warning{GenerateCode(errorCode)}: {message}"); + } + + void IErrorReporter.WarnLine(IcoErrorCode errorCode, string message, string fileName, uint frameNumber) + { + _logger.LogDebug($"{fileName}({frameNumber + 1}): Warning{GenerateCode(errorCode)}: {message}"); + } + + private string GenerateCode(IcoErrorCode code) + { + if (code == IcoErrorCode.NoError) { + return ""; + } else { + return $" ICO{(uint) code}"; + } + } + } + + #endregion +} diff --git a/src/Velopack.Packaging.Windows/Rcedit.cs b/src/Velopack.Packaging.Windows/Rcedit.cs deleted file mode 100644 index 2137fee6..00000000 --- a/src/Velopack.Packaging.Windows/Rcedit.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Runtime.Versioning; -using Velopack.NuGet; - -namespace Velopack.Packaging.Windows; - -[SupportedOSPlatform("windows")] -public class Rcedit -{ - public static void SetExeIcon(string exePath, string iconPath) - { - var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath }; - Utility.Retry(() => Exe.InvokeAndThrowIfNonZero(HelperFile.RceditPath, args, null)); - } - - [SupportedOSPlatform("windows")] - public static void SetPEVersionBlockFromPackageInfo(string exePath, PackageManifest package, string iconPath = null) - { - var realExePath = Path.GetFullPath(exePath); - - List args = new List() { - realExePath, - "--set-version-string", "CompanyName", package.ProductCompany, - "--set-version-string", "LegalCopyright", package.ProductCopyright, - "--set-version-string", "FileDescription", package.ProductDescription, - "--set-version-string", "ProductName", package.ProductName, - "--set-file-version", package.Version.ToString(), - "--set-product-version", package.Version.ToString(), - }; - - if (iconPath != null) { - args.Add("--set-icon"); - args.Add(Path.GetFullPath(iconPath)); - } - - Utility.Retry(() => Exe.InvokeAndThrowIfNonZero(HelperFile.RceditPath, args, null)); - } -} diff --git a/src/Velopack.Packaging.Windows/ResourceEdit.cs b/src/Velopack.Packaging.Windows/ResourceEdit.cs new file mode 100644 index 00000000..b5fb1be0 --- /dev/null +++ b/src/Velopack.Packaging.Windows/ResourceEdit.cs @@ -0,0 +1,194 @@ +using AsmResolver.PE; +using AsmResolver.PE.File; +using AsmResolver.PE.File.Headers; +using AsmResolver.PE.Win32Resources; +using AsmResolver.PE.Win32Resources.Builder; +using AsmResolver.PE.Win32Resources.Icon; +using AsmResolver.PE.Win32Resources.Version; +using Microsoft.Extensions.Logging; +using Velopack.NuGet; + +namespace Velopack.Packaging.Windows; + +public class ResourceEdit +{ + const ushort kLangNeutral = 0; + const ushort kCodePageUtf16 = 1200; + + private readonly string _exePath; + private readonly ILogger _logger; + private readonly PEFile _file; + private readonly ushort _langId = kLangNeutral; + + IResourceDirectory _resources; + private bool _disposed; + + public ResourceEdit(string exeFile, ILogger logger) + { + _exePath = exeFile; + _logger = logger; + _file = PEFile.FromBytes(File.ReadAllBytes(exeFile)); + var image = PEImage.FromFile(_file); + _resources = image.Resources ?? new ResourceDirectory((uint) 0); + + // if there is already a manifest, we want to keep the existing language ID + try { + var existingInfo = VersionInfoResource.FromDirectory(_resources); + if (existingInfo != null) { + _langId = (ushort) existingInfo.Lcid; + } + } catch { } + } + + public void SetExeIcon(string iconPath) + { + ThrowIfDisposed(); + + // should use this commented code once these issues are fixed in AsmResolver + // https://github.com/Washi1337/AsmResolver/issues/532 + // https://github.com/Washi1337/AsmResolver/issues/533 + // var iconResource = new IconResource(); + // var group = new IconGroupDirectory(); + // iconResource.AddEntry(1, group); + // iconResource.WriteToDirectory(_resources); + + var group = new IconGroupDirectory() { + Type = 1, + }; + + var extractor = new IcoExtract(_logger); + var frames = extractor.ExtractFrames(new FileInfo(iconPath)); + + for (var p = 0; p < frames.Count; p++) { + var f = frames[p]; + + var dictEntry = new IconGroupDirectoryEntry() { + BytesInRes = (uint) f.RawData.Length, + Height = (byte) f.CookedData.Height, + Width = (byte) f.CookedData.Width, + Id = (ushort) (p + 1), + Reserved = 0, + PixelBitCount = (ushort) f.CookedData.PixelType.BitsPerPixel, + ColorCount = (byte) f.Encoding.PaletteSize, + ColorPlanes = 1, + }; + + var iconEntry = new IconEntry() { + RawIcon = f.RawData, + }; + + group.AddEntry(dictEntry, iconEntry); + group.Count++; + } + + WriteToDirectory(_resources, new Dictionary { { 1, group } }); + } + + private void WriteToDirectory(IResourceDirectory rootDirectory, Dictionary _entries) + { + ThrowIfDisposed(); + + // this function can be removed once these issues are fixed in AsmResolver + // https://github.com/Washi1337/AsmResolver/issues/532 + // https://github.com/Washi1337/AsmResolver/issues/533 + + var newIconDirectory = new ResourceDirectory(ResourceType.Icon); + foreach (var entry in _entries) { + foreach (var (groupEntry, iconEntry) in entry.Value.GetIconEntries()) { + newIconDirectory.Entries.Add(new ResourceDirectory(groupEntry.Id) { Entries = { new ResourceData(_langId, iconEntry) } }); + } + } + + var newGroupIconDirectory = new ResourceDirectory(ResourceType.GroupIcon); + foreach (var entry in _entries) { + newGroupIconDirectory.Entries.Add(new ResourceDirectory(entry.Key) { Entries = { new ResourceData(_langId, entry.Value) } }); + } + + rootDirectory.AddOrReplaceEntry(newIconDirectory); + rootDirectory.AddOrReplaceEntry(newGroupIconDirectory); + } + + public void SetVersionInfo(PackageManifest package) + { + ThrowIfDisposed(); + + // We just replace the entire VersionInfo section, so we know that the + // VarFileInfo languages will be correct. + var fileVersion = new Version(package.Version.Major, package.Version.Minor, package.Version.Patch, 0); + + var versionInfo = new VersionInfoResource(_langId); + versionInfo.FixedVersionInfo.FileOS = FileOS.NT; + versionInfo.FixedVersionInfo.FileType = FileType.App; + versionInfo.FixedVersionInfo.FileVersion = fileVersion; + versionInfo.FixedVersionInfo.ProductVersion = fileVersion; + + StringFileInfo stringInfo = new StringFileInfo(); + versionInfo.AddEntry(stringInfo); + + VarFileInfo varInfo = new VarFileInfo(); + versionInfo.AddEntry(varInfo); + + var stringTable = new StringTable(_langId, kCodePageUtf16); + stringTable[StringTable.CompanyNameKey] = package.ProductCompany; + stringTable[StringTable.FileDescriptionKey] = package.ProductDescription; + stringTable[StringTable.FileVersionKey] = package.Version.ToFullString(); + stringTable[StringTable.LegalCopyrightKey] = package.ProductCopyright; + stringTable[StringTable.ProductNameKey] = package.ProductName; + stringTable[StringTable.ProductVersionKey] = package.Version.ToFullString(); + stringTable[StringTable.CommentsKey] = $"Generated by Velopack {VelopackRuntimeInfo.VelopackNugetVersion}"; + stringInfo.Tables.Add(stringTable); + + var varTable = new VarTable(); + varTable.Values.Add(((uint) kCodePageUtf16 << 16) | _langId); + varInfo.Tables.Add(varTable); + + versionInfo.WriteToDirectory(_resources); + } + + public void CopyResourcesFrom(string otherExeFile) + { + ThrowIfDisposed(); + + var file = PEFile.FromBytes(File.ReadAllBytes(otherExeFile)); + var image = PEImage.FromFile(file); + _resources = image.Resources; + } + + public void Commit() + { + ThrowIfDisposed(); + _disposed = true; + + var sortedResources = new ResourceDirectory((uint) 0); + foreach (var entry in _resources.Entries.OrderBy(e => e.Id).ToArray()) { + _resources.RemoveEntry(entry.Id); + sortedResources.Entries.Add(entry); + } + + var resourceBuffer = new ResourceDirectoryBuffer(); + resourceBuffer.AddDirectory(sortedResources); + + var resourceDirectory = _file.OptionalHeader.GetDataDirectory(DataDirectoryIndex.ResourceDirectory); + + if (resourceDirectory.IsPresentInPE) { + var section = _file.GetSectionContainingRva(resourceDirectory.VirtualAddress); + section.Contents = resourceBuffer; + } else { + _file.Sections.Add(new PESection(".rsrc", SectionFlags.MemoryRead | SectionFlags.ContentInitializedData, resourceBuffer)); + } + + _file.UpdateHeaders(); + _file.OptionalHeader.SetDataDirectory(DataDirectoryIndex.ResourceDirectory, + new DataDirectory(resourceBuffer.Rva, resourceBuffer.GetPhysicalSize())); + + Utility.Retry(() => { + using var fs = File.Create(_exePath); + _file.Write(fs); + }); + } + + private void ThrowIfDisposed() + { + if (_disposed) throw new ObjectDisposedException(nameof(ResourceEdit)); + } +} diff --git a/src/Velopack.Packaging.Windows/SetupBundle.cs b/src/Velopack.Packaging.Windows/SetupBundle.cs index eed496b8..3f15dde4 100644 --- a/src/Velopack.Packaging.Windows/SetupBundle.cs +++ b/src/Velopack.Packaging.Windows/SetupBundle.cs @@ -1,6 +1,4 @@ using System.IO.MemoryMappedFiles; -using Microsoft.NET.HostModel; -using Microsoft.NET.HostModel.AppHost; namespace Velopack.Packaging.Windows; @@ -24,9 +22,9 @@ public static class SetupBundle using var memoryMappedFile = MemoryMappedFile.CreateFromFile(setupPath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); using MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); - int position = BinaryUtils.SearchInFile(accessor, bundleSignature); + int position = SearchInFile(accessor, bundleSignature); if (position == -1) { - throw new PlaceHolderNotFoundInAppHostException(bundleSignature); + throw new Exception("PlaceHolderNotFoundInAppHostException"); } offset = accessor.ReadInt64(position - 16); @@ -74,11 +72,11 @@ public static class SetupBundle Array.Copy(BitConverter.GetBytes(bundleLength), 0, data, 8, 8); // replace the beginning of the placeholder with the bytes from 'data' - RetryUtil.RetryOnIOError(() => - BinaryUtils.SearchAndReplace(setupPath, placeholder, data, pad0s: false)); + RetryOnIOError(() => + SearchAndReplace(setupPath, placeholder, data, pad0s: false)); // memory-mapped write does not updating last write time - RetryUtil.RetryOnIOError(() => + RetryOnIOError(() => File.SetLastWriteTimeUtc(setupPath, DateTime.UtcNow)); if (!IsBundle(setupPath, out var offset, out var length)) @@ -86,4 +84,237 @@ public static class SetupBundle return bundleOffset; } + + internal static unsafe void SearchAndReplace( + MemoryMappedViewAccessor accessor, + byte[] searchPattern, + byte[] patternToReplace, + bool pad0s = true) + { + byte* pointer = null; + + try { + accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer); + byte* bytes = pointer + accessor.PointerOffset; + + int position = KMPSearch(searchPattern, bytes, accessor.Capacity); + if (position < 0) { + throw new Exception("PlaceHolderNotFoundInAppHostException"); + } + + accessor.WriteArray( + position: position, + array: patternToReplace, + offset: 0, + count: patternToReplace.Length); + + if (pad0s) { + Pad0(searchPattern, patternToReplace, bytes, position); + } + } finally { + if (pointer != null) { + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + } + } + } + + private static unsafe void Pad0(byte[] searchPattern, byte[] patternToReplace, byte* bytes, int offset) + { + if (patternToReplace.Length < searchPattern.Length) { + for (int i = patternToReplace.Length; i < searchPattern.Length; i++) { + bytes[i + offset] = 0x0; + } + } + } + + public static unsafe void SearchAndReplace( + string filePath, + byte[] searchPattern, + byte[] patternToReplace, + bool pad0s = true) + { + using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath)) { + using (var accessor = mappedFile.CreateViewAccessor()) { + SearchAndReplace(accessor, searchPattern, patternToReplace, pad0s); + } + } + } + + public static unsafe int SearchInFile(MemoryMappedViewAccessor accessor, byte[] searchPattern) + { + var safeBuffer = accessor.SafeMemoryMappedViewHandle; + return KMPSearch(searchPattern, (byte*) safeBuffer.DangerousGetHandle(), (int) safeBuffer.ByteLength); + } + + public static unsafe int SearchInFile(string filePath, byte[] searchPattern) + { + using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read)) { + using (var accessor = mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) { + return SearchInFile(accessor, searchPattern); + } + } + } + + // See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm + private static int[] ComputeKMPFailureFunction(byte[] pattern) + { + int[] table = new int[pattern.Length]; + if (pattern.Length >= 1) { + table[0] = -1; + } + if (pattern.Length >= 2) { + table[1] = 0; + } + + int pos = 2; + int cnd = 0; + while (pos < pattern.Length) { + if (pattern[pos - 1] == pattern[cnd]) { + table[pos] = cnd + 1; + cnd++; + pos++; + } else if (cnd > 0) { + cnd = table[cnd]; + } else { + table[pos] = 0; + pos++; + } + } + return table; + } + + // See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm + private static unsafe int KMPSearch(byte[] pattern, byte* bytes, long bytesLength) + { + int m = 0; + int i = 0; + int[] table = ComputeKMPFailureFunction(pattern); + + while (m + i < bytesLength) { + if (pattern[i] == bytes[m + i]) { + if (i == pattern.Length - 1) { + return m; + } + i++; + } else { + if (table[i] > -1) { + m = m + i - table[i]; + i = table[i]; + } else { + m++; + i = 0; + } + } + } + + return -1; + } + + public static void CopyFile(string sourcePath, string destinationPath) + { + var destinationDirectory = new FileInfo(destinationPath).Directory.FullName; + if (!Directory.Exists(destinationDirectory)) { + Directory.CreateDirectory(destinationDirectory); + } + + // Copy file to destination path so it inherits the same attributes/permissions. + File.Copy(sourcePath, destinationPath, overwrite: true); + } + + internal static void WriteToStream(MemoryMappedViewAccessor sourceViewAccessor, FileStream fileStream, long length) + { + int pos = 0; + int bufSize = 16384; //16K + + byte[] buf = new byte[bufSize]; + length = Math.Min(length, sourceViewAccessor.Capacity); + do { + int bytesRequested = Math.Min((int) length - pos, bufSize); + if (bytesRequested <= 0) { + break; + } + + int bytesRead = sourceViewAccessor.ReadArray(pos, buf, 0, bytesRequested); + if (bytesRead > 0) { + fileStream.Write(buf, 0, bytesRead); + pos += bytesRead; + } + } + while (true); + } + + public const int NumberOfRetries = 500; + public const int NumMilliSecondsToWait = 100; + + public static void RetryOnIOError(Action func) + { + for (int i = 1; i <= NumberOfRetries; i++) { + try { + func(); + break; + } catch (IOException) when (i < NumberOfRetries) { + Thread.Sleep(NumMilliSecondsToWait); + } + } + } + + public static void RetryOnWin32Error(Action func) + { + static bool IsKnownIrrecoverableError(int hresult) + { + // Error codes are defined in winerror.h + // The error code is stored in the lowest 16 bits of the HResult + + switch (hresult & 0xffff) { + case 0x00000001: // ERROR_INVALID_FUNCTION + case 0x00000002: // ERROR_FILE_NOT_FOUND + case 0x00000003: // ERROR_PATH_NOT_FOUND + case 0x00000006: // ERROR_INVALID_HANDLE + case 0x00000008: // ERROR_NOT_ENOUGH_MEMORY + case 0x0000000B: // ERROR_BAD_FORMAT + case 0x0000000E: // ERROR_OUTOFMEMORY + case 0x0000000F: // ERROR_INVALID_DRIVE + case 0x00000012: // ERROR_NO_MORE_FILES + case 0x00000035: // ERROR_BAD_NETPATH + case 0x00000057: // ERROR_INVALID_PARAMETER + case 0x00000071: // ERROR_NO_MORE_SEARCH_HANDLES + case 0x00000072: // ERROR_INVALID_TARGET_HANDLE + case 0x00000078: // ERROR_CALL_NOT_IMPLEMENTED + case 0x0000007B: // ERROR_INVALID_NAME + case 0x0000007C: // ERROR_INVALID_LEVEL + case 0x0000007D: // ERROR_NO_VOLUME_LABEL + case 0x0000009A: // ERROR_LABEL_TOO_LONG + case 0x000000A0: // ERROR_BAD_ARGUMENTS + case 0x000000A1: // ERROR_BAD_PATHNAME + case 0x000000CE: // ERROR_FILENAME_EXCED_RANGE + case 0x000000DF: // ERROR_FILE_TOO_LARGE + case 0x000003ED: // ERROR_UNRECOGNIZED_VOLUME + case 0x000003EE: // ERROR_FILE_INVALID + case 0x00000651: // ERROR_DEVICE_REMOVED + return true; + + default: + return false; + } + } + + for (int i = 1; i <= NumberOfRetries; i++) { + try { + func(); + break; + } catch (HResultException hrex) + when (i < NumberOfRetries && !IsKnownIrrecoverableError(hrex.Win32HResult)) { + Thread.Sleep(NumMilliSecondsToWait); + } + } + } + + public class HResultException : Exception + { + public readonly int Win32HResult; + public HResultException(int hResult) : base(hResult.ToString("X4")) + { + Win32HResult = hResult; + } + } } \ No newline at end of file diff --git a/src/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj b/src/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj index 3ff44a9f..fc896d17 100644 --- a/src/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj +++ b/src/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj @@ -4,10 +4,11 @@ net472;net6.0 enable $(NoWarn);CA2007;CS8002 + true - + @@ -15,5 +16,5 @@ - + diff --git a/src/Velopack.Packaging.Windows/packages.lock.json b/src/Velopack.Packaging.Windows/packages.lock.json index 779de4cd..2c487861 100644 --- a/src/Velopack.Packaging.Windows/packages.lock.json +++ b/src/Velopack.Packaging.Windows/packages.lock.json @@ -35,8 +35,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "AsmResolver": { "type": "Transitive", @@ -87,8 +87,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -96,27 +96,27 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -135,23 +135,26 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" + }, + "SixLabors.ImageSharp": { + "type": "Transitive", + "resolved": "2.1.8", + "contentHash": "ML1++vactR5xMW36wHA2nAX3vc6VpR62qYnpopQdLNGbP8BAJ/ckv1IrGIIFRUEQ0JS9EgWr1y1gEa/81f+HaA==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } }, "System.Buffers": { "type": "Transitive", "resolved": "4.5.1", "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", - "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "6.0.1", @@ -206,15 +209,6 @@ "resolved": "4.5.0", "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", - "dependencies": { - "System.Collections.Immutable": "8.0.0", - "System.Memory": "4.5.5" - } - }, "System.Runtime": { "type": "Transitive", "resolved": "4.3.0", @@ -273,6 +267,14 @@ "resolved": "5.0.0", "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "6.0.0", @@ -316,26 +318,26 @@ "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Newtonsoft.Json": "[13.0.1, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" + } + }, + "velopack.icolib": { + "type": "Project", + "dependencies": { + "SixLabors.ImageSharp": "[2.1.8, )" } }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" } - }, - "velopack.packaging.hostmodel": { - "type": "Project", - "dependencies": { - "System.Reflection.Metadata": "[8.0.0, )" - } } }, "net6.0": { @@ -372,8 +374,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "AsmResolver": { "type": "Transitive", @@ -418,8 +420,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -427,26 +429,26 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -455,8 +457,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -470,8 +472,8 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", @@ -571,6 +573,15 @@ "resolved": "4.3.2", "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" }, + "SixLabors.ImageSharp": { + "type": "Transitive", + "resolved": "2.1.8", + "contentHash": "ML1++vactR5xMW36wHA2nAX3vc6VpR62qYnpopQdLNGbP8BAJ/ckv1IrGIIFRUEQ0JS9EgWr1y1gEa/81f+HaA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } + }, "System.Collections": { "type": "Transitive", "resolved": "4.3.0", @@ -1010,6 +1021,14 @@ "System.Runtime": "4.3.0" } }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "6.0.0", @@ -1050,23 +1069,26 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" + } + }, + "velopack.icolib": { + "type": "Project", + "dependencies": { + "SixLabors.ImageSharp": "[2.1.8, )" } }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" } - }, - "velopack.packaging.hostmodel": { - "type": "Project" } } } diff --git a/src/Velopack.Packaging/Exe.cs b/src/Velopack.Packaging/Exe.cs index db5a40cd..e348593d 100644 --- a/src/Velopack.Packaging/Exe.cs +++ b/src/Velopack.Packaging/Exe.cs @@ -5,7 +5,7 @@ namespace Velopack.Packaging; public static class Exe { - public static void AssertSystemBinaryExists(string binaryName) + public static void AssertSystemBinaryExists(string binaryName, string linuxInstallCmd, string osxInstallCmd) { try { if (VelopackRuntimeInfo.IsWindows) { @@ -20,10 +20,52 @@ public static class Exe throw new PlatformNotSupportedException(); } } catch (ProcessFailedException) { - throw new Exception($"Could not find '{binaryName}' on the system, ensure it is installed and on the PATH."); + string recommendedCmd = null; + if (VelopackRuntimeInfo.IsLinux && !String.IsNullOrEmpty(linuxInstallCmd)) + recommendedCmd = linuxInstallCmd; + else if (VelopackRuntimeInfo.IsOSX && !String.IsNullOrEmpty(osxInstallCmd)) + recommendedCmd = osxInstallCmd; + + string message = $"Could not find '{binaryName}' binary on the system, ensure it is installed and on the PATH."; + if (!String.IsNullOrEmpty(recommendedCmd)) { + message += $" You might be able to install it by running: '{recommendedCmd}'"; + } + + throw new UserInfoException(message); } } + public static string RunHostedCommand(string command, string workDir = null) + { + using var _1 = Utility.GetTempFileName(out var outputFile); + File.Create(outputFile).Close(); + + var fileName = "cmd.exe"; + var args = $"/S /C \"{command} >> \"{outputFile}\" 2>&1\""; + + if (!VelopackRuntimeInfo.IsWindows) { + fileName = "/bin/bash"; + string escapedCommand = command.Replace("'", "'\\''"); + args = $"-c '{escapedCommand} >> \"{outputFile}\" 2>&1'"; + } + + var psi = new ProcessStartInfo { + FileName = fileName, + Arguments = args, + UseShellExecute = false, + WorkingDirectory = workDir, + CreateNoWindow = true, + }; + + using var process = Process.Start(psi); + process.WaitForExit(); + + var stdout = Utility.Retry(() => File.ReadAllText(outputFile).Trim(), 10, 1000); + var result = (process.ExitCode, stdout, command); + ProcessFailedException.ThrowIfNonZero(result); + return result.Item2; + } + public static string InvokeAndThrowIfNonZero(string exePath, IEnumerable args, string workingDir, IDictionary envVar = null) { var result = InvokeProcess(exePath, args, workingDir, CancellationToken.None, envVar); diff --git a/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs b/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs index 03ca6c4e..5919b7fb 100644 --- a/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs +++ b/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs @@ -19,7 +19,7 @@ public interface IVelopackFlowServiceClient Task GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken); - Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, CancellationToken cancellationToken); + Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, RuntimeOs os, CancellationToken cancellationToken); } public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : IVelopackFlowServiceClient @@ -91,10 +91,11 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : return await HttpClient.GetFromJsonAsync(endpoint, cancellationToken); } - public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, CancellationToken cancellationToken) + public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, + RuntimeOs os, CancellationToken cancellationToken) { - channel ??= ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs); - ReleaseEntryHelper helper = new(releaseDirectory, channel, Logger); + channel ??= ReleaseEntryHelper.GetDefaultChannel(os); + ReleaseEntryHelper helper = new(releaseDirectory, channel, Logger, os); var latestAssets = helper.GetLatestAssets().ToList(); List installers = []; @@ -107,13 +108,13 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : version = latestAssets[0].Version; if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) { - var setupName = ReleaseEntryHelper.GetSuggestedSetupName(packageId, channel); + var setupName = ReleaseEntryHelper.GetSuggestedSetupName(packageId, channel, os); if (File.Exists(Path.Combine(releaseDirectory, setupName))) { installers.Add(setupName); } } - var portableName = ReleaseEntryHelper.GetSuggestedPortableName(packageId, channel); + var portableName = ReleaseEntryHelper.GetSuggestedPortableName(packageId, channel, os); if (File.Exists(Path.Combine(releaseDirectory, portableName))) { installers.Add(portableName); } diff --git a/src/Velopack.Packaging/Flow/VelopackServiceOptions.cs b/src/Velopack.Packaging/Flow/VelopackServiceOptions.cs index fb411bfc..9844d56f 100644 --- a/src/Velopack.Packaging/Flow/VelopackServiceOptions.cs +++ b/src/Velopack.Packaging/Flow/VelopackServiceOptions.cs @@ -5,5 +5,6 @@ public class VelopackServiceOptions public const string DefaultBaseUrl = "https://api.velopack.io/"; public string VelopackBaseUrl { get; set; } = DefaultBaseUrl; + public string ApiKey { get; set; } = string.Empty; } diff --git a/src/Velopack.Packaging/HelperFile.cs b/src/Velopack.Packaging/HelperFile.cs index 4de1ea51..8a4b48f4 100644 --- a/src/Velopack.Packaging/HelperFile.cs +++ b/src/Velopack.Packaging/HelperFile.cs @@ -4,12 +4,11 @@ namespace Velopack.Packaging; public static class HelperFile { - public static string GetUpdateExeName(RuntimeOs? os = null) + public static string GetUpdateExeName(RuntimeOs os) { - var _os = os ?? VelopackRuntimeInfo.SystemOs; - switch (_os) { + switch (os) { case RuntimeOs.Windows: - return FindHelperFile("Update.exe"); + return FindHelperFile("update.exe"); #if DEBUG case RuntimeOs.Linux: return FindHelperFile("update"); @@ -26,38 +25,43 @@ public static class HelperFile } } - public static string GetUpdatePath(RuntimeOs? os = null) => FindHelperFile(GetUpdateExeName(os)); + public static string GetUpdatePath(RuntimeOs os) => FindHelperFile(GetUpdateExeName(os)); public static string GetZstdPath() { if (VelopackRuntimeInfo.IsWindows) return FindHelperFile("zstd.exe"); - Exe.AssertSystemBinaryExists("zstd"); + Exe.AssertSystemBinaryExists("zstd", "sudo apt install zstd", "brew install zstd"); return "zstd"; } + public static string GetMkSquashFsPath() + { + if (VelopackRuntimeInfo.IsWindows) + return FindHelperFile("squashfs-tools\\gensquashfs.exe"); + Exe.AssertSystemBinaryExists("mksquashfs", "sudo apt install squashfs-tools", "brew install squashfs"); + return "mksquashfs"; + } + [SupportedOSPlatform("macos")] public static string VelopackEntitlements => FindHelperFile("Velopack.entitlements"); - [SupportedOSPlatform("linux")] - public static string AppImageToolX64 => FindHelperFile("appimagetool-x86_64.AppImage"); + public static string AppImageRuntimeArm64 => FindHelperFile("appimagekit-runtime-aarch64"); - [SupportedOSPlatform("windows")] - public static string SetupPath => FindHelperFile("Setup.exe"); + public static string AppImageRuntimeX64 => FindHelperFile("appimagekit-runtime-x86_64"); + + public static string AppImageRuntimeX86 => FindHelperFile("appimagekit-runtime-i686"); + + public static string SetupPath => FindHelperFile("setup.exe"); - [SupportedOSPlatform("windows")] public static string StubExecutablePath => FindHelperFile("stub.exe"); [SupportedOSPlatform("windows")] public static string SignToolPath => FindHelperFile("signtool.exe"); - [SupportedOSPlatform("windows")] - public static string RceditPath => FindHelperFile("rcedit.exe"); - - public static string GetDefaultAppIcon(RuntimeOs? os = null) + public static string GetDefaultAppIcon(RuntimeOs os) { - var _os = os ?? VelopackRuntimeInfo.SystemOs; - switch (_os) { + switch (os) { case RuntimeOs.Windows: return null; case RuntimeOs.Linux: diff --git a/src/Velopack.Packaging/PackageBuilder.cs b/src/Velopack.Packaging/PackageBuilder.cs index d4ef2c4f..e9745ca9 100644 --- a/src/Velopack.Packaging/PackageBuilder.cs +++ b/src/Velopack.Packaging/PackageBuilder.cs @@ -14,7 +14,7 @@ namespace Velopack.Packaging; public abstract class PackageBuilder : ICommand where T : class, IPackOptions { - protected RuntimeOs SupportedTargetOs { get; } + protected RuntimeOs TargetOs { get; } protected ILogger Log { get; } @@ -36,25 +36,28 @@ public abstract class PackageBuilder : ICommand public PackageBuilder(RuntimeOs supportedOs, ILogger logger, IFancyConsole console) { - SupportedTargetOs = supportedOs; + TargetOs = supportedOs; Log = logger; Console = console; } public async Task Run(T options) { - if (options.TargetRuntime?.BaseRID != SupportedTargetOs) - throw new UserInfoException($"To build packages for {SupportedTargetOs.GetOsLongName()}, " + - $"the target rid must be {SupportedTargetOs} (actually was {options.TargetRuntime?.BaseRID})."); + if (options.TargetRuntime?.BaseRID != TargetOs) { + throw new UserInfoException($"To build packages for {TargetOs.GetOsLongName()}, " + + $"the target rid must be {TargetOs} (actually was {options.TargetRuntime?.BaseRID}). " + + $"If your real intention was to cross-compile a release for {options.TargetRuntime?.BaseRID} then you " + + $"should provide an OS directive: eg. 'vpk [{options.TargetRuntime?.BaseRID.GetOsShortName()}] pack ...'"); + } Log.Info($"Beginning to package Velopack release {options.PackVersion}."); Log.Info("Releases Directory: " + options.ReleaseDir.FullName); var releaseDir = options.ReleaseDir; - var channel = options.Channel?.ToLower() ?? ReleaseEntryHelper.GetDefaultChannel(SupportedTargetOs); + var channel = options.Channel?.ToLower() ?? ReleaseEntryHelper.GetDefaultChannel(TargetOs); Channel = channel; - var entryHelper = new ReleaseEntryHelper(releaseDir.FullName, channel, Log); + var entryHelper = new ReleaseEntryHelper(releaseDir.FullName, channel, Log, TargetOs); if (entryHelper.DoesSimilarVersionExist(SemanticVersion.Parse(options.PackVersion))) { if (await Console.PromptYesNo("A release in this channel with the same or greater version already exists. Do you want to continue and potentially overwrite files?") != true) { throw new UserInfoException($"There is a release in channel {channel} which is equal or greater to the current version {options.PackVersion}. Please increase the current package version or remove that release."); @@ -110,35 +113,35 @@ public abstract class PackageBuilder : ICommand packDirectory = await PreprocessPackDir(progress, packDirectory); }); - if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) { + if (TargetOs != RuntimeOs.Linux) { await ctx.RunTask("Code-sign application", async (progress) => { await CodeSign(progress, packDirectory); }); } Task portableTask = null; - if (VelopackRuntimeInfo.IsLinux || !Options.NoPortable) { + if (TargetOs == RuntimeOs.Linux || !Options.NoPortable) { portableTask = ctx.RunTask("Building portable package", async (progress) => { - var suggestedName = ReleaseEntryHelper.GetSuggestedPortableName(packId, channel); + var suggestedName = ReleaseEntryHelper.GetSuggestedPortableName(packId, channel, TargetOs); var path = getIncompletePath(suggestedName); await CreatePortablePackage(progress, packDirectory, path); }); } // TODO: hack, this is a prerequisite for building full package but only on linux - if (VelopackRuntimeInfo.IsLinux) await portableTask; + if (TargetOs == RuntimeOs.Linux) await portableTask; string releasePath = null; await ctx.RunTask($"Building release {packVersion}", async (progress) => { - var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, false); + var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, false, TargetOs); releasePath = getIncompletePath(suggestedName); await CreateReleasePackage(progress, packDirectory, releasePath); }); Task setupTask = null; - if (!Options.NoInst && (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX)) { + if (!Options.NoInst && TargetOs != RuntimeOs.Linux) { setupTask = ctx.RunTask("Building setup package", async (progress) => { - var suggestedName = ReleaseEntryHelper.GetSuggestedSetupName(packId, channel); + var suggestedName = ReleaseEntryHelper.GetSuggestedSetupName(packId, channel, TargetOs); var path = getIncompletePath(suggestedName); await CreateSetupPackage(progress, releasePath, packDirectory, path); }); @@ -146,12 +149,12 @@ public abstract class PackageBuilder : ICommand if (prev != null && options.DeltaMode != DeltaMode.None) { await ctx.RunTask($"Building delta {prev.Version} -> {packVersion}", async (progress) => { - var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, true); + var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, true, TargetOs); var deltaPkg = await CreateDeltaPackage(progress, releasePath, prev.PackageFile, getIncompletePath(suggestedName), options.DeltaMode); }); } - if (!VelopackRuntimeInfo.IsLinux && portableTask != null) await portableTask; + if (TargetOs != RuntimeOs.Linux && portableTask != null) await portableTask; if (setupTask != null) await setupTask; await ctx.RunTask("Post-process steps", (progress) => { diff --git a/src/Velopack.Packaging/ReleaseEntryHelper.cs b/src/Velopack.Packaging/ReleaseEntryHelper.cs index b78ca0b8..23e1a65c 100644 --- a/src/Velopack.Packaging/ReleaseEntryHelper.cs +++ b/src/Velopack.Packaging/ReleaseEntryHelper.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using Microsoft.Extensions.Logging; using NuGet.Versioning; using Velopack.Json; @@ -13,11 +13,11 @@ public class ReleaseEntryHelper private readonly string _channel; private Dictionary> _releases; - public ReleaseEntryHelper(string outputDir, string channel, ILogger logger) + public ReleaseEntryHelper(string outputDir, string channel, ILogger logger, RuntimeOs os) { _outputDir = outputDir; _logger = logger; - _channel = channel ?? GetDefaultChannel(); + _channel = channel ?? GetDefaultChannel(os); _releases = GetReleasesFromDir(outputDir); } @@ -153,20 +153,20 @@ public class ReleaseEntryHelper return Encoding.UTF8.GetString(ms.ToArray()); } - public static string GetSuggestedReleaseName(string id, string version, string channel, bool delta) + public static string GetSuggestedReleaseName(string id, string version, string channel, bool delta, RuntimeOs os) { var suffix = GetUniqueAssetSuffix(channel); version = SemanticVersion.Parse(version).ToNormalizedString(); - if (VelopackRuntimeInfo.IsWindows && channel == GetDefaultChannel(RuntimeOs.Windows)) { + if (os == RuntimeOs.Windows && channel == GetDefaultChannel(RuntimeOs.Windows)) { return $"{id}-{version}{(delta ? "-delta" : "-full")}.nupkg"; } return $"{id}-{version}{suffix}{(delta ? "-delta" : "-full")}.nupkg"; } - public static string GetSuggestedPortableName(string id, string channel) + public static string GetSuggestedPortableName(string id, string channel, RuntimeOs os) { var suffix = GetUniqueAssetSuffix(channel); - if (VelopackRuntimeInfo.IsLinux) { + if (os == RuntimeOs.Linux) { if (channel == GetDefaultChannel(RuntimeOs.Linux)) { return $"{id}.AppImage"; } else { @@ -177,12 +177,12 @@ public class ReleaseEntryHelper } } - public static string GetSuggestedSetupName(string id, string channel) + public static string GetSuggestedSetupName(string id, string channel, RuntimeOs os) { var suffix = GetUniqueAssetSuffix(channel); - if (VelopackRuntimeInfo.IsWindows) + if (os == RuntimeOs.Windows) return $"{id}{suffix}-Setup.exe"; - else if (VelopackRuntimeInfo.IsOSX) + else if (os == RuntimeOs.OSX) return $"{id}{suffix}-Setup.pkg"; else throw new PlatformNotSupportedException("Platform not supported."); @@ -193,9 +193,8 @@ public class ReleaseEntryHelper return "-" + channel; } - public static string GetDefaultChannel(RuntimeOs? os = null) + public static string GetDefaultChannel(RuntimeOs os) { - os ??= VelopackRuntimeInfo.SystemOs; if (os == RuntimeOs.Windows) return "win"; if (os == RuntimeOs.OSX) return "osx"; if (os == RuntimeOs.Linux) return "linux"; diff --git a/src/Velopack.Packaging/Velopack.Packaging.csproj b/src/Velopack.Packaging/Velopack.Packaging.csproj index e49951cd..0d93bf8c 100644 --- a/src/Velopack.Packaging/Velopack.Packaging.csproj +++ b/src/Velopack.Packaging/Velopack.Packaging.csproj @@ -15,9 +15,9 @@ - - - + + + diff --git a/src/Velopack.Packaging/packages.lock.json b/src/Velopack.Packaging/packages.lock.json index 9962ea2d..8db18076 100644 --- a/src/Velopack.Packaging/packages.lock.json +++ b/src/Velopack.Packaging/packages.lock.json @@ -13,9 +13,9 @@ }, "Microsoft.Identity.Client": { "type": "Direct", - "requested": "[4.61.0, )", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "requested": "[4.61.2, )", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -23,21 +23,21 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Direct", - "requested": "[4.61.0, )", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "requested": "[4.61.2, )", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Direct", - "requested": "[4.61.0, )", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "requested": "[4.61.2, )", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Security.Cryptography.ProtectedData": "4.5.0" } @@ -55,8 +55,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "System.Linq.Async": { "type": "Direct", @@ -96,8 +96,8 @@ }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -116,8 +116,8 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "System.Buffers": { "type": "Transitive", @@ -233,7 +233,7 @@ "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Newtonsoft.Json": "[13.0.1, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" } } }, @@ -246,9 +246,9 @@ }, "Microsoft.Identity.Client": { "type": "Direct", - "requested": "[4.61.0, )", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "requested": "[4.61.2, )", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -256,21 +256,21 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Direct", - "requested": "[4.61.0, )", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "requested": "[4.61.2, )", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Direct", - "requested": "[4.61.0, )", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "requested": "[4.61.2, )", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, @@ -287,8 +287,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "System.Linq.Async": { "type": "Direct", @@ -350,8 +350,8 @@ }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -375,8 +375,8 @@ }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", @@ -897,7 +897,7 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" } } } diff --git a/src/Velopack.Vpk/Commands/Flow/PublishCommandRunner.cs b/src/Velopack.Vpk/Commands/Flow/PublishCommandRunner.cs index 00a1823c..4004d4ec 100644 --- a/src/Velopack.Vpk/Commands/Flow/PublishCommandRunner.cs +++ b/src/Velopack.Vpk/Commands/Flow/PublishCommandRunner.cs @@ -19,6 +19,7 @@ public class PublishCommandRunner(IVelopackFlowServiceClient Client) : ICommand< return; } - await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory, options.VelopackBaseUrl, token); + await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory, + options.VelopackBaseUrl, options.TargetOs, token); } } diff --git a/src/Velopack.Vpk/Commands/Flow/PublishOptions.cs b/src/Velopack.Vpk/Commands/Flow/PublishOptions.cs index ce2cbfbd..8e051320 100644 --- a/src/Velopack.Vpk/Commands/Flow/PublishOptions.cs +++ b/src/Velopack.Vpk/Commands/Flow/PublishOptions.cs @@ -5,6 +5,8 @@ namespace Velopack.Vpk.Commands.Flow; #nullable enable public sealed class PublishOptions : VelopackServiceOptions { + public RuntimeOs TargetOs { get; set; } + public string ReleaseDirectory { get; set; } = ""; public string? Channel { get; set; } diff --git a/src/Velopack.Vpk/Commands/_BaseCommand.cs b/src/Velopack.Vpk/Commands/_BaseCommand.cs index b09a00b7..03cf5805 100644 --- a/src/Velopack.Vpk/Commands/_BaseCommand.cs +++ b/src/Velopack.Vpk/Commands/_BaseCommand.cs @@ -5,6 +5,8 @@ namespace Velopack.Vpk.Commands; public class BaseCommand : CliCommand { + public RuntimeOs TargetOs { get; private set; } + private readonly Dictionary> _setters = new(); private readonly Dictionary _envHelp = new(); @@ -60,17 +62,18 @@ public class BaseCommand : CliCommand public string GetEnvVariableName(CliOption option) => _envHelp.ContainsKey(option) ? _envHelp[option] : null; - public virtual void SetProperties(ParseResult context, IConfiguration config) + public virtual void SetProperties(ParseResult context, IConfiguration config, RuntimeOs targetOs) { + TargetOs = targetOs; foreach (var kvp in _setters) { kvp.Value(context, config); } } - public virtual ParseResult ParseAndApply(string command, IConfiguration config = null) + public virtual ParseResult ParseAndApply(string command, IConfiguration config = null, RuntimeOs? targetOs = null) { var x = Parse(command); - SetProperties(x, config ?? new ConfigurationManager()); + SetProperties(x, config ?? new ConfigurationManager(), targetOs ?? VelopackRuntimeInfo.SystemOs); return x; } } diff --git a/src/Velopack.Vpk/Commands/_PlatformCommand.cs b/src/Velopack.Vpk/Commands/_PlatformCommand.cs index 54131708..63862d6c 100644 --- a/src/Velopack.Vpk/Commands/_PlatformCommand.cs +++ b/src/Velopack.Vpk/Commands/_PlatformCommand.cs @@ -11,11 +11,6 @@ public abstract class PlatformCommand : OutputCommand TargetRuntimeOption = AddOption((v) => TargetRuntime = v, "-r", "--runtime") .SetDescription("The target runtime to build packages for.") .SetArgumentHelpName("RID") - .SetDefault(VelopackRuntimeInfo.SystemOs.GetOsShortName()) .MustBeSupportedRid(); } - - public RID GetRid() => RID.Parse(TargetRuntime ?? VelopackRuntimeInfo.SystemOs.GetOsShortName()); - - public RuntimeOs GetRuntimeOs() => GetRid().BaseRID; } diff --git a/src/Velopack.Vpk/Logging/BasicConsole.cs b/src/Velopack.Vpk/Logging/BasicConsole.cs index 027f274a..c1102947 100644 --- a/src/Velopack.Vpk/Logging/BasicConsole.cs +++ b/src/Velopack.Vpk/Logging/BasicConsole.cs @@ -5,9 +5,9 @@ namespace Velopack.Vpk.Logging; public class BasicConsole : IFancyConsole { private readonly ILogger logger; - private readonly DefaultPromptValueFactory defaultFactory; + private readonly VelopackDefaults defaultFactory; - public BasicConsole(ILogger logger, DefaultPromptValueFactory defaultFactory) + public BasicConsole(ILogger logger, VelopackDefaults defaultFactory) { this.logger = logger; this.defaultFactory = defaultFactory; diff --git a/src/Velopack.Vpk/Logging/DefaultPromptValueFactory.cs b/src/Velopack.Vpk/Logging/DefaultPromptValueFactory.cs deleted file mode 100644 index c4350654..00000000 --- a/src/Velopack.Vpk/Logging/DefaultPromptValueFactory.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Velopack.Vpk.Logging; - -public record DefaultPromptValueFactory(bool DefaultPromptValue) -{ -} diff --git a/src/Velopack.Vpk/Logging/SpectreConsole.cs b/src/Velopack.Vpk/Logging/SpectreConsole.cs index 7b645b27..63a6321e 100644 --- a/src/Velopack.Vpk/Logging/SpectreConsole.cs +++ b/src/Velopack.Vpk/Logging/SpectreConsole.cs @@ -7,9 +7,9 @@ namespace Velopack.Vpk.Logging; public class SpectreConsole : IFancyConsole { private readonly ILogger logger; - private readonly DefaultPromptValueFactory defaultFactory; + private readonly VelopackDefaults defaultFactory; - public SpectreConsole(ILogger logger, DefaultPromptValueFactory defaultFactory) + public SpectreConsole(ILogger logger, VelopackDefaults defaultFactory) { this.logger = logger; this.defaultFactory = defaultFactory; diff --git a/src/Velopack.Vpk/Program.cs b/src/Velopack.Vpk/Program.cs index fd0a8398..0a39a312 100644 --- a/src/Velopack.Vpk/Program.cs +++ b/src/Velopack.Vpk/Program.cs @@ -37,17 +37,32 @@ public class Program .SetRecursive(true) .SetDescription("'yes' by instead of 'no' in non-interactive prompts."); + public static CliDirective WindowsDirective { get; } = new CliDirective("win") { + Description = "Show and run Windows specific commands." + }; + + public static CliDirective LinuxDirective { get; } = new CliDirective("linux") { + Description = "Show and run Linux specific commands." + }; + + public static CliDirective OsxDirective { get; } = new CliDirective("osx") { + Description = "Show and run MacOS specific commands." + }; + public static readonly string INTRO = $"Velopack CLI {VelopackRuntimeInfo.VelopackDisplayVersion}, for distributing applications."; public static async Task Main(string[] args) { - CliCommand rootCommand = new CliCommand("vpk", INTRO) { - new LongHelpCommand(), - LegacyConsoleOption, - YesOption, - VerboseOption, - }; + CliRootCommand rootCommand = new CliRootCommand(INTRO); + rootCommand.Options.Clear(); // remove the default help option + rootCommand.Options.Add(new LongHelpCommand()); + rootCommand.Options.Add(LegacyConsoleOption); + rootCommand.Options.Add(YesOption); + rootCommand.Options.Add(VerboseOption); + rootCommand.Directives.Add(WindowsDirective); + rootCommand.Directives.Add(LinuxDirective); + rootCommand.Directives.Add(OsxDirective); rootCommand.TreatUnmatchedTokensAsErrors = false; ParseResult parseResult = rootCommand.Parse(args); @@ -56,6 +71,9 @@ public class Program || Console.IsOutputRedirected || Console.IsErrorRedirected; bool defaultYes = parseResult.GetValue(YesOption); + bool directiveWin = parseResult.GetResult(WindowsDirective) != null; + bool directiveLinux = parseResult.GetResult(LinuxDirective) != null; + bool directiveOsx = parseResult.GetResult(OsxDirective) != null; rootCommand.TreatUnmatchedTokensAsErrors = true; var builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings { @@ -66,20 +84,49 @@ public class Program }); SetupConfig(builder); - SetupLogging(builder, verbose, legacyConsole, defaultYes); + SetupLogging(builder, verbose, legacyConsole); SetupVelopackService(builder.Services); + RuntimeOs targetOs = VelopackRuntimeInfo.SystemOs; + if (new bool[] { directiveWin, directiveLinux, directiveOsx }.Count(x => x) > 1) { + throw new UserInfoException( + "Invalid arguments: Only one OS directive can be specified at a time: either [win], [linux], or [osx]."); + } + + if (directiveWin) { + targetOs = RuntimeOs.Windows; + } else if (directiveLinux) { + targetOs = RuntimeOs.Linux; + } else if (directiveOsx) { + targetOs = RuntimeOs.OSX; + } + + builder.Services.AddSingleton(new VelopackDefaults(defaultYes, targetOs)); + var host = builder.Build(); var provider = host.Services; + var logger = provider.GetRequiredService(); - if (VelopackRuntimeInfo.IsWindows) { + if (targetOs != VelopackRuntimeInfo.SystemOs) { + logger.LogInformation($"Directive enabled for cross-compiling from {VelopackRuntimeInfo.SystemOs} (current os) to {targetOs}."); + } + + switch (targetOs) { + case RuntimeOs.Windows: rootCommand.AddCommand(provider); - } else if (VelopackRuntimeInfo.IsOSX) { - rootCommand.AddCommand(provider); - rootCommand.AddCommand(provider); - } else if (VelopackRuntimeInfo.IsLinux) { + break; + case RuntimeOs.Linux: rootCommand.AddCommand(provider); - } else { + break; + case RuntimeOs.OSX: + if (VelopackRuntimeInfo.IsOSX) { + rootCommand.AddCommand(provider); + rootCommand.AddCommand(provider); + } else { + throw new NotSupportedException($"Cross-compiling from {VelopackRuntimeInfo.SystemOs} to MacOS is not supported."); + } + break; + default: throw new NotSupportedException("Unsupported OS platform: " + VelopackRuntimeInfo.SystemOs.GetOsLongName()); } @@ -122,15 +169,13 @@ public class Program builder.Services.AddTransient(s => s.GetService().CreateLogger("vpk")); } - private static void SetupLogging(IHostApplicationBuilder builder, bool verbose, bool legacyConsole, bool defaultPromptValue) + private static void SetupLogging(IHostApplicationBuilder builder, bool verbose, bool legacyConsole) { var conf = new LoggerConfiguration() .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("System", LogEventLevel.Warning); - builder.Services.AddSingleton(new DefaultPromptValueFactory(defaultPromptValue)); - if (legacyConsole) { // spectre can have issues with redirected output, so we disable it. builder.Services.AddSingleton(); @@ -195,12 +240,15 @@ public static class ProgramCommandExtensions var command = new TCli(); command.SetAction(async (ctx, token) => { var logger = provider.GetRequiredService(); + var console = provider.GetRequiredService(); var config = provider.GetRequiredService(); + var defaults = provider.GetRequiredService(); + logger.LogInformation($"[bold]{Program.INTRO}[/]"); var updateCheck = new UpdateChecker(logger); await updateCheck.CheckForUpdates(); - command.SetProperties(ctx, config); + command.SetProperties(ctx, config, defaults.TargetOs); var options = OptionMapper.Map(command); try { @@ -210,10 +258,10 @@ public static class ProgramCommandExtensions return 0; } catch (Exception ex) when (ex is ProcessFailedException or UserInfoException) { // some exceptions are just user info / user error, so don't need a stack trace. - logger.Fatal($"[bold orange3]{ex.Message}[/]"); + logger.Fatal($"[bold orange3]{console.EscapeMarkup(ex.Message)}[/]"); return -1; } catch (Exception ex) { - logger.Fatal(ex, $"Command {typeof(TCli).Name} had an exception."); + logger.Fatal(ex); return -1; } }); diff --git a/src/Velopack.Vpk/Velopack.Vpk.csproj b/src/Velopack.Vpk/Velopack.Vpk.csproj index 82675d15..f4d7afee 100644 --- a/src/Velopack.Vpk/Velopack.Vpk.csproj +++ b/src/Velopack.Vpk/Velopack.Vpk.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Velopack.Vpk/VelopackDefaults.cs b/src/Velopack.Vpk/VelopackDefaults.cs new file mode 100644 index 00000000..61ae81b4 --- /dev/null +++ b/src/Velopack.Vpk/VelopackDefaults.cs @@ -0,0 +1,18 @@ +namespace Velopack.Vpk; + +public record VelopackDefaults +{ + public bool DefaultPromptValue { get; } + public RuntimeOs TargetOs { get; } + + public VelopackDefaults(bool defaultPromptValue) + : this(defaultPromptValue, VelopackRuntimeInfo.SystemOs) + { + } + + public VelopackDefaults(bool defaultPromptValue, RuntimeOs targetOs) + { + DefaultPromptValue = defaultPromptValue; + TargetOs = targetOs; + } +} diff --git a/src/Velopack.Vpk/packages.lock.json b/src/Velopack.Vpk/packages.lock.json index 7bd89d40..b372495d 100644 --- a/src/Velopack.Vpk/packages.lock.json +++ b/src/Velopack.Vpk/packages.lock.json @@ -64,16 +64,16 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "NuGet.Protocol": { "type": "Direct", - "requested": "[6.9.1, )", - "resolved": "6.9.1", - "contentHash": "h3bdjqUY4jvwM02D/7QM4FR8x/bbf4Pyjrc1Etw7an2OrWKPUSx0vJPdapKzioxIw7GXl/aVUM/DCeIc3S9brA==", + "requested": "[6.10.0, )", + "resolved": "6.10.0", + "contentHash": "/3r1avHk5IlqoGlXlb1ezNgtTIQyMTR5DAgh1WBcllivpbADpM9rvsFeemvcnndaFuQkEgc7a2egQZEnOK15ew==", "dependencies": { - "NuGet.Packaging": "6.9.1" + "NuGet.Packaging": "6.10.0" } }, "Riok.Mapperly": { @@ -169,15 +169,15 @@ }, "AWSSDK.Core": { "type": "Transitive", - "resolved": "3.7.304.1", - "contentHash": "O/gyE5ptF5zEc3QDk3JI3+AOgBfRfmg6eLH5z3x7hUPmV1Wxu0V4Fm86FSyT6czRviMmQGY0q0SVMbWRbP4vDA==" + "resolved": "3.7.304.10", + "contentHash": "0CXnPzoM+KYXODm2bvRW8eYs7ic2VLop45sphL8FBfvxHfBK/3OJgpxEE2InSdWS1Iby+6KXpz3NplJJY5+Y1A==" }, "AWSSDK.S3": { "type": "Transitive", - "resolved": "3.7.308", - "contentHash": "U4LWi1yTKVK6IFHWQc4anKPJKXj9dPN+dT1oXto4ndXXlr+DVzm9dULkquyXBjAHlr6jf339ojuYlE6t9Mk6pQ==", + "resolved": "3.7.308.8", + "contentHash": "lUQgJsj9n/sJo1CRQFsiB2Gqh3rC21I4e5CWR1b+oYBnc9m8LIsGewctX6YQRzGzuv7Im7iIZo7XoirmYhhqEA==", "dependencies": { - "AWSSDK.Core": "[3.7.304.1, 4.0.0)" + "AWSSDK.Core": "[3.7.304.10, 4.0.0)" } }, "Azure.Core": { @@ -493,8 +493,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -502,26 +502,26 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -530,8 +530,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -550,41 +550,41 @@ }, "NuGet.Common": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "FbuWZBjQ1NJXBDqCwSddN2yvw3Plq3sTCIh0nc66Hu8jrNr+BOaxlKZv78jvJ+pSy8BvurYOdF9sl9KoORjrtg==", + "resolved": "6.10.0", + "contentHash": "ujuzfDwUylDALwZmqRi7I5Jx4E9/8vR2c0Hq8zRj8zCkR8KarSp84WPJ2uE/qwAOjpBYGek06wWgGJ3ABQ/WxA==", "dependencies": { - "NuGet.Frameworks": "6.9.1" + "NuGet.Frameworks": "6.10.0" } }, "NuGet.Configuration": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "GM06pcUzWdNsizeGciqCjAhryfI1F/rQPETLDF+8pDRgzVpA+wKAR01/4aFU+IXzugnQ9LqOb5YyCRuR1OVZiQ==", + "resolved": "6.10.0", + "contentHash": "PbtCCdFC/K3GG3fdstIIMVraiXcxC1IgnzqayowckSlIg2xSxvcqAWd5Z7G1V6st5JzJi9THSnvmy/kchwh80A==", "dependencies": { - "NuGet.Common": "6.9.1", + "NuGet.Common": "6.10.0", "System.Security.Cryptography.ProtectedData": "4.4.0" } }, "NuGet.Frameworks": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "DaKh3lenPUvzGccPkbI97BIvA27z+/UsL3ankfoZlX/4vBVDK5N1sheFTQ+GuJf+IgSzsJz/A21SPUpQLHwUtA==" + "resolved": "6.10.0", + "contentHash": "bKaC87Q8rxK7ozFN9Eyo0YVUmd4r2s8pbNxHI7sHRqL16OAP0yEdCU5wpDkG9cKLHpZCahgoQUfAVC0UadVU+A==" }, "NuGet.Packaging": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "6FyasOxKInCELJ+pGy8f17ub9st6ofFeNcBNTo7CRiPJlxyhJfKGKNpfe3HHYwvnZhc3Vdfr0kSR+f1BVGDuTA==", + "resolved": "6.10.0", + "contentHash": "QSznYvf+HejToKt9zOB2z9F1C4R01lnrUUcJ+WxL1ihazTBfrXWPCCAQDtQFSlJlibiJBn/GEoq1gMeaeSo2qQ==", "dependencies": { "Newtonsoft.Json": "13.0.3", - "NuGet.Configuration": "6.9.1", - "NuGet.Versioning": "6.9.1", + "NuGet.Configuration": "6.10.0", + "NuGet.Versioning": "6.10.0", "System.Security.Cryptography.Pkcs": "6.0.4" } }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "Octokit": { "type": "Transitive", @@ -703,6 +703,20 @@ "Serilog": "3.1.1" } }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "SixLabors.ImageSharp": { + "type": "Transitive", + "resolved": "2.1.8", + "contentHash": "ML1++vactR5xMW36wHA2nAX3vc6VpR62qYnpopQdLNGbP8BAJ/ckv1IrGIIFRUEQ0JS9EgWr1y1gEa/81f+HaA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } + }, "System.ClientModel": { "type": "Transitive", "resolved": "1.0.0", @@ -1193,6 +1207,14 @@ "System.Runtime": "4.3.0" } }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "8.0.0", @@ -1238,37 +1260,41 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" } }, "velopack.deployment": { "type": "Project", "dependencies": { - "AWSSDK.S3": "[3.7.308, )", + "AWSSDK.S3": "[3.7.308.8, )", "Azure.Storage.Blobs": "[12.20.0, )", "Octokit": "[11.0.1, )", "Velopack.Packaging": "[1.0.0, )" } }, + "velopack.icolib": { + "type": "Project", + "dependencies": { + "SixLabors.ImageSharp": "[2.1.8, )" + } + }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" } }, - "velopack.packaging.hostmodel": { - "type": "Project" - }, "velopack.packaging.unix": { "type": "Project", "dependencies": { "ELFSharp": "[2.17.3, )", + "SharpZipLib": "[1.4.2, )", "Velopack.Packaging": "[1.0.0, )" } }, @@ -1277,8 +1303,8 @@ "dependencies": { "AsmResolver.DotNet": "[5.5.1, )", "AsmResolver.PE.Win32Resources": "[5.5.1, )", - "Velopack.Packaging": "[1.0.0, )", - "Velopack.Packaging.HostModel": "[1.0.0, )" + "Velopack.IcoLib": "[1.1.1, )", + "Velopack.Packaging": "[1.0.0, )" } } }, @@ -1346,16 +1372,16 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "NuGet.Protocol": { "type": "Direct", - "requested": "[6.9.1, )", - "resolved": "6.9.1", - "contentHash": "h3bdjqUY4jvwM02D/7QM4FR8x/bbf4Pyjrc1Etw7an2OrWKPUSx0vJPdapKzioxIw7GXl/aVUM/DCeIc3S9brA==", + "requested": "[6.10.0, )", + "resolved": "6.10.0", + "contentHash": "/3r1avHk5IlqoGlXlb1ezNgtTIQyMTR5DAgh1WBcllivpbADpM9rvsFeemvcnndaFuQkEgc7a2egQZEnOK15ew==", "dependencies": { - "NuGet.Packaging": "6.9.1" + "NuGet.Packaging": "6.10.0" } }, "Riok.Mapperly": { @@ -1448,15 +1474,15 @@ }, "AWSSDK.Core": { "type": "Transitive", - "resolved": "3.7.304.1", - "contentHash": "O/gyE5ptF5zEc3QDk3JI3+AOgBfRfmg6eLH5z3x7hUPmV1Wxu0V4Fm86FSyT6czRviMmQGY0q0SVMbWRbP4vDA==" + "resolved": "3.7.304.10", + "contentHash": "0CXnPzoM+KYXODm2bvRW8eYs7ic2VLop45sphL8FBfvxHfBK/3OJgpxEE2InSdWS1Iby+6KXpz3NplJJY5+Y1A==" }, "AWSSDK.S3": { "type": "Transitive", - "resolved": "3.7.308", - "contentHash": "U4LWi1yTKVK6IFHWQc4anKPJKXj9dPN+dT1oXto4ndXXlr+DVzm9dULkquyXBjAHlr6jf339ojuYlE6t9Mk6pQ==", + "resolved": "3.7.308.8", + "contentHash": "lUQgJsj9n/sJo1CRQFsiB2Gqh3rC21I4e5CWR1b+oYBnc9m8LIsGewctX6YQRzGzuv7Im7iIZo7XoirmYhhqEA==", "dependencies": { - "AWSSDK.Core": "[3.7.304.1, 4.0.0)" + "AWSSDK.Core": "[3.7.304.10, 4.0.0)" } }, "Azure.Core": { @@ -1767,8 +1793,8 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "3TDPEie+9t/NgBhoNifLFwM6nkypYUa8GUHfMnkYtovDDTQM8bsS2KEIP5tAh28BgfJZgof/KlCEKvGxz0H3Eg==", + "resolved": "4.61.2", + "contentHash": "zXPDXjTju1yOZQzi5PeNmQfBzlxv+VlCfgfZ70VqyFuhaPtFXwvbLSlwCOMoVALR/MzOR6xkiadoyVDmWDmqKw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.35.0", "System.Diagnostics.DiagnosticSource": "6.0.1" @@ -1776,26 +1802,26 @@ }, "Microsoft.Identity.Client.Broker": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "D5M7plyrXrQwhj/PILHLS7G3TqDg8rUO9lR+5awUSXueGxsdA2qHpeKiSR0sVfX6wxziWszOLDUR2vjFFKHPSg==", + "resolved": "4.61.2", + "contentHash": "+13pImNYfPGl04qZh3Rtq8wPfo55FScAUEio4ymPzeWJ/PE04jCsKH6ZIlrYbPCqe6t9u0lru1eZ+ho9Dsnvpw==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", - "Microsoft.Identity.Client.NativeInterop": "0.16.0" + "Microsoft.Identity.Client": "4.61.2", + "Microsoft.Identity.Client.NativeInterop": "0.16.1" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "4.61.0", - "contentHash": "tCX3c1bSkwH89AKjtB/oAOLlv661tOsEA8ULOCavOc6te/CKlqmMmFs8zMI58Qlv3XkbLOrswYr50IhDntiiPA==", + "resolved": "4.61.2", + "contentHash": "GCSBhzkr7/7DZDk2kYS9dfihB5ieM2CqwGAhtjuE6T1GtAqzuh62UJhMgO0dSx/PLgIYMzNHnh+pfHK8aWJQJQ==", "dependencies": { - "Microsoft.Identity.Client": "4.61.0", + "Microsoft.Identity.Client": "4.61.2", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, "Microsoft.Identity.Client.NativeInterop": { "type": "Transitive", - "resolved": "0.16.0", - "contentHash": "riaIH9tT4kRG4xKFxx+wpuo+VAZVOr1ElbNubcvpFXb82gjEmZtJRzpdiS4tdRYTVh6CUKNz9AINAptActTtBQ==" + "resolved": "0.16.1", + "contentHash": "OvY/+/AHESi24f5zOCf9kL4HXwPxXVQ3A+tMQsJvFk2DmP+sc88FYWL49zlku5q0bvx5yFvBLNpHQeRT9a6A5g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -1804,8 +1830,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -1824,41 +1850,41 @@ }, "NuGet.Common": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "FbuWZBjQ1NJXBDqCwSddN2yvw3Plq3sTCIh0nc66Hu8jrNr+BOaxlKZv78jvJ+pSy8BvurYOdF9sl9KoORjrtg==", + "resolved": "6.10.0", + "contentHash": "ujuzfDwUylDALwZmqRi7I5Jx4E9/8vR2c0Hq8zRj8zCkR8KarSp84WPJ2uE/qwAOjpBYGek06wWgGJ3ABQ/WxA==", "dependencies": { - "NuGet.Frameworks": "6.9.1" + "NuGet.Frameworks": "6.10.0" } }, "NuGet.Configuration": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "GM06pcUzWdNsizeGciqCjAhryfI1F/rQPETLDF+8pDRgzVpA+wKAR01/4aFU+IXzugnQ9LqOb5YyCRuR1OVZiQ==", + "resolved": "6.10.0", + "contentHash": "PbtCCdFC/K3GG3fdstIIMVraiXcxC1IgnzqayowckSlIg2xSxvcqAWd5Z7G1V6st5JzJi9THSnvmy/kchwh80A==", "dependencies": { - "NuGet.Common": "6.9.1", + "NuGet.Common": "6.10.0", "System.Security.Cryptography.ProtectedData": "4.4.0" } }, "NuGet.Frameworks": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "DaKh3lenPUvzGccPkbI97BIvA27z+/UsL3ankfoZlX/4vBVDK5N1sheFTQ+GuJf+IgSzsJz/A21SPUpQLHwUtA==" + "resolved": "6.10.0", + "contentHash": "bKaC87Q8rxK7ozFN9Eyo0YVUmd4r2s8pbNxHI7sHRqL16OAP0yEdCU5wpDkG9cKLHpZCahgoQUfAVC0UadVU+A==" }, "NuGet.Packaging": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "6FyasOxKInCELJ+pGy8f17ub9st6ofFeNcBNTo7CRiPJlxyhJfKGKNpfe3HHYwvnZhc3Vdfr0kSR+f1BVGDuTA==", + "resolved": "6.10.0", + "contentHash": "QSznYvf+HejToKt9zOB2z9F1C4R01lnrUUcJ+WxL1ihazTBfrXWPCCAQDtQFSlJlibiJBn/GEoq1gMeaeSo2qQ==", "dependencies": { "Newtonsoft.Json": "13.0.3", - "NuGet.Configuration": "6.9.1", - "NuGet.Versioning": "6.9.1", + "NuGet.Configuration": "6.10.0", + "NuGet.Versioning": "6.10.0", "System.Security.Cryptography.Pkcs": "6.0.4" } }, "NuGet.Versioning": { "type": "Transitive", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "Octokit": { "type": "Transitive", @@ -1977,6 +2003,20 @@ "Serilog": "3.1.1" } }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "SixLabors.ImageSharp": { + "type": "Transitive", + "resolved": "2.1.8", + "contentHash": "ML1++vactR5xMW36wHA2nAX3vc6VpR62qYnpopQdLNGbP8BAJ/ckv1IrGIIFRUEQ0JS9EgWr1y1gEa/81f+HaA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } + }, "System.ClientModel": { "type": "Transitive", "resolved": "1.0.0", @@ -2243,6 +2283,11 @@ "Microsoft.NETCore.Targets": "1.1.0" } }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" + }, "System.Runtime.Extensions": { "type": "Transitive", "resolved": "4.3.0", @@ -2454,6 +2499,14 @@ "System.Runtime": "4.3.0" } }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "8.0.0", @@ -2495,37 +2548,41 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )", - "NuGet.Versioning": "[6.9.1, )" + "NuGet.Versioning": "[6.10.0, )" } }, "velopack.deployment": { "type": "Project", "dependencies": { - "AWSSDK.S3": "[3.7.308, )", + "AWSSDK.S3": "[3.7.308.8, )", "Azure.Storage.Blobs": "[12.20.0, )", "Octokit": "[11.0.1, )", "Velopack.Packaging": "[1.0.0, )" } }, + "velopack.icolib": { + "type": "Project", + "dependencies": { + "SixLabors.ImageSharp": "[2.1.8, )" + } + }, "velopack.packaging": { "type": "Project", "dependencies": { "Markdig": "[0.37.0, )", - "Microsoft.Identity.Client": "[4.61.0, )", - "Microsoft.Identity.Client.Broker": "[4.61.0, )", - "Microsoft.Identity.Client.Extensions.Msal": "[4.61.0, )", + "Microsoft.Identity.Client": "[4.61.2, )", + "Microsoft.Identity.Client.Broker": "[4.61.2, )", + "Microsoft.Identity.Client.Extensions.Msal": "[4.61.2, )", "System.Linq.Async": "[6.0.1, )", "System.Net.Http": "[4.3.4, )", "Velopack": "[1.0.0, )" } }, - "velopack.packaging.hostmodel": { - "type": "Project" - }, "velopack.packaging.unix": { "type": "Project", "dependencies": { "ELFSharp": "[2.17.3, )", + "SharpZipLib": "[1.4.2, )", "Velopack.Packaging": "[1.0.0, )" } }, @@ -2534,8 +2591,8 @@ "dependencies": { "AsmResolver.DotNet": "[5.5.1, )", "AsmResolver.PE.Win32Resources": "[5.5.1, )", - "Velopack.Packaging": "[1.0.0, )", - "Velopack.Packaging.HostModel": "[1.0.0, )" + "Velopack.IcoLib": "[1.1.1, )", + "Velopack.Packaging": "[1.0.0, )" } } } diff --git a/src/Velopack/Compression/EasyZip.cs b/src/Velopack/Compression/EasyZip.cs index ff31dc09..814296d8 100644 --- a/src/Velopack/Compression/EasyZip.cs +++ b/src/Velopack/Compression/EasyZip.cs @@ -99,7 +99,7 @@ namespace Velopack.Compression } private static char s_pathSeperator = '/'; - private static readonly DateTime ZipFormatMinDate = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Utc); + public static readonly DateTime ZipFormatMinDate = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static async Task DeterministicCreateFromDirectoryAsync(string sourceDirectoryName, string destinationArchiveFileName, CompressionLevel compressionLevel, Action progress, CancellationToken cancelToken) diff --git a/src/Velopack/Velopack.csproj b/src/Velopack/Velopack.csproj index 92f6cab7..ec63f84c 100644 --- a/src/Velopack/Velopack.csproj +++ b/src/Velopack/Velopack.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Velopack/packages.lock.json b/src/Velopack/packages.lock.json index a98ae222..f0e61172 100644 --- a/src/Velopack/packages.lock.json +++ b/src/Velopack/packages.lock.json @@ -30,8 +30,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "Newtonsoft.Json": { "type": "Direct", @@ -41,9 +41,9 @@ }, "NuGet.Versioning": { "type": "Direct", - "requested": "[6.9.1, )", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "requested": "[6.10.0, )", + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -81,8 +81,8 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "Newtonsoft.Json": { "type": "Direct", @@ -92,9 +92,9 @@ }, "NuGet.Versioning": { "type": "Direct", - "requested": "[6.9.1, )", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "requested": "[6.10.0, )", + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -127,14 +127,14 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "NuGet.Versioning": { "type": "Direct", - "requested": "[6.9.1, )", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "requested": "[6.10.0, )", + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -176,14 +176,14 @@ "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.6.*, )", - "resolved": "3.6.133", - "contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw==" + "resolved": "3.6.139", + "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, "NuGet.Versioning": { "type": "Direct", - "requested": "[6.9.1, )", - "resolved": "6.9.1", - "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==" + "requested": "[6.10.0, )", + "resolved": "6.10.0", + "contentHash": "ytTA08tgZWo/Pbk333hnzQfiMyyynkkwMt4GyC5T0bzExYYSRNrkwv4jT8jmWr5LUOYsumEZvXdoven2SA7YZw==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", diff --git a/test/Directory.Build.props b/test/Directory.Build.props index da42d76b..6b7f766a 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -38,10 +38,10 @@ - + - - + + all diff --git a/test/Velopack.CommandLine.Tests/Velopack.CommandLine.Tests.csproj b/test/Velopack.CommandLine.Tests/Velopack.CommandLine.Tests.csproj index 7b12fd7e..a94e763e 100644 --- a/test/Velopack.CommandLine.Tests/Velopack.CommandLine.Tests.csproj +++ b/test/Velopack.CommandLine.Tests/Velopack.CommandLine.Tests.csproj @@ -15,6 +15,10 @@ all runtime; build; native; contentfiles; analyzers + + all + runtime; build; native; contentfiles; analyzers + diff --git a/test/Velopack.Packaging.Tests/CompatUtilTests.cs b/test/Velopack.Packaging.Tests/CompatUtilTests.cs index 9bc557dc..08bcd81f 100644 --- a/test/Velopack.Packaging.Tests/CompatUtilTests.cs +++ b/test/Velopack.Packaging.Tests/CompatUtilTests.cs @@ -1,6 +1,7 @@ using Divergic.Logging.Xunit; using Velopack.Packaging.Exceptions; using Velopack.Packaging.Windows; +using Velopack.Vpk; using Velopack.Vpk.Logging; namespace Velopack.Packaging.Tests; @@ -17,7 +18,7 @@ public class CompatUtilTests private ICacheLogger GetCompat(out CompatUtil compat) { var logger = _output.BuildLoggerFor(); - compat = new CompatUtil(logger, new BasicConsole(logger, new DefaultPromptValueFactory(true))); + compat = new CompatUtil(logger, new BasicConsole(logger, new VelopackDefaults(true))); return logger; } diff --git a/test/Velopack.Packaging.Tests/CrossCompile.cs b/test/Velopack.Packaging.Tests/CrossCompile.cs new file mode 100644 index 00000000..ef792c90 --- /dev/null +++ b/test/Velopack.Packaging.Tests/CrossCompile.cs @@ -0,0 +1,100 @@ +using Velopack.Packaging.Unix; + +namespace Velopack.Packaging.Tests; + +public class CrossCompile +{ + private readonly ITestOutputHelper _output; + + public CrossCompile(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [InlineData("win-x64")] + [InlineData("linux-x64")] + public void PackCrossApp(string target) + { + using var logger = _output.BuildLoggerFor(); + var rid = RID.Parse(target); + + string id = $"from-{VelopackRuntimeInfo.SystemOs.GetOsShortName()}-targets-{rid.BaseRID.GetOsShortName()}"; + using var _1 = Utility.GetTempDirectory(out var tempDir); + TestApp.PackTestApp(id, "1.0.0", id, tempDir, logger, targetRid: rid); + + var artifactsDir = PathHelper.GetTestRootPath("artifacts"); + Directory.CreateDirectory(artifactsDir); + + string src, dest; + if (rid.BaseRID == RuntimeOs.Windows) { + src = Path.Combine(tempDir, id + "-win-Setup.exe"); + dest = Path.Combine(artifactsDir, id + ".exe"); + } else { + src = Path.Combine(tempDir, id + ".AppImage"); + dest = Path.Combine(artifactsDir, id + ".AppImage"); + } + + Assert.True(File.Exists(src), $"Expected {src} to exist"); + File.Copy(src, dest, overwrite: true); + } + + [SkippableTheory] + [InlineData("from-win-targets-linux")] + [InlineData("from-linux-targets-linux")] + [InlineData("from-osx-targets-linux")] + public void RunCrossAppLinux(string artifactId) + { + using var logger = _output.BuildLoggerFor(); + Skip.If(String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("VELOPACK_CROSS_ARTIFACTS")), + "VELOPACK_CROSS_ARTIFACTS not set"); + Skip.IfNot(VelopackRuntimeInfo.IsLinux, "AppImage's can only run on Linux"); + + var artifactsDir = PathHelper.GetTestRootPath("artifacts"); + var artifactPath = Path.Combine(artifactsDir, artifactId + ".AppImage"); + + Assert.True(File.Exists(artifactPath), $"Expected {artifactPath} to exist"); + Chmod.ChmodFileAsExecutable(artifactPath); + + var output = Exe.InvokeAndThrowIfNonZero(artifactPath, new[] { "test" }, null); + logger.LogInformation(output); + Assert.EndsWith(artifactId, output.Trim()); + } + + [SkippableTheory] + [InlineData("from-win-targets-win")] + [InlineData("from-linux-targets-win")] + [InlineData("from-osx-targets-win")] + public void RunCrossAppWindows(string artifactId) + { + using var logger = _output.BuildLoggerFor(); + Skip.If(String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("VELOPACK_CROSS_ARTIFACTS")), + "VELOPACK_CROSS_ARTIFACTS not set"); + Skip.IfNot(VelopackRuntimeInfo.IsWindows, "PE files can only run on Windows"); + + var artifactsDir = PathHelper.GetTestRootPath("artifacts"); + var artifactPath = Path.Combine(artifactsDir, artifactId + ".exe"); + + Assert.True(File.Exists(artifactPath), $"Expected {artifactPath} to exist"); + + var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var appRoot = Path.Combine(appData, artifactId); + var appExe = Path.Combine(appRoot, "current", "TestApp.exe"); + var appUpdate = Path.Combine(appRoot, "Update.exe"); + + Utility.DeleteFileOrDirectoryHard(appRoot); + + Assert.False(File.Exists(appExe)); + var installOutput = Exe.InvokeAndThrowIfNonZero(artifactPath, new[] { "--silent" }, null); + logger.LogInformation(installOutput); + + Assert.True(File.Exists(appExe)); + + var output = Exe.InvokeAndThrowIfNonZero(appExe, new[] { "test" }, null); + logger.LogInformation(output); + Assert.EndsWith(artifactId, output.Trim()); + + Exe.RunHostedCommand($"\"{appUpdate}\" --uninstall --silent"); + Assert.False(File.Exists(appExe)); + } +} diff --git a/test/Velopack.Packaging.Tests/ResourceEditTests.cs b/test/Velopack.Packaging.Tests/ResourceEditTests.cs new file mode 100644 index 00000000..907fdbfe --- /dev/null +++ b/test/Velopack.Packaging.Tests/ResourceEditTests.cs @@ -0,0 +1,220 @@ +using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using AsmResolver.PE; +using AsmResolver.PE.File; +using AsmResolver.PE.Win32Resources.Icon; +using AsmResolver.PE.Win32Resources.Version; +using Velopack.NuGet; +using Velopack.Packaging.Windows; + +namespace Velopack.Packaging.Tests; + +public class ResourceEditTests +{ + private readonly ITestOutputHelper _output; + + public ResourceEditTests(ITestOutputHelper output) + { + _output = output; + } + + private void CreateTestPEFileWithoutRsrc(string tempFile) + { + var peBuilder = new ManagedPEBuilder( + PEHeaderBuilder.CreateExecutableHeader(), + new MetadataRootBuilder(new MetadataBuilder()), + ilStream: new BlobBuilder()); + var peImageBuilder = new BlobBuilder(); + peBuilder.Serialize(peImageBuilder); + + using var fs = File.OpenWrite(tempFile); + fs.Write(peImageBuilder.ToArray()); + } + + [Fact] + public void CommitResourcesInCorrectOrder() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempFileName(out var tempFile); + var exe = PathHelper.GetRustAsset("setup.exe"); + File.Copy(exe, tempFile); + + var nuspec = PathHelper.GetFixture("FullNuspec.nuspec"); + var manifest = PackageManifest.ParseFromFile(nuspec); + var pkgVersion = manifest.Version!; + + var edit = new ResourceEdit(tempFile, logger); + edit.SetExeIcon(PathHelper.GetFixture("clowd.ico")); + edit.SetVersionInfo(manifest); + edit.Commit(); + + var afterRsrc = PEImage.FromFile(PEFile.FromFile(tempFile)).Resources; + Assert.NotNull(afterRsrc); + + uint lastId = 0; + foreach (var e in afterRsrc.Entries) { + Assert.True(e.Id > lastId, "Resource entry ID must be greater than the previous"); + lastId = e.Id; + } + } + + [Fact] + public void CopyResourcesWithoutRsrc() + { + using var logger = _output.BuildLoggerFor(); + + using var _1 = Utility.GetTempFileName(out var tempFile); + CreateTestPEFileWithoutRsrc(tempFile); + + var edit = new ResourceEdit(tempFile, logger); + edit.CopyResourcesFrom(PathHelper.GetFixture("Clowd.exe")); + edit.Commit(); + + AssertVersionInfo(tempFile, "3.4.439.61274", "3.4.439+ef5a83", "Copyright © Caelan Sayler, 2014-2022", + "Clowd", "Clowd", "Caelan Sayler"); + } + + [Fact] + public void CopyResourcesWithPreExistingRsrc() + { + using var logger = _output.BuildLoggerFor(); + + using var _1 = Utility.GetTempFileName(out var tempFile); + var exe = PathHelper.GetFixture("SquirrelAwareTweakedNetCoreApp.exe"); + File.Copy(exe, tempFile); + + var edit = new ResourceEdit(tempFile, logger); + edit.CopyResourcesFrom(PathHelper.GetFixture("Clowd.exe")); + edit.Commit(); + + AssertVersionInfo(tempFile, "3.4.439.61274", "3.4.439+ef5a83", "Copyright © Caelan Sayler, 2014-2022", + "Clowd", "Clowd", "Caelan Sayler"); + } + + [Fact] + public void SetIconWithPreExistingRsrc() + { + using var logger = _output.BuildLoggerFor(); + + using var _1 = Utility.GetTempFileName(out var tempFile); + var exe = PathHelper.GetFixture("atom.exe"); + File.Copy(exe, tempFile); + + var beforeRsrc = PEImage.FromFile(PEFile.FromFile(tempFile)).Resources; + Assert.NotNull(beforeRsrc); + var beforeIcon = IconResource.FromDirectory(beforeRsrc); + Assert.Single(beforeIcon.GetIconGroups()); + Assert.Equal(6, beforeIcon.GetIconGroups().ToList()[0].GetIconEntries().Count()); + + var edit = new ResourceEdit(tempFile, logger); + edit.SetExeIcon(PathHelper.GetFixture("clowd.ico")); + edit.Commit(); + + var afterRsrc = PEImage.FromFile(PEFile.FromFile(tempFile)).Resources; + Assert.NotNull(afterRsrc); + var afterIcon = IconResource.FromDirectory(afterRsrc); + Assert.Single(afterIcon.GetIconGroups()); + Assert.Equal(1, afterIcon.GetIconGroups().Single().Type); + Assert.Equal(7, afterIcon.GetIconGroups().ToList()[0].GetIconEntries().Count()); + } + + [Fact] + public void SetIconWithoutRsrc() + { + using var logger = _output.BuildLoggerFor(); + + using var _1 = Utility.GetTempFileName(out var tempFile); + CreateTestPEFileWithoutRsrc(tempFile); + + var beforeRsrc = PEImage.FromFile(PEFile.FromFile(tempFile)).Resources; + Assert.Null(beforeRsrc); + + var edit = new ResourceEdit(tempFile, logger); + edit.SetExeIcon(PathHelper.GetFixture("clowd.ico")); + edit.Commit(); + + var afterRsrc = PEImage.FromFile(PEFile.FromFile(tempFile)).Resources; + Assert.NotNull(afterRsrc); + var afterIcon = IconResource.FromDirectory(afterRsrc); + Assert.Single(afterIcon.GetIconGroups()); + Assert.Equal(7, afterIcon.GetIconGroups().ToList()[0].GetIconEntries().Count()); + } + + [Fact] + public void SetVersionInfoWithPreExistingRsrc() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempFileName(out var tempFile); + var exe = PathHelper.GetFixture("atom.exe"); + File.Copy(exe, tempFile); + + var nuspec = PathHelper.GetFixture("FullNuspec.nuspec"); + var manifest = PackageManifest.ParseFromFile(nuspec); + var pkgVersion = manifest.Version!; + + var edit = new ResourceEdit(tempFile, logger); + edit.SetVersionInfo(manifest); + edit.Commit(); + + AssertVersionInfo(tempFile, manifest); + } + + [Fact] + public void SetVersionInfoWithoutRsrc() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempFileName(out var tempFile); + CreateTestPEFileWithoutRsrc(tempFile); + + var nuspec = PathHelper.GetFixture("FullNuspec.nuspec"); + var manifest = PackageManifest.ParseFromFile(nuspec); + var pkgVersion = manifest.Version!; + + var edit = new ResourceEdit(tempFile, logger); + edit.SetVersionInfo(manifest); + edit.Commit(); + + AssertVersionInfo(tempFile, manifest); + } + + private void AssertVersionInfo(string exeFile, PackageManifest manifest) + { + AssertVersionInfo(exeFile, manifest.Version!.ToFullString(), manifest.Version!.ToFullString(), + manifest.ProductCopyright, manifest.ProductName, manifest.ProductDescription, manifest.ProductCompany); + } + + private void AssertVersionInfo(string exeFile, string fileVersion, string productVersion, + string legalCopyright, string productName, string fileDescription, string companyName) + { + if (VelopackRuntimeInfo.IsWindows) { + // on Windows FileVersionInfo uses win32 methods to retrieve info from the PE resources + // on Unix, this function just looks for managed assembly attributes so is not suitable + var versionInfo = FileVersionInfo.GetVersionInfo(exeFile); + Assert.Equal(fileVersion, versionInfo.FileVersion); + Assert.Equal(productVersion, versionInfo.ProductVersion); + Assert.Equal(legalCopyright, versionInfo.LegalCopyright); + Assert.Equal(productName, versionInfo.ProductName); + Assert.Equal(fileDescription, versionInfo.FileDescription); + Assert.Equal(companyName, versionInfo.CompanyName); + } else { + var file = PEFile.FromFile(exeFile); + var image = PEImage.FromFile(file); + Assert.NotNull(image.Resources); + var versionInfo = VersionInfoResource.FromDirectory(image.Resources); + + var stringInfo = versionInfo.GetChild(StringFileInfo.StringFileInfoKey); + Assert.NotNull(stringInfo); + Assert.Single(stringInfo.Tables); + + var stringTable = stringInfo.Tables[0]; + Assert.Equal(companyName, stringTable[StringTable.CompanyNameKey]); + Assert.Equal(fileDescription, stringTable[StringTable.FileDescriptionKey]); + Assert.Equal(fileVersion, stringTable[StringTable.FileVersionKey]); + Assert.Equal(legalCopyright, stringTable[StringTable.LegalCopyrightKey]); + Assert.Equal(productName, stringTable[StringTable.ProductNameKey]); + Assert.Equal(productVersion, stringTable[StringTable.ProductVersionKey]); + } + } +} diff --git a/test/Velopack.Packaging.Tests/TestApp.cs b/test/Velopack.Packaging.Tests/TestApp.cs index a019b8f6..b7f56b27 100644 --- a/test/Velopack.Packaging.Tests/TestApp.cs +++ b/test/Velopack.Packaging.Tests/TestApp.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using Velopack.Packaging.Unix.Commands; using Velopack.Packaging.Windows.Commands; +using Velopack.Vpk; using Velopack.Vpk.Logging; namespace Velopack.Packaging.Tests; @@ -8,8 +9,10 @@ namespace Velopack.Packaging.Tests; public static class TestApp { public static void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger, - string releaseNotes = null, string channel = null) + string releaseNotes = null, string channel = null, RID targetRid = null) { + targetRid ??= RID.Parse(VelopackRuntimeInfo.SystemRid); + var projDir = PathHelper.GetTestRootPath("TestApp"); var testStringFile = Path.Combine(projDir, "Const.cs"); var oldText = File.ReadAllText(testStringFile); @@ -17,7 +20,7 @@ public static class TestApp try { File.WriteAllText(testStringFile, $"class Const {{ public const string TEST_STRING = \"{testString}\"; }}"); - var args = new string[] { "publish", "--no-self-contained", "-c", "Release", "-r", VelopackRuntimeInfo.SystemRid, "-o", "publish" }; + var args = new string[] { "publish", "--no-self-contained", "-c", "Release", "-r", targetRid.ToString(), "-o", "publish" }; var psi = new ProcessStartInfo("dotnet"); psi.WorkingDirectory = projDir; @@ -31,14 +34,14 @@ public static class TestApp if (p.ExitCode != 0) throw new Exception($"dotnet publish failed with exit code {p.ExitCode}"); - var console = new BasicConsole(logger, new DefaultPromptValueFactory(false)); + var console = new BasicConsole(logger, new VelopackDefaults(false)); - if (VelopackRuntimeInfo.IsWindows) { + if (targetRid.BaseRID == RuntimeOs.Windows) { var options = new WindowsPackOptions { EntryExecutableName = "TestApp.exe", ReleaseDir = new DirectoryInfo(releaseDir), PackId = id, - TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), + TargetRuntime = targetRid, PackVersion = version, PackDirectory = Path.Combine(projDir, "publish"), ReleaseNotes = releaseNotes, @@ -46,25 +49,29 @@ public static class TestApp }; var runner = new WindowsPackCommandRunner(logger, console); runner.Run(options).GetAwaiterResult(); - } else if (VelopackRuntimeInfo.IsOSX) { + } else if (targetRid.BaseRID == RuntimeOs.OSX) { var options = new OsxPackOptions { EntryExecutableName = "TestApp", ReleaseDir = new DirectoryInfo(releaseDir), PackId = id, - TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), + TargetRuntime = targetRid, PackVersion = version, PackDirectory = Path.Combine(projDir, "publish"), ReleaseNotes = releaseNotes, Channel = channel, }; - var runner = new OsxPackCommandRunner(logger, console); - runner.Run(options).GetAwaiterResult(); - } else if (VelopackRuntimeInfo.IsLinux) { + if (VelopackRuntimeInfo.IsOSX) { + var runner = new OsxPackCommandRunner(logger, console); + runner.Run(options).GetAwaiterResult(); + } else { + throw new PlatformNotSupportedException(); + } + } else if (targetRid.BaseRID == RuntimeOs.Linux) { var options = new LinuxPackOptions { EntryExecutableName = "TestApp", ReleaseDir = new DirectoryInfo(releaseDir), PackId = id, - TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), + TargetRuntime = targetRid, PackVersion = version, PackDirectory = Path.Combine(projDir, "publish"), ReleaseNotes = releaseNotes, diff --git a/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj b/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj index d0bed100..313a1b66 100644 --- a/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj +++ b/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj @@ -10,7 +10,7 @@ - + @@ -18,6 +18,10 @@ all runtime; build; native; contentfiles; analyzers + + all + runtime; build; native; contentfiles; analyzers + diff --git a/test/Velopack.Packaging.Tests/WindowsPackTests.cs b/test/Velopack.Packaging.Tests/WindowsPackTests.cs index 66e85234..e44a48ce 100644 --- a/test/Velopack.Packaging.Tests/WindowsPackTests.cs +++ b/test/Velopack.Packaging.Tests/WindowsPackTests.cs @@ -8,6 +8,7 @@ using Velopack.Compression; using Velopack.Packaging.Commands; using Velopack.Packaging.Exceptions; using Velopack.Packaging.Windows.Commands; +using Velopack.Vpk; using Velopack.Vpk.Logging; using Velopack.Windows; @@ -25,7 +26,7 @@ public class WindowsPackTests private WindowsPackCommandRunner GetPackRunner(ILogger logger) { - var console = new BasicConsole(logger, new DefaultPromptValueFactory(false)); + var console = new BasicConsole(logger, new VelopackDefaults(false)); return new WindowsPackCommandRunner(logger, console); } @@ -327,7 +328,7 @@ public class WindowsPackTests // apply delta and check package var output = Path.Combine(releaseDir, "delta.patched"); - new DeltaPatchCommandRunner(logger, new BasicConsole(logger, new DefaultPromptValueFactory(false))).Run(new DeltaPatchOptions { + new DeltaPatchCommandRunner(logger, new BasicConsole(logger, new VelopackDefaults(false))).Run(new DeltaPatchOptions { BasePackage = Path.Combine(releaseDir, $"{id}-1.0.0-full.nupkg"), OutputFile = output, PatchFiles = new[] { new FileInfo(deltaPath) }, @@ -344,7 +345,7 @@ public class WindowsPackTests // can apply multiple deltas, and handle add/removing files? output = Path.Combine(releaseDir, "delta.patched2"); var deltav3 = Path.Combine(releaseDir, $"{id}-3.0.0-delta.nupkg"); - new DeltaPatchCommandRunner(logger, new BasicConsole(logger, new DefaultPromptValueFactory(false))).Run(new DeltaPatchOptions { + new DeltaPatchCommandRunner(logger, new BasicConsole(logger, new VelopackDefaults(false))).Run(new DeltaPatchOptions { BasePackage = Path.Combine(releaseDir, $"{id}-1.0.0-full.nupkg"), OutputFile = output, PatchFiles = [new FileInfo(deltaPath), new FileInfo(deltav3)], diff --git a/test/Velopack.Tests/Velopack.Tests.csproj b/test/Velopack.Tests/Velopack.Tests.csproj index 41559489..66d3ed99 100644 --- a/test/Velopack.Tests/Velopack.Tests.csproj +++ b/test/Velopack.Tests/Velopack.Tests.csproj @@ -39,6 +39,10 @@ all runtime; build; native; contentfiles; analyzers + + all + runtime; build; native; contentfiles; analyzers + diff --git a/test/fixtures/Clowd.exe b/test/fixtures/Clowd.exe new file mode 100644 index 00000000..2c47b7bb Binary files /dev/null and b/test/fixtures/Clowd.exe differ diff --git a/test/fixtures/clowd.ico b/test/fixtures/clowd.ico new file mode 100644 index 00000000..486bf28b Binary files /dev/null and b/test/fixtures/clowd.ico differ diff --git a/vendor/7za.exe b/vendor/7za.exe deleted file mode 100644 index 9544a638..00000000 Binary files a/vendor/7za.exe and /dev/null differ diff --git a/vendor/THIRD_PARTY_NOTICES.md b/vendor/THIRD_PARTY_NOTICES.md index 93075955..99339d07 100644 --- a/vendor/THIRD_PARTY_NOTICES.md +++ b/vendor/THIRD_PARTY_NOTICES.md @@ -1,27 +1,22 @@ # Vendor Binaries This folder contains pre-compiled binaries from a variety of sources. These should be updated periodically. -### rcedit.exe v2.0.0 -- Updates PE resources, like VersionInfo or icons. It is used when generating `Setup.exe` and `Update.exe` to apply the user preferences. -- Can be found at https://github.com/electron/rcedit/releases -- MIT License: https://github.com/electron/rcedit/blob/master/LICENSE - ### signtool.exe v10.0.22621 - Signs application binaries while building packages. - Can be found in the Windows SDK at "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\signtool.exe" or similar, depending on the version of the SDK you have installed. - License? https://github.com/dotnet/docs/issues/10478 -### 7za.exe / 7zz v21.07 -- Incldued because it is much faster at zipping / unzipping than the available managed algorithms. -- Can be found at https://www.7-zip.org/ -- License is LGPL & BSD 3: https://www.7-zip.org/license.txt - ### zstd.exe v1.5.5 - Fast compression and diff/patch - Can be found at https://github.com/facebook/zstd - License is GPL-2.0 & BSD 3: https://github.com/facebook/zstd/blob/dev/LICENSE, https://github.com/facebook/zstd/blob/dev/COPYING -### appimagetool -- Create .AppImage for Linux +### appimagekit (continuous Mar 8, 2023) +- Only include the "runtime" binaries needed to create a .AppImage for Linux - Can be found at https://github.com/AppImage/AppImageKit -- License is MIT https://github.com/AppImage/AppImageKit/blob/master/LICENSE \ No newline at end of file +- License is MIT https://github.com/AppImage/AppImageKit/blob/master/LICENSE + +### squashfs-tools-ng v1.3.0 +- Squashfs utilities for Windows +- Can be found at https://github.com/AgentD/squashfs-tools-ng +- License is GPL-3 https://github.com/AgentD/squashfs-tools-ng/blob/master/COPYING.md \ No newline at end of file diff --git a/vendor/appimagekit-runtime-aarch64 b/vendor/appimagekit-runtime-aarch64 new file mode 100644 index 00000000..c60ccae0 Binary files /dev/null and b/vendor/appimagekit-runtime-aarch64 differ diff --git a/vendor/appimagekit-runtime-i686 b/vendor/appimagekit-runtime-i686 new file mode 100644 index 00000000..5319ae61 Binary files /dev/null and b/vendor/appimagekit-runtime-i686 differ diff --git a/vendor/appimagekit-runtime-x86_64 b/vendor/appimagekit-runtime-x86_64 new file mode 100644 index 00000000..3d73e2ab Binary files /dev/null and b/vendor/appimagekit-runtime-x86_64 differ diff --git a/vendor/appimagetool-x86_64.AppImage b/vendor/appimagetool-x86_64.AppImage deleted file mode 100755 index 89ff93ca..00000000 Binary files a/vendor/appimagetool-x86_64.AppImage and /dev/null differ diff --git a/vendor/rcedit.exe b/vendor/rcedit.exe deleted file mode 100644 index 0d817e90..00000000 Binary files a/vendor/rcedit.exe and /dev/null differ diff --git a/vendor/squashfs-tools/gensquashfs.exe b/vendor/squashfs-tools/gensquashfs.exe new file mode 100644 index 00000000..ce4b58b5 Binary files /dev/null and b/vendor/squashfs-tools/gensquashfs.exe differ diff --git a/vendor/squashfs-tools/liblz4-1.dll b/vendor/squashfs-tools/liblz4-1.dll new file mode 100644 index 00000000..41d26d2f Binary files /dev/null and b/vendor/squashfs-tools/liblz4-1.dll differ diff --git a/vendor/squashfs-tools/liblzma-5.dll b/vendor/squashfs-tools/liblzma-5.dll new file mode 100644 index 00000000..8fa6c428 Binary files /dev/null and b/vendor/squashfs-tools/liblzma-5.dll differ diff --git a/vendor/squashfs-tools/liblzo2-2.dll b/vendor/squashfs-tools/liblzo2-2.dll new file mode 100644 index 00000000..32a534f1 Binary files /dev/null and b/vendor/squashfs-tools/liblzo2-2.dll differ diff --git a/vendor/squashfs-tools/libsquashfs.dll b/vendor/squashfs-tools/libsquashfs.dll new file mode 100644 index 00000000..9398b5e8 Binary files /dev/null and b/vendor/squashfs-tools/libsquashfs.dll differ diff --git a/vendor/squashfs-tools/libzstd.dll b/vendor/squashfs-tools/libzstd.dll new file mode 100644 index 00000000..2c928494 Binary files /dev/null and b/vendor/squashfs-tools/libzstd.dll differ diff --git a/vendor/squashfs-tools/rdsquashfs.exe b/vendor/squashfs-tools/rdsquashfs.exe new file mode 100644 index 00000000..241ce118 Binary files /dev/null and b/vendor/squashfs-tools/rdsquashfs.exe differ diff --git a/vendor/squashfs-tools/sqfs2tar.exe b/vendor/squashfs-tools/sqfs2tar.exe new file mode 100644 index 00000000..bdfe752e Binary files /dev/null and b/vendor/squashfs-tools/sqfs2tar.exe differ diff --git a/vendor/squashfs-tools/sqfsdiff.exe b/vendor/squashfs-tools/sqfsdiff.exe new file mode 100644 index 00000000..4197b5af Binary files /dev/null and b/vendor/squashfs-tools/sqfsdiff.exe differ diff --git a/vendor/squashfs-tools/tar2sqfs.exe b/vendor/squashfs-tools/tar2sqfs.exe new file mode 100644 index 00000000..70721c95 Binary files /dev/null and b/vendor/squashfs-tools/tar2sqfs.exe differ diff --git a/vendor/squashfs-tools/zlib1.dll b/vendor/squashfs-tools/zlib1.dll new file mode 100644 index 00000000..2f12f9e5 Binary files /dev/null and b/vendor/squashfs-tools/zlib1.dll differ