Public/Import-VcConfigMgrApplication.ps1

Function Import-VcConfigMgrApplication {
    <#
        .SYNOPSIS
            Creates Visual C++ Redistributable applications in a ConfigMgr site.

        .DESCRIPTION
            Creates an application in a Configuration Manager site for each Visual C++ Redistributable and includes setting whether the Redistributable can run on 32-bit or 64-bit Windows and the Uninstall key for detecting whether the Redistributable is installed.

            Use Get-VcList and Get-VcRedist to download the Redistributable and create the array of Redistributables for importing into ConfigMgr.

            Applications will be imported into the ConfigMgr with the default properties similar to the following:

                Location: Applications\VcRedists
                Name: Visual C++ Redistributable 2019 x86 14.28.29913.0
                Administrator comments: Microsoft Visual C++ Redistributable 2019 x86 14.28.29913.0 imported by Import-VcConfigMgrApplication
                Publisher: Microsoft
                Software version: 14.28.29913.0
                Language: en-US
                Date published: 18/03/2021
                Localized application name: Visual C++ Redistributable 2019 x86 14.28.29913.0
                User documentation: https://visualstudio.microsoft.com/vs/support/
                Link text: https://www.visualstudio.com/downloads/
                Privacy URL: https://go.microsoft.com/fwlink/?LinkId=521839
                Keywords: Visual C++ Redistributable

                Deployment type:
                Name: SCRIPT_Visual C++ Redistributable for Visual Studio 2019
                Technology: Script Installer
                Administrator comments: Generated by Import-VcConfigMgrApplication
                Content location: \\configmgr\Applications\VcRedists\2019\14.28.29913.0\x86\
                Installation program: VC_redist.x86.exe /install /quiet /norestart
                Uninstall program: "%ProgramData%\Package Cache\{03d1453c-7d5c-479c-afea-8482f406e036}\VC_redist.x86.exe" /uninstall /quiet /noreboot
                Detection method: (Registry path)
        
        .NOTES
            Author: Aaron Parker
            Twitter: @stealthpuppy

        .LINK
            https://stealthpuppy.com/VcRedist/import-vcconfigmgrapplication.html

        .PARAMETER VcList
            An array containing details of the Visual C++ Redistributables from Get-VcList.

        .PARAMETER Path
            A folder containing the downloaded Visual C++ Redistributables.

        .PARAMETER CMPath
            Specify a UNC path where the Visual C++ Redistributables will be distributed from

        .PARAMETER SMSSiteCode
            Specify the Site Code for ConfigMgr app creation.

        .PARAMETER AppFolder
            Import the Visual C++ Redistributables into a sub-folder. Defaults to "VcRedists".

        .PARAMETER Silent
            Add a completely silent command line install of the VcRedist with no UI. The default install is passive.

        .EXAMPLE
            $VcList = Get-VcList
            Save-VcRedist -VcList $VcList -Path "C:\Temp\VcRedist"
            Import-VcConfigMgrApplication -VcList $VcList -Path "C:\Temp\VcRedist" -CMPath "\\server\share\VcRedist" -SMSSiteCode LAB

            Description:
            Download the supported Visual C++ Redistributables to "C:\Temp\VcRedist", copy them to "\\server\share\VcRedist" and import as applications into the ConfigMgr site LAB.
    #>

    [Alias('Import-VcCmApp')]
    [CmdletBinding(SupportsShouldProcess = $True, HelpURI = "https://stealthpuppy.com/VcRedist/import-vcconfigmgrapplication.html")]
    [OutputType([System.Management.Automation.PSObject])]
    Param (
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline)]
        [ValidateNotNull()]
        [System.Management.Automation.PSObject] $VcList,

        [Parameter(Mandatory = $True, Position = 1)]
        [ValidateScript( { If (Test-Path -Path $_ -PathType 'Container' -ErrorAction "SilentlyContinue") { $True } Else { Throw "Cannot find path $_." } })]
        [System.String] $Path,

        [Parameter(Mandatory = $True, Position = 2)]
        [System.String] $CMPath,

        [Parameter(Mandatory = $True, Position = 3)]
        [ValidateScript( { If ($_ -match "^[a-zA-Z0-9]{3}$") { $True } Else { Throw "$_ is not a valid ConfigMgr site code." } })]
        [System.String] $SMSSiteCode,

        [Parameter(Mandatory = $False, Position = 4)]
        [ValidatePattern('^[a-zA-Z0-9]+$')]
        [System.String] $AppFolder = "VcRedists",

        [Parameter(Mandatory = $False)]
        [System.Management.Automation.SwitchParameter] $Silent,

        [Parameter(Mandatory = $False)]
        [System.Management.Automation.SwitchParameter] $NoCopy,

        [Parameter(Mandatory = $False, Position = 5)]
        [ValidatePattern('^[a-zA-Z0-9]+$')]
        [System.String] $Publisher = "Microsoft",

        [Parameter(Mandatory = $False, Position = 6)]
        [ValidatePattern('^[a-zA-Z0-9\+ ]+$')]
        [System.String] $Keyword = "Visual C++ Redistributable"
    )

    Begin {
        #region CMPath will be the network location for copying the Visual C++ Redistributables to
        try {
            Set-Location -Path $Path -ErrorAction "SilentlyContinue"
        }
        catch [System.Exception] {
            Write-Warning -Message "$($MyInvocation.MyCommand): Failed to set location to [$Path]."
            Throw $_.Exception.Message
            Break
        }
        Write-Verbose -Message "$($MyInvocation.MyCommand): Set location to [$Path]."
        #endregion
        
        #region Validate $CMPath
        If (Resolve-Path -Path $CMPath) {
            $CMPath = $CMPath.TrimEnd("\")

            #region If the ConfigMgr console is installed, load the PowerShell module; Requires PowerShell module to be installed
            If (Test-Path -Path env:SMS_ADMIN_UI_PATH -ErrorAction "SilentlyContinue") {
                try {            
                    # Import the ConfigurationManager.psd1 module
                    Write-Verbose -Message "$($MyInvocation.MyCommand): Importing module: $($env:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1."
                    Import-Module "$($env:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1" -Verbose:$False > $Null

                    # Create the folder for importing the Redistributables into
                    If ($AppFolder) {
                        $DestFolder = "$($SMSSiteCode):\Application\$($AppFolder)"
                        If ($PSCmdlet.ShouldProcess($DestFolder, "Creating")) {
                            try {
                                New-Item -Path $DestFolder -ErrorAction "SilentlyContinue" > $Null
                            }
                            catch [System.Exception] {
                                Write-Warning -Message "$($MyInvocation.MyCommand): Failed to create folder: [$DestFolder]."
                                Throw $_.Exception.Message
                                Break
                            }
                        }
                        If (Test-Path -Path $DestFolder -ErrorAction "SilentlyContinue") {
                            Write-Verbose -Message "$($MyInvocation.MyCommand): Importing into: [$DestFolder]."
                        }
                    }
                    Else {
                        Write-Verbose -Message "$($MyInvocation.MyCommand): Importing into: [$($SMSSiteCode):\Application]."
                        $DestFolder = "$($SMSSiteCode):\Application"
                    }
                }
                catch [System.Exception] {
                    Write-Warning -Message "$($MyInvocation.MyCommand): Could not load ConfigMgr Module. Please make sure that the ConfigMgr Console is installed."
                    Throw $_.Exception.Message
                    Break
                }
            }
            Else {
                Write-Warning -Message "$($MyInvocation.MyCommand): Cannot find environment variable SMS_ADMIN_UI_PATH. Is the ConfigMgr console and PowerShell module installed?"
                Break
            }
            #endregion
        }
        Else {
            Write-Warning -Message "$($MyInvocation.MyCommand): Unable to confirm $CMPath exists. Please check that $CMPath is valid."
            Break
        }
        #endregion
    }
    
    Process {
        ForEach ($VcRedist in $VcList) {
            Write-Verbose -Message "Importing VcRedist app: [Visual C++ Redistributable $($VcRedist.Release) $($VcRedist.Architecture) $($VcRedist.Version)]"

            # If SMS_ADMIN_UI_PATH variable exists, assume module imported successfully earlier
            If (Test-Path -Path env:SMS_ADMIN_UI_PATH -ErrorAction "SilentlyContinue") {

                # Import as an application into ConfigMgr
                If ($PSCmdlet.ShouldProcess("$($VcRedist.Name) in $CMPath", "Import ConfigMgr app")) {
                
                    # Create the ConfigMgr application with properties from the manifest
                    If ((Get-Item -Path $DestFolder).PSDrive.Name -eq $SMSSiteCode) {
                        If ($PSCmdlet.ShouldProcess($VcRedist.Name + " $($VcRedist.Architecture)", "Creating ConfigMgr application")) {

                            # Build paths
                            $folder = [System.IO.Path]::Combine((Resolve-Path -Path $Path), $VcRedist.Release, $VcRedist.Version, $VcRedist.Architecture)
                            $ContentLocation = [System.IO.Path]::Combine($CMPath, $VcRedist.Release, $VcRedist.Version, $VcRedist.Architecture)
                            
                            #region Copy VcRedists to the network location. Use robocopy for robustness
                            If ($NoCopy) {
                                Write-Warning -Message "$($MyInvocation.MyCommand): NoCopy specified, skipping copy to $ContentLocation. Ensure VcRedists exist in the target."
                            }
                            Else {
                                If ($PSCmdlet.ShouldProcess("$($folder) to $($ContentLocation)", "Copy")) {
                                    try {
                                        If (!(Test-Path -Path $ContentLocation -ErrorAction "SilentlyContinue")) {
                                            New-Item -Path $ContentLocation -ItemType "Directory" -ErrorAction "SilentlyContinue" > $Null
                                        }
                                    }
                                    catch {
                                        Write-Warning -Message "$($MyInvocation.MyCommand): Failed to create: [$ContentLocation]."
                                        Throw $_.Exception.Message
                                        Break
                                    }
                                    try {
                                        $invokeProcessParams = @{
                                            FilePath     = "$env:SystemRoot\System32\robocopy.exe"
                                            ArgumentList = "*.exe `"$folder`" `"$ContentLocation`" /S /XJ /R:1 /W:1 /NP /NJH /NJS /NFL /NDL"
                                        }
                                        $result = Invoke-Process @invokeProcessParams
                                    }
                                    catch [System.Exception] {
                                        $Target = Join-Path -Path $ContentLocation -ChildPath $(Split-Path -Path $VcRedist.Download -Leaf)
                                        If (Test-Path -Path $Target -ErrorAction "SilentlyContinue") {
                                            Write-Verbose -Message "$($MyInvocation.MyCommand): Copy successful: [$Target]."
                                        }
                                        Else {
                                            Write-Warning -Message "$($MyInvocation.MyCommand): Failed to copy Redistributables from [$folder] to [$ContentLocation]."
                                            Write-Warning -Message "$($MyInvocation.MyCommand): Captured error (if any): [$result]."
                                            Throw $_.Exception.Message
                                            Break
                                        }
                                    }
                                }
                            }
                            #endregion

                            # Change to the SMS Application folder before importing the applications
                            Write-Verbose -Message "$($MyInvocation.MyCommand): Setting location to $($DestFolder)"
                            try {
                                Set-Location -Path $DestFolder -ErrorAction "SilentlyContinue"
                            }
                            catch [System.Exception] {
                                Write-Warning -Message "$($MyInvocation.MyCommand): Failed to set location to [$DestFolder]."
                                Throw $_.Exception.Message
                                Continue
                            }
                                                
                            try {
                                # Splat New-CMApplication parameters, add the application and move into the target folder
                                $ApplicationName = "Visual C++ Redistributable $($VcRedist.Release) $($VcRedist.Architecture) $($VcRedist.Version)"
                                $cmAppParams = @{
                                    Name              = $ApplicationName
                                    Description       = "$Publisher $ApplicationName imported by $($MyInvocation.MyCommand)"
                                    SoftwareVersion   = $VcRedist.Version
                                    LinkText          = $VcRedist.URL
                                    Publisher         = $Publisher
                                    Keyword           = $Keyword
                                    ReleaseDate       = (Get-Date -Format dd/MM/yyyy)
                                    PrivacyUrl        = "https://go.microsoft.com/fwlink/?LinkId=521839"
                                    UserDocumentation = "https://visualstudio.microsoft.com/vs/support/"
                                }
                                $app = New-CMApplication @cmAppParams
                                If ($AppFolder) {
                                    $app | Move-CMObject -FolderPath $DestFolder -ErrorAction "SilentlyContinue" > $Null
                                }
                            }
                            catch [System.Exception] {
                                Write-Warning -Message "$($MyInvocation.MyCommand): Failed to create application $($VcRedist.Name) $($VcRedist.Architecture)."
                                Throw $_.Exception.Message
                                Break
                            }
                            finally {
                                # Write app detail to the pipeline
                                Write-Output -InputObject $app
                            }

                            try {
                                Write-Verbose -Message "$($MyInvocation.MyCommand): Setting location to [$Path]."
                                Set-Location -Path $Path -ErrorAction "SilentlyContinue"
                            }
                            catch [System.Exception] {
                                Write-Warning -Message "$($MyInvocation.MyCommand): Failed to set location to [$Path]."
                                Throw $_.Exception.Message
                                Continue
                            }
                        }

                        # Add a deployment type to the application
                        If ($PSCmdlet.ShouldProcess($("$($VcRedist.Name) $($VcRedist.Architecture) $($VcRedist.Version)"), "Adding deployment type")) {

                            # Change to the SMS Application folder before importing the applications
                            try {
                                Set-Location -Path $DestFolder -ErrorAction "SilentlyContinue"
                            }
                            catch [System.Exception] {
                                Write-Warning -Message "$($MyInvocation.MyCommand): Failed to set location to [$DestFolder]."
                                Throw $_.Exception.Message
                                Break
                            }
                            Write-Verbose -Message "$($MyInvocation.MyCommand): Set location to [$DestFolder]."

                            try {
                                # Create the detection method
                                $params = @{
                                    Hive    = "LocalMachine"
                                    Is64Bit = If ($VcRedist.UninstallKey -eq "64") { $True } Else { $False }
                                    KeyName = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$($VcRedist.ProductCode)"  
                                }
                                $detectionClause = New-CMDetectionClauseRegistryKey @params

                                # Splat Add-CMScriptDeploymentType parameters and add the application deployment type
                                $cmScriptParams = @{
                                    ApplicationName          = $ApplicationName
                                    InstallCommand           = "$(Split-Path -Path $VcRedist.Download -Leaf) $(If ($Silent) { $VcRedist.SilentInstall } Else { $VcRedist.Install })"
                                    ContentLocation          = $ContentLocation
                                    AddDetectionClause       = $detectionClause
                                    DeploymentTypeName       = "SCRIPT_$($VcRedist.Name)"
                                    UserInteractionMode      = "Hidden"
                                    UninstallCommand         = $VcRedist.SilentUninstall
                                    LogonRequirementType     = "WhetherOrNotUserLoggedOn"
                                    InstallationBehaviorType = "InstallForSystem"
                                    Comment                  = "Generated by $($MyInvocation.MyCommand)"
                                }
                                Add-CMScriptDeploymentType @cmScriptParams > $Null
                            }
                            catch [System.Exception] {
                                Write-Warning -Message "$($MyInvocation.MyCommand): Failed to add script deployment type."
                                Throw $_.Exception.Message
                                Break
                            }

                            try {
                                Write-Verbose -Message "$($MyInvocation.MyCommand): Setting location to [$Path]."
                                Set-Location -Path $Path -ErrorAction "SilentlyContinue"
                            }
                            catch [System.Exception] {
                                Write-Warning -Message "$($MyInvocation.MyCommand): Failed to set location to [$Path]."
                                Throw $_.Exception.Message
                                Break
                            }
                        }
                    }
                }
            }
        }
    }

    End {
        try {
            Set-Location -Path $Path -ErrorAction "SilentlyContinue"
        }
        catch [System.Exception] {
            Write-Warning -Message "$($MyInvocation.MyCommand): Failed to set location to [$Path]."
            Throw $_.Exception.Message
        }
        Write-Verbose -Message "$($MyInvocation.MyCommand): Set location to [$Path]."
    }
}