Context:
PowerShell (Core) SDK projects built with the PowerShell NuGet package - that is, applications or libraries that host their own copy of PowerShell - do not bundle the same non-built-in modules that stand-alone PowerShell (Core) 7+ installations do.
Notably, this means that a PowerShell SDK project does not come with the PowerShellGet
module that the Install-Module
cmdlet is a part of.
Specifically, an SDK project bundles only the following modules:
Microsoft.PowerShell.Host
Microsoft.PowerShell.Management
Microsoft.PowerShell.Security
Microsoft.PowerShell.Utility
On Windows, where there is a predefined PSModulePath
environment variable ($env:PSModulePath
) that points to legacy Windows PowerShell (all-users only) directories even when running an executable from outside a PowerShell session, you should still be able to call Install-Module
, which - in the absence of a -Scope
argument - would result in a user-level installation of the targeted module.
- Due to PowerShell itself augmenting
PSModulePath
on startup (see below), its user-level module root directory will be targeted ($HOME\Documents\PowerShell\Modules
on Windows), which means that stand-alone PowerShell (Core) 7+ sessions will see any installed module there too. (Similarly, a -Scope AllUsers
installation from a PowerShell (Core) SDK project would install to the all-users PowerShell (Core) 7+ location, $env:ProgramFiles\PowerShell\Modules
on Windows.)
On Unix-like platforms - such as in your case (Linux) - there is no predefined PSModulePath
environment variable, which explains your symptom.
- While PowerShell itself (in both editions) defines
PSModulePath
with default values (also for user-specific locations) when it starts up, in SDK projects it will use the directory of the hosting application as the directory for its system modules, i.e. the ones bundled with PowerShell itself.
Upshot:
An application / library hosting PowerShell may find a PowerShellGet
module (which hosts the Install-Module
cmdlet) via a preexisting stand-alone PowerShell installation; the same applies to all system modules, i.e. those that come bundled with such an installation:
Always on Windows, given that Windows PowerShell ships with Windows.
If you happen to start your application from a PowerShell (Core) 7+ session, your application will use the latter's PowerShellGet
module.
As an aside re non-system modules:
- If there happens to be a stand-alone PowerShell 7+ installation, your application will also see the modules that were installed by the user there, typically via
Install-Module
.
Only incidentally, if at all, on Unix-like platforms:
If you happen to start your application from a PowerShell 7+ session (as opposed to from, say, Bash), your application will use the latter's PowerShellGet
module.
Otherwise, your application won't find that module.
As an aside re non-system modules:
- If there happens to be a stand-alone PowerShell 7+ installation, your application will also see the modules that were installed by the user there, typically via
Install-Module
.
Caveat:
- Even if present, use of
Install-Module
will invariably target the module directories of the stand-alone PowerShell installation that the module is "borrowed" from - which may be undesired.
Custom on-demand installation of PowerShell modules from applications hosting their own copy of PowerShell:
Since you cannot assume the presence of the Install-Module
cmdlet on non-Windows platforms, you'll need custom code to download and install modules on demand.
While this is obviously cumbersome, it has the advantage of letting you install private copies of the modules you need, in a local directory of your choice.
A potential alternative is to download the PowerShellGet
and PackageManagement
modules at development time and bundle them with your application / library - but this will again install to module directories that are shared with stand-alone PowerShell (Core) installations.
The following code automates the manual steps documented in Manual Package Download for directly downloading and installing a module from the PowerShell Gallery:
# Make the Write-Verbose statements below produce output.
# Set to $false to silence them.
$verbose = $true
# The name(s) of the module(s) to download.
# If a module has *dependencies*, i.e. requires other modules to function,
# append them to the array.
# SEE NOTE ABOUT VERSIONS BELOW.
$requiredModules = @('ExchangeOnlineManagement')
# Where to install manually downloaded modules.
# Note: Be sure to use a FULL PATH.
$modulesRootDir = "$HOME\.MyApp\Modules"
# Add the root dir. of all manually installed modules to $env:PSModulePath,
# if necessary.
if (($env:PSModulePath -split [IO.Path]::PathSeparator) -notcontains $modulesRootDir) {
$env:PSModulePath += [IO.Path]::PathSeparator + $modulesRootDir
}
# Determine which modules need to be dowloaded, if any.
$missingModules =
Compare-Object -PassThru $requiredModules @(Get-Module -ListAvailable $requiredModules | ForEach-Object Name)
# Download and install any missing modules.
foreach ($moduleName in $missingModules) {
# Silence the progress display during download and ZIP archive extraction.
# Note: For this to be effective for Expand-Archive, the *global* variable must be set.
$prevProgressPreference = $global:ProgressPreference
$global:ProgressPreference = 'SilentlyContinue'
try {
# NOTE RE VERSIONING:
# For simplicity, this code does NOT support versioning, which means:
# * The *latest stable version* of each module is downloaded.
# * Such a version is placed directly in directory named for the module,
# i.e. installation of *multiple versions*, side by side - which would
# require version-specific subdirs. - is *not* supported here.
# To download a specific version, simply append /<version> to the URL below, e.g.:
# https://www.powershellgallery.com/api/v2/package/PSReadLine/2.2.6
# Derive the download URL, the local installation dir., and path of the temp. ZIP file.
$downloadUrl = 'https://www.powershellgallery.com/api/v2/package/{0}' -f $moduleName
$moduleDir = Join-Path $modulesRootDir $moduleName
$tempZipFile = Join-Path $moduleDir "$moduleName.zip"
Write-Verbose -Verbose:$verbose "Downloading and installing module $moduleName to $moduleDir..."
# Make sure the target directory exists.
$null = New-Item -ErrorAction Stop -ItemType Directory -Force $moduleDir
# Download the *.nupkg file, as a *.zip file (which it technically is)
Invoke-WebRequest -ErrorAction Stop $downloadUrl -OutFile $tempZipFile
# Extract the contents of the *.zip file.
Expand-Archive -ErrorAction Stop -Force -LiteralPath $tempZipFile -DestinationPath $moduleDir
# Clean up files that aren't needed locally.
Get-Item $moduleDir\* -Include *.zip, _rels, '`[Content_Types`].xml', *.nuspec, package |
Remove-Item -Recurse -Force
}
finally {
$global:ProgressPreference = $prevProgressPreference
}
}
# Now you can import the modules - either explicitly, as in this example,
# or implicitly, by module auto-loading via $env:PSModulePath
Write-Verbose -Verbose:$verbose "Importing module $($requiredModules[0])..."
Import-Module -ErrorAction Stop $requiredModules[0]