Skip to content

Instantly share code, notes, and snippets.

@mklement0
Last active January 10, 2024 18:59
Show Gist options
  • Save mklement0/7436c9e4b2f73d7256498f959f0d5a7c to your computer and use it in GitHub Desktop.
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
<#
Prerequisites: PowerShell v5.1 and above (verified; may also work in earlier versions)
License: MIT
Author: Michael Klement <mklement0@gmail.com>
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