2

This will be a bit of a deep dive.

Nuget packages.config and packages folders are nice. They're pinned by default, they're a single point-of-truth for the dependencies of your powershell script, and the things you download are local to the packages folder instead of being global to the system.

Meanwhile, Powershell modules are global to the system, and installing a powershell module into a system changes behavior for every use of that system. So if I'm providing powershell code to teammates, I feel like it's rude to muddy up their system.

So, I've taken to using nuget.exe to fetch dependencies for my powershell code. Reference some modules in a packages.config file, and then nuget install them down, and then import them by path in my powershell code.

Nice encapsulated powershell where I don't have to think so much about the state of the messy shared powershell packages folder on the machine I'm running on.

But this is a little clumsy because I'm importing by path, and dependencies of dependencies don't work nicely.

So what I was thinking was: just add the Nuget Packages folder to my module search path temporarily! This almost works. The problem is the folder layout: Nuget packages are downloaded in the format PackageName.VersionNumber, while the module search looks for PackageName/VersionNumber. Nuget uses that format for its internal cache! But it doesn't use that for its packages folder.

Is there any way to fix that? To tell Nuget to lay out its packages folder cache-style instead of packages style? Then it would be a simple 3-line

& nuget restore $PSScriptRoot/packages.config
$Env:PsModulePath += ";$PSScriptRoot/packages"
import module FooModule
import module BarModule

Thanks.

Pxtl
  • 880
  • 8
  • 18

1 Answers1

2

tl;dr

  • As of version 7.3.3, PowerShell lacks proper support for general-purpose (non-PowerShell) NuGet packages, such as those hosted publicly at nuget.org and downloadable via nuget.exe

    • A potential future enhancement is discussed in GitHub issue #6724, which suggests making the use of NuGet packages in PowerShell easier by extending Add-Type

    • This answer shows a - cumbersome - workaround that requires the presence of the .NET SDK.

  • However, in the realm of PowerShell modules you can bundle other modules and .NET assemblies for module-private use, and it is possible to adapt this technique to bundling private-use modules with scripts too, as discussed below.

    • Caveat:

      • A fundamental limitation is that there can be assembly version conflicts, both with general-purpose .NET assemblies as well as with binary PowerShell modules (those implemented as .NET assemblies themselves), because all .NET assemblies are currently loaded session-globally, into the same load context - script-only modules are not affected, however.

      • The version of a given assembly that is loaded first into a PowerShell session "wins" - unfortunately, there is no direct support for side-by-side loading of assemblies. Depending on the use case, solutions may be possible, but they are far from trivial - see this answer and Resolving PowerShell module assembly dependency conflicts.


Bundling modules and assemblies for private use in PowerShell modules / scripts:

PowerShell modules offer bundling of other modules with a given module, namely via the NestedModules entry in the module's manifest.

Such nested modules are loaded into the module's scope only (not visible outside of it), and can coexist with potentially globally imported modules of the same name, such as those available via the directories listed in the $env:PSModulePath environment variable.

Similarly, you can bundle general (non-PowerShell) assemblies with your module, using the RequiredAssemblies module-manifest entry.

Note the caveat re assembly version conflicts above.


Therefore, your best option is to distribute your code to your teammates in the forms of modules whose dependencies are provided as nested modules:

  • Place those modules directly in your module's root folder and reference them by name only in the NestedModules manifest entry.

  • If you've already installed those modules on the machine where you author the module (with Install-Module), you can simply copy them to your module's root folder - use
    Split-Path (Get-Module -ListAvailable $yourModulesToNest).Path to find their location.


If it's a script (*.ps1) you want to distribute, you can still use the bundling approach, although it takes more work:

  • Put the bundled modules in, say, a <ScriptFileName>_modules subfolder (which you'll have to distribute with your script; <ScriptFileName> represent your script's file name.

  • Then import them as follows:

    Get-ChildItem -LiteralPath ${PSCommandPath}_modules | Import-Module -Scope Local
    
    • Caveat: Unlike with the nested-module approach, any custom class and enum definitions from the bundled modules are not imported into the script; while that aspect can be addressed by using using module statements instead, as of PowerShell 7.3.3 they invariably import modules globally, which bears precisely the risk of interfering with globally available modules that you're trying to avoid.
      For the sake of completeness: to make using module work, all bundled modules most be imported individually by their literal names; e.g., for a bundled module named Foo from a script named script.ps1:

      # CAVEAT: Implicitly imports the module GLOBALLY,
      #         which can interfere with the modules that
      #         other code in the session sees.
      # Do this for every bundled module; must be at the start of the script.
      using module .\script.ps1_modules\Foo 
      
mklement0
  • 382,024
  • 64
  • 607
  • 775