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
229 changes: 195 additions & 34 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Install ndx from GitHub Releases.
# Install ndx.
# The script is published on GitHub Releases. The binary comes from the
# nuget.org RID package; the blob feed is used when nuget.org is unreachable.
# irm https://github.com/devlooped/ndx/releases/latest/download/install.ps1 | iex
# Env / flags: NDX_VERSION, NDX_PREFIX, NDX_ARCHIVE, NDX_RID, NDX_REPO, NDX_SKIP_PATH
# NDX_NUGET_FLAT, NDX_NUGET_REG, NDX_BLOB_FLAT
# Also accepts --version --prefix --archive --rid --repo --skip-path

$ErrorActionPreference = 'Stop'
Expand All @@ -11,6 +14,9 @@ $Prefix = $env:NDX_PREFIX
$Archive = $env:NDX_ARCHIVE
$Rid = $env:NDX_RID
$SkipPath = $env:NDX_SKIP_PATH -eq '1'
$NugetFlat = if ($env:NDX_NUGET_FLAT) { $env:NDX_NUGET_FLAT } else { 'https://api.nuget.org/v3-flatcontainer' }
$NugetReg = if ($env:NDX_NUGET_REG) { $env:NDX_NUGET_REG } else { 'https://api.nuget.org/v3/registration5-gz-semver2' }
$BlobFlat = if ($env:NDX_BLOB_FLAT) { $env:NDX_BLOB_FLAT } else { 'https://kzu.blob.core.windows.net/nuget/flatcontainer' }

function Get-NdxRuntimeIdentifier {
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
Expand Down Expand Up @@ -68,6 +74,150 @@ public static extern IntPtr SendMessageTimeout(
[ref]$result)
}

function Get-NdxHttp {
if (-not $script:NdxClient) {
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
} catch {}
Add-Type -AssemblyName System.Net.Http
$handler = [System.Net.Http.HttpClientHandler]::new()
$handler.AutomaticDecompression = [System.Net.DecompressionMethods]::GZip -bor [System.Net.DecompressionMethods]::Deflate
$script:NdxClient = [System.Net.Http.HttpClient]::new($handler)
$script:NdxClient.Timeout = [TimeSpan]::FromMinutes(5)
$script:NdxClient.DefaultRequestHeaders.UserAgent.ParseAdd('ndx')
}
return $script:NdxClient
}

function Save-NdxUrl([string]$url, [string]$dest) {
$client = Get-NdxHttp
try {
$resp = $client.GetAsync($url).GetAwaiter().GetResult()
} catch {
return $false
}
try {
if (-not $resp.IsSuccessStatusCode) { return $false }
$fs = [IO.File]::Create($dest)
try {
$stream = $resp.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
try { $stream.CopyTo($fs) } finally { $stream.Dispose() }
} finally { $fs.Dispose() }
return $true
} finally {
$resp.Dispose()
}
}

function Get-NdxText([string]$url) {
$client = Get-NdxHttp
try {
$resp = $client.GetAsync($url).GetAwaiter().GetResult()
} catch {
return $null
}
try {
if (-not $resp.IsSuccessStatusCode) { return $null }
return $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult()
} finally {
$resp.Dispose()
}
}

function Get-NdxLatestStable([string]$flat, [string]$id) {
$text = Get-NdxText ("{0}/{1}/index.json" -f $flat.TrimEnd('/'), $id.ToLowerInvariant())
if (-not $text) { return $null }
try { $obj = $text | ConvertFrom-Json } catch { return $null }
$best = $null
$bestText = $null
foreach ($v in @($obj.versions)) {
if (-not $v -or "$v".Contains('-')) { continue }
try { $parsed = [version]$v } catch { continue }
if ($null -eq $best -or $parsed -gt $best) {
$best = $parsed
$bestText = [string]$v
}
}
return $bestText
}

