Last active
January 10, 2024 18:59
-
-
Save mklement0/7436c9e4b2f73d7256498f959f0d5a7c to your computer and use it in GitHub Desktop.
PowerShell function for experimenting with loading .NET assemblies from NuGet packages that are downloaded and cached on demand
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
<# | |
Prerequisites: PowerShell v5.1 and above (verified; may also work in earlier versions) | |
License: MIT | |
Author: Michael Klement <[email protected]> | |
DOWNLOAD and DEFINITION OF THE FUNCTION: | |
irm https://gist.github.com/mklement0/7436c9e4b2f73d7256498f959f0d5a7c/raw/Add-NuGetType.ps1 | iex | |
The above directly defines the function below in your session and offers guidance for making it available in future | |
sessions too. | |
DOWNLOAD ONLY: | |
irm https://gist.github.com/mklement0/7436c9e4b2f73d7256498f959f0d5a7c/raw > Add-NuGetType.ps1 | |
The above downloads to the specified file, which you then need to dot-source to make the function available | |
in the current session: | |
. ./Add-NuGetType.ps1 | |
To learn what the function does: | |
* see the next comment block | |
* or, once downloaded and defined, invoke the function with -? or pass its name to Get-Help. | |
#> | |
function Add-NuGetType { | |
<# | |
.SYNOPSIS | |
Loads assemblies from NuGet packages for experimentation. | |
.DESCRIPTION | |
Loads assemblies from NuGet packages for experimentation, which are | |
downloaded on demand into a custom, PowerShell-friendly cache. | |
To force re-download of packages, use -Force. | |
To clear packages from the cache, use -ClearFromCache. | |
To reset the entire cache and remove the embedded .NET SDK, use -Reset. | |
By default, this command is silent if no errors occur, but note that | |
commands may situationally take quite a while to complete, notably | |
when installing the .NET SDK on demand, but also when downloading | |
packages. | |
Use -Verbose for explicit status and progress information. | |
Note: | |
* An embedded, private version of the .NET SDK is installed on demand, | |
using the current stable version. | |
To upgrade it later, use -RefreshSdk | |
* Currently, this command is limited to (down-)loading the *latest* package | |
versions only. | |
To upgrade to an already cached package's latest version (which replaces | |
the previous one), use -Force. | |
IMPORTANT: | |
The primary purpose of this command is to facilitate EXPERIMENTATION with | |
NuGet packages. | |
In production code, this command should NOT be used directly, as it offers | |
no version control. | |
Assemblies your code depends on should be bundled with it, using a properly | |
defined PowerShell module. | |
While you can use a cached package folder created by this command as the | |
basis for creating such a module (use -ListCached to find the locations), the | |
caveats are that the assemblies are potentially PowerShell-edition specific | |
and, if they have native-library dependencies, platform-specific. | |
.PARAMETER PackageName | |
The name(s) of the NuGet packages to load, first downloading and caching them | |
on demand. | |
By default, the full, literal package name(s) must be specified. | |
However, the interpretation changes with the presence of switches: | |
* -SearchGallery: the name(s) are intepreted as search terms | |
(no wildcards needed or supported) | |
* -ListCached, -ClearFromCache: wildcards may be specified. | |
.PARAMETER ListCached | |
Lists cached packages by name, version, and full directory path. | |
By default, *all* cached packages are listed, but you may filter them | |
by package names or package-name wildcard patterns. | |
.PARAMETER SearchGallery | |
Interprets the specified package name(s) as search terms to use for finding | |
packages online, in the NuGet Gallery - wildcards are neither needed nor | |
supported. | |
The search terms may be parts of package names or keywords, and if multiple | |
are specified, they are combined into a *single* lookup, meaning that all | |
terms must match. | |
Note that at most 20 matches are listed. | |
Use -Online to perform the search in your default web browser instead. | |
.PARAMETER Online | |
Implies -SearchGallery. | |
Performs the NuGet Gallery search in your default web browser. | |
.PARAMETER ClearFromCache | |
Clears (deletes) the specified packages from the cache. | |
Wildcard patterns are accepted. | |
While you may specify * to clear *all* packages, it is simpler (and more | |
thorough) to use -Reset. | |
Note: On Windows, clearing will fail if processes currently have the assemblies | |
loaded. When that happens, terminate all relevant processes and try again. | |
.PARAMETER Reset | |
Deletes the entire package cache, along with the embedded .NET SDK and the | |
helper .NET SDK project that is used to prepare packages for the cache. | |
Call this to free up all local disk storage used by this command or if you want | |
to force updating all packages to their latest version | |
on the next run, along with reinstallation of the embedded .NET SDK | |
to the then-current stable version. | |
Note: On Windows, clearing will fail if processes currently have any of the | |
cached assemblies loaded. | |
When that happens, terminate all relevant processes and try again. | |
.PARAMETER RefreshSdk | |
(Re)installs the private .NET SDK that is embdded in the cache folder using the | |
then-current stable version. | |
Use -Info to see information about this embedded SDK. | |
Future on-demand package downloads then use the updated SDK, though note | |
that in Windows PowerShell it is .NET Framework (v4.8 at most) that is targeted | |
in the helper project used to prepare the package's cache. | |
.PARAMETER Info | |
List the location of the package cache folder, the number of cached packages, | |
and information about the embedded .NET SDK. | |
.PARAMETER Force | |
Forces download of the latest version of the specified package(s), even | |
if already cached. | |
.EXAMPLE | |
Add-NuGetType SqlKata, SqlKata.Execution | |
Downloads, caches, and loads the assemblies from the specified packages. | |
.EXAMPLE | |
Add-NuGetType Microsoft.Data.Sqlite -Force | |
Force download and caching of the latest package version before loading | |
the assemblies. | |
.EXAMPLE | |
Add-NuGetType -RefreshSdk | |
(Re)installs the private .NET SDK that is embdded in the cache folder | |
using the then-current stable version. | |
.EXAMPLE | |
Add-NuGetType sqlite -SearchGallery | |
Searches for packages related to SQLite in the NuGet Gallery and lists up | |
to 20 results. | |
.EXAMPLE | |
Add-NuGetType sqlite -SearchGallery -Online | |
Searches for packages related to SQLite in the NuGet Gallery in your default | |
web browser. | |
.EXAMPLE | |
Add-NuGetType *sqlite* -ListCached | |
Lists cached packages whose name matches the specified wildcard pattern. | |
Not specifying a pattern lists *all* cached packages. | |
.EXAMPLE | |
Add-NuGetType *sqlite* -ClearFromCache | |
Clears (deletes) packages whose matches the specified wildcard pattern from | |
the cache directory. | |
To clear *all* cached packages, use * | |
.EXAMPLE | |
Add-NuGetType -Reset | |
Deletes the entire package cache, including the embedded .NET SDK and helper | |
project. | |
#> | |
[CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = 'Add')] | |
param( | |
[Parameter(Mandatory, ValueFromPipeline, Position = 0, ParameterSetName = 'Add')] | |
[Parameter(Mandatory, ValueFromPipeline, Position = 0, ParameterSetName = 'Search')] | |
[Parameter(ValueFromPipeline, Position = 0, ParameterSetName = 'List')] | |
[Parameter(Mandatory, ValueFromPipeline, Position = 0, ParameterSetName = 'Clear')] | |
[SupportsWildcards()] # Note: Only with -ListCached and -ClearFromCache | |
[Alias('Name')] | |
[string[]] $PackageName | |
, | |
[Parameter(ParameterSetName = 'Add')] | |
[switch] $Force | |
, | |
[Parameter(ParameterSetName = 'List')] | |
[Alias('l')] | |
[Alias('ListAvailable')] | |
[switch] $ListCached | |
, | |
[Parameter(ParameterSetName = 'Search')] | |
[Alias('Search')] | |
[switch] $SearchGallery | |
, | |
[Parameter(ParameterSetName = 'Search')] | |
[switch] $Online | |
, | |
[Parameter(ParameterSetName = 'Clear')] | |
[switch] $ClearFromCache | |
, | |
[Parameter(ParameterSetName = 'Reset')] | |
[switch] $Reset | |
, | |
[Parameter(ParameterSetName = 'Sdk')] | |
[switch] $RefreshSdk | |
, | |
[Parameter(ParameterSetName = 'Info')] | |
[switch] $Info | |
) | |
begin { | |
Set-StrictMode -Version 1 | |
# -Online implies -SearchGallery | |
if ($Online -and -not $SearchGallery) { $SearchGallery = [switch]::new($true) } | |
$isWinPs = -not (Get-Variable IsCoreCLR -ValueOnly -ErrorAction Ignore) | |
$isWin = $env:OS -eq 'Windows_NT' | |
if (-not ($PSVersionTable.PSVersion.Major -ge 7 -or ($PSVersionTable.PSVersion.Major -eq 5 -and $PSVersionTable.PSVersion.Minor -ge 1))) { | |
throw "This command requires Windows PowerShell version 5.1 or PowerShell (Core) 7+." | |
} | |
$CACHE_ROOT = "$HOME/.nuget-pwsh" # Both Windows and Unix, following the model of using ~/.nuget for SDK-cached packages on all platforms. | |
# Note: WinPS needs its own package cache, because the cache is prepared via a helper project that targets .NET Framework rather than .NET (Core). | |
# !! Targeting the 'netstandard2.0' TFM in the helper project is seemingly NOT enough, as that doesn't include the native libraries that some packages require. | |
# ?? Is there an easy way to examine up front whether a package has native dependencies, and could that be used to only install edition-specific packages is actually needed? | |
$CACHE_PACKAGES = ("$CACHE_ROOT/packages", "$CACHE_ROOT/packages-winps")[$isWinPs] | |
# -- | |
$CACHE_HELPERPROJECT_NAME = 'helper' | |
$CACHE_HELPERPROJECT = "$CACHE_ROOT/$CACHE_HELPERPROJECT_NAME" | |
# -- | |
# NOTE: We maintain only *one* .NET SDK installation, which is used to download and prepare packages | |
# for WinPS too. | |
$EMBEDDED_SDK_DIR = "$CACHE_ROOT/dotnet" | |
$DOTNET_CLI = ("$EMBEDDED_SDK_DIR/dotnet", "$EMBEDDED_SDK_DIR\dotnet.exe")[$isWin] | |
$INSTALLSCRIPT_URL = ('https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh', 'https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1')[$isWin] | |
$isSdkInstalled = Test-Path -LiteralPath $DOTNET_CLI | |
$allPackageNames = [System.Collections.Generic.List[string]]::new() | |
# Helper function for installing the .NET SDK on demand. | |
# Note: Assumes that `$dotNetSdkInfo | |
function install-dotNetSdk { | |
if ($isSdkInstalled) { | |
# Write-Verbose "SDK already installed at '$(Slit-Path -LiteralPath $DOTNET_CLI)'" | |
Write-Verbose "Reinstalling the embedded .NET SDK..." | |
Remove-Item -ErrorAction Stop -Force -Recurse -Path $EMBEDDED_SDK_DIR/* | |
} | |
Write-Verbose "Performing user-level .NET SDK installation to '$EMBEDDED_SDK_DIR' via '$INSTALLSCRIPT_URL'..." | |
# Note: We use the *current stable (not LTS) version*. | |
# Trying to find a version matching the underlying runtime (framework) would be non-trivial, | |
# because a full semver version number must be supplied to -Version / --version, and the | |
# runtime version number (embbedded in [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription) | |
# is NOT a valid *SDK* version number. | |
# In practice, this approach should be good enough. Reinstallation with the | |
# then-current stable version can be achieved with -RefreshSdk (which also | |
# refreshes the version-independent .NET host components). | |
$sdkVersionArg = (('--channel', 'Current'), @{ Channel = 'Current' })[$isWin] | |
$verbose = $VerbosePreference -eq 'Continue' | |
$tempScript = $null; $global:LASTEXITCODE = 0 | |
$pathBefore = $env:PATH | |
try { | |
if ($isWin) { | |
# !! The .ps1 script content seemingly calls `exit`, which would also causes this script to exit if executed via | |
# !! Invoke-Expression -> save to temp. *.ps1 file and execute from there. | |
$tempScript = [System.IO.Path]::GetTempFileName(); Remove-Item -LiteralPath $tempScript; $tempScript += '.ps1' | |
Invoke-RestMethod $INSTALLSCRIPT_URL -OutFile $tempScript | |
$cmd = { & $tempScript @sdkVersionArg -InstallDir $EMBEDDED_SDK_DIR } | |
} | |
else { | |
# Unix | |
$cmd = { Invoke-RestMethod $INSTALLSCRIPT_URL | /bin/bash -s -- @sdkVersionArg --install-dir $EMBEDDED_SDK_DIR } | |
} | |
if ($verbose) { | |
& $cmd | |
} | |
else { | |
& $cmd >$null | |
} | |
} | |
catch { throw } | |
finally { | |
# The in-process *.ps1 file put the SDK dir in $env:PATH, which we don't want, so | |
# we restore the original value here. | |
$env:PATH = $pathBefore | |
if ($tempScript) { Remove-Item -LiteralPath $tempScript } | |
} | |
if ($LASTEXITCODE) { throw "Execution of '$INSTALLSCRIPT_URL' failed with exit code $LASTEXITCODE." } | |
# $hint = if (-not $dotNetSdkInfo.IsInPath) { | |
# @" | |
# NOTE: | |
# * The SDK's directory was NOT persistently added to the PATH environment variable. | |
# * This command will still find it, but if you want other commands to find | |
# it too, you'll have to add it to `$env:PATH persistently yourself. | |
# "@ | |
# } | |
Write-Verbose @" | |
Installation of the embedded .NET SDK succeeded: | |
Directory: $EMBEDDED_SDK_DIR | |
SDK version: $(& $DOTNET_CLI --version) | |
"@ | |
# Since a potentially *new* version was just installed, we want the helper .NET SDK project to | |
# use it, so we delete the existing one to force its recreation. | |
if (Test-Path -LiteralPath $CACHE_HELPERPROJECT) { | |
Remove-Item -ErrorAction Stop -Force "$CACHE_HELPERPROJECT/*" | |
} | |
} # install-dotNetSdk | |
# $dotNetSdkInfo = get-dotNetSdkInfo | |
# $DOTNET_CLI = $dotNetSdkInfo.Cli | |
if ($PSCmdlet.ParameterSetName -eq 'Add') { | |
# Install the .NET SDK on demand and create the helper project on demand. | |
if (-not $isSdkInstalled) { | |
# install SDK | |
Write-Verbose "Performing one-time on-demand installation of a private .NET SDK embedded in the cache folder.`nThe requested packages will be downloaded, cached, and loaded afterwards." | |
install-dotNetSdk # $dotNetSdkInfo | |
# $dotNetSdkInfo = get-dotNetSdkInfo | |
# $DOTNET_CLI = $dotNetSdkInfo.Cli | |
} | |
# Make sure the custom cache folder and helper projects exist. | |
$null = New-Item -ErrorAction Stop -Type Directory -Force $CACHE_PACKAGES, $CACHE_HELPERPROJECT | |
$projFile = $CACHE_HELPERPROJECT + "/$CACHE_HELPERPROJECT_NAME.csproj" | |
$projFile_Pristine = (($projFile + '.orig'), ($projFile + '.orig-winps'))[$isWinPs] | |
if (-not (Test-Path -LiteralPath $projFile_Pristine)) { | |
Write-Verbose "Creating helper .NET SDK project in '$CACHE_HELPERPROJECT'..." | |
Push-Location -ErrorAction Stop -LiteralPath $CACHE_HELPERPROJECT | |
# !! We can NOT target `--framework netstandard2.0`, because doing so | |
# !! seemingly doesn't unpack the *native library* depndencies, which are needed at runtime. | |
& $DOTNET_CLI new classlib --force >$null | |
# !! Remove the default .cs file that was created: it isn't necessary for publishing, and | |
# !! its syntax may be too new for compiling for .NET Framework. | |
Remove-Item -LiteralPath Class1.cs | |
if ($isWinPs) { | |
# WinPS: Update the project file (*.csproj) to target the same version of the .NET Framework | |
# that underlies the running Windows PowerShell session; as of mid-2021, this is already | |
# v4.8 ('net48'), the last-ever version of .NET Framework | |
# ?? Is the presence of this .NET Framework version enough to use it an SDK project, | |
# ?? or does a targeting / developer pack (meant for Visual Studio) have to be installed? | |
# Note: As of .NET 5.0, you can NOT request this via `--framework` on the `dotnet new ...` command line. | |
try { | |
$runtimeVersion = | |
try { | |
[version] (-split [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription)[-1] | |
} catch { | |
# !! Implies that the WinPS version is built on .NET Framework 4.7.0 or below, where [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription isn't available. | |
# !! Use the highest .NET Framework version installed, but note that this may be HIGHER than the version WinPS was built on, as is the case in our W11 22H2 VM, | |
# !! but it seems to work in practice. | |
[version] (Get-ItemPropertyValue 'registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' Version) | |
} | |
($xml = [xml]::new()).Load($projFile) | |
@($xml.Project.PropertyGroup)[0].TargetFramework = 'net{0}{1}' -f $runtimeVersion.Major, $runtimeVersion.Minor | |
$xml.Save($projFile) | |
} | |
catch { throw } | |
} | |
Pop-Location | |
if ($LASTEXITCODE) { throw "Failed to create temporary .NET SDK project in '$CACHE_HELPERPROJECT'." } | |
Copy-Item $projFile $projFile_Pristine # Keep a pristine copy of the *.csproj file. | |
} | |
} | |
} | |
process { | |
if ($PackageName) { $allPackageNames.AddRange($PackageName) } | |
} | |
end { | |
if ($allPackageNames -match '[*?[]' -and -not ($ListCached -or $ClearFromCache) ) { | |
throw "Wildcard package-name patterns are only supported with -ListCached and -ClearFromCache." | |
} | |
if ($Reset) { | |
if (-not (Test-Path -LiteralPath $CACHE_ROOT)) { | |
Write-Verbose "Nothing to do: Cache folder '$CACHE_ROOT' does not exist." | |
} | |
else { | |
Write-Verbose "Removing cache folder '$CACHE_ROOT'..." | |
try { | |
Remove-Item -ErrorAction Stop -Force -Recurse -LiteralPath $CACHE_ROOT | |
} | |
catch { | |
if ($isWin) { | |
throw "Removal of cache folder '$CACHE_ROOT' failed, possibly because active processes still have package loaded. Terminate them, then try again:`n $_" | |
} | |
else { | |
throw | |
} | |
} | |
} | |
} | |
elseif ($Info) { | |
@" | |
Cache info: | |
----------- | |
Directory: $CACHE_PACKAGES | |
# of cached packages: $((Get-ChildItem -ErrorAction Ignore -LiteralPath $CACHE_PACKAGES).Count) | |
Embedded .NET SDK info: | |
----------------------- | |
Installation dir.: $EMBEDDED_SDK_DIR | |
Active SDK version: $(if ($isSdkInstalled) { & $DOTNET_CLI --version } else { '(n/a)' }) | |
"@ | |
} | |
elseif ($RefreshSdk) { | |
install-dotNetSdk # $dotNetSdkInfo | |
} | |
elseif ($SearchGallery) { | |
# Simpy stringify multiple names (search terms) to form a single, | |
# space-separated search term - the NuGet API gallery accepts that, and the order of terms is seemingly irrelevant. | |
$searchTerm = "$allPackageNames" | |
if ($Online) { | |
Write-Verbose "Searching the NuGet Gallery for '$searchTerm' in your default browser..." | |
Start-Process "https://www.nuget.org/packages?q=$searchTerm" | |
} | |
else { | |
Write-Verbose "Searching the NuGet Gallery for '$searchTerm' (for at most 20 matches)..." | |
# Note: The API endpoint for searches was determined via https://api.nuget.org/v3/index.json | |
# See https://docs.microsoft.com/en-us/nuget/api/overview | |
# Seemingly, by default only up to 20 matches are shown. | |
try { | |
(Invoke-RestMethod "https://azuresearch-usnc.nuget.org/query?q=$searchTerm").data | Select-Object title, version | |
} | |
catch { throw } | |
} | |
} | |
elseif ($ListCached) { | |
if (-not (Test-Path -LiteralPath $CACHE_PACKAGES)) { | |
Write-Verbose "Nothing to list, because cache folder '$CACHE_PACKAGES' doesn't exist." | |
} | |
else { | |
Write-Verbose "Listing cached NuGet packages in '$CACHE_PACKAGES'." | |
Get-Item -Path $CACHE_PACKAGES/* -Include $allPackageNames | Get-ChildItem | ForEach-Object { | |
[pscustomObject] @{ | |
Name = $_.Parent.Name | |
Version = $_.Name | |
FullName = $_.FullName | |
} | |
} | |
} | |
} | |
elseif ($ClearFromCache) { | |
if (-not (Test-Path -LiteralPath $CACHE_PACKAGES)) { | |
Write-Verbose "Nothing to clear, because cache folder '$CACHE_PACKAGES' doesn't exist." | |
} | |
else { | |
$toRemove = Get-Item $CACHE_PACKAGES/* -Include $allPackageNames | |
if (-not $toRemove) { | |
Write-Verbose "No NuGet packages whose name matches '$allPackageNames' found in cache folder '$CACHE_PACKAGES'" | |
} | |
else { | |
Write-Verbose "Clearing NuGet packages matching '$allPackageNames' from cache folder '$CACHE_PACKAGES':`n$($toRemove.Name -join "`n")" | |
$toRemove | Remove-Item -Recurse -Force | |
if (-not $? -and $isWin) { | |
Write-Warning "Presumably, active processes still have at least some of the packages loaded. Terminate these processes, then try again." | |
} | |
} | |
} | |
} | |
else { | |
# Add types of the specified packages to the session by loading their assemblies, with on-demand package download, prepping, and caching. | |
foreach ($packageName in $allPackageNames) { | |
# Construct the path to the cached package. | |
$cacheDir = (Get-ChildItem -ErrorAction Ignore -Directory -LiteralPath $CACHE_PACKAGES/$packageName | Sort-Object -Descending { [version] $_.Name } | Select-Object -First 1).FullName | |
if ($Force -or -not $cacheDir) { | |
try { | |
Copy-Item -ErrorAction Stop $projFile_Pristine $projFile | |
Write-Verbose "Downloading latest version of package '$packageName' to project '$CACHE_HELPERPROJECT'..." | |
& $DOTNET_CLI add $projFile package $packageName >$null | |
if ($LASTEXITCODE) { Write-Error "Failed to add package '$packageName' to project file '$projFile`nTypically, this indicates EITHER that (a) NO SUCH PACKAGE EXISTS or (b) the NUGET PACKAGE PROVIDER ISN'T INSTALLED."; continue } | |
Write-Verbose "Extracting package version information from '$projFile'..." | |
($xml = [xml]::new()).Load($projFile) | |
$packageName = $xml.Project.ItemGroup.PackageReference.Include | |
$packageVersion = $xml.Project.ItemGroup.PackageReference.Version | |
Write-Verbose "Simplifying '$projFile' for backward compatibility..." | |
# Remove the <ImplicitUsings> and <Nullable> elements, which prevent compilation for older .NET Framework versions. | |
$xml.SelectNodes('//*[self::ImplicitUsings or self::Nullable]').ForEach({ $_.ParentNode.RemoveChild($_) }) | |
$xml.Save($projFile) | |
Write-Verbose "Publishing helper project..." | |
& $DOTNET_CLI publish -c Release $projFile >$null | |
if ($LASTEXITCODE) { Write-Error "Failed to publish aux. project '$projFile"; continue } | |
} | |
catch { throw } | |
# Create the package- and version-spefici cache directory. | |
$cacheDir = (New-Item -ErrorAction Stop -Force -Type Directory "$CACHE_PACKAGES/$packageName/$packageVersion").FullName | |
# Note: Since we're only supporting one cached version at a time, we're removing any previous ones for the package at hand. | |
Remove-Item $cacheDir/* -Recurse -Force | |
if (-not $?) { Write-Error "Failed to clear contents of existing cache folder '$cacheDir'"; continue } | |
Write-Verbose "Copying published DLLs to cache directory '$cacheDir'..." | |
Copy-Item -Force $CACHE_HELPERPROJECT/bin/Release/*/publish/*.dll $cacheDir -Exclude "$CACHE_HELPERPROJECT_NAME.dll" | |
# If supporting native libraries are present (subfolder "runtimes"): | |
if (Test-Path -Path $CACHE_HELPERPROJECT/bin/Release/*/publish/runtimes) { | |
if ($isWinPs) { | |
# .NET Framework | |
# Copy the "runtimes" subdirectory tree too. | |
Copy-Item -Recurse -Force $CACHE_HELPERPROJECT/bin/Release/*/publish/runtimes $cacheDir | |
} | |
else { | |
# .NET Core / 5+ | |
# !! At least with the Microsoft.Data.Sqlite package, version 5.0.9, and its "SQLitePCLRaw.nativelibrary.dll" assembly, | |
# !! copying the "runtimes" folder is NOT enough for said DLL to find its depedent native libraries - even though | |
# !! when compiling for .NET Framework it is (all except this managed *.dll are identical when compiling to .NET 5.0 vs. .NET Framework 4.8) | |
# !! The workaround is to copy the *platform-relevant* library from the relevant subfolder of "runtimes" *directly* into the cache folder, | |
# !! alongside the managed DLL that depends on it. As as side effect, this locks the cache folder into the host platform. | |
# !! Note: When compiling for 'net48', i.e. .NET Framework, a *different* "SQLitePCLRaw.nativelibrary.dll" is | |
# !! copied to the publish folder, and Assembly.LoadFrom() in .NET *Core* then *does* find the relevant native library | |
# !! in the "runtimes" subfolder tree - even without a *.deps.json file. | |
# !! ?? What does this ability depend on? Is the .NET Core 5.0 Microsoft.Data.Sqlite package, specifically, broken? | |
# !! ?? Note that the helper project *does* work with it, however, presumably because it reads the *.deps.json file | |
# !! ?? it generates as part of the build / publish operation on application startup. | |
# Derive the platform-/architecture-/bitness-relevant RID (runtime identifier; e.g. 'osx-x64') from the host OS. | |
# !! Does not account for 'arm' architectures ('win-arm', ...) and Linux variants such as 'linux-musl-x64' | |
$rid = ([System.Runtime.InteropServices.OSPlatform].GetProperties().Name.Where( | |
{ | |
[System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::$_) | |
}, | |
'First' | |
).ToLower() -replace '^windows$', 'win') + ('-x86', '-x64')[[Environment]::Is64BitOperatingSystem] | |
Write-Verbose "Copying platform-appropriate native libraries ($rid/native) directly to cache directory '$cacheDir'..." | |
Copy-Item -Force -Path $CACHE_HELPERPROJECT/bin/Release/*/publish/runtimes/$rid/native/* $cacheDir | |
if (-not $?) { Write-Error "Failed to copy platform-appropriate native libraries from subfolder '$rid/native' to cache folder '$cacheDir'"; continue } | |
} | |
} | |
Write-Verbose "Cleaning up helper project..." | |
Remove-Item -LiteralPath $CACHE_HELPERPROJECT/bin, $CACHE_HELPERPROJECT/obj -Recurse -Force | |
Copy-Item $projFile_Pristine $projFile | |
} | |
Write-Verbose "Loading assemblies from package '$packageName' via cache directory '$cacheDir'..." | |
# ?? Is it always sufficient to load just the DLL named for the package? | |
# !! Note that wildcard-based loading with *.dll (Add-Type -Path "$cacheDir/*") | |
# !! would fail if *native* libraries are present (on Windows) that we've had to copy directly | |
# !! alongside the managed assemblies. | |
Add-Type -LiteralPath "$cacheDir/$packageName.dll" | |
} | |
} | |
} # end of end block | |
} # Add-NuGetType | |
# -------------------------------- | |
# GENERIC INSTALLATION HELPER CODE | |
# -------------------------------- | |
# Provides guidance for making the function persistently available when | |
# this script is either directly invoked from the originating Gist or | |
# dot-sourced after download. | |
# IMPORTANT: | |
# * DO NOT USE `exit` in the code below, because it would exit | |
# the calling shell when Invoke-Expression is used to directly | |
# execute this script's content from GitHub. | |
# * Because the typical invocation is DOT-SOURCED (via Invoke-Expression), | |
# do not define variables or alter the session state via Set-StrictMode, ... | |
# *except in child scopes*, via & { ... } | |
if ($MyInvocation.Line -eq '') { | |
# Most likely, this code is being executed via Invoke-Expression directly | |
# from gist.github.com | |
# To simulate for testing with a local script, use the following: | |
# Note: Be sure to use a path and to use "/" as the separator. | |
# iex (Get-Content -Raw ./script.ps1) | |
# Derive the function name from the invocation command, via the enclosing | |
# script name presumed to be contained in the URL. | |
# NOTE: Unfortunately, when invoked via Invoke-Expression, $MyInvocation.MyCommand.ScriptBlock | |
# with the actual script content is NOT available, so we cannot extract | |
# the function name this way. | |
& { | |
param($invocationCmdLine) | |
# Try to extract the function name from the URL. | |
$funcName = $invocationCmdLine -replace '^.+/(.+?)(?:\.ps1).*$', '$1' | |
if ($funcName -eq $invocationCmdLine) { | |
# Function name could not be extracted, just provide a generic message. | |
# Note: Hypothetically, we could try to extract the Gist ID from the URL | |
# and use the REST API to determine the first filename. | |
Write-Verbose -Verbose "Function is now defined in this session." | |
} | |
else { | |
# Indicate that the function is now defined and also show how to | |
# add it to the $PROFILE or convert it to a script file. | |
Write-Verbose -Verbose @" | |
Function `"$funcName`" is now defined in this session. | |
* If you want to add this function to your `$PROFILE, run the following: | |
"``nfunction $funcName {``n`${function:$funcName}``n}" | Add-Content `$PROFILE | |
* If you want to convert this function into a script file that you can invoke | |
directly, run: | |
"`${function:$funcName}" | Set-Content $funcName.ps1 -Encoding $('utf8' + ('', 'bom')[[bool] (Get-Variable -ErrorAction Ignore IsCoreCLR -ValueOnly)]) | |
"@ | |
} | |
} $MyInvocation.MyCommand.Definition # Pass the original invocation command line to the script block. | |
} | |
else { | |
# Invocation presumably as a local file after manual download, | |
# either dot-sourced (as it should be) or mistakenly directly. | |
& { | |
param($originalInvocation) | |
# Parse this file to reliably extract the name of the embedded function, | |
# irrespective of the name of the script file. | |
$ast = $originalInvocation.MyCommand.ScriptBlock.Ast | |
$funcName = $ast.Find( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false).Name | |
if ($originalInvocation.InvocationName -eq '.') { | |
# Being dot-sourced as a file. | |
# Provide a hint that the function is now loaded and provide | |
# guidance for how to add it to the $PROFILE. | |
Write-Verbose -Verbose @" | |
Function `"$funcName`" is now defined in this session. | |
If you want to add this function to your `$PROFILE, run the following: | |
"``nfunction $funcName {``n`${function:$funcName}``n}" | Add-Content `$PROFILE | |
"@ | |
} | |
else { | |
# Mistakenly directly invoked. | |
# Issue a warning that the function definition didn't take effect and | |
# provide guidance for reinvocation and adding to the $PROFILE. | |
Write-Warning @" | |
This script contains a definition for function "$funcName", but this definition | |
only takes effect if you dot-source this script. | |
To define this function for the current session, run: | |
. "$($originalInvocation.MyCommand.Path)" | |
"@ | |
} | |
} $MyInvocation # Pass the original invocation info to the helper script block. | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment