linuxpowershellpowershell-core

Error 'Install-Module is not recognised.' when Linux Azure App Service tries to install PS module


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);
}

Solution

  • Context:


    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:

    Caveat:


    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.

    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]
    

    [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).