From 39e81120c5ee1b252e6daf30d41efaaf2a8c4897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Mon, 11 May 2026 10:42:17 -0400 Subject: [PATCH 1/8] Package Pinget CLI executables for NuGet Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/cli-packages.yml | 250 ++++++++++++++++++ .github/workflows/release.yml | 160 +++++++++++ .gitignore | 6 + README.md | 25 ++ .../Devolutions.Pinget.Cli.csproj | 4 + .../Devolutions.Pinget.Cli.DotNet.csproj | 34 +++ .../Devolutions.Pinget.Cli.DotNet.targets | 27 ++ .../Devolutions.Pinget.Cli.Rust.csproj | 46 ++++ .../Devolutions.Pinget.Cli.Rust.targets | 72 +++++ rust/crates/pinget-cli/Cargo.toml | 3 + rust/crates/pinget-cli/build.rs | 28 ++ scripts/Build-CliNativeNuGetPackages.ps1 | 227 ++++++++++++++++ 12 files changed, 882 insertions(+) create mode 100644 .github/workflows/cli-packages.yml create mode 100644 nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj create mode 100644 nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.targets create mode 100644 nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj create mode 100644 nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.targets create mode 100644 rust/crates/pinget-cli/build.rs create mode 100644 scripts/Build-CliNativeNuGetPackages.ps1 diff --git a/.github/workflows/cli-packages.yml b/.github/workflows/cli-packages.yml new file mode 100644 index 0000000..8654a7f --- /dev/null +++ b/.github/workflows/cli-packages.yml @@ -0,0 +1,250 @@ +name: CLI NuGet Packages + +on: + push: + tags-ignore: + - 'v*' + pull_request: + workflow_dispatch: + inputs: + dry-run: + description: Build and package only; skip publish/release actions. + required: false + default: true + type: boolean + +permissions: + contents: read + +env: + DOTNET_NOLOGO: true + CARGO_TERM_COLOR: always + +jobs: + rust-cli: + name: Rust CLI (${{ matrix.rid }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - rid: linux-x64 + os: ubuntu-latest + rust-target: x86_64-unknown-linux-gnu + - rid: linux-arm64 + os: ubuntu-latest + rust-target: aarch64-unknown-linux-gnu + - rid: win-x64 + os: windows-latest + rust-target: x86_64-pc-windows-msvc + - rid: win-arm64 + os: windows-latest + rust-target: aarch64-pc-windows-msvc + - rid: macos-x64 + os: macos-14 + rust-target: x86_64-apple-darwin + - rid: macos-arm64 + os: macos-14 + rust-target: aarch64-apple-darwin + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Dry-run mode + if: github.event_name == 'workflow_dispatch' + shell: pwsh + run: | + Write-Host "::notice::DryRun=${{ inputs['dry-run'] }}" + + - name: Install Rust toolchain + shell: pwsh + run: | + rustup toolchain install stable --profile minimal --target "${{ matrix.rust-target }}" + rustup default stable + + - name: Install Linux ARM64 linker + if: matrix.rid == 'linux-arm64' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Configure static MSVC runtime + if: contains(matrix.rust-target, 'windows-msvc') + shell: pwsh + run: | + "RUSTFLAGS=-C target-feature=+crt-static" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Build and stage Rust CLI + shell: pwsh + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + run: | + pwsh -NoLogo -NoProfile -File (Resolve-Path 'scripts\Build-CliNativeNuGetPackages.ps1') ` + -Package Rust ` + -RustRuntimeIdentifiers '${{ matrix.rid }}' ` + -NoPack ` + -Clean + + - name: Upload Rust CLI artifact + uses: actions/upload-artifact@v7 + with: + name: rust-cli-${{ matrix.rid }} + path: artifacts/cli/rust/${{ matrix.rid }}/* + if-no-files-found: error + + dotnet-cli: + name: .NET NativeAOT CLI (${{ matrix.rid }}) + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + rid: + - win-x64 + - win-arm64 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Build and stage .NET NativeAOT CLI + shell: pwsh + run: | + pwsh -NoLogo -NoProfile -File (Resolve-Path 'scripts\Build-CliNativeNuGetPackages.ps1') ` + -Package DotNet ` + -DotNetRuntimeIdentifiers '${{ matrix.rid }}' ` + -NoPack ` + -Clean + + - name: Upload .NET CLI artifact + uses: actions/upload-artifact@v7 + with: + name: dotnet-cli-${{ matrix.rid }} + path: artifacts/cli/dotnet-nativeaot/${{ matrix.rid }}/* + if-no-files-found: error + + package: + name: Pack and smoke test + runs-on: ubuntu-latest + needs: + - rust-cli + - dotnet-cli + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Download CLI artifacts + uses: actions/download-artifact@v8 + with: + path: artifacts/download + + - name: Arrange staged artifacts + shell: pwsh + run: | + $Mappings = @{ + 'rust-cli-linux-x64' = 'artifacts/cli/rust/linux-x64' + 'rust-cli-linux-arm64' = 'artifacts/cli/rust/linux-arm64' + 'rust-cli-win-x64' = 'artifacts/cli/rust/win-x64' + 'rust-cli-win-arm64' = 'artifacts/cli/rust/win-arm64' + 'rust-cli-macos-x64' = 'artifacts/cli/rust/osx-x64' + 'rust-cli-macos-arm64' = 'artifacts/cli/rust/osx-arm64' + 'dotnet-cli-win-x64' = 'artifacts/cli/dotnet-nativeaot/win-x64' + 'dotnet-cli-win-arm64' = 'artifacts/cli/dotnet-nativeaot/win-arm64' + } + + foreach ($mapping in $Mappings.GetEnumerator()) { + $source = Join-Path 'artifacts/download' $mapping.Key + if (-not (Test-Path -Path $source)) { + throw "Downloaded artifact directory not found: $source" + } + + New-Item -Path $mapping.Value -ItemType Directory -Force | Out-Null + Copy-Item -Path (Join-Path $source '*') -Destination $mapping.Value -Recurse -Force + } + + - name: Pack CLI NuGet packages + shell: pwsh + run: | + pwsh -NoLogo -NoProfile -File (Resolve-Path 'scripts\Build-CliNativeNuGetPackages.ps1') -NoBuild -Clean:$false + + - name: Smoke test PackageReference copy targets + shell: pwsh + run: | + $packageSource = (Resolve-Path 'artifacts/cli-nuget').Path + $smokeRoot = Join-Path $env:RUNNER_TEMP 'pinget-cli-package-smoke' + $env:NUGET_PACKAGES = Join-Path $env:RUNNER_TEMP 'pinget-cli-package-smoke-nuget-cache' + New-Item -Path $smokeRoot -ItemType Directory -Force | Out-Null + New-Item -Path $env:NUGET_PACKAGES -ItemType Directory -Force | Out-Null + $rustPackage = Get-ChildItem -Path $packageSource -Filter 'Devolutions.Pinget.Cli.Rust.*.nupkg' | Select-Object -First 1 + $dotNetPackage = Get-ChildItem -Path $packageSource -Filter 'Devolutions.Pinget.Cli.DotNet.*.nupkg' | Select-Object -First 1 + if (($null -eq $rustPackage) -or ($null -eq $dotNetPackage)) { + throw 'Smoke test packages were not found.' + } + $rustVersion = $rustPackage.BaseName.Substring('Devolutions.Pinget.Cli.Rust.'.Length) + $dotNetVersion = $dotNetPackage.BaseName.Substring('Devolutions.Pinget.Cli.DotNet.'.Length) + + dotnet new console --framework net10.0 --output $smokeRoot + if ($LASTEXITCODE -ne 0) { throw 'Smoke project creation failed.' } + + $projectPath = Join-Path $smokeRoot 'pinget-cli-package-smoke.csproj' + $project = Get-Content -Path $projectPath -Raw + $project = $project -replace '', @" + + pinget-rust.exe + pinget-dotnet.exe + + + + + + +"@ + Set-Content -Path $projectPath -Value $project -Encoding utf8 + + $nugetConfig = Join-Path $smokeRoot 'NuGet.Config' + @" + + + + + + + + +"@ | Set-Content -Path $nugetConfig -Encoding utf8 + + dotnet build $projectPath --configfile $nugetConfig + if ($LASTEXITCODE -ne 0) { throw 'Smoke project build failed.' } + + $expectedFiles = @( + 'bin/Debug/net10.0/runtimes/win-x64/native/pinget.exe', + 'bin/Debug/net10.0/runtimes/win-x64/native/pinget-rust.exe', + 'bin/Debug/net10.0/runtimes/win-x64/native/pinget-dotnet.exe', + 'bin/Debug/net10.0/runtimes/linux-x64/native/pinget' + ) + + foreach ($relativePath in $expectedFiles) { + $path = Join-Path $smokeRoot $relativePath + if (-not (Test-Path -Path $path -PathType Leaf)) { + throw "Expected smoke output file was not found: $path" + } + } + + - name: Upload CLI NuGet packages + uses: actions/upload-artifact@v7 + with: + name: cli-native-nuget + path: artifacts/cli-nuget/*.nupkg + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5bcef4..d12d9df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -503,6 +503,157 @@ jobs: path: dist/Devolutions.Pinget.Client.*.zip if-no-files-found: error + cli-native-packages: + name: CLI Native NuGet Packages + runs-on: windows-latest + needs: + - preflight + - rust-artifacts + environment: ${{ needs.preflight.outputs.package-env }} + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Download Rust release artifacts + uses: actions/download-artifact@v8 + with: + pattern: pinget-*.zip + path: artifacts/release-rust + + - name: Stage Rust CLI artifacts + shell: pwsh + run: | + $Artifacts = @( + @{ Archive = 'pinget-windows-x64.zip'; Rid = 'win-x64'; SourceBinary = 'pinget.exe'; PackageBinary = 'pinget.exe' }, + @{ Archive = 'pinget-windows-arm64.zip'; Rid = 'win-arm64'; SourceBinary = 'pinget.exe'; PackageBinary = 'pinget.exe' }, + @{ Archive = 'pinget-linux-x64.zip'; Rid = 'linux-x64'; SourceBinary = 'pinget'; PackageBinary = 'pinget' }, + @{ Archive = 'pinget-linux-arm64.zip'; Rid = 'linux-arm64'; SourceBinary = 'pinget'; PackageBinary = 'pinget' }, + @{ Archive = 'pinget-macos-x64.zip'; Rid = 'osx-x64'; SourceBinary = 'pinget'; PackageBinary = 'pinget' }, + @{ Archive = 'pinget-macos-arm64.zip'; Rid = 'osx-arm64'; SourceBinary = 'pinget'; PackageBinary = 'pinget' } + ) + + foreach ($Artifact in $Artifacts) { + $archive = Get-ChildItem -Path artifacts/release-rust -Recurse -File -Filter $Artifact.Archive | Select-Object -First 1 + if ($null -eq $archive) { + throw "Rust release archive not found: $($Artifact.Archive)" + } + + $extractRoot = Join-Path $env:RUNNER_TEMP "pinget-$($Artifact.Rid)" + Remove-Item -Path $extractRoot -Recurse -Force -ErrorAction SilentlyContinue + New-Item -Path $extractRoot -ItemType Directory -Force | Out-Null + Expand-Archive -Path $archive.FullName -DestinationPath $extractRoot -Force + + $binary = Join-Path $extractRoot $Artifact.SourceBinary + if (-not (Test-Path -Path $binary -PathType Leaf)) { + throw "Rust release binary not found after extracting $($Artifact.Archive): $binary" + } + + $stageDir = Join-Path 'artifacts/cli/rust' $Artifact.Rid + New-Item -Path $stageDir -ItemType Directory -Force | Out-Null + Copy-Item -Path $binary -Destination (Join-Path $stageDir $Artifact.PackageBinary) -Force + } + + - name: Build .NET NativeAOT CLI artifacts + shell: pwsh + env: + PACKAGE_VERSION: ${{ needs.preflight.outputs.version }} + run: | + pwsh -NoLogo -NoProfile -File (Resolve-Path 'scripts\Build-CliNativeNuGetPackages.ps1') ` + -Package DotNet ` + -Version $env:PACKAGE_VERSION ` + -NoPack ` + -Clean + + - name: Install code signing tools + shell: pwsh + run: | + dotnet tool install --global AzureSignTool + + $TestCertsUrl = "https://raw.githubusercontent.com/Devolutions/devolutions-authenticode/master/data/certs" + Invoke-WebRequest -Uri "$TestCertsUrl/authenticode-test-ca.crt" -OutFile ".\authenticode-test-ca.crt" + Import-Certificate -FilePath ".\authenticode-test-ca.crt" -CertStoreLocation "cert:\LocalMachine\Root" + Remove-Item ".\authenticode-test-ca.crt" -ErrorAction SilentlyContinue | Out-Null + + - name: Code sign .NET NativeAOT CLI artifacts + shell: pwsh + env: + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + CODE_SIGNING_CERTIFICATE_NAME: ${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }} + CODE_SIGNING_CLIENT_ID: ${{ secrets.CODE_SIGNING_CLIENT_ID }} + CODE_SIGNING_CLIENT_SECRET: ${{ secrets.CODE_SIGNING_CLIENT_SECRET }} + CODE_SIGNING_KEYVAULT_URL: ${{ secrets.CODE_SIGNING_KEYVAULT_URL }} + CODE_SIGNING_TIMESTAMP_SERVER: ${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }} + run: | + $PackageEnv = '${{ needs.preflight.outputs.package-env }}' + $DryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry-run }}') + $RequiredVariables = @( + 'AZURE_TENANT_ID', + 'CODE_SIGNING_CERTIFICATE_NAME', + 'CODE_SIGNING_CLIENT_ID', + 'CODE_SIGNING_CLIENT_SECRET', + 'CODE_SIGNING_KEYVAULT_URL', + 'CODE_SIGNING_TIMESTAMP_SERVER' + ) + $MissingVariables = @($RequiredVariables | Where-Object { + [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) + }) + + if ($MissingVariables.Count -gt 0) { + $Message = "Code signing configuration missing: $($MissingVariables -join ', ')" + if ($PackageEnv -eq 'publish-prod' -and -not $DryRun) { + throw $Message + } + + Write-Host "$Message; skipping code signing." + return + } + + $SignParams = @( + 'sign', + '-kvt', $env:AZURE_TENANT_ID, + '-kvu', $env:CODE_SIGNING_KEYVAULT_URL, + '-kvi', $env:CODE_SIGNING_CLIENT_ID, + '-kvs', $env:CODE_SIGNING_CLIENT_SECRET, + '-kvc', $env:CODE_SIGNING_CERTIFICATE_NAME, + '-tr', $env:CODE_SIGNING_TIMESTAMP_SERVER, + '-v' + ) + + $Binaries = @(Get-ChildItem -Path 'artifacts/cli/dotnet-nativeaot' -Recurse -File -Include '*.exe') + if ($Binaries.Count -eq 0) { + throw 'No .NET NativeAOT CLI executables were found for code signing.' + } + + foreach ($Binary in $Binaries) { + AzureSignTool @SignParams $Binary.FullName + if ($LASTEXITCODE -ne 0) { + throw "Code signing failed for $($Binary.FullName)." + } + } + + - name: Pack CLI NuGet packages + shell: pwsh + env: + PACKAGE_VERSION: ${{ needs.preflight.outputs.version }} + run: | + pwsh -NoLogo -NoProfile -File (Resolve-Path 'scripts\Build-CliNativeNuGetPackages.ps1') ` + -Version $env:PACKAGE_VERSION ` + -NoBuild ` + -Clean:$false + + - name: Upload CLI NuGet package artifacts + uses: actions/upload-artifact@v7 + with: + name: cli-native-nuget + path: artifacts/cli-nuget/*.nupkg + if-no-files-found: error + create-tag: name: Create Tag runs-on: ubuntu-latest @@ -510,6 +661,7 @@ jobs: - preflight - rust-artifacts - csharp-packages + - cli-native-packages if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }} steps: @@ -542,6 +694,7 @@ jobs: - create-tag - rust-artifacts - csharp-packages + - cli-native-packages if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }} steps: @@ -567,6 +720,7 @@ jobs: needs: - preflight - csharp-packages + - cli-native-packages - create-tag - github-release if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false && fromJSON(needs.preflight.outputs.publish-nuget) == true }} @@ -589,6 +743,12 @@ jobs: name: csharp-nuget path: dist/nuget + - name: Download CLI package artifacts + uses: actions/download-artifact@v8 + with: + name: cli-native-nuget + path: dist/nuget + - name: Push packages to NuGet.org shell: pwsh run: | diff --git a/.gitignore b/.gitignore index 22778eb..92295da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ # Optional local reference clone of the upstream winget-cli repository. winget-cli/ + +# Local build outputs. +artifacts/ +dist/ +bin/ +obj/ diff --git a/README.md b/README.md index 0e7a77e..be29698 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,31 @@ For embedded `show` and exact package resolution, Core exposes structured diagno `Repository.ShowManifest(query)` and `ShowResult.ToSerializableManifest()` return the same serializable manifest model used by the C# CLI `show --output json|yaml` path, including all installers and the Core-selected installer. Use `PingetJsonContext` for reflection-free `System.Text.Json` serialization in hosts that set `JsonSerializer.IsReflectionEnabledByDefault` to `false`. +### CLI executable packages + +Pinget also publishes build-output packages for .NET projects that need a prebuilt `pinget` executable alongside their own application: + +| Package | Executable implementation | Notes | +| --- | --- | --- | +| `Devolutions.Pinget.Cli.Rust` | Rust CLI | Cross-platform native executable assets named `pinget[.exe]` under `runtimes\\native` | +| `Devolutions.Pinget.Cli.DotNet` | C# CLI | Trimmed NativeAOT executable assets named `pinget.exe`, starting with Windows x64 and arm64 | + +These packages are intended for `PackageReference` consumption and copy the executable assets into the consuming project's output/publish output through MSBuild targets. They are not the future install-facing `dotnet tool` package; reserve `Devolutions.Pinget.Tool` for that scenario. + +```powershell +dotnet add package Devolutions.Pinget.Cli.Rust +dotnet add package Devolutions.Pinget.Cli.DotNet +``` + +Both packages use `pinget[.exe]` inside the NuGet package. If a project intentionally references both implementations, set package-specific output names to add distinguishable copies in the build output: + +```xml + + pinget.exe + pinget-managed.exe + +``` + ## Non-goals Pinget intentionally excludes: diff --git a/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj b/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj index deb666b..3f5b0b3 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj +++ b/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj @@ -15,6 +15,10 @@ enable pinget Devolutions.Pinget.Cli + Pinget CLI + Pinget + Pinget CLI + Copyright 2021-2026 Devolutions Inc. diff --git a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj new file mode 100644 index 0000000..74ec5b9 --- /dev/null +++ b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj @@ -0,0 +1,34 @@ + + + + 1.0.0 + Devolutions Inc. + Devolutions + Devolutions.Pinget.Cli.DotNet + pinget;winget;package-manager;cli;dotnet;nativeaot + Prebuilt trimmed NativeAOT .NET Pinget CLI executable for use from .NET projects. + netstandard2.0 + true + false + false + true + false + true + MIT + https://github.com/Devolutions/pinget + https://github.com/Devolutions/pinget.git + git + false + + + + + runtimes\win-x64\native\ + + + runtimes\win-arm64\native\ + + + + + diff --git a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.targets b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.targets new file mode 100644 index 0000000..8be9b37 --- /dev/null +++ b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.targets @@ -0,0 +1,27 @@ + + + pinget.exe + + + + + runtimes\win-x64\native\$(DevolutionsPingetDotNetCliWindowsOutputName) + PreserveNewest + PreserveNewest + Included + false + false + + + + + + runtimes\win-arm64\native\$(DevolutionsPingetDotNetCliWindowsOutputName) + PreserveNewest + PreserveNewest + Included + false + false + + + diff --git a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj new file mode 100644 index 0000000..da68933 --- /dev/null +++ b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj @@ -0,0 +1,46 @@ + + + + 1.0.0 + Devolutions Inc. + Devolutions + Devolutions.Pinget.Cli.Rust + pinget;winget;package-manager;cli;rust;native + Prebuilt Rust Pinget CLI executable for use from .NET projects. + netstandard2.0 + true + false + false + true + false + true + MIT + https://github.com/Devolutions/pinget + https://github.com/Devolutions/pinget.git + git + false + + + + + runtimes\win-x64\native\ + + + runtimes\win-arm64\native\ + + + runtimes\linux-x64\native\ + + + runtimes\linux-arm64\native\ + + + runtimes\osx-x64\native\ + + + runtimes\osx-arm64\native\ + + + + + diff --git a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.targets b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.targets new file mode 100644 index 0000000..53334e0 --- /dev/null +++ b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.targets @@ -0,0 +1,72 @@ + + + pinget.exe + pinget + + + + + runtimes\win-x64\native\$(DevolutionsPingetRustCliWindowsOutputName) + PreserveNewest + PreserveNewest + Included + false + false + + + + + + runtimes\win-arm64\native\$(DevolutionsPingetRustCliWindowsOutputName) + PreserveNewest + PreserveNewest + Included + false + false + + + + + + runtimes\linux-x64\native\$(DevolutionsPingetRustCliUnixOutputName) + PreserveNewest + PreserveNewest + Included + false + false + + + + + + runtimes\linux-arm64\native\$(DevolutionsPingetRustCliUnixOutputName) + PreserveNewest + PreserveNewest + Included + false + false + + + + + + runtimes\osx-x64\native\$(DevolutionsPingetRustCliUnixOutputName) + PreserveNewest + PreserveNewest + Included + false + false + + + + + + runtimes\osx-arm64\native\$(DevolutionsPingetRustCliUnixOutputName) + PreserveNewest + PreserveNewest + Included + false + false + + + diff --git a/rust/crates/pinget-cli/Cargo.toml b/rust/crates/pinget-cli/Cargo.toml index 362b032..d875d84 100644 --- a/rust/crates/pinget-cli/Cargo.toml +++ b/rust/crates/pinget-cli/Cargo.toml @@ -27,3 +27,6 @@ sha2 = "0.11.0" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console"] } winreg = "0.55.0" + +[target.'cfg(windows)'.build-dependencies] +winresource = "0.1.30" diff --git a/rust/crates/pinget-cli/build.rs b/rust/crates/pinget-cli/build.rs new file mode 100644 index 0000000..949f068 --- /dev/null +++ b/rust/crates/pinget-cli/build.rs @@ -0,0 +1,28 @@ +#[cfg(windows)] +fn main() { + let version = env!("CARGO_PKG_VERSION"); + let version_string = format!("{version}.0"); + let version_info = version + .split('.') + .chain(std::iter::repeat("0")) + .take(4) + .map(|part| part.parse::().expect("package version segment must fit in u16")) + .fold(0u64, |acc, part| (acc << 16) | u64::from(part)); + + let mut resource = winresource::WindowsResource::new(); + resource + .set("CompanyName", "Devolutions Inc.") + .set("FileDescription", "Pinget CLI") + .set("FileVersion", &version_string) + .set("InternalName", "pinget") + .set("LegalCopyright", "Copyright 2021-2026 Devolutions Inc.") + .set("OriginalFilename", "pinget.exe") + .set("ProductName", "Pinget") + .set("ProductVersion", &version_string) + .set_version_info(winresource::VersionInfo::FILEVERSION, version_info) + .set_version_info(winresource::VersionInfo::PRODUCTVERSION, version_info); + resource.compile().expect("failed to compile Windows version resource"); +} + +#[cfg(not(windows))] +fn main() {} diff --git a/scripts/Build-CliNativeNuGetPackages.ps1 b/scripts/Build-CliNativeNuGetPackages.ps1 new file mode 100644 index 0000000..e846c24 --- /dev/null +++ b/scripts/Build-CliNativeNuGetPackages.ps1 @@ -0,0 +1,227 @@ +param( + [ValidateSet('All', 'Rust', 'DotNet')] + [string]$Package = 'All', + + [string]$Version, + + [string]$Configuration = 'Release', + + [string]$StagingRoot, + + [string]$OutputRoot, + + [string[]]$RustRuntimeIdentifiers = @('win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64'), + + [string[]]$DotNetRuntimeIdentifiers = @('win-x64', 'win-arm64'), + + [switch]$NoBuild, + + [switch]$NoPack, + + [switch]$Clean +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +if ([string]::IsNullOrWhiteSpace($StagingRoot)) { + $StagingRoot = Join-Path $repoRoot 'artifacts\cli' +} +elseif (-not [System.IO.Path]::IsPathRooted($StagingRoot)) { + $StagingRoot = Join-Path $repoRoot $StagingRoot +} + +if ([string]::IsNullOrWhiteSpace($OutputRoot)) { + $OutputRoot = Join-Path $repoRoot 'artifacts\cli-nuget' +} +elseif (-not [System.IO.Path]::IsPathRooted($OutputRoot)) { + $OutputRoot = Join-Path $repoRoot $OutputRoot +} + +function Invoke-NativeCommand { + param( + [Parameter(Mandatory)] + [string]$FilePath, + + [string[]]$ArgumentList + ) + + Write-Host ">> $FilePath $($ArgumentList -join ' ')" + & $FilePath @ArgumentList + if ($LASTEXITCODE -ne 0) { + throw "Command failed with exit code $LASTEXITCODE`: $FilePath $($ArgumentList -join ' ')" + } +} + +function Get-SourceVersion { + $rustCliManifest = Join-Path $repoRoot 'rust\crates\pinget-cli\Cargo.toml' + $dotNetCliProgram = Join-Path $repoRoot 'dotnet\src\Devolutions.Pinget.Cli\Program.cs' + + $rustMatch = Select-String -Path $rustCliManifest -Pattern '^version = "([^"]+)"$' | Select-Object -First 1 + $dotNetMatch = Select-String -Path $dotNetCliProgram -Pattern '^const string Version = "([^"]+)";$' | Select-Object -First 1 + + if (($null -eq $rustMatch) -or ($null -eq $dotNetMatch)) { + throw 'Unable to detect CLI package version from source files.' + } + + $rustVersion = $rustMatch.Matches[0].Groups[1].Value + $dotNetVersion = $dotNetMatch.Matches[0].Groups[1].Value + if ($rustVersion -ne $dotNetVersion) { + throw "CLI version mismatch detected: rust=$rustVersion dotnet=$dotNetVersion" + } + + $rustVersion +} + +function Resolve-RustTarget { + param([Parameter(Mandatory)][string]$RuntimeIdentifier) + + switch ($RuntimeIdentifier) { + 'win-x64' { @{ CargoTarget = 'x86_64-pc-windows-msvc'; SourceBinaryName = 'pinget.exe'; PackageBinaryName = 'pinget.exe' } } + 'win-arm64' { @{ CargoTarget = 'aarch64-pc-windows-msvc'; SourceBinaryName = 'pinget.exe'; PackageBinaryName = 'pinget.exe' } } + 'linux-x64' { @{ CargoTarget = 'x86_64-unknown-linux-gnu'; SourceBinaryName = 'pinget'; PackageBinaryName = 'pinget' } } + 'linux-arm64' { @{ CargoTarget = 'aarch64-unknown-linux-gnu'; SourceBinaryName = 'pinget'; PackageBinaryName = 'pinget' } } + 'osx-x64' { @{ CargoTarget = 'x86_64-apple-darwin'; SourceBinaryName = 'pinget'; PackageBinaryName = 'pinget' } } + 'osx-arm64' { @{ CargoTarget = 'aarch64-apple-darwin'; SourceBinaryName = 'pinget'; PackageBinaryName = 'pinget' } } + default { throw "Unsupported Rust runtime identifier: $RuntimeIdentifier" } + } +} + +function Get-DotNetBinaryNames { + param([Parameter(Mandatory)][string]$RuntimeIdentifier) + + switch ($RuntimeIdentifier) { + 'win-x64' { @{ SourceBinaryName = 'pinget.exe'; PackageBinaryName = 'pinget.exe' } } + 'win-arm64' { @{ SourceBinaryName = 'pinget.exe'; PackageBinaryName = 'pinget.exe' } } + default { throw "Unsupported .NET NativeAOT runtime identifier: $RuntimeIdentifier" } + } +} + +function Assert-FileExists { + param([Parameter(Mandatory)][string]$Path) + + if (-not (Test-Path -Path $Path -PathType Leaf)) { + throw "Expected file was not found: $Path" + } +} + +function Set-NupkgUnixExecutablePermissions { + param([Parameter(Mandatory)][string]$PackagePath) + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + $archive = [System.IO.Compression.ZipFile]::Open($PackagePath, [System.IO.Compression.ZipArchiveMode]::Update) + try { + foreach ($entry in $archive.Entries) { + if ($entry.FullName -match '^runtimes/(linux|osx)-[^/]+/native/pinget$') { + $entry.ExternalAttributes = -2115174400 # 0o100755 << 16 as a signed Int32. + } + } + } + finally { + $archive.Dispose() + } +} + +if ([string]::IsNullOrWhiteSpace($Version)) { + $Version = Get-SourceVersion +} + +if ([string]::IsNullOrWhiteSpace($Version)) { + throw 'Package version is empty.' +} + +$buildRust = $Package -in @('All', 'Rust') +$buildDotNet = $Package -in @('All', 'DotNet') + +if ($Clean) { + if ($buildRust) { + Remove-Item -Path (Join-Path $StagingRoot 'rust') -Recurse -Force -ErrorAction SilentlyContinue + } + if ($buildDotNet) { + Remove-Item -Path (Join-Path $StagingRoot 'dotnet-nativeaot') -Recurse -Force -ErrorAction SilentlyContinue + } + Remove-Item -Path $OutputRoot -Recurse -Force -ErrorAction SilentlyContinue +} + +New-Item -Path $StagingRoot -ItemType Directory -Force | Out-Null +New-Item -Path $OutputRoot -ItemType Directory -Force | Out-Null + +if ($buildRust) { + $cargoManifest = Join-Path $repoRoot 'rust\Cargo.toml' + + foreach ($rid in $RustRuntimeIdentifiers) { + $target = Resolve-RustTarget -RuntimeIdentifier $rid + $stageDir = Join-Path $StagingRoot "rust\$rid" + New-Item -Path $stageDir -ItemType Directory -Force | Out-Null + + if (-not $NoBuild) { + Invoke-NativeCommand -FilePath rustup -ArgumentList @('target', 'add', $target['CargoTarget']) + Invoke-NativeCommand -FilePath cargo -ArgumentList @('build', '--release', '--package', 'pinget-cli', '--bin', 'pinget', '--manifest-path', $cargoManifest, '--target', $target['CargoTarget']) + + $builtBinary = Join-Path $repoRoot "rust\target\$($target['CargoTarget'])\release\$($target['SourceBinaryName'])" + Assert-FileExists -Path $builtBinary + Copy-Item -Path $builtBinary -Destination (Join-Path $stageDir $target['PackageBinaryName']) -Force + } + + Assert-FileExists -Path (Join-Path $stageDir $target['PackageBinaryName']) + } +} + +if ($buildDotNet) { + $dotNetProject = Join-Path $repoRoot 'dotnet\src\Devolutions.Pinget.Cli\Devolutions.Pinget.Cli.csproj' + + foreach ($rid in $DotNetRuntimeIdentifiers) { + $binaryNames = Get-DotNetBinaryNames -RuntimeIdentifier $rid + $stageDir = Join-Path $StagingRoot "dotnet-nativeaot\$rid" + New-Item -Path $stageDir -ItemType Directory -Force | Out-Null + + if (-not $NoBuild) { + Invoke-NativeCommand -FilePath dotnet -ArgumentList @( + 'publish', + $dotNetProject, + '-c', + $Configuration, + '-r', + $rid, + '--self-contained', + 'true', + '-o', + $stageDir, + '/p:PublishAot=true', + '/p:PublishTrimmed=true', + "/p:Version=$Version", + "/p:AssemblyVersion=$Version", + "/p:FileVersion=$Version", + "/p:InformationalVersion=$Version" + ) + + $sourceBinary = Join-Path $stageDir $binaryNames['SourceBinaryName'] + $packageBinary = Join-Path $stageDir $binaryNames['PackageBinaryName'] + Assert-FileExists -Path $sourceBinary + if ($sourceBinary -ne $packageBinary) { + Move-Item -Path $sourceBinary -Destination $packageBinary -Force + } + } + + Assert-FileExists -Path (Join-Path $stageDir $binaryNames['PackageBinaryName']) + } +} + +if (-not $NoPack) { + if ($buildRust) { + $rustPackageProject = Join-Path $repoRoot 'nuget\Devolutions.Pinget.Cli.Rust\Devolutions.Pinget.Cli.Rust.csproj' + Invoke-NativeCommand -FilePath dotnet -ArgumentList @('pack', $rustPackageProject, '-c', $Configuration, '-o', $OutputRoot, "/p:Version=$Version", '/p:ContinuousIntegrationBuild=true') + + $rustPackage = Join-Path $OutputRoot "Devolutions.Pinget.Cli.Rust.$Version.nupkg" + Assert-FileExists -Path $rustPackage + Set-NupkgUnixExecutablePermissions -PackagePath $rustPackage + } + + if ($buildDotNet) { + $dotNetPackageProject = Join-Path $repoRoot 'nuget\Devolutions.Pinget.Cli.DotNet\Devolutions.Pinget.Cli.DotNet.csproj' + Invoke-NativeCommand -FilePath dotnet -ArgumentList @('pack', $dotNetPackageProject, '-c', $Configuration, '-o', $OutputRoot, "/p:Version=$Version", '/p:ContinuousIntegrationBuild=true') + + Assert-FileExists -Path (Join-Path $OutputRoot "Devolutions.Pinget.Cli.DotNet.$Version.nupkg") + } +} From 66b239a82cb7c904f46f18cd27fd267da132488d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Mon, 11 May 2026 10:45:19 -0400 Subject: [PATCH 2/8] Fix CLI package workflow dispatch expression Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/cli-packages.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/cli-packages.yml b/.github/workflows/cli-packages.yml index 8654a7f..895174b 100644 --- a/.github/workflows/cli-packages.yml +++ b/.github/workflows/cli-packages.yml @@ -51,12 +51,6 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Dry-run mode - if: github.event_name == 'workflow_dispatch' - shell: pwsh - run: | - Write-Host "::notice::DryRun=${{ inputs['dry-run'] }}" - - name: Install Rust toolchain shell: pwsh run: | From 5fc9c13c9bceef2d2afb452170289264b93d882e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Mon, 11 May 2026 10:47:03 -0400 Subject: [PATCH 3/8] Fix CLI package workflow YAML Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/cli-packages.yml | 38 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cli-packages.yml b/.github/workflows/cli-packages.yml index 895174b..d6d0c88 100644 --- a/.github/workflows/cli-packages.yml +++ b/.github/workflows/cli-packages.yml @@ -195,29 +195,29 @@ jobs: $projectPath = Join-Path $smokeRoot 'pinget-cli-package-smoke.csproj' $project = Get-Content -Path $projectPath -Raw $project = $project -replace '', @" - - pinget-rust.exe - pinget-dotnet.exe - - - - - - -"@ + + pinget-rust.exe + pinget-dotnet.exe + + + + + + + "@ Set-Content -Path $projectPath -Value $project -Encoding utf8 $nugetConfig = Join-Path $smokeRoot 'NuGet.Config' @" - - - - - - - - -"@ | Set-Content -Path $nugetConfig -Encoding utf8 + + + + + + + + + "@ | Set-Content -Path $nugetConfig -Encoding utf8 dotnet build $projectPath --configfile $nugetConfig if ($LASTEXITCODE -ne 0) { throw 'Smoke project build failed.' } From c3aa13018621a47e8d1db66c8d200b85ca1367b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Mon, 11 May 2026 11:00:48 -0400 Subject: [PATCH 4/8] Fix macOS package runtime IDs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/cli-packages.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cli-packages.yml b/.github/workflows/cli-packages.yml index d6d0c88..a15b1f8 100644 --- a/.github/workflows/cli-packages.yml +++ b/.github/workflows/cli-packages.yml @@ -40,10 +40,10 @@ jobs: - rid: win-arm64 os: windows-latest rust-target: aarch64-pc-windows-msvc - - rid: macos-x64 + - rid: osx-x64 os: macos-14 rust-target: x86_64-apple-darwin - - rid: macos-arm64 + - rid: osx-arm64 os: macos-14 rust-target: aarch64-apple-darwin @@ -152,8 +152,8 @@ jobs: 'rust-cli-linux-arm64' = 'artifacts/cli/rust/linux-arm64' 'rust-cli-win-x64' = 'artifacts/cli/rust/win-x64' 'rust-cli-win-arm64' = 'artifacts/cli/rust/win-arm64' - 'rust-cli-macos-x64' = 'artifacts/cli/rust/osx-x64' - 'rust-cli-macos-arm64' = 'artifacts/cli/rust/osx-arm64' + 'rust-cli-osx-x64' = 'artifacts/cli/rust/osx-x64' + 'rust-cli-osx-arm64' = 'artifacts/cli/rust/osx-arm64' 'dotnet-cli-win-x64' = 'artifacts/cli/dotnet-nativeaot/win-x64' 'dotnet-cli-win-arm64' = 'artifacts/cli/dotnet-nativeaot/win-arm64' } From 505c13a65909b81b4a64133e99b0fafce0040444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Mon, 11 May 2026 11:25:10 -0400 Subject: [PATCH 5/8] Fix native CLI package contents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/cli-packages.yml | 1 + README.md | 2 +- .../Devolutions.Pinget.Cli.DotNet.csproj | 10 ++++++++-- .../Devolutions.Pinget.Cli.DotNet.targets | 16 ++++++++++++++++ .../Devolutions.Pinget.Cli.Rust.csproj | 12 ++++++------ 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cli-packages.yml b/.github/workflows/cli-packages.yml index a15b1f8..a214824 100644 --- a/.github/workflows/cli-packages.yml +++ b/.github/workflows/cli-packages.yml @@ -226,6 +226,7 @@ jobs: 'bin/Debug/net10.0/runtimes/win-x64/native/pinget.exe', 'bin/Debug/net10.0/runtimes/win-x64/native/pinget-rust.exe', 'bin/Debug/net10.0/runtimes/win-x64/native/pinget-dotnet.exe', + 'bin/Debug/net10.0/runtimes/win-x64/native/e_sqlite3.dll', 'bin/Debug/net10.0/runtimes/linux-x64/native/pinget' ) diff --git a/README.md b/README.md index be29698..a33c4a3 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Pinget also publishes build-output packages for .NET projects that need a prebui | Package | Executable implementation | Notes | | --- | --- | --- | | `Devolutions.Pinget.Cli.Rust` | Rust CLI | Cross-platform native executable assets named `pinget[.exe]` under `runtimes\\native` | -| `Devolutions.Pinget.Cli.DotNet` | C# CLI | Trimmed NativeAOT executable assets named `pinget.exe`, starting with Windows x64 and arm64 | +| `Devolutions.Pinget.Cli.DotNet` | C# CLI | Trimmed NativeAOT executable assets named `pinget.exe`, plus required native sidecars, starting with Windows x64 and arm64 | These packages are intended for `PackageReference` consumption and copy the executable assets into the consuming project's output/publish output through MSBuild targets. They are not the future install-facing `dotnet tool` package; reserve `Devolutions.Pinget.Tool` for that scenario. diff --git a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj index 74ec5b9..9160063 100644 --- a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj +++ b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj @@ -23,10 +23,16 @@ - runtimes\win-x64\native\ + runtimes\win-x64\native\pinget.exe + + + runtimes\win-x64\native\e_sqlite3.dll - runtimes\win-arm64\native\ + runtimes\win-arm64\native\pinget.exe + + + runtimes\win-arm64\native\e_sqlite3.dll diff --git a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.targets b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.targets index 8be9b37..9572bbd 100644 --- a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.targets +++ b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.targets @@ -12,6 +12,14 @@ false false + + runtimes\win-x64\native\e_sqlite3.dll + PreserveNewest + PreserveNewest + Included + false + false + @@ -23,5 +31,13 @@ false false + + runtimes\win-arm64\native\e_sqlite3.dll + PreserveNewest + PreserveNewest + Included + false + false + diff --git a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj index da68933..251b33d 100644 --- a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj +++ b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj @@ -23,22 +23,22 @@ - runtimes\win-x64\native\ + runtimes\win-x64\native\pinget.exe - runtimes\win-arm64\native\ + runtimes\win-arm64\native\pinget.exe - runtimes\linux-x64\native\ + runtimes\linux-x64\native - runtimes\linux-arm64\native\ + runtimes\linux-arm64\native - runtimes\osx-x64\native\ + runtimes\osx-x64\native - runtimes\osx-arm64\native\ + runtimes\osx-arm64\native From da33c6e8da171e8ffe2634f72e48ba9cdd2c3539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Mon, 11 May 2026 13:05:45 -0400 Subject: [PATCH 6/8] Enforce Windows CLI metadata and static runtime Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Devolutions.Pinget.Cli.csproj | 14 + scripts/Build-CliNativeNuGetPackages.ps1 | 280 +++++++++++++++++- 2 files changed, 290 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj b/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj index 3f5b0b3..bcecb23 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj +++ b/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj @@ -19,6 +19,20 @@ Pinget Pinget CLI Copyright 2021-2026 Devolutions Inc. + true + + + + + + + + + diff --git a/scripts/Build-CliNativeNuGetPackages.ps1 b/scripts/Build-CliNativeNuGetPackages.ps1 index e846c24..8bac3b7 100644 --- a/scripts/Build-CliNativeNuGetPackages.ps1 +++ b/scripts/Build-CliNativeNuGetPackages.ps1 @@ -53,6 +53,252 @@ function Invoke-NativeCommand { } } +function Get-Win32VersionParts { + param([Parameter(Mandatory)][string]$Version) + + $parts = @($Version.Split('+')[0].Split('-')[0].Split('.') | ForEach-Object { + if ($_ -notmatch '^\d+$') { + throw "Version segment is not numeric: $_" + } + + $value = [int]$_ + if (($value -lt 0) -or ($value -gt 65535)) { + throw "Version segment is outside the Win32 VERSIONINFO range: $_" + } + + $value + }) + + while ($parts.Count -lt 4) { + $parts += 0 + } + + if ($parts.Count -gt 4) { + throw "Win32 VERSIONINFO supports at most four version segments: $Version" + } + + $parts +} + +function Resolve-WindowsResourceCompiler { + $rcCommand = Get-Command rc.exe -ErrorAction SilentlyContinue + if ($null -ne $rcCommand) { + return $rcCommand.Source + } + + $windowsKitsRoot = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin' + if (Test-Path -Path $windowsKitsRoot -PathType Container) { + $kitVersions = @(Get-ChildItem -Path $windowsKitsRoot -Directory | Sort-Object -Property Name -Descending) + foreach ($kitVersion in $kitVersions) { + foreach ($arch in @('x64', 'x86', 'arm64')) { + $candidate = Join-Path $kitVersion.FullName "$arch\rc.exe" + if (Test-Path -Path $candidate -PathType Leaf) { + return $candidate + } + } + } + } + + throw 'Unable to locate rc.exe from the Windows SDK.' +} + +function New-PingetWindowsVersionResource { + param( + [Parameter(Mandatory)][string]$OutputDirectory, + [Parameter(Mandatory)][string]$Version + ) + + if (-not $IsWindows) { + throw 'Windows version resources can only be generated on Windows.' + } + + New-Item -Path $OutputDirectory -ItemType Directory -Force | Out-Null + + $versionParts = @(Get-Win32VersionParts -Version $Version) + $versionCsv = $versionParts -join ',' + $versionString = $versionParts -join '.' + $resourceScript = Join-Path $OutputDirectory 'pinget-version.rc' + $resourceFile = Join-Path $OutputDirectory 'pinget-version.res' + + @" +1 VERSIONINFO + FILEVERSION $versionCsv + PRODUCTVERSION $versionCsv + FILEFLAGSMASK 0x3fL + FILEFLAGS 0x0L + FILEOS 0x40004L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Devolutions Inc." + VALUE "FileDescription", "Pinget CLI" + VALUE "FileVersion", "$versionString" + VALUE "InternalName", "pinget" + VALUE "LegalCopyright", "Copyright 2021-2026 Devolutions Inc." + VALUE "OriginalFilename", "pinget.exe" + VALUE "ProductName", "Pinget" + VALUE "ProductVersion", "$versionString" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 + END +END +"@ | Set-Content -Path $resourceScript -Encoding ascii + + $rc = Resolve-WindowsResourceCompiler + Invoke-NativeCommand -FilePath $rc -ArgumentList @('/nologo', "/fo$resourceFile", $resourceScript) + + $resourceFile +} + +function Add-RustFlag { + param( + [string]$RustFlags, + [Parameter(Mandatory)][string]$Flag + ) + + if ([string]::IsNullOrWhiteSpace($RustFlags)) { + return $Flag + } + + if ($RustFlags -match [regex]::Escape($Flag)) { + return $RustFlags + } + + "$RustFlags $Flag" +} + +function Convert-PeRvaToOffset { + param( + [Parameter(Mandatory)][object[]]$Sections, + [Parameter(Mandatory)][uint32]$Rva + ) + + foreach ($section in $Sections) { + $size = [Math]::Max($section.VirtualSize, $section.RawDataSize) + if (($Rva -ge $section.VirtualAddress) -and ($Rva -lt ($section.VirtualAddress + $size))) { + return [int]($section.RawDataPointer + ($Rva - $section.VirtualAddress)) + } + } + + $null +} + +function Get-NullTerminatedAsciiString { + param( + [Parameter(Mandatory)][byte[]]$Bytes, + [Parameter(Mandatory)][int]$Offset + ) + + $end = $Offset + while (($end -lt $Bytes.Length) -and ($Bytes[$end] -ne 0)) { + $end++ + } + + [System.Text.Encoding]::ASCII.GetString($Bytes, $Offset, $end - $Offset) +} + +function Get-PeImportedDllNames { + param([Parameter(Mandatory)][string]$Path) + + $bytes = [System.IO.File]::ReadAllBytes($Path) + $peOffset = [BitConverter]::ToUInt32($bytes, 0x3c) + $coffOffset = [int]$peOffset + 4 + $sectionCount = [BitConverter]::ToUInt16($bytes, $coffOffset + 2) + $optionalHeaderSize = [BitConverter]::ToUInt16($bytes, $coffOffset + 16) + $optionalHeaderOffset = $coffOffset + 20 + $magic = [BitConverter]::ToUInt16($bytes, $optionalHeaderOffset) + $dataDirectoryOffset = if ($magic -eq 0x20b) { $optionalHeaderOffset + 112 } elseif ($magic -eq 0x10b) { $optionalHeaderOffset + 96 } else { throw "Unsupported PE optional header magic: 0x$($magic.ToString('x'))" } + $importDirectoryRva = [BitConverter]::ToUInt32($bytes, $dataDirectoryOffset + 8) + + if ($importDirectoryRva -eq 0) { + return @() + } + + $sectionTableOffset = $optionalHeaderOffset + $optionalHeaderSize + $sections = @() + for ($index = 0; $index -lt $sectionCount; $index++) { + $sectionOffset = $sectionTableOffset + ($index * 40) + $sections += [pscustomobject]@{ + VirtualSize = [BitConverter]::ToUInt32($bytes, $sectionOffset + 8) + VirtualAddress = [BitConverter]::ToUInt32($bytes, $sectionOffset + 12) + RawDataSize = [BitConverter]::ToUInt32($bytes, $sectionOffset + 16) + RawDataPointer = [BitConverter]::ToUInt32($bytes, $sectionOffset + 20) + } + } + + $importDirectoryOffset = Convert-PeRvaToOffset -Sections $sections -Rva $importDirectoryRva + if ($null -eq $importDirectoryOffset) { + throw "Unable to resolve import directory RVA for $Path." + } + + $importedDlls = @() + for ($descriptorOffset = $importDirectoryOffset; ; $descriptorOffset += 20) { + $originalFirstThunk = [BitConverter]::ToUInt32($bytes, $descriptorOffset) + $timeDateStamp = [BitConverter]::ToUInt32($bytes, $descriptorOffset + 4) + $forwarderChain = [BitConverter]::ToUInt32($bytes, $descriptorOffset + 8) + $nameRva = [BitConverter]::ToUInt32($bytes, $descriptorOffset + 12) + $firstThunk = [BitConverter]::ToUInt32($bytes, $descriptorOffset + 16) + + if (($originalFirstThunk -eq 0) -and ($timeDateStamp -eq 0) -and ($forwarderChain -eq 0) -and ($nameRva -eq 0) -and ($firstThunk -eq 0)) { + break + } + + $nameOffset = Convert-PeRvaToOffset -Sections $sections -Rva $nameRva + if ($null -eq $nameOffset) { + throw "Unable to resolve import name RVA for $Path." + } + + $importedDlls += Get-NullTerminatedAsciiString -Bytes $bytes -Offset $nameOffset + } + + $importedDlls +} + +function Assert-WindowsExecutableMetadata { + param( + [Parameter(Mandatory)][string]$Path, + [Parameter(Mandatory)][string]$ExpectedOriginalFilename + ) + + if (-not $IsWindows) { + return + } + + $versionInfo = (Get-Item -Path $Path).VersionInfo + $requiredFields = @{ + CompanyName = 'Devolutions Inc.' + FileDescription = 'Pinget CLI' + OriginalFilename = $ExpectedOriginalFilename + ProductName = 'Pinget' + } + + foreach ($field in $requiredFields.GetEnumerator()) { + $actualValue = $versionInfo.PSObject.Properties[$field.Key].Value + if ($actualValue -ne $field.Value) { + throw "Unexpected $($field.Key) in $Path. Expected '$($field.Value)', got '$actualValue'." + } + } + + if ([string]::IsNullOrWhiteSpace($versionInfo.FileVersion) -or [string]::IsNullOrWhiteSpace($versionInfo.ProductVersion)) { + throw "Missing Windows file/product version information in $Path." + } + + $dynamicMsvcRuntimeImports = @(Get-PeImportedDllNames -Path $Path | Where-Object { + $_ -match '^(vcruntime|msvcp|msvcr|ucrtbase)' -or $_ -match '^api-ms-win-crt-' + }) + + if ($dynamicMsvcRuntimeImports.Count -gt 0) { + throw "Expected static MSVC/UCRT runtime linkage for $Path, but found dynamic imports: $($dynamicMsvcRuntimeImports -join ', ')" + } +} + function Get-SourceVersion { $rustCliManifest = Join-Path $repoRoot 'rust\crates\pinget-cli\Cargo.toml' $dotNetCliProgram = Join-Path $repoRoot 'dotnet\src\Devolutions.Pinget.Cli\Program.cs' @@ -140,6 +386,7 @@ if ($Clean) { } if ($buildDotNet) { Remove-Item -Path (Join-Path $StagingRoot 'dotnet-nativeaot') -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $StagingRoot 'dotnet-nativeaot-obj') -Recurse -Force -ErrorAction SilentlyContinue } Remove-Item -Path $OutputRoot -Recurse -Force -ErrorAction SilentlyContinue } @@ -157,14 +404,28 @@ if ($buildRust) { if (-not $NoBuild) { Invoke-NativeCommand -FilePath rustup -ArgumentList @('target', 'add', $target['CargoTarget']) - Invoke-NativeCommand -FilePath cargo -ArgumentList @('build', '--release', '--package', 'pinget-cli', '--bin', 'pinget', '--manifest-path', $cargoManifest, '--target', $target['CargoTarget']) + $previousRustFlags = $env:RUSTFLAGS + try { + if ($target['CargoTarget'] -like '*-windows-msvc') { + $env:RUSTFLAGS = Add-RustFlag -RustFlags $env:RUSTFLAGS -Flag '-C target-feature=+crt-static' + } + + Invoke-NativeCommand -FilePath cargo -ArgumentList @('build', '--release', '--package', 'pinget-cli', '--bin', 'pinget', '--manifest-path', $cargoManifest, '--target', $target['CargoTarget']) + } + finally { + $env:RUSTFLAGS = $previousRustFlags + } $builtBinary = Join-Path $repoRoot "rust\target\$($target['CargoTarget'])\release\$($target['SourceBinaryName'])" Assert-FileExists -Path $builtBinary Copy-Item -Path $builtBinary -Destination (Join-Path $stageDir $target['PackageBinaryName']) -Force } - Assert-FileExists -Path (Join-Path $stageDir $target['PackageBinaryName']) + $stagedBinary = Join-Path $stageDir $target['PackageBinaryName'] + Assert-FileExists -Path $stagedBinary + if ($target['PackageBinaryName'] -eq 'pinget.exe') { + Assert-WindowsExecutableMetadata -Path $stagedBinary -ExpectedOriginalFilename 'pinget.exe' + } } } @@ -177,7 +438,7 @@ if ($buildDotNet) { New-Item -Path $stageDir -ItemType Directory -Force | Out-Null if (-not $NoBuild) { - Invoke-NativeCommand -FilePath dotnet -ArgumentList @( + $publishArguments = @( 'publish', $dotNetProject, '-c', @@ -196,6 +457,13 @@ if ($buildDotNet) { "/p:InformationalVersion=$Version" ) + if ($rid -like 'win-*') { + $resourceFile = New-PingetWindowsVersionResource -OutputDirectory (Join-Path $StagingRoot "dotnet-nativeaot-obj\$rid") -Version $Version + $publishArguments += "/p:Win32Resource=$resourceFile" + } + + Invoke-NativeCommand -FilePath dotnet -ArgumentList $publishArguments + $sourceBinary = Join-Path $stageDir $binaryNames['SourceBinaryName'] $packageBinary = Join-Path $stageDir $binaryNames['PackageBinaryName'] Assert-FileExists -Path $sourceBinary @@ -204,7 +472,11 @@ if ($buildDotNet) { } } - Assert-FileExists -Path (Join-Path $stageDir $binaryNames['PackageBinaryName']) + $stagedBinary = Join-Path $stageDir $binaryNames['PackageBinaryName'] + Assert-FileExists -Path $stagedBinary + if ($binaryNames['PackageBinaryName'] -eq 'pinget.exe') { + Assert-WindowsExecutableMetadata -Path $stagedBinary -ExpectedOriginalFilename 'pinget.exe' + } } } From f99c88d6e328efe3015e3bddbe0ac5b1bff68801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Mon, 11 May 2026 14:21:14 -0400 Subject: [PATCH 7/8] Add Pinget dotnet tool package Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/cli-packages.yml | 53 ++++++++++++++++++- .github/workflows/release.yml | 4 +- README.md | 21 +++++++- .../Devolutions.Pinget.Cli.csproj | 9 ++++ dotnet/src/Devolutions.Pinget.Cli/README.md | 26 +++++++++ scripts/Build-CliNativeNuGetPackages.ps1 | 51 +++++++++++++++++- 6 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 dotnet/src/Devolutions.Pinget.Cli/README.md diff --git a/.github/workflows/cli-packages.yml b/.github/workflows/cli-packages.yml index a214824..2f50202 100644 --- a/.github/workflows/cli-packages.yml +++ b/.github/workflows/cli-packages.yml @@ -237,9 +237,60 @@ jobs: } } + - name: Smoke test dotnet tool package + shell: pwsh + run: | + $packageSource = (Resolve-Path 'artifacts/cli-nuget').Path + $toolPackage = Get-ChildItem -Path $packageSource -Filter 'Devolutions.Pinget.Tool.*.nupkg' | Select-Object -First 1 + if ($null -eq $toolPackage) { + throw 'Devolutions.Pinget.Tool package was not found.' + } + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::OpenRead($toolPackage.FullName) + try { + $entries = @($archive.Entries | ForEach-Object { $_.FullName }) + $nuspecEntry = $archive.Entries | Where-Object { $_.FullName -match '\.nuspec$' } | Select-Object -First 1 + if ($null -eq $nuspecEntry) { + throw "Unable to find nuspec entry in $($toolPackage.Name)." + } + + $reader = [System.IO.StreamReader]::new($nuspecEntry.Open()) + try { + $nuspec = $reader.ReadToEnd() + } + finally { + $reader.Dispose() + } + + if ($nuspec -notmatch '\native` | | `Devolutions.Pinget.Cli.DotNet` | C# CLI | Trimmed NativeAOT executable assets named `pinget.exe`, plus required native sidecars, starting with Windows x64 and arm64 | -These packages are intended for `PackageReference` consumption and copy the executable assets into the consuming project's output/publish output through MSBuild targets. They are not the future install-facing `dotnet tool` package; reserve `Devolutions.Pinget.Tool` for that scenario. +These packages are intended for `PackageReference` consumption and copy the executable assets into the consuming project's output/publish output through MSBuild targets. They are not `dotnet tool` packages; use `Devolutions.Pinget.Tool` when you want to install the CLI with `dotnet tool install`. ```powershell dotnet add package Devolutions.Pinget.Cli.Rust diff --git a/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj b/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj index bcecb23..e98a569 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj +++ b/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj @@ -19,9 +19,18 @@ Pinget Pinget CLI Copyright 2021-2026 Devolutions Inc. + true + pinget + Devolutions.Pinget.Tool + pinget;winget;package-manager;cli;dotnet-tool + README.md true + + + + Date: Mon, 11 May 2026 14:45:55 -0400 Subject: [PATCH 8/8] Bump Pinget to 0.4.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Devolutions.Pinget.Cli/Program.cs | 2 +- .../ModuleFiles/Devolutions.Pinget.Client.psd1 | 2 +- .../PowerShellEngineVersion.cs | 2 +- .../Devolutions.Pinget.Cli.DotNet.csproj | 2 +- .../Devolutions.Pinget.Cli.Rust.csproj | 2 +- rust/crates/pinget-cli/Cargo.toml | 4 ++-- rust/crates/pinget-com/Cargo.toml | 2 +- rust/crates/pinget-core/Cargo.toml | 2 +- scripts/Set-PingetVersion.ps1 | 17 +++++++++++++++++ 9 files changed, 26 insertions(+), 9 deletions(-) diff --git a/dotnet/src/Devolutions.Pinget.Cli/Program.cs b/dotnet/src/Devolutions.Pinget.Cli/Program.cs index fa43cc4..4548497 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Program.cs +++ b/dotnet/src/Devolutions.Pinget.Cli/Program.cs @@ -5,7 +5,7 @@ using Devolutions.Pinget.Core; using YamlDotNet.Serialization; -const string Version = "0.3.0"; +const string Version = "0.4.0"; const string UpgradeUnsupportedWarning = "Upgrading packages is not supported on this platform; no changes were made."; if (args.Length == 1 && (string.Equals(args[0], "--version", StringComparison.OrdinalIgnoreCase) || string.Equals(args[0], "-v", StringComparison.OrdinalIgnoreCase))) diff --git a/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 b/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 index e7d520d..5c32fbf 100644 --- a/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 +++ b/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'Devolutions.Pinget.Client.psm1' - ModuleVersion = '0.3.0' + ModuleVersion = '0.4.0' CompatiblePSEditions = @('Desktop', 'Core') GUID = 'c6d1b5f2-5ccd-4771-9480-25caad7c58bd' Author = 'Devolutions' diff --git a/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs b/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs index b0feeae..84b1cab 100644 --- a/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs +++ b/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs @@ -2,5 +2,5 @@ namespace Devolutions.Pinget.PowerShell.Engine; public static class PowerShellEngineVersion { - public const string Current = "0.3.0"; + public const string Current = "0.4.0"; } diff --git a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj index 9160063..c45a78c 100644 --- a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj +++ b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj @@ -1,7 +1,7 @@ - 1.0.0 + 0.4.0 Devolutions Inc. Devolutions Devolutions.Pinget.Cli.DotNet diff --git a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj index 251b33d..dd25847 100644 --- a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj +++ b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj @@ -1,7 +1,7 @@ - 1.0.0 + 0.4.0 Devolutions Inc. Devolutions Devolutions.Pinget.Cli.Rust diff --git a/rust/crates/pinget-cli/Cargo.toml b/rust/crates/pinget-cli/Cargo.toml index d875d84..e515ccc 100644 --- a/rust/crates/pinget-cli/Cargo.toml +++ b/rust/crates/pinget-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-cli" -version = "0.3.0" +version = "0.4.0" edition = "2024" [lints] @@ -13,7 +13,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.102" clap = { version = "4.6.1", features = ["derive"] } -pinget-core = { version = "0.3.1", path = "../pinget-core" } +pinget-core = { version = "0.4.0", path = "../pinget-core" } chrono = "0.4.44" dirs = "6.0" jsonschema = "0.30" diff --git a/rust/crates/pinget-com/Cargo.toml b/rust/crates/pinget-com/Cargo.toml index 120d6c4..e22a5b7 100644 --- a/rust/crates/pinget-com/Cargo.toml +++ b/rust/crates/pinget-com/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-com" -version = "0.3.0" +version = "0.4.0" edition = "2024" description = "Windows-only native COM bridge for Pinget backed by pinget-core." license = "MIT" diff --git a/rust/crates/pinget-core/Cargo.toml b/rust/crates/pinget-core/Cargo.toml index 2acd108..7226887 100644 --- a/rust/crates/pinget-core/Cargo.toml +++ b/rust/crates/pinget-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-core" -version = "0.3.1" +version = "0.4.0" edition = "2024" description = "Pure Rust Pinget core library that works directly with source caches, REST endpoints, and installed package state without COM." license = "MIT" diff --git a/scripts/Set-PingetVersion.ps1 b/scripts/Set-PingetVersion.ps1 index 6ed2282..ed7ec08 100644 --- a/scripts/Set-PingetVersion.ps1 +++ b/scripts/Set-PingetVersion.ps1 @@ -65,6 +65,13 @@ Set-VersionInFile ` -Pattern '^(version\s*=\s*")[^"]+(")' ` -Replacement { param($match) "$($match.Groups[1].Value)$Version$($match.Groups[2].Value)" } +foreach ($crateName in @('pinget-core', 'pinget-cli', 'pinget-com')) { + Set-VersionInFile ` + -RelativePath 'rust\Cargo.lock' ` + -Pattern "(\[\[package\]\]\r?\nname = `"$crateName`"\r?\nversion = `")[^`"]+(`")" ` + -Replacement { param($match) "$($match.Groups[1].Value)$Version$($match.Groups[2].Value)" } +} + Set-VersionInFile ` -RelativePath 'dotnet\src\Devolutions.Pinget.Cli\Program.cs' ` -Pattern '^(const string Version\s*=\s*")[^"]+(";)' ` @@ -80,3 +87,13 @@ Set-VersionInFile ` -RelativePath 'dotnet\src\Devolutions.Pinget.PowerShell.Cmdlets\ModuleFiles\Devolutions.Pinget.Client.psd1' ` -Pattern "^( ModuleVersion\s*=\s*')[^']+(')" ` -Replacement { param($match) "$($match.Groups[1].Value)$moduleVersion$($match.Groups[2].Value)" } + +Set-VersionInFile ` + -RelativePath 'nuget\Devolutions.Pinget.Cli.Rust\Devolutions.Pinget.Cli.Rust.csproj' ` + -Pattern '^( )[^<]+()' ` + -Replacement { param($match) "$($match.Groups[1].Value)$Version$($match.Groups[2].Value)" } + +Set-VersionInFile ` + -RelativePath 'nuget\Devolutions.Pinget.Cli.DotNet\Devolutions.Pinget.Cli.DotNet.csproj' ` + -Pattern '^( )[^<]+()' ` + -Replacement { param($match) "$($match.Groups[1].Value)$Version$($match.Groups[2].Value)" }