Last active
October 7, 2023 11:29
-
-
Save anonhostpi/f385e0ec2bca28cc5cc2a136627a4c69 to your computer and use it in GitHub Desktop.
NuGet.Frameworks and Microsft.NETCore.Platforms wrappers for PowerShell (bootstrap code for writing an Import-Package cmdlet)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
& { | |
Add-Type -AssemblyName System.Runtime # Useful for RID detection | |
Add-Type -AssemblyName System.IO.Compression.FileSystem # Useful for reading nupkg/zip files | |
# Exported methods and properties | |
$Exported = New-Object psobject | |
$Exported | Add-Member ` | |
-MemberType ScriptMethod ` | |
-Name GetLatest ` | |
-Value { | |
param( $Name ) | |
$apis = Invoke-WebRequest https://api.nuget.org/v3/index.json | |
$apis = ConvertFrom-Json $apis | |
$resource = $apis.resources | Where-Object { | |
($_."@type" -eq "SearchQueryService") -and | |
($_.comment -like "*(primary)*") | |
} | |
$id = $resource."@id" | |
$results = Invoke-WebRequest "$id`?q=packageid:$Name&prerelease=false&take=1" | |
$results = ConvertFrom-Json $results | |
$results.data[0].version | |
} | |
$Exported | Add-Member ` | |
-MemberType ScriptMethod ` | |
-Name ReadNuspec ` | |
-Value { | |
param( $Package ) | |
$nupkg = $Package.Source | |
$nupkg = [System.IO.Compression.ZipFile]::OpenRead( $nupkg ) | |
$nuspec = $nupkg.Entries | Where-Object { $_.FullName -eq "$($Package.Name).nuspec" } | |
$stream = $nuspec.Open() | |
$reader = New-Object System.IO.StreamReader( $stream ) | |
[xml]($reader.ReadToEnd()) | |
$reader.Close() | |
$stream.Close() | |
$nupkg.Dispose() | |
} | |
$Exported | Add-Member ` | |
-MemberType ScriptMethod ` | |
-Name Load ` | |
-Value { | |
param( | |
[string] $Path, | |
[bool] $Partial = $false | |
) | |
# todo: add handling of native/unmanaged assemblies | |
try { | |
$AddTypeParams = @{ | |
# PassThru = $false | |
} | |
if( $Partial ) { | |
$AddTypeParams.AssemblyName = $Path | |
} elseif ( Test-Path $Path ) { | |
$AddTypeParams.Path = $Path | |
} else { | |
Write-Host "Unable to load $Path" | |
return | |
} | |
Add-Type @AddTypeParams | |
} catch { Write-Host "Unable to load $Path" } | |
} | |
$Exported | Add-Member ` | |
-MemberType ScriptMethod ` | |
-Name Init ` | |
-Value { | |
$Loaded = Try { | |
[NuGet.Frameworks.FrameworkConstants+FrameworkIdentifiers] | |
} Catch { | |
$false | |
} | |
If( -not( $Loaded )){ | |
$load_order = [System.Collections.ArrayList]::new() | |
$load_order.Add( "NuGet.Frameworks" ) | Out-Null | |
# Loop init | |
$i = 0 | |
$package_name = "" | |
# Caching for performance | |
$package_table = @{} | |
while( $i -lt $load_order.count ){ | |
$package_name = $load_order[$i] | |
If( -not( $package_table.ContainsKey( $package_name ) ) ){ | |
$package = Get-Package $package_name -ProviderName NuGet -ErrorAction SilentlyContinue | |
$latest = Try { | |
$this.GetLatest( $package_name ) | |
} Catch { $package.Version } | |
if( (-not $package) -or ($package.Version -ne $latest) ){ | |
Try { | |
Install-Package $package_name ` | |
-ProviderName NuGet ` | |
-SkipDependencies ` | |
-Force | Out-Null | |
} Catch {} | |
$package = Get-Package $package_name -ProviderName NuGet -ErrorAction Stop | |
} | |
$package_table[ $package_name ] = $package.Source.ToString() | |
$dependencies = ($this.ReadNuspec( $package ).package.metadata.dependencies.group | Where-Object { $_.targetframework -eq "netstandard2.0" }).dependency | Where-Object { $_.id } | |
foreach( $dependency in $dependencies ){ | |
$load_order.Add( $dependency.id ) | Out-Null | |
} | |
} else { | |
$oldindex = $load_order.IndexOf( $package_name ) | |
$load_order.RemoveAt( $oldindex ) | |
$load_order.Add( $package_name ) | |
} | |
$i += 1 | |
} | |
$this | Add-Member ` | |
-MemberType NoteProperty ` | |
-Name Dependencies ` | |
-Value ($load_order | Select-Object -Unique) | |
[array]::Reverse( $this.Dependencies ) | |
$this.Dependencies | ForEach-Object { | |
$package_source = $package_table[ $_ ] | |
$dll = Resolve-Path "$(Split-Path $package_source -ErrorAction SilentlyContinue)\lib\netstandard2.0\$_.dll" -ErrorAction SilentlyContinue | |
$this.Load( $dll ) | |
} | |
$this | Add-Member ` | |
-MemberType NoteProperty ` | |
-Name Reducer ` | |
-Value ([NuGet.Frameworks.FrameworkReducer]::new()) | |
$this | Add-Member ` | |
-MemberType NoteProperty ` | |
-Name Frameworks ` | |
-Value @{} | |
[NuGet.Frameworks.FrameworkConstants+FrameworkIdentifiers].DeclaredFields | ForEach-Object { | |
$this.Frameworks[$_.Name] = $_.GetValue( $null ) | |
} | |
$this | Add-Member ` | |
-MemberType NoteProperty ` | |
-Name System ` | |
-Value (& { | |
$runtime = [System.Runtime.InteropServices.RuntimeInformation, mscorlib]::FrameworkDescription | |
$version = $runtime -split " " | Select-Object -Last 1 | |
$framework_name = ($runtime -split " " | Select-Object -SkipLast 1) -join " " | |
If( $framework_name -eq ".NET Framework" ) { | |
$framework_name = "Net" | |
} else { | |
$framework_name = "NETCoreApp" | |
} | |
[NuGet.Frameworks.NuGetFramework]::new( "$($this.Frameworks[ $framework_name ]),Version=v$version" ) | |
}) | |
# RuntimeIdentifier Handling | |
Try { | |
& "$PSScriptRoot/platforms.ps1" $this | Out-Null | |
} Catch { } | |
# Native/Unmanaged assemblies | |
Try { | |
$this | Add-Member ` | |
-MemberType ScriptMethod ` | |
-Name LoadNative ` | |
-Value (& { | |
Add-Type -MemberDefinition @" | |
[DllImport("kernel32")] | |
public static extern IntPtr LoadLibrary(string path); | |
[DllImport("libdl")] | |
public static extern IntPtr dlopen(string path, int flags); | |
"@ -Namespace "_Native" -Name "Loaders" | |
{ | |
param( $Path, $CopyTo ) | |
If( $CopyTo ){ | |
Copy-Item $Path $CopyTo -Force -ErrorAction SilentlyContinue | Out-Null | |
$Path = "$CopyTo\$($Path | Split-Path -Leaf)" | |
} | |
$lib_handle = [System.IntPtr]::Zero | |
If( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ){ | |
$lib_handle = [_Native.Loaders]::LoadLibrary( $Path ) | |
} else { | |
$lib_handle = [_Native.Loaders]::dlopen( $Path, 0 ) | |
} | |
If( $lib_handle -eq [System.IntPtr]::Zero ){ | |
Throw "Unable to load $Path" | |
} | |
# BUG: Leaky handle | |
$lib_handle | |
} | |
}) | |
-Force | |
$this | Add-Member ` | |
-MemberType ScriptMethod ` | |
-Name TestNative ` | |
-Value { | |
param( $Path ) | |
try { | |
[Reflection.AssemblyName]::GetAssemblyName($Path) | Out-Null | |
return $false | |
} catch { | |
return $true | |
} | |
} | |
} Catch {} | |
} | |
$this | |
} | |
$Exported.Init() | |
} | % { $global:Test = $_ } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
param( | |
[parameter(Mandatory = $true)] | |
[psobject] | |
$Exported | |
) | |
& { | |
## Get all NuGet and Microsoft Supported RIDs from Microsoft.NETCore.Platforms | |
$package = Get-Package "Microsoft.NETCore.Platforms" -ProviderName NuGet -ErrorAction SilentlyContinue | |
$latest = Try { | |
# $Exported.GetLatest( "Microsoft.NETCore.Platforms" ) | |
& { | |
param( $Name ) | |
$apis = Invoke-WebRequest https://api.nuget.org/v3/index.json | |
$apis = ConvertFrom-Json $apis | |
$resource = $apis.resources | Where-Object { | |
($_."@type" -eq "SearchQueryService") -and | |
($_.comment -like "*(primary)*") | |
} | |
$id = $resource."@id" | |
$results = Invoke-WebRequest "$id`?q=packageid:$Name&prerelease=false&take=1" | |
$results = ConvertFrom-Json $results | |
$results.data[0].version | |
} "Microsoft.NETCore.Platforms" | |
} Catch { $package.Version } | |
if( (-not $package) -or ($package.Version -ne $latest) ){ | |
Try { | |
Install-Package "Microsoft.NETCore.Platforms" ` | |
-ProviderName NuGet ` | |
-SkipDependencies ` | |
-Force | Out-Null | |
} Catch {} | |
$package = Get-Package "Microsoft.NETCore.Platforms" -ProviderName NuGet -ErrorAction Stop | |
} | |
$Exported | Add-Member ` | |
-MemberType NoteProperty ` | |
-Name Runtimes ` | |
-Value (Get-Content "$($package.source | Split-Path )\runtime.json" -Raw | ConvertFrom-Json).runtimes | |
$grapher = { | |
param( [string] $_rid, [System.Collections.ArrayList] $_graph, [System.Collections.ArrayList] $_graphs = $Exported.Graphs ) | |
if( $_rid ){ | |
$_graph.Add( $_rid ) | Out-Null | |
$_kids = $Exported.Runtimes."$_rid".'#import' | |
If( $_kids.Count -gt 0 ){ | |
$_kids | Select-Object -Skip 1 | ForEach-Object { | |
$_clone = $_graph.Clone() | |
$_graphs.Add( $_clone ) | Out-Null | |
& $grapher $_ $_clone $_graphs | |
} | |
} | |
& $grapher $_kids[0] $_graph $_graphs | |
} | |
} | |
& "$PSScriptRoot\runtimeidentifier.ps1" $Exported | Out-Null | |
$Exported | Add-Member ` | |
-MemberType NoteProperty ` | |
-Name Graphs ` | |
-Value ([System.Collections.ArrayList]::new()) | |
$Exported.Graphs.Add( [System.Collections.ArrayList]::new() ) | Out-Null | |
& $grapher $Exported.Runtime $Exported.Graphs[0] | |
$Exported | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
param( | |
[parameter(Mandatory = $true)] | |
[psobject] | |
$Exported | |
) | |
Try { | |
$Exported | Add-Member ` | |
-MemberType NoteProperty ` | |
-Name Runtime ` | |
-Value ([System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier.ToString()) | |
} Catch { | |
# The C# Architecture Enum should cover all architectures listed in Microsoft.NETCore.Platforms | |
# - see: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.architecture?view=net-7.0 | |
$arch = Try { | |
[System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString() | |
} Catch { | |
Try { | |
[System.Runtime.InteropServices.RuntimeInformation, mscorlib]::OSArchitecture.ToString() | |
} Catch {} | |
} | |
$os_releases_file = Get-Content -Path "/etc/os-release" -ErrorAction SilentlyContinue | |
$os_checks = @{ | |
# Standard Linux Checks | |
@( | |
"alpine", "arch", "centos", "debian", "exherbo", "fedora", | |
"gentoo", "linuxmint", "manjaro", "miraclelinux", "ol", | |
"opensuse", "rhel", "rocky", "sles", "tizen", "ubuntu" | |
) = { param( $OS ); Try { $os_releases_file -match "ID\=[`"']?$OS" } Catch { $false } } | |
# Unsupported OS Checks | |
@( | |
"android", # Android currently requires Xamarin/.NET MAUI to run, and PowerShell only supports .NET Core and .NET Framework | |
"browser", # While Blazor Web Assembly technically has limited capability to run .NET Core, there is no current implementation of PowerShell for it | |
"illumos", "omnios", "openindiana", "smartos", "solaris", # Solaris systems are not supported by PowerShell | |
"ios", "iossimulator", "tvos", "tvossimulator", # iOS currently requires Xamarin/.NET MAUI to run, and PowerShell only supports .NET Core and .NET Framework | |
"maccatalyst", # While maccatalyst supports .NET core, there is no current implementation of PowerShell for it | |
"freebsd" # FreeBSD does not yet support .NET Core | |
) = { $false } | |
# Built-in Checks | |
@( "osx", "linux" ) = { param( $OS ); [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::"$OS") } | |
"win" = { [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) } | |
"unix" = { | |
[System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux) -or ` | |
[System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::OSX) | |
} | |
} | |
$oses = $os_checks.GetEnumerator() | ForEach-Object { | |
$os_array = $_.Key | |
$os_check = $_.Value | |
$os_array | Where-Object { | |
$os_check.Invoke( $_ ) | |
} | |
} | Where-Object { $_ } | |
$rids = if( $oses.Count ){ | |
$global:version_ammendments = @( | |
@( | |
@( | |
"alpine", "centos", "debian", "fedora", "linuxmint", | |
"miraclelinux", "ol", "opensuse", "rhel", "rocky", | |
"sles", "tizen", "ubuntu" | |
), | |
{ param( $OS ); ($os_releases_file | Where-Object { $_ -match "VERSION_ID" }).Split("=")[1].Trim("`"' ") }, | |
{ param( $OS, $Version ); "$OS.$Version" } | |
), | |
# Currently unsupported systems that do have version checks | |
# "android", "freebsd", "ios", "iossimulator" | |
# "maccatalyst", "omnios", "smartos", "solaris" | |
# "tvos", "tvossimulator" | |
@( | |
"osx", | |
{ | |
param( $OS ) | |
$v = $(sw_vers -productVersion) | |
$maj = $v.Split(".")[0] | |
$min = $v.Split(".")[1] | |
if( $maj -eq "10" ){ | |
"$maj.$min" | |
} else { | |
$maj | |
} | |
}, | |
{ param( $OS, $Version ); "$OS.$Version" } | |
), | |
@( | |
"win", | |
{ | |
param( $OS ) | |
$version = [System.Environment]::OSVersion.Version | |
switch -Wildcard ($version.ToString()) | |
{ | |
"10.0*" { "10" } | |
"6.3*" { "81" } | |
"6.2*" { "8" } | |
"6.1*" { "7" } | |
default { "" } | |
} | |
}, | |
{ param( $OS, $Version ); "$($OS)$($Version)" } | |
) | |
) | |
$oses = $oses | ForEach-Object { | |
$os = $_ | |
$version_ammendment = $version_ammendments | Where-Object { $_[0] -contains $os } | |
$ammended = if( $version_ammendment.Count ){ | |
$version_ammendment[2].Invoke( $os, $version_ammendment[1].Invoke( $os ) ) | |
} else { | |
$os | |
} | |
$ammended | |
} | |
$oses | ForEach-Object { | |
$os = $_ | |
(@( $os, $arch.ToLower() ) | Where-Object { $_ }) -join "-" | |
} | |
} else { | |
(@( "any", $arch.ToLower() ) | Where-Object { $_ }) -join "-" | |
} | |
# aot checks are not required for PowerShell | |
# need to check the RID graph for the top most RID in the list of detected RIDs | |
# I think I will create a set of graphs for each RID, then check which RID only occurs in one set of graphs | |
$graphs = @{} | |
$rids | ForEach-Object { | |
$graphs[$_] = [System.Collections.ArrayList]::new() | |
$graphs[$_].add( [System.Collections.ArrayList]::new() ) | Out-Null | |
& $grapher $_ $graphs[$_][0] $graphs[$_] | |
} | |
$rid = $rids | ForEach-Object { | |
# iterate through the other graph sets and count how many of the sets contain the current RID | |
$output = @{ | |
"rid" = $_ | |
"tally" = 0 | |
} | |
If( $graphs.Keys.Count -ne 1 ){ | |
$graphs.Keys | Where-Object { $_ -ne $output.rid } | ForEach-Object { | |
$graph = $graphs[$_] | |
$graph | Where-Object { $_.Contains( $output.rid ) } | ForEach-Object { $output.tally += 1 } | |
} | |
} | |
$output | |
} | Where-Object { $_.tally -eq 0 } | ForEach-Object { $_.rid } | Select-Object -First 1 | |
if( $rid.Count -ne 1 ){ | |
Write-Host "Unable to determine the correct RID for this system." | |
Write-Host "Possible RID Count: $($rid.Count)" | |
Write-Host "Possible RIDs: $($rid -join ", ")" | |
Write-Host "All RIDs Detected: $($rids -join ", ")" | |
} | |
$Exported | Add-Member ` | |
-MemberType NoteProperty ` | |
-Name Runtime ` | |
-Value $rid | |
$Exported | |
} |
Improvement 3:
- Add support for native/unmanaged libraries
- Add a LoadNative method and a TestNative method for handling native libraries
- Revisions:
Improvement 4:
-
Split packaging, platform, and runtimeidentifier functionality into 3 files
- reason for doing this is that I can see others finding my code for powershell platform detection useful
- one of the files is a wrapper for the Microsoft.NETCore.Platforms library and the other new file is a custom implementation of RuntimeInformation.RuntimeIdentifier for non-".NET Core" platforms
-
Added custom implementation of RuntimeInformation.RuntimeIdentifier
-
Revisions:
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Improvement 2:
Refactored self-initialization
Added new properties to exported object:
Revisions: