Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packaging/chocolatey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# DevNav Chocolatey packaging source

This directory contains the source for a future Chocolatey package. It is not
published and is deliberately not tied to v0.14.0: the package must be
materialized from the release manifest of a future release that includes this
integration.

The package is machine-owned and installs the official, versioned release
assets into:

```text
%ChocolateyInstall%\lib\devnav\tools\DevNav\
dev.exe
DevNav.psm1
DevNav.psd1
.devnav-managed-by-chocolatey
```

It registers `%ChocolateyInstall%\lib\devnav\tools` in the machine
`PSModulePath`, without changing any PowerShell profile. The module keeps each
user's configuration in that user's `%LOCALAPPDATA%\DevNav` directory.

## Materialize and pack

The release workflow's `release-manifest.json` is the input source of truth.
The materializer constructs the immutable GitHub Release URLs and injects the
published SHA-256 values; it rejects other URLs, missing architectures and
invalid hashes.

```powershell
./scripts/New-DevNavChocolateyPackage.ps1 `
-Version 0.15.0 `
-ReleaseManifest .\release-manifest.json `
-OutputDirectory $env:TEMP\devnav-chocolatey
choco pack $env:TEMP\devnav-chocolatey\devnav.nuspec
```

Do not add compiled binaries to this repository and do not run `choco push`
from this project without a separate release decision.

## Architecture and ownership

Native architecture is detected from `PROCESSOR_ARCHITECTURE` and
`PROCESSOR_ARCHITEW6432`, not Chocolatey's processor-width helpers. Native
x64 and ARM64 select their matching official binary. Real x86 is rejected,
and `--forcex86` cannot select an incorrect asset.

Installation, upgrade and uninstall are expected to be idempotent. The
Chocolatey marker prevents DevNav self-updates; `dev update` only tells the
user to run `choco upgrade devnav`. The package never runs DevNav as
administrator to initialize user configuration.

## Community verifier limitation

DevNav supports Windows 10/11 on x64 and ARM64. It does not support x86 and
does not declare Windows Server 2019. The Chocolatey Community verifier runs
on Windows Server 2019 and also exercises `forcex86`. Publishing will
therefore require a verifier exemption request; this source does not broaden
DevNav's supported platforms and does not request the exemption yet.
16 changes: 16 additions & 0 deletions packaging/chocolatey/devnav.nuspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>devnav</id>
<version>__VERSION__</version>
<title>DevNav</title>
<authors>JacobOptimiza</authors>
<owners>JacobOptimiza</owners>
<licenseUrl>https://github.com/JacobOptimiza/dev-nav/blob/main/LICENSE</licenseUrl>
<projectUrl>https://github.com/JacobOptimiza/dev-nav</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Native Rust workspace navigator and coding-agent launcher for PowerShell 7 on Windows.</description>
<summary>Keyboard-first workspace navigation for Windows.</summary>
<tags>devnav developer powershell tui workspace navigator portable</tags>
</metadata>
</package>
80 changes: 80 additions & 0 deletions packaging/chocolatey/tools/DevNavChocolatey.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
Set-StrictMode -Version Latest

function Get-DevNavNativeArchitecture {
[CmdletBinding()]
param(
[string] $ProcessorArchitecture = $env:PROCESSOR_ARCHITECTURE,
[string] $ProcessorArchW6432 = $env:PROCESSOR_ARCHITEW6432,
[bool] $ForceX86 = ($env:ChocolateyForceX86 -eq 'true')
)

$process = ([string]$ProcessorArchitecture).Trim().ToUpperInvariant()
$wow = ([string]$ProcessorArchW6432).Trim().ToUpperInvariant()
$native = if ($process -eq 'ARM64' -or $wow -eq 'ARM64') {
'arm64'
}
elseif ($process -eq 'AMD64' -or $wow -eq 'AMD64') {
'x64'
}
elseif ($process -eq 'X86' -and [string]::IsNullOrWhiteSpace($wow)) {
'x86'
}
else {
throw "Unsupported Windows architecture environment: PROCESSOR_ARCHITECTURE='$process', PROCESSOR_ARCHITEW6432='$wow'."
}

if ($ForceX86 -and $native -ne 'x86') {
throw "Chocolatey --forcex86 is unsafe for DevNav: the native OS architecture is $native. DevNav does not provide x86 assets."
}
if ($native -eq 'x86') {
throw 'DevNav does not support x86 Windows. Use a native x64 or ARM64 system.'
}
return $native
}

function Get-DevNavAssetSet {
param(
[Parameter(Mandatory)][ValidateSet('x64', 'arm64')][string] $Architecture,
[Parameter(Mandatory)][hashtable] $Assets
)

$selected = $Assets[$Architecture]
if ($null -eq $selected) { throw "No DevNav release assets were supplied for $Architecture." }
foreach ($name in @('dev', 'module', 'manifest')) {
if ([string]::IsNullOrWhiteSpace([string]$selected[$name].url) -or
[string]::IsNullOrWhiteSpace([string]$selected[$name].sha256)) {
throw "The $Architecture DevNav asset '$name' is missing its URL or SHA-256."
}
}
return $selected
}

function Assert-DevNavSha256 {
param(
[Parameter(Mandatory)][string] $Path,
[Parameter(Mandatory)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string] $Expected
)
$actual = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
if ($actual -ne $Expected) { throw "SHA-256 mismatch for '$Path'." }
}

function Add-DevNavMachineModulePath {
param([Parameter(Mandatory)][string] $PackageTools)

$machine = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine')
$entries = @($machine -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
if ($entries -notcontains $PackageTools) {
[Environment]::SetEnvironmentVariable('PSModulePath', (($entries + $PackageTools) -join ';'), 'Machine')
}
}

function Remove-DevNavMachineModulePath {
[CmdletBinding(SupportsShouldProcess)]
param([Parameter(Mandatory)][string] $PackageTools)

$machine = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine')
$entries = @($machine -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -ne $PackageTools })
if ($PSCmdlet.ShouldProcess('Machine PSModulePath', "Remove '$PackageTools'")) {
[Environment]::SetEnvironmentVariable('PSModulePath', ($entries -join ';'), 'Machine')
}
}
29 changes: 29 additions & 0 deletions packaging/chocolatey/tools/chocolateyInstall.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'DevNavChocolatey.ps1')

$architecture = Get-DevNavNativeArchitecture
$assets = @{
x64 = @{
dev = @{ url = '__X64_DEV_URL__'; sha256 = '__X64_DEV_SHA256__' }
module = @{ url = '__MODULE_URL__'; sha256 = '__MODULE_SHA256__' }
manifest = @{ url = '__MANIFEST_URL__'; sha256 = '__MANIFEST_SHA256__' }
}
arm64 = @{
dev = @{ url = '__ARM64_DEV_URL__'; sha256 = '__ARM64_DEV_SHA256__' }
module = @{ url = '__MODULE_URL__'; sha256 = '__MODULE_SHA256__' }
manifest = @{ url = '__MANIFEST_URL__'; sha256 = '__MANIFEST_SHA256__' }
}
}
$selected = Get-DevNavAssetSet -Architecture $architecture -Assets $assets
$packageTools = Join-Path $env:ChocolateyPackageFolder 'tools'
$moduleRoot = Join-Path $packageTools 'DevNav'
New-Item -ItemType Directory -Path $moduleRoot -Force | Out-Null

Get-ChocolateyWebFile -PackageName 'devnav' -FileFullPath (Join-Path $moduleRoot 'dev.exe') -Url $selected.dev.url -Checksum $selected.dev.sha256 -ChecksumType 'sha256'
Get-ChocolateyWebFile -PackageName 'devnav' -FileFullPath (Join-Path $moduleRoot 'DevNav.psm1') -Url $selected.module.url -Checksum $selected.module.sha256 -ChecksumType 'sha256'
Get-ChocolateyWebFile -PackageName 'devnav' -FileFullPath (Join-Path $moduleRoot 'DevNav.psd1') -Url $selected.manifest.url -Checksum $selected.manifest.sha256 -ChecksumType 'sha256'
Assert-DevNavSha256 -Path (Join-Path $moduleRoot 'dev.exe') -Expected $selected.dev.sha256
Assert-DevNavSha256 -Path (Join-Path $moduleRoot 'DevNav.psm1') -Expected $selected.module.sha256
Assert-DevNavSha256 -Path (Join-Path $moduleRoot 'DevNav.psd1') -Expected $selected.manifest.sha256
New-Item -ItemType File -Path (Join-Path $moduleRoot '.devnav-managed-by-chocolatey') -Force | Out-Null
Add-DevNavMachineModulePath -PackageTools $packageTools
3 changes: 3 additions & 0 deletions packaging/chocolatey/tools/chocolateyUninstall.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'DevNavChocolatey.ps1')
Remove-DevNavMachineModulePath -PackageTools (Join-Path $env:ChocolateyPackageFolder 'tools')
55 changes: 55 additions & 0 deletions scripts/New-DevNavChocolateyPackage.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)][ValidatePattern('^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$')][string] $Version,
[Parameter(Mandatory)][ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })][string] $ReleaseManifest,
[Parameter(Mandatory)][string] $OutputDirectory
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$templateRoot = Join-Path $PSScriptRoot '..\packaging\chocolatey'
$manifest = Get-Content -LiteralPath $ReleaseManifest -Raw | ConvertFrom-Json
$baseUrl = "https://github.com/JacobOptimiza/dev-nav/releases/download/v$Version"
$artifacts = $manifest.artifacts
$assets = @{}
foreach ($architecture in @('x64', 'arm64')) {
$suffix = if ($architecture -eq 'x64') { 'x86_64' } else { 'aarch64' }
$binary = $artifacts."binary-$architecture"
if ($null -eq $binary) { throw "Release manifest has no binary-$architecture artifact." }
$assets[$architecture] = @{
dev = @{ url = "$baseUrl/dev-windows-$suffix.exe"; sha256 = [string]$binary.sha256 }
module = @{ url = "$baseUrl/DevNav.psm1"; sha256 = [string]$artifacts.module.sha256 }
manifest = @{ url = "$baseUrl/DevNav.psd1"; sha256 = [string]$artifacts.'module-manifest'.sha256 }
}
}

function Assert-ReleaseUrl([string]$url, [string]$expectedAsset) {
$prefix = "https://github.com/JacobOptimiza/dev-nav/releases/download/v$Version/"
if ($url -ne "$prefix$expectedAsset") { throw "Unexpected release URL for ${expectedAsset}: $url" }
}
Assert-ReleaseUrl $assets.x64.dev.url 'dev-windows-x86_64.exe'
Assert-ReleaseUrl $assets.arm64.dev.url 'dev-windows-aarch64.exe'
Assert-ReleaseUrl $assets.x64.module.url 'DevNav.psm1'
Assert-ReleaseUrl $assets.x64.manifest.url 'DevNav.psd1'
foreach ($architecture in @('x64', 'arm64')) {
foreach ($name in @('dev', 'module', 'manifest')) {
if ($assets[$architecture][$name].sha256 -notmatch '^[A-Fa-f0-9]{64}$') { throw "Invalid SHA-256 for $architecture/$name." }
}
}

New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
Copy-Item -LiteralPath (Join-Path $templateRoot 'devnav.nuspec') -Destination $OutputDirectory -Force
Copy-Item -LiteralPath (Join-Path $templateRoot 'tools') -Destination $OutputDirectory -Recurse -Force
$nuspec = Join-Path $OutputDirectory 'devnav.nuspec'
(Get-Content -LiteralPath $nuspec -Raw).Replace('__VERSION__', $Version) | Set-Content -LiteralPath $nuspec -Encoding utf8NoBOM
$install = Join-Path $OutputDirectory 'tools\chocolateyInstall.ps1'
$text = Get-Content -LiteralPath $install -Raw
$replacements = @{
'__X64_DEV_URL__' = $assets.x64.dev.url; '__X64_DEV_SHA256__' = $assets.x64.dev.sha256
'__ARM64_DEV_URL__' = $assets.arm64.dev.url; '__ARM64_DEV_SHA256__' = $assets.arm64.dev.sha256
'__MODULE_URL__' = $assets.x64.module.url; '__MODULE_SHA256__' = $assets.x64.module.sha256
'__MANIFEST_URL__' = $assets.x64.manifest.url; '__MANIFEST_SHA256__' = $assets.x64.manifest.sha256
}
foreach ($replacement in $replacements.GetEnumerator()) { $text = $text.Replace($replacement.Key, $replacement.Value) }
$text | Set-Content -LiteralPath $install -Encoding utf8NoBOM
Write-Output $OutputDirectory
75 changes: 75 additions & 0 deletions tests/powershell/ChocolateyPackaging.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
BeforeAll {
$script:repositoryRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$script:helperPath = Join-Path $repositoryRoot 'packaging\chocolatey\tools\DevNavChocolatey.ps1'
$script:installPath = Join-Path $repositoryRoot 'packaging\chocolatey\tools\chocolateyInstall.ps1'
$script:materializerPath = Join-Path $repositoryRoot 'scripts\New-DevNavChocolateyPackage.ps1'
. $script:helperPath
}

Describe 'Chocolatey native architecture routing' {
It 'routes native x64 to x64' {
Get-DevNavNativeArchitecture -ProcessorArchitecture AMD64 -ProcessorArchW6432 '' | Should -Be 'x64'
}
It 'routes an x86 process on x64 to x64' {
Get-DevNavNativeArchitecture -ProcessorArchitecture x86 -ProcessorArchW6432 AMD64 | Should -Be 'x64'
}
It 'routes native ARM64 to arm64' {
Get-DevNavNativeArchitecture -ProcessorArchitecture ARM64 -ProcessorArchW6432 '' | Should -Be 'arm64'
}
It 'routes an x86 process on ARM64 to arm64' {
Get-DevNavNativeArchitecture -ProcessorArchitecture x86 -ProcessorArchW6432 ARM64 | Should -Be 'arm64'
}
It 'rejects real x86' {
{ Get-DevNavNativeArchitecture -ProcessorArchitecture x86 -ProcessorArchW6432 '' } | Should -Throw '*does not support x86*'
}
It 'rejects forcex86 on a native supported OS' {
{ Get-DevNavNativeArchitecture -ProcessorArchitecture AMD64 -ProcessorArchW6432 '' -ForceX86 $true } | Should -Throw '*forcex86*'
}
}

Describe 'Chocolatey package source contract' {
It 'uses official direct assets and the machine-owned marker' {
$source = (Get-Content -LiteralPath $script:installPath -Raw) + (Get-Content -LiteralPath $script:helperPath -Raw)
$source | Should -Match 'Get-ChocolateyWebFile'
$source | Should -Match '\.devnav-managed-by-chocolatey'
$source | Should -Not -Match 'DevNavSetup|Get-OSArchitectureWidth|Get-ProcessorBits'
$source | Should -Match 'PSModulePath'
}

It 'materializes versioned URLs and hashes from a release manifest' {
$root = Join-Path ([IO.Path]::GetTempPath()) ('devnav-choco-materialize-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $root | Out-Null
try {
$manifest = [ordered]@{
schemaVersion = 1
version = '9.8.7'
artifacts = [ordered]@{
'binary-x64' = @{ file = 'dev-windows-x86_64.exe'; sha256 = ('a' * 64) }
'binary-arm64' = @{ file = 'dev-windows-aarch64.exe'; sha256 = ('b' * 64) }
module = @{ file = 'DevNav.psm1'; sha256 = ('c' * 64) }
'module-manifest' = @{ file = 'DevNav.psd1'; sha256 = ('d' * 64) }
}
}
$manifestPath = Join-Path $root 'release-manifest.json'
$manifest | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $manifestPath -Encoding utf8NoBOM
$output = Join-Path $root 'package'
& $script:materializerPath -Version '9.8.7' -ReleaseManifest $manifestPath -OutputDirectory $output | Out-Null
$nuspec = Get-Content (Join-Path $output 'devnav.nuspec') -Raw
$install = Get-Content (Join-Path $output 'tools\chocolateyInstall.ps1') -Raw
$nuspec | Should -Match '<version>9\.8\.7</version>'
$install | Should -Match 'releases/download/v9\.8\.7/dev-windows-x86_64\.exe'
$install | Should -Match ('a' * 64)
$install | Should -Not -Match '__[A-Z0-9_]+__'
}
finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
}

It 'rejects a tampered file checksum' {
$path = Join-Path ([IO.Path]::GetTempPath()) ('devnav-choco-tamper-' + [guid]::NewGuid().ToString('N'))
'original' | Set-Content -LiteralPath $path -NoNewline
try {
{ Assert-DevNavSha256 -Path $path -Expected ('0' * 64) } | Should -Throw '*SHA-256 mismatch*'
}
finally { Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue }
}
}