Add cross-compiling support

This commit is contained in:
Caelan Sayler
2024-06-04 21:22:04 +01:00
parent ac498494bd
commit bfb469566a
127 changed files with 3881 additions and 3525 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -10,7 +10,7 @@
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<BeforeTargetFrameworkInferenceTargets>$(MSBuildThisFileDirectory)SelfContained.targets</BeforeTargetFrameworkInferenceTargets>
<NoWarn>$(NoWarn);NETSDK1188</NoWarn>
<NoWarn>$(NoWarn);NETSDK1188;NU5100</NoWarn>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<PathMap>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=./</PathMap>

View File

@@ -35,10 +35,7 @@
<Target Name="AddNugetVendorLibs" BeforeTargets="Build" Condition=" '$(VelopackPackageVendorLibs)' == 'true' ">
<ItemGroup>
<None Include="..\..\vendor\rcedit.exe" Pack="true" PackagePath="vendor" />
<None Include="..\..\vendor\zstd.exe" Pack="true" PackagePath="vendor" />
<None Include="..\..\vendor\signtool.exe" Pack="true" PackagePath="vendor" />
<None Include="..\..\vendor\appimagetool-x86_64.AppImage" Pack="true" PackagePath="vendor" />
<None Include="..\..\vendor\**" Pack="true" PackagePath="vendor" />
<None Include="..\..\Velopack.entitlements" Pack="true" PackagePath="vendor" />
<None Include="..\..\artwork\DefaultApp.icns" Pack="true" PackagePath="vendor" />
<None Include="..\..\artwork\DefaultApp.png" Pack="true" PackagePath="vendor" />

View File

@@ -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;

View File

@@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.9.5" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.10.4" />
<PackageReference Include="Riok.Mapperly" Version="3.5.1" />
</ItemGroup>

View File

@@ -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, )"
}
}
}

View File

@@ -56,7 +56,7 @@ public class GitHubRepository : SourceRepository<GitHubDownloadOptions, GithubSo
public async Task UploadMissingAssetsAsync(GitHubUploadOptions options)
{
var (repoOwner, repoName) = GetOwnerAndRepo(options.RepoUrl);
var helper = new ReleaseEntryHelper(options.ReleaseDir.FullName, options.Channel, Log);
var helper = new ReleaseEntryHelper(options.ReleaseDir.FullName, options.Channel, Log, options.TargetOs);
var build = BuildAssets.Read(options.ReleaseDir.FullName, options.Channel);
var latest = helper.GetLatestFullRelease();
var latestPath = Path.Combine(options.ReleaseDir.FullName, latest.FileName);

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.308" />
<PackageReference Include="AWSSDK.S3" Version="3.7.308.8" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.20.0" />
<PackageReference Include="Octokit" Version="11.0.1" />
</ItemGroup>

View File

@@ -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; }
}

View File

@@ -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, )"

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,53 @@
using System;
namespace Ico.Binary
{
public class ByteReader
{
public Memory<byte> Data { get; }
public ByteOrder Endianness { get; }
public int SeekOffset { get; set; }
public ByteReader(Memory<byte> 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;
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
namespace Ico.Binary
{
internal class ByteWriter
{
public List<byte> Data { get; } = new List<byte>();
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;
}
}
}
}

View File

@@ -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<Rgba32>(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);
}
}
}
}

View File

@@ -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<Rgba32, int>();
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<Rgba32, uint>();
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();
}
}
}

View File

@@ -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<Rgba32> image)
{
return IsAnyPixel(image, (x, y, pixel)
=> !IsCompletelyOpaque(pixel) && !IsCompletelyTransparent(pixel));
}
public static bool IsAnyAlphaChannel(Image<Rgba32> image)
{
return IsAnyPixel(image, (x, y, pixel)
=> !IsCompletelyOpaque(pixel));
}
public static ulong GetNumberOfDistinctColors(Image<Rgba32> image, bool includeAlpha)
{
var colors = new HashSet<uint>();
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<Rgba32> 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<Rgba32> 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<Rgba32> 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<Rgba32> image, Action<int, int, Rgba32> 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<Rgba32> image, Func<int, int, Rgba32, bool> 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;
}
}
}

View File

@@ -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,
}
}

View File

@@ -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;
}
}

View File

@@ -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<IcoFrame> 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;
}
}
}

View File

@@ -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<uint>();
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());
}
}
}

View File

@@ -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<Rgba32>(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<byte> 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<Rgba32> 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;
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<Rgba32> CookedData { get; set; }
public bool[,] Mask { get; set; }
public uint TotalDiskUsage { get; set; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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<IcoFrame> 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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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();
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Ico</RootNamespace>
<LangVersion>7.3</LangVersion>
<Version>1.1.1</Version>
<Authors>Jeffrey Tippet</Authors>
<Product>IcoTools</Product>
<Copyright>Copyright 2019, Jeffrey Tippet. All rights reserved.</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8" />
</ItemGroup>
</Project>

View File

@@ -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"
}
}
}
}
}

View File

@@ -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
{
/// <summary>
/// An instance of this exception is thrown when an AppHost binary update
/// fails due to known user errors.
/// </summary>
public class AppHostUpdateException : Exception
{
internal AppHostUpdateException(string message = null)
: base(message)
{
}
}
/// <summary>
/// The application host executable cannot be customized because adding resources requires
/// that the build be performed on Windows (excluding Nano Server).
/// </summary>
public sealed class AppHostCustomizationUnsupportedOSException : AppHostUpdateException
{
internal AppHostCustomizationUnsupportedOSException()
{
}
}
/// <summary>
/// The MachO application host executable cannot be customized because
/// it was not in the expected format
/// </summary>
public sealed class AppHostMachOFormatException : AppHostUpdateException
{
public readonly MachOFormatError Error;
internal AppHostMachOFormatException(MachOFormatError error)
{
Error = error;
}
}
/// <summary>
/// Unable to use the input file as application host executable because it's not a
/// Windows executable for the CUI (Console) subsystem.
/// </summary>
public sealed class AppHostNotCUIException : AppHostUpdateException
{
internal AppHostNotCUIException()
{
}
}
/// <summary>
/// Unable to use the input file as an application host executable
/// because it's not a Windows PE file
/// </summary>
public sealed class AppHostNotPEFileException : AppHostUpdateException
{
internal AppHostNotPEFileException()
{
}
}
/// <summary>
/// Unable to sign the apphost binary.
/// </summary>
public sealed class AppHostSigningException : AppHostUpdateException
{
public readonly int ExitCode;
internal AppHostSigningException(int exitCode, string signingErrorMessage)
: base(signingErrorMessage)
{
}
}
/// <summary>
/// Given app file name is longer than 1024 bytes
/// </summary>
public sealed class AppNameTooLongException : AppHostUpdateException
{
public string LongName { get; }
internal AppNameTooLongException(string name)
{
LongName = name;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents an exception thrown because of a Win32 error
/// </summary>
public class HResultException : Exception
{
public readonly int Win32HResult;
public HResultException(int hResult) : base(hResult.ToString("X4"))
{
Win32HResult = hResult;
}
}
}

View File

@@ -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
{
/// <summary>
/// Embeds the App Name into the AppHost.exe
/// If an apphost is a single-file bundle, updates the location of the bundle headers.
/// </summary>
public static class HostWriter
{
/// <summary>
/// hash value embedded in default apphost executable in a place where the path to the app binary should be stored.
/// </summary>
private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
/// <summary>
/// Create an AppHost with embedded configuration of app binary location
/// </summary>
/// <param name="appHostSourceFilePath">The path of Apphost template, which has the place holder</param>
/// <param name="appHostDestinationFilePath">The destination path for desired location to place, including the file name</param>
/// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param>
/// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param>
/// <param name="assemblyToCopyResorcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param>
/// <param name="enableMacOSCodeSign">Sign the app binary using codesign with an anonymous certificate.</param>
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;
}
}
/// <summary>
/// Set the current AppHost as a single-file bundle.
/// </summary>
/// <param name="appHostPath">The path of Apphost template, which has the place holder</param>
/// <param name="bundleHeaderOffset">The offset to the location of bundle header</param>
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));
}
/// <summary>
/// Check if the an AppHost is a single-file bundle
/// </summary>
/// <param name="appHostFilePath">The path of Apphost to check</param>
/// <returns>True if the AppHost is a single-file bundle, false otherwise</returns>
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);
}
}

View File

@@ -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
{
/// <summary>
/// Additional details about the failure with caused an AppHostMachOFormatException
/// </summary>
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)
}
}

View File

@@ -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);
}
}
/// <summary>
/// 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
///
/// </summary>
/// <param name="stream">Stream containing the AppHost</param>
/// <returns>
/// True if
/// - The input is a MachO binary, and
/// - It is a signed binary, and
/// - The signature was successfully removed
/// False otherwise
/// </returns>
/// <exception cref="AppHostMachOFormatException">
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
/// </exception>
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;
}
/// <summary>
/// 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.
///
/// </summary>
/// <param name="filePath">Path to the AppHost</param>
/// <returns>
/// True if
/// - The input is a MachO binary, and
/// - The additional bytes were successfully accomodated within the MachO segments.
/// False otherwise
/// </returns>
/// <exception cref="AppHostMachOFormatException">
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
/// </exception>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// The first two bytes of a PE file are a constant signature.
/// </summary>
private const ushort PEFileSignature = 0x5A4D;
/// <summary>
/// The offset of the PE header pointer in the DOS header.
/// </summary>
private const int PEHeaderPointerOffset = 0x3C;
/// <summary>
/// The offset of the Subsystem field in the PE header.
/// </summary>
private const int SubsystemOffset = 0x5C;
/// <summary>
/// The value of the sybsystem field which indicates Windows GUI (Graphical UI)
/// </summary>
private const ushort WindowsGUISubsystem = 0x2;
/// <summary>
/// The value of the subsystem field which indicates Windows CUI (Console)
/// </summary>
private const ushort WindowsCUISubsystem = 0x3;
/// <summary>
/// Check whether the apphost file is a windows PE image by looking at the first few bytes.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
/// <returns>true if the accessor represents a PE image, false otherwise.</returns>
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;
}
}
/// <summary>
/// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
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);
}
}
}
/// <summary>
/// This method will return the subsystem CUI/GUI value. The apphost file should be a windows PE file.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
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);
}
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Unable to use input file as a valid application host executable, as it does not contain
/// the expected placeholder byte sequence.
/// </summary>
public class PlaceHolderNotFoundInAppHostException : AppHostUpdateException
{
public byte[] MissingPattern { get; }
public PlaceHolderNotFoundInAppHostException(byte[] pattern)
{
MissingPattern = pattern;
}
}
}

View File

@@ -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
{
/// <summary>
/// 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
/// </summary>
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);
}
}
}
}
}

View File

@@ -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
{
/// <summary>
/// BundleOptions: Optional settings for configuring the type of files
/// included in the single file bundle.
/// </summary>
[Flags]
public enum BundleOptions
{
None = 0,
BundleNativeBinaries = 1,
BundleOtherFiles = 2,
BundleSymbolFiles = 4,
BundleAllContent = BundleNativeBinaries | BundleOtherFiles,
EnableCompression = 8,
};
}

View File

@@ -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
{
/// <summary>
/// Bundler: Functionality to embed the managed app and its dependencies
/// into the host native binary.
/// </summary>
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;
}
}
/// <summary>
/// Embed 'file' into 'bundle'
/// </summary>
/// <returns>
/// startOffset: offset of the start 'file' within 'bundle'
/// compressedSize: size of the compressed data, if entry was compressed, otherwise 0
/// </returns>
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;
}
/// <summary>
/// Generate a bundle, given the specification of embedded files
/// </summary>
/// <param name="fileSpecs">
/// 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.
/// </param>
/// <returns>
/// The full path the the generated bundle file
/// </returns>
/// <exceptions>
/// ArgumentException if input is invalid
/// IOExceptions and ArgumentExceptions from callees flow to the caller.
/// </exceptions>
public string GenerateBundle(IReadOnlyList<FileSpec> 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<string, FileSpec>(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}");
}
}
}
}
}
}

View File

@@ -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
{
/// <summary>
/// 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
/// </summary>
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}";
}
}

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
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]" : "")}";
}
}

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
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
};
}

View File

@@ -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
{
/// <summary>
/// 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]
///
///
///
/// _________________________________________________
/// </summary>
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<FileEntry> Files;
public string BundleVersion => $"{BundleMajorVersion}.{BundleMinorVersion}";
public Manifest(uint bundleMajorVersion, bool netcoreapp3CompatMode = false)
{
BundleMajorVersion = bundleMajorVersion;
Files = new List<FileEntry>();
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<byte>(), 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));
}
}
}