function Get-NdxCatalogHash([string]$reg, [string]$id, [string]$ver) {
$leafText = Get-NdxText ("{0}/{1}/{2}.json" -f $reg.TrimEnd('/'), $id, $ver)
if (-not $leafText) { return $null }
try { $leaf = $leafText | ConvertFrom-Json } catch { return $null }
$catalog = $leaf.catalogEntry
if ($catalog -isnot [string]) {
if ($null -eq $catalog) { return $null }
$catalog = $catalog.'@id'
}
if (-not $catalog) { return $null }
$entryText = Get-NdxText ([string]$catalog)
if (-not $entryText) { return $null }
try { $entry = $entryText | ConvertFrom-Json } catch { return $null }
$algo = [string]$entry.packageHashAlgorithm
if ($algo -and $algo -ne 'SHA512') { return $null }
$hash = [string]$entry.packageHash
if (-not $hash) { return $null }
return $hash.Trim()
}

function Get-NdxSha512([string]$path) {
$sha = [Security.Cryptography.SHA512]::Create()
try {
$fs = [IO.File]::OpenRead($path)
try { return [Convert]::ToBase64String($sha.ComputeHash($fs)) }
finally { $fs.Dispose() }
} finally {
$sha.Dispose()
}
}

function Save-NdxRidPackage([string]$id, [string]$ver, [string]$dest) {
$idL = $id.ToLowerInvariant()
$verL = $ver.ToLowerInvariant()
$rel = "$idL/$verL/$idL.$verL.nupkg"
if (Save-NdxUrl ("{0}/{1}" -f $NugetFlat.TrimEnd('/'), $rel) $dest) {
$expected = Get-NdxCatalogHash $NugetReg $idL $verL
if (-not $expected) {
Remove-Item -LiteralPath $dest -Force -ErrorAction SilentlyContinue
} else {
$actual = Get-NdxSha512 $dest
if ($actual -ne $expected) {
throw "ndx: SHA512 mismatch for $idL.$verL.nupkg`n expected: $expected`n actual: $actual"
}
return $true
}
}
return (Save-NdxUrl ("{0}/{1}" -f $BlobFlat.TrimEnd('/'), $rel) $dest)
}

