I have an ASP.NET Core web app (Linux) as an Azure App Service and a page tries to install the ExchangeOnlineManagement PowerShell module. The script needs to connect to exchange and call Get-EXOMailbox. The script reports PowerShell v7.3.6.
I get this error: "The term 'Install-Module' is not recognized as a name of a cmdlet, function, script file, or executable program"
var scriptContents = "if(-not (Get-Module ExchangeOnlineManagement -ListAvailable))" + Environment.NewLine +
"{ " + Environment.NewLine +
"Write-Host $PSVersionTable.PSVersion" + Environment.NewLine +
"Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force" + Environment.NewLine +
"}";
using (PowerShell ps = PowerShell.Create())
{
ps.AddScript(scriptContents);
var pipelineObjects = await ps.InvokeAsync().ConfigureAwait(false);
}
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 (as of PowerShell 7.4), an SDK project bundles only the following, built-in[1] modules:
Microsoft.PowerShell.Host
Microsoft.PowerShell.Management
Microsoft.PowerShell.Security
Microsoft.PowerShell.Utility
And does not come with the following external ones:
PowerShellGet
PackageManagement
Microsoft.PowerShell.PSResourceGet
Microsoft.PowerShell.Archive
ThreadJob
PSReadLine
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.
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.
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:
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:
Install-Module
.Caveat:
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 - see this answer.
The following code automates the manual steps documented in Manual Package Download for directly downloading and installing a module from the PowerShell Gallery:
Specify the full path of the local directory below in which modules downloaded on demand should be installed.
Limitations of the code:
Dependencies, i.e. additional modules that your target module(s) require to function must be explicitly downloaded as well.
For simplicity, the code simply downloads the latest stable version of the specified (module), though adding support for versioning doesn't require much more effort.
See the source-code comments for details.
# 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]
[1] Built-in refers to those modules whose implementing assemblies (*.dll
) are part of PowerShell itself (built directly from the source-code repository), whereas the additional, external modules that a stand-alone PowerShell installation ships with are copied from elsewhere as part of the packaging process. In the $PSHOME/Modules
folder, you can distinguish between those two module types as follows: built-in modules have only a *.psd1
file in their folder (which references the relevant *.dll
located directly in $PSHOME
).