View File

@@ -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
{
/// <summary>
/// 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
/// </summary>
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";
}
}

View File

@@ -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
{
/// <summary>
/// Tracing utilities for diagnostic output
/// </summary>
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}");
}
}
}

View File

@@ -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());
}
}
}
}

View File

@@ -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.

View File

@@ -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;
//}
}
}

View File

@@ -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
{
/// <summary>
/// Provides methods for modifying the embedded native resources
/// in a PE image. It currently only works on Windows, because it
/// requires various kernel32 APIs.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
/// Holds the native handle for the resource update.
/// </summary>
private readonly SafeUpdateHandle hUpdate;
///<summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
public ResourceUpdater(string peFile)
{
hUpdate = Kernel32.BeginUpdateResource(peFile, false);
if (hUpdate.IsInvalid) {
ThrowExceptionForLastWin32Error();
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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();
}
}
}
}

View File

@@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2007;CS8002;IDE0161</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RootNamespace>Microsoft.NET.HostModel</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Reflection.Metadata" Version="8.0.0" Condition="'$(TargetFramework)' == 'net472'" />
</ItemGroup>
</Project>

View File

@@ -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=="
}
}
}
}

View File

@@ -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<string, string>() {
{ "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<string> 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<string> 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);
}
}
}

View File

@@ -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<byte> 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];
}
}

View File

@@ -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";

View File

@@ -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<LinuxPackOptions>
{
protected string PortablePackagePath { get; set; }
@@ -21,36 +19,34 @@ public class LinuxPackCommandRunner : PackageBuilder<LinuxPackOptions>
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);
}

View File

@@ -25,7 +25,7 @@ public class OsxBundleCommandRunner : ICommand<OsxBundleOptions>
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<OsxBundleOptions>
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.");
}

View File

@@ -32,10 +32,10 @@ public class OsxPackCommandRunner : PackageBuilder<OsxPackOptions>
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);
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="ELFSharp" Version="2.17.3" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -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, )"

View File

@@ -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 +

View File

@@ -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<WindowsPackOptions>
{
public WindowsPackCommandRunner(ILogger logger, IFancyConsole console)
@@ -48,7 +46,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
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<WindowsPackOptions>
}
// 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<WindowsPackOptions>
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<WindowsPackOptions>
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.");
}

View File

@@ -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<IcoFrame> _frames = new();
public IcoExtract(ILogger logger)
{
_logger = logger;
}
public List<IcoFrame> ExtractFrames(FileInfo file)
{
var reporter = new IcoILoggerReporter(_logger);
var context = new ParseContext {
DisplayedPath = file.Name,
FullPath = file.FullName,
GeneratedFrames = new List<IcoFrame>(),
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
}

View File

@@ -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<string> args = new List<string>() {
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));
}
}

View File

@@ -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<uint, IconGroupDirectory> { { 1, group } });
}
private void WriteToDirectory(IResourceDirectory rootDirectory, Dictionary<uint, IconGroupDirectory> _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));
}
}

View File

@@ -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;
}
}
}

View File

@@ -4,10 +4,11 @@
<TargetFrameworks>net472;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2007;CS8002</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Velopack.Packaging.HostModel\Velopack.Packaging.HostModel.csproj" />
<ProjectReference Include="..\Velopack.IcoLib\Velopack.IcoLib.csproj" />
<ProjectReference Include="..\Velopack.Packaging\Velopack.Packaging.csproj" />
</ItemGroup>
@@ -15,5 +16,5 @@
<PackageReference Include="AsmResolver.DotNet" Version="5.5.1" />
<PackageReference Include="AsmResolver.PE.Win32Resources" Version="5.5.1" />
</ItemGroup>
</Project>

View File

@@ -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"
}
}
}

View File

@@ -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<string> args, string workingDir, IDictionary<string, string> envVar = null)
{
var result = InvokeProcess(exePath, args, workingDir, CancellationToken.None, envVar);

View File

@@ -19,7 +19,7 @@ public interface IVelopackFlowServiceClient
Task<Profile?> 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<Profile>(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<string> 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);
}

View File

@@ -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;
}

View File

@@ -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:

View File

@@ -14,7 +14,7 @@ namespace Velopack.Packaging;
public abstract class PackageBuilder<T> : ICommand<T>
where T : class, IPackOptions
{
protected RuntimeOs SupportedTargetOs { get; }
protected RuntimeOs TargetOs { get; }
protected ILogger Log { get; }
@@ -36,25 +36,28 @@ public abstract class PackageBuilder<T> : ICommand<T>
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<T> : ICommand<T>
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<T> : ICommand<T>
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) => {

View File

@@ -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<string, List<VelopackAsset>> _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";

View File

@@ -15,9 +15,9 @@
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.61.0" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.61.0" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.61.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.61.2" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.61.2" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.61.2" />
</ItemGroup>
</Project>

View File

@@ -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, )"
}
}
}

View File

@@ -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);
}
}

View File

@@ -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; }

View File

@@ -5,6 +5,8 @@ namespace Velopack.Vpk.Commands;
public class BaseCommand : CliCommand
{
public RuntimeOs TargetOs { get; private set; }
private readonly Dictionary<CliOption, Action<ParseResult, IConfiguration>> _setters = new();
private readonly Dictionary<CliOption, string> _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;
}
}

View File

@@ -11,11 +11,6 @@ public abstract class PlatformCommand : OutputCommand
TargetRuntimeOption = AddOption<string>((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;
}

View File

@@ -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;

View File

@@ -1,5 +0,0 @@
namespace Velopack.Vpk.Logging;
public record DefaultPromptValueFactory(bool DefaultPromptValue)
{
}

View File

@@ -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;

View File

@@ -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<int> 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<Microsoft.Extensions.Logging.ILogger>();
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<WindowsPackCommand, WindowsPackCommandRunner, WindowsPackOptions>(provider);
} else if (VelopackRuntimeInfo.IsOSX) {
rootCommand.AddCommand<OsxBundleCommand, OsxBundleCommandRunner, OsxBundleOptions>(provider);
rootCommand.AddCommand<OsxPackCommand, OsxPackCommandRunner, OsxPackOptions>(provider);
} else if (VelopackRuntimeInfo.IsLinux) {
break;
case RuntimeOs.Linux:
rootCommand.AddCommand<LinuxPackCommand, LinuxPackCommandRunner, LinuxPackOptions>(provider);
} else {
break;
case RuntimeOs.OSX:
if (VelopackRuntimeInfo.IsOSX) {
rootCommand.AddCommand<OsxBundleCommand, OsxBundleCommandRunner, OsxBundleOptions>(provider);
rootCommand.AddCommand<OsxPackCommand, OsxPackCommandRunner, OsxPackOptions>(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<ILoggerFactory>().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<IFancyConsole, BasicConsole>();
@@ -195,12 +240,15 @@ public static class ProgramCommandExtensions
var command = new TCli();
command.SetAction(async (ctx, token) => {
var logger = provider.GetRequiredService<Microsoft.Extensions.Logging.ILogger>();
var console = provider.GetRequiredService<IFancyConsole>();
var config = provider.GetRequiredService<IConfiguration>();
var defaults = provider.GetRequiredService<VelopackDefaults>();
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<TOpt>(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;
}
});

View File

@@ -23,7 +23,7 @@
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.23407.1" />
<PackageReference Include="NuGet.Protocol" Version="6.9.1" />
<PackageReference Include="NuGet.Protocol" Version="6.10.0" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="Riok.Mapperly" Version="3.5.1" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />

View File

@@ -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;
}
}

View File

@@ -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, )"
}
}
}

View File

@@ -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<int> progress, CancellationToken cancelToken)

View File

@@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NuGet.Versioning" Version="6.9.1" />
<PackageReference Include="NuGet.Versioning" Version="6.10.0" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('net4')) ">

View File

@@ -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",

View File

@@ -38,10 +38,10 @@
<ItemGroup Condition=" $(MSBuildProjectName.EndsWith('Tests')) ">
<ProjectReference Include="..\Divergic.Logging.Xunit\Divergic.Logging.Xunit.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" />
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageReference Include="coverlet.msbuild" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>

View File

@@ -15,6 +15,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="GitHubActionsTestLogger" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More