function Expand-NdxPackage([string]$nupkg, [string]$destFile, [string]$entryName) {
if (-not ('System.IO.Compression.ZipFile' -as [type])) {
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
}
if (-not ('System.IO.Compression.ZipFile' -as [type])) {
Add-Type -AssemblyName System.IO.Compression
}
$zip = [IO.Compression.ZipFile]::OpenRead($nupkg)
try {
$entry = $null
foreach ($item in $zip.Entries) {
if ($item.FullName.Replace('\', '/') -eq $entryName) {
$entry = $item
break
}
}
if (-not $entry) { throw "ndx: package did not contain $entryName" }
$out = [IO.File]::Create($destFile)
try {
$input = $entry.Open()
try { $input.CopyTo($out) } finally { $input.Dispose() }
} finally { $out.Dispose() }
} finally {
$zip.Dispose()
}
}

function Add-NdxToUserPath([string]$dir) {
$parts = [Environment]::GetEnvironmentVariable('Path', 'User')
if (-not $parts) { $parts = '' }
Expand Down Expand Up @@ -98,6 +248,7 @@ for ($i = 0; $i -lt $args.Count; $i++) {
if (-not $Rid) {
$Rid = Get-NdxRuntimeIdentifier
}
$Rid = $Rid.ToLowerInvariant()

$windows = $Rid.StartsWith('win', [StringComparison]::OrdinalIgnoreCase)
$binary = if ($windows) { 'ndx.exe' } else { 'ndx' }
Expand All @@ -114,47 +265,56 @@ if (-not $Prefix) {
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("ndx-install-" + [guid]::NewGuid().ToString('n'))
New-Item -ItemType Directory -Path $tmp | Out-Null
try {
$extract = Join-Path $tmp 'extract'
New-Item -ItemType Directory -Path $extract | Out-Null
$fromPackage = $false
if (-not $Archive) {
if ($Version) {
if ($Version -eq 'ci') {
$tag = 'ci'
$resolved = 'ci'
} elseif ($Version.StartsWith('v')) {
$tag = $Version
$resolved = $tag.TrimStart('v')
} else {
$tag = "v$Version"
$pkg = "ndx.$Rid"
if ($Version -and $Version.ToLowerInvariant() -eq 'ci') {
$tag = 'ci'
$resolved = 'ci'
$name = "ndx-$resolved-$Rid.$ext"
$base = "https://github.com/$Repo/releases/download/$tag"
$Archive = Join-Path $tmp $name
Invoke-WebRequest -Uri "$base/$name" -OutFile $Archive
Invoke-WebRequest -Uri "$base/$name.sha256" -OutFile "$Archive.sha256"
} else {
# GitHub's unauthenticated releases API returns 403 once the hourly
# quota is spent. The RID package on nuget.org is the same binary.
if ($Version) {
$resolved = $Version
if ($resolved.StartsWith('v') -or $resolved.StartsWith('V')) {
$resolved = $resolved.Substring(1)
}
} else {
$resolved = Get-NdxLatestStable $NugetFlat $pkg
if (-not $resolved) { $resolved = Get-NdxLatestStable $BlobFlat $pkg }
if (-not $resolved) { throw "ndx: could not resolve the latest stable version of $pkg" }
}
} else {
$release = Invoke-RestMethod -Headers @{ Accept = 'application/vnd.github+json' } `
-Uri "https://api.github.com/repos/$Repo/releases/latest"
$tag = $release.tag_name
if (-not $tag) { throw "ndx: could not resolve latest release of $Repo" }
$resolved = $tag.TrimStart('v')
}

$name = "ndx-$resolved-$Rid.$ext"
$base = "https://github.com/$Repo/releases/download/$tag"
$Archive = Join-Path $tmp $name
Invoke-WebRequest -Uri "$base/$name" -OutFile $Archive
Invoke-WebRequest -Uri "$base/$name.sha256" -OutFile "$Archive.sha256"
$nupkg = Join-Path $tmp "$pkg.$resolved.nupkg"
if (-not (Save-NdxRidPackage $pkg $resolved $nupkg)) {
throw "ndx: could not download $pkg $resolved"
}
Expand-NdxPackage $nupkg (Join-Path $extract $binary) "tools/any/$Rid/$binary"
$fromPackage = $true
}
}

if (Test-Path "$Archive.sha256") {
$expected = ((Get-Content -Raw "$Archive.sha256").Trim() -split '\s+')[0].ToLowerInvariant()
$actual = (Get-FileHash -Algorithm SHA256 -Path $Archive).Hash.ToLowerInvariant()
if ($actual -ne $expected) {
throw "ndx: SHA256 mismatch for $(Split-Path $Archive -Leaf)`n expected: $expected`n actual: $actual"
if (-not $fromPackage) {
if (Test-Path "$Archive.sha256") {
$expected = ((Get-Content -Raw "$Archive.sha256").Trim() -split '\s+')[0].ToLowerInvariant()
$actual = (Get-FileHash -Algorithm SHA256 -Path $Archive).Hash.ToLowerInvariant()
if ($actual -ne $expected) {
throw "ndx: SHA256 mismatch for $(Split-Path $Archive -Leaf)`n expected: $expected`n actual: $actual"
}
}
}

$extract = Join-Path $tmp 'extract'
New-Item -ItemType Directory -Path $extract | Out-Null
if ($windows) {
Expand-Archive -Path $Archive -DestinationPath $extract -Force
} else {
tar -xzf $Archive -C $extract
if ($windows) {
Expand-Archive -Path $Archive -DestinationPath $extract -Force
} else {
tar -xzf $Archive -C $extract
}
}

$source = Join-Path $extract $binary
Expand All @@ -172,5 +332,6 @@ try {
}
}
finally {
if ($script:NdxClient) { $script:NdxClient.Dispose() }
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
Loading
Loading