Select Page

In my previous post i wrote about a convoluted way of hiding credentials whereever possible when working with Task Sequences. Fortunately, this entire solution became obsolete when Microsoft decided to offer to mask sensitive data in a Task Sequence with a mere checkbox. First in the 1804 preview, then in release with 1806.

This time, i’d like to share something that is a little less situational. It’s a Powershell script to create Applications and (almost) everything with it. There are plenty similar scripts around on Technet or so, so you may wonder: what makes this one so different? Honestly, probably not that much. If anything, it would be its flexibility and ease of use as you can basically go through it with just a few mouse clicks. It evolved from simply automating repetitive tasks to a handy tool that I use at pretty much all my customers.

 

What does it do:

Depending on the specified parameters it will;

  • Create an Application in an optional specific folder within the ConfigMgr console.
  • Create either a script-based or MSI-based Deployment Type for that Application, including its Detection rule.
  • Create an AD Group with a Prefix based on your naming-convention.
  • Link this Group to an ‘All Apps’ Group, so an admin or device in this group has access to all created apps in one go.
  • Create either a User or Device Collection in an optional specific folder within the ConfigMgr console.
  • Create a Membership rule for said Collection based on the AD Group created earlier.

Once executed (without any parameters), it will load the required modules and prompt you to browse to an installation file.
If you select an MSI file and you have an icon file in your source folder, the script will do everything else. If there’s no icon file; it will ask you to select one. Though you can cancel the prompt and ConfigMgr will use the rather ugly default icon.
If you select a .cmd or .ps1 file, the script will prompt you for an uninstall file and a detection script file. And again an optional extra prompt for an icon file.

 

What doesn’t it do:

It does not create Deployments. When i get around to it i’ll probably add a switch for that too. But since in most cases deployments need to be tested first, so far I’ve always preferred to create these manually.

It has no option to remove stuff. I may or may not integrate that into this script. For now, automating cleanup is something for a future blog post 🙂

 

Requirements:

  • you have your content share set up in a specific way:
    \\server\share\folder\AppVendor\AppName\Version\Bitness(Optional)
    This is needed because the script will fill in several fields such as Manufacturer, Name and Version based on this folder structure. This is then also used to create AD Groups and/or Collection name.
  • you have the ConfigMgr Cmdlets available or installed,
  • you have the AD Cmdlets available or installed,
  • you prepare your (script-based) Application properly.
    In most cases, all you’ll need is an install, uninstall and detection script. For MSI-based installers you have the option to specify arguments or let the script handle everything for you.
    Ideally, you also have an icon file present that is, at the time of writing, no larger than 250x250px.

For example, you could have the following files;

install.cmd:
Calling Setup.exe with some silent parameter from the same folder as the batch file:

"%~dp0setup.exe" /S /Q

 

uninstall.cmd:
Calling Setup.exe with some silent parameter from the same folder as the batch file:

"%~dp0setup.exe" /uninstall /S

 

detect.ps1:
Most properly built installers will write their application info to this location so that it shows up in Add/Remove Programs in Windows. So its fairly reliable to detect a successful installation. You can make your detection as complex as you want. Just make sure the script ends with an exitcode 0 and non-error string. We only return a True and no False (or any other string), as doing so would be picked up by ConfigMgr as a failure.  See this documentation over at Microsoft for valid return values.

        $Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SomeApplicationName'
        If (Test-Path $Key){ echo $true }

Note that 32-bit software on a 64-bit system will redirect to HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall’.

 

The script:

For ease of use, you may want to check some parameter defaults and edit them to match your environment. Most notably the AppPath and GroupOU parameters.

<#
.SYNOPSIS
    Add an application to the Software Center.

.DESCRIPTION
    This script performs the following actions;
    - Adds an Application
    - Adds a Deployment Type to the Application
    - Creates an AD Group
    - Creates device collections that are populated with devices that would be members of previously mentioned groups.

    You need to meet the following requirements:
    - Have your contentshare set up as:
        \\server\share\<FolderName>\<App Publisher>\<Application Name>\<Application Version> 
        Eg; \\SCCMServer01.domain.com\src$\Apps\Promethean\ActivInspire\2.10\setup.exe'

      It is critical to follow this folder layout for the script to function correctly. 
      It assumes that certain parts of the path contain certain values and it does so relative to the location of the setup file.
      If you deviate from the mentioned folder layout, both your application list and Software Center will look...messy.

    - Have access to create and modify AD objects. 
        Specifically, Create and modify Group objects within the OU specified with the -GroupOU parameter
        
    - Have the Configuration Manager Powershell Cmdlets installed.

    - Have the AD Cmdlets installed.
        Either available from the Windows Feature list (installable on a server using Add-WindowsFeature RSAT-AD-PowerShell) or by enabling it in
        the Windows features after installing Windows Remote Server Administrations Tools (RSAT).

.EXAMPLE
    To create an application based on a MSI file, you could do something like:

        .\Add-AppToSoftwareCenter.ps1 -AppPath '\\SCCMServer01.domain.com\src$\Apps\Promethean\ActivInspire\2.10' `
        -AppCategory 'Digiboard software' -MSIInstallerFile '\\SCCMServer01.domain.com\src$\Apps\Promethean\ActivInspire\2.10\setup.msi'
    
    Which will create the following: 
    - An Application called "Promethean ActivInspire 2.10".
    - A Deployment Type for setup.msi with parameters /QN /NoRestart /L*V "C:\Windows\Promethean ActivInspire 2.10.log".
    - The Detection method is automatically set using MSI Product code (GUID).
    - An AD group called 'GG_APP_PROMETHEAN_ACTIVINSPIRE_2.10' which will have a member called GG_APP_ALL.
    - A Collection called 'Promethean ActivInspire 2.10' with a membership query for AD group 'GG_APP_PROMETHEAN_ACTIVINSPIRE_2.10'.

.EXAMPLE
    To create an application based on a Script file, you could do something like:
        
        .\Add-AppToSoftwareCenter.ps1 -ScriptInstallerFile '\\SCCMServer01.domain.com\src$\Apps\Promethean\ActivInspire\2.10\install.cmd' `
        -UninstallFile '\\SCCMServer01.domain.com\src$\Apps\Promethean\ActivInspire\2.10\uninstall.cmd' `
        -DetectionFile '\\SCCMServer01.domain.com\src$\Apps\Promethean\ActivInspire\2.10\detection.ps1'
    
    Which will create the following: 
    - An Application called "Promethean ActivInspire 2.10".
    - A Deployment Type which runs install.cmd and uninstall.cmd as for their respective program actions.
    - Configures a Script-based Detection method based on the Powershell script contents.
    - An AD group called 'GG_APP_PROMETHEAN_ACTIVINSPIRE_2.10' which will have a member called GG_APP_ALL.
    - A Collection called 'Promethean ActivInspire 2.10' with a membership query for AD group 'GG_APP_PROMETHEAN_ACTIVINSPIRE_2.10'.

.EXAMPLE
    Sample Detection script code:
  
        $Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\YourAppsRegistryKey'
        If (Test-Path $Key){ echo $true }

    ConfigMgr will pick up the echo and process the result.

.NOTES  
    Version:        1.7
    Author:         M.Foppen
    Creation Date:  28-06-2016

    28-06-2016 - v1.0:
        - Initial working script

    26-07-2016 - v1.1:
        - Added error handling.
        - Split standard and verbose output.
        - Improved performance by limiting imported CmdLets to only those that are needed.
        - Modified device-collection creation logic to use CollectionID rather than CollectionName as it broke on a rename of the collection.
        - Moved all hardcoded values near the top of the script for easy modification.
        - Updated Help.
        
    04-04-2018 - v1.2:
        - Made script more portable by replacing hardcoded values with parameters.

    28-05-2018 - v1.3:
        - Automated Deployment Type creation based on Installer file extension.
        - Reorganised parameters into parametersets and added support for pipeline input.  
          When running the script without specifying anything, it will prompt to select an installer.
          Based on whether that installer is a MSI or something else (.cmd or .exe), it will then ask to select uninstall and detection scripts.
        - Rewrote Help and integrated most of it in the parameters.

    30-05-2018 - v1.4:
        - Removed AppPath variable for Content Source. You now only need to specify a setup file. The script will derive the path from there.
        - If AppIconPath is not specified, the script will now attempt to fill in the AppIconPath variable by itself
          It will look for a custom.ico file in the same folder containing the setup file. If it can't find one but does find one or more other 
          icon files, it will use the first one. If it can't find any icon files at all, it will prompt you to select one. Cancelling the prompt
          will make ConfigMgr use its default placeholder icon.

    28-06-2018 - v1.5:
        - Improved error and warning handling.
        - Added a switch parameter -AppOnly. When using this switch, no AD Groups or Collections are created, just the Application itself (and its Deployment Type).
        - Added a string parameter -Target. It accepts either 'User' or 'Device' as argument and determines what type of Collection should be created.
        - Trimmed the Distinguished name of the AD Group to its supported maximum of 64 characters. The full name of the Goup is still used but only for the 
          SamAccountName as that can be up to 256 characters. This prevents generating errors on long Group names.
        - Added suppport for Script-based Deployment Type for powershell scripts. It allows the use of .ps1 files for install, uninstall and detection. Note that it
          assumes there are no further parameters in use.
        - Added parameter for EstimatedRunTime. Default value is 10 minutes (the default for the Add-CMMsiDeploymentType and Add-CMScriptDeploymentType Cmdlets is 
          actually 60 minutes).

    10-07-2018 - v1.6:
        - Added -AppPath parameter again to allow just clicking through the whole thing when no arguments are given.
        - Added transcription log.

    10-07-2018 - v1.7:
        - Trimmed build numbers from version when determining AD Group Name to reduce the chance of endimg up with a too long string that then needs to be trimmed to
          something not so-pretty-looking. 
        - Cleaned up code.

.LINK
    http://www.detron.nl

#>

[CmdletBinding(DefaultParameterSetName='MSI')]
    param (

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage="A description of the App as you want it to appear to users in Software Center."
        )][string]$AppDescription,

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='A Category for the App. You can specify multiple categories on a comma-separated list. If the category does not yet exist, it will be created.'
        )][string[]]$AppCategory,

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='UNC path to an icon file. Ideally a single layer image at a resulotion no greater than 250x250px. If the prompt is cancelled (thus none gets specified), ConfigMgr will use the default placeholder icon.'
        )][string]$AppIconPath,

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='The Content Source location containing the installation files.'
        )][string]$AppPath = '\\server\share\Apps',

        [Parameter(
            ParameterSetName='MSI',
            Position=0,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            Mandatory=$false,
            HelpMessage='UNC filepath to an MSI file.'
        )][string]$MSIInstallerFile,

        [Parameter(
            ParameterSetName='MSI',
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='Parameters for the MSI. If none are specified, defaults will be added to allow a basic unattended install with verbose logging in "C:\Windows\Logs".'
        )][string]$Arguments,

        [Parameter(
            ParameterSetName='Script',
            Position=0,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            Mandatory=$false,
            HelpMessage='UNC filepath to an (what classifies in ConfigMgr as Script-based) installation file. This can be a .cmd or .bat file.'
        )][string]$ScriptInstallerFile,

        [Parameter(
            ParameterSetName='Script',
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            mandatory=$true,
            HelpMessage='UNC filepath to an uninstall file. This can be a .cmd or .bat file.'
        )][string]$UninstallFile,
        
        [Parameter(
            ParameterSetName='Script',
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            Mandatory=$true,
            HelpMessage='UNC filepath to a Powershell detection script; thus a .ps1 file. This script itself should end with exitcode 0 and echo a non-error message when detection is succesful. See also: https://docs.microsoft.com/en-us/previous-versions/system-center/system-center-2012-R2/gg682159(v=technet.10)#BKMK_Step4'
        )][string]$DetectionFile,

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='Estimate of how long this install/uninstall takes (in minutes).'
        )][string]$EstimatedRunTime = '10',

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='The folder path in ConfigMgr where the Application should be created. Default location on the "Software Libary" node, is the root under "Overwiew -> Application Management -> Applications".'
        )][string]$ApplicationFolderPath = '.\Application',

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='The folder path in ConfigMgr where the Collection should be created. Default location on the "Assets and Compliance" node, is the root under "Overview -> Device Collections".'
        )][string]$DeviceCollectionFolderPath = '.\DeviceCollection',

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='The folder path in ConfigMgr where the Collection should be created. Default location on the "Assets and Compliance" node, is the root under "Overview -> User Collections".'
        )][string]$UserCollectionFolderPath = '.\UserCollection',

        [ValidateSet('User','Device')]
        [Parameter(ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='Will this be a deployment to User collections or Device collections?'
        )][string]$Target = 'User',

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='Include this switch if you only want the Application and Deployment Types to be created and no AD Group or Collection.'
        )][switch]$AppOnly = $true,

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='The Distinguished Name of the OU in which to create AD group(s) for the App.'
        )][string]$GroupOU = 'OU=SomeOU,DC=YourDomain,DC=local',

        [Parameter(HelpMessage='For administrative purposes. One Group that all other application Groups are a member of in AD.')]
        [string]$AllAppsGroup = 'GG_APP_ALL',

        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='AD Group name prefix. To fit your naming convention.')]
        [string]$GroupNamePrefix = 'GG_APP_'
     )

BEGIN {
    # Set up some variables:
    $LogName = Split-Path $PSCommandPath -Leaf
    Start-Transcript -Path "$AppPath\$LogName.log" -IncludeInvocationHeader 
    $ErrorActionPreference = 'continue'
    # $VerbosePreference = 'continue'
    [int]$errorcount = 0
    [int]$warningcount = 0
    $ScriptPath = Get-Location

    # Import Cmdlets:
    Import-Module ActiveDirectory -Cmdlet New-ADGroup, Add-ADGroupMember, Get-ADDomain
    Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1" -ErrorAction SilentlyContinue

    # Load WinForms:
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
    [System.Windows.Forms.OpenFileDialog]$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $OpenFileDialog.ShowHelp = $false
    
    # If no installerfile was selected:
    if (!($MSIInstallerFile -and $ScriptInstallerFile))
    {
        write-verbose "No Installer specified. Prompting user to select one."
        $OpenFileDialog.Reset()
        $OpenFileDialog.initialDirectory = $AppPath
        $OpenFileDialog.Title = "Select a setup file or installation script."
        $OpenFileDialog.Filter = "Setup file (*.exe, *.msi, *.cmd, *.ps1)|*.exe;*.bat;*.msi;*.cmd;*.ps1|All Files (*.*)|*.*"
        if ($OpenFileDialog.ShowDialog() -eq "OK")
        { 
            $InstallFile = Split-Path $OpenFileDialog.FileName -Leaf
            $AppPath = Split-Path $OpenFileDialog.FileName -Parent
        }
        else { 
            write-error "Cancelled by user. Nothing to do but exit."; Stop-Transcript; break 
        }      
    }
    else
    {
        # Due to the parameterset, it's either one or the other:
        if ($MSIInstallerFile)
        { 
            $InstallFile = Split-Path $MSIInstallerFile -Leaf
            $AppPath = Split-Path $MSIInstallerFile -Parent
        }
        if ($ScriptInstallerFile)
        { 
            $InstallFile = Split-Path $ScriptInstallerFile -Leaf 
            $AppPath = Split-Path $ScriptInstallerFile -Parent
        }
    }
    
    # Construct the full AppName by getting rid of the \\server\share\folder bit. And replace the remaining \'s with spaces:
    $AppName = (($AppPath.Split('\',6)).Replace('\',' ') | select -last 1)
    
    # Get the publisher and version from the AppPath string:
    $AppPublisher = ($AppPath.Split('\'))[5]
    $AppVersion = ($AppPath.Split('\'))[7]

    # Define AD Group name by trimming version to just major and minor, then uppercase everything and replace spaces with underscores:
    $AppNameInGroup = (((($AppName.Split('.')) | Select -First 2) -join '.').toUpper()).Replace(" ","_")
    $GroupProd = $GroupNamePrefix + $AppNameInGroup

    # Trim Groups' Distinguished Name to max length. The full name (up to 256 characters) is still used for the Groups' SamAccountName.
    $TrimmedGroupName = $GroupProd[0..63] -join "" 
   
    # Prompt for an Uninstall script:
    if (!($UninstallFile))
    {
        if (!($InstallFile.EndsWith('msi')))
        {
            Write-Verbose "Script installer selected. Uninstall script required. Prompting user to select one."
            $OpenFileDialog.Reset()
            $OpenFileDialog.initialDirectory = $AppPath
            $OpenFileDialog.Title = "Select an Uninstall script."
            $OpenFileDialog.Filter = "Uninstall script (*.exe, *.bat, *.cmd, *.ps1)|*.exe;*.bat;*.cmd;*.ps1|All Files (*.*)|*.*"
            if ($OpenFileDialog.ShowDialog() -eq "OK")
            { 
                $UninstallFile = Split-Path $OpenFileDialog.FileName -Leaf
            }
            else 
            {
                $warningcount++
                write-warning "No Installation file selected, No Deployment Type will be added for [$AppName]."
                return
            }
        }
    }

    # Prompt for a Detection script:
    if (!($DetectionFile))
    {
        if (!($InstallFile.EndsWith('msi')))
        {
            Write-Verbose "Script installer selected. Detection script required. Prompting user to select one."
            $OpenFileDialog.Reset()
            $OpenFileDialog.initialDirectory = $AppPath
            $OpenFileDialog.Title = "Select a Detection script."
            $OpenFileDialog.Filter = "Detection script (*.ps1)|*.ps1|All Files (*.*)|*.*"
            if ($OpenFileDialog.ShowDialog() -eq "OK")
            {
                $DetectionFile = $OpenFileDialog.FileName
            }
            else 
            {
                $warningcount++
                write-warning "No Detection file selected, No Deployment Type will be added for [$AppName]. A Detection Clause is required for Script-based Deployment Types."
                return
            }
        }
    }

    # Search for an icon file and use the first one found. If none are found; prompt user to browse to one:
    if (!($AppIconPath))
    {
        write-verbose "Searching for a suitable icon file in [$AppPath]..."
        $icons = Get-Childitem -Path $AppPath -Filter *.ico 
        if ($icons)
        {
            $AppIconPath = $Icons[0].FullName
        }
        else
        {
            write-verbose "No icon found. Prompting user to select one."
            $OpenFileDialog.Reset()
            $OpenFileDialog.initialDirectory = $AppPath
            $OpenFileDialog.Title = "Select an icon file for use in the Software Center or cancel to use the default placeholder."
            $OpenFileDialog.Filter = "Icon Files (*.ico)|*.ico|All Files (*.*)|*.*"
            if ($OpenFileDialog.ShowDialog() -eq "OK")
            {
                $AppIconPath = $OpenFileDialog.FileName
            }
            else
            {
                write-output "Icon selection cancelled. Default icon will be used."
            }
        }
    }
}


PROCESS { 

    # Get and set our CM Drive:
    $SiteCode = Get-PSDrive -PSProvider CmSite
    Try
    { 
        Set-Location "$($SiteCode.Name):\" -ErrorAction stop | write-verbose
        write-output "Successfully set location to [$($SiteCode.Name):\]" 
    }
    Catch
    { 
        write-error "Failed to set location to CM drive because [$($_.Exception.Message)]. This is a terminating error."
        Stop-Transcript
        break 
    }

    # Create the Application:
    Try
    { 
        New-CMApplication -Name $AppName -Publisher $AppPublisher -SoftwareVersion $AppVersion -LocalizedApplicationName $AppName -Description $AppDescription -LocalizedApplicationDescription $AppDescription | Out-String | write-verbose
        write-output "Successfully created [$AppName]" 
    } 
    Catch
    { 
        $errorcount++ 
        write-error "Failed to create [$AppName] at [$ApplicationFolderPath] because [$($_.Exception.Message)]" 
    }

    # Move the Application to the correct folder:
    Try
    { 
        Move-CMObject -InputObject (Get-CMApplication -Name $AppName) -FolderPath $ApplicationFolderPath | Out-String | write-verbose
        write-output "Successfully moved [$AppName] to [$ApplicationFolderPath]" 
    }
    Catch
    { 
        $errorcount++ 
        write-error "Failed to move Application [$AppName] to [$ApplicationFolderPath] because [$($_.Exception.Message)]"
    }

    # If specified, set Application to use a custom icon:
    if ($AppIconPath)
    {
        Try
        { 
            Set-CMApplication -Name $AppName -IconLocationFile $AppIconPath | Out-String | write-verbose
            write-output "Successfully added [$AppIconPath] to [$AppName]"
        }
        Catch
        { 
            $errorcount++ 
            write-error "Failed to [$AppIconPath] to [$AppName] because [$($_.Exception.Message)]" 
        }
    }

    # If a Category is specified, check if it exists, if not create it:
    if ($AppCategory)
    {
        $AppCategory | foreach {
            if (-not(Get-CMCategory -Name $_))
            {
                Try 
                { 
                    New-CMCategory -CategoryType AppCategories -Name $_ | Out-String | write-verbose 
                    write-output "Successfully created Category [$_]" 
                }
                Catch
                {
                    $errorcount++
                    write-error "Failed to create Category [$_] because [$($_.Exception.Message)]" 
                }
            }
        }
           
        # Add Application to category:
        Try
        { 
            Set-CMApplication -Name $AppName -AppCategories $AppCategory -SendToProtectedDistributionPoint $false | Out-String | write-verbose
            write-output "Successfully added [$AppName] to Category [$AppCategory]" 
        }
        Catch
        { 
            $errorcount++
            write-error "Failed to add [$AppCategory] to [$AppName] because [$($_.Exception.Message)]" 
        }
    }

    # Create Deployment Type:
    Try 
    {
        if ($InstallFile.EndsWith('.msi'))
        {
            # If no MSI parameters were set, use these:
            if (!($Arguments)){ $Arguments = "/qn /norestart /l*v `"$env:windir\Logs\$AppName.log`"" }

            # Construct the Command line:       
            $InstallCmd = "msiexec.exe /i `"$InstallFile`" $Arguments"

            # Create MSI-based Deployment Type:
            Write-output "Attempting to read properties of [$InstallFile]..."
            Add-CMMsiDeploymentType -ApplicationName $AppName `
                -ContentLocation (Join-Path $AppPath $InstallFile) `
                -InstallCommand $InstallCmd `
                -AllowClientsToUseFallbackSourceLocationForContent `
                -InstallationBehaviorType InstallForSystem `
                -LogonRequirementType WhetherOrNotUserLoggedOn `
                -EstimatedRuntimeMins $EstimatedRunTime `
                -Force # -Verbose
        }
        else
        {
            # Construct Command line for installation:    
            if ($InstallFile.EndsWith('.ps1')){ $InstallCmd = "powershell.exe -executionpolicy bypass -file `".\$InstallFile`"" }
            else { $InstallCmd = "`"$InstallFile`"" }

            # Construct Command line for uninstallation:
            if ($UninstallFile.EndsWith('.ps1')){ $UninstallCmd = "powershell.exe -executionpolicy bypass -file `".\$UninstallFile`"" }
            else { $UninstallCmd = "`"$UninstallFile`"" }

            # Create Script-based Deployment Type:
            Add-CMScriptDeploymentType -ApplicationName $AppName `
                -DeploymentTypeName $AppName `
                -ContentLocation $AppPath `
                -InstallCommand $InstallCmd `
                -UninstallCommand $UninstallCmd `
                -ScriptType PowerShell `
                -ScriptFile $DetectionFile `
                -AllowClientsToUseFallbackSourceLocationForContent `
                -InstallationBehaviorType InstallForSystem `
                -LogonRequirementType WhetherOrNotUserLoggedOn `
                -EstimatedRuntimeMins $EstimatedRunTime `
                -Force # -Verbose
        }
    }
    catch
    {
        $errorcount++
        write-error "Failed to add Deployment Type for [$AppName] because [$($_.Exception.Message)]."   
    }

    if (!($AppOnly))
    {   
        # Create AD Groups:
        Try
        { 
            New-ADGroup -Name $TrimmedGroupName -SamAccountName $GroupProd -Path $GroupOU -GroupScope Global | write-verbose
            write-output "Successfully created [$GroupProd] in [$GroupOU]" 
        } 
        Catch
        { 
            $errorcount++
            write-error "Failed to create $GroupProd because [$($_.Exception.Message)]"
        }

        # Add Group to the 'All Apps' group (if there is one):
        if ($AllAppsGroup)
        {
            Try 
            {    
                Add-ADGroupMember $GroupProd -Members $AllAppsGroup | write-verbose
                write-output "Successfully added [$GroupProd] to [$AllAppsGroup]" 
            } 
            Catch 
            { 
                $errorcount++
                write-error "Failed to add [$GroupProd] to [$AllAppsGroup] because [$($_.Exception.Message)]"
            }
        }
    
        # Get domain name for use in the collection membership query:
        if ($NetBIOSName = (Get-ADDomain).NetBIOSName){ write-output "Successfully obtained NetBIOS name for domain: [$NetBIOSName]" }

        # Create Device Collection:    
        if ($Target -eq 'Device')
        {
            $LimitingCollectionID = 'SMS00001' # Default ID for the 'All Systems' collection.
            Try
            { 
                New-CMDeviceCollection -Name $AppName -LimitingCollectionID $LimitingCollectionID -RefreshType Both | Out-String | write-verbose
                write-output "Successfully created device collection for [$AppName]" 
            }
            Catch
            { 
                $errorcount++
                write-error "Failed to create Device Collection for [$AppName] with LimitingCollection [$LimitingCollectionID] because [$($_.Exception.Message)]" 
            }
    
            # Move the collection to the correct folder:
            Try
            { 
                Move-CMObject -InputObject (Get-CMCollection -Name $AppName) -FolderPath $DeviceCollectionFolderPath | Out-String | write-verbose 
                write-output "Successfully moved Device collection for [$AppName] to [$DeviceCollectionFolderPath]"    
            }
            Catch
            { 
                $errorcount++
                write-warning "Failed to move Device collection [$AppName] to [$DeviceCollectionFolderPath] because [$($_.Exception.Message)]"
            }

            # Create the membership query for the collection:
            Try
            {
                $QueryEx = "select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client from SMS_R_System where SMS_R_System.SecurityGroupName = `"$NetBIOSName\\$GroupProd`""
                Add-CMDeviceCollectionQueryMembershipRule -CollectionName $AppName -QueryExpression $QueryEx -RuleName $GroupProd | Out-String | write-verbose 
                write-output "Successfully added membership query to [$AppName]"         
            }
            Catch
            { 
                $errorcount++
                write-error "Failed to set membership query [$QueryEx] on collection [$AppName] because [$($_.Exception.Message)]"
            }
        }

        # Create User Collection:
        $LimitingCollectionID = 'SMS00002' # Default ID for the 'All Users' collection.
        if ($Target -eq 'User')
        {
            Try
            {
                New-CMUserCollection -Name $AppName -LimitingCollectionId $LimitingCollectionID -RefreshType Both | Out-String | Write-Verbose
                write-output "Successfully created User collection for [$AppName]" 
            }
            Catch
            { 
                $errorcount++
                write-error "Failed to create User Collection for [$AppName] with LimitingCollection [$LimitingCollectionID] because [$($_.Exception.Message)]" 
            }

            # Move the collection to the correct folder:
            Try
            { 
                Move-CMObject -InputObject (Get-CMCollection -Name $AppName) -FolderPath $UserCollectionFolderPath | Out-String | write-verbose 
                write-output "Successfully moved User collection for [$AppName] to [$UserCollectionFolderPath]"    
            }
            Catch
            { 
                $errorcount++
                write-warning "Failed to move User collection [$AppName] to [$UserCollectionFolderPath] because [$($_.Exception.Message)]"
            }

            # Create the membership query for the collection:
            Try
            {
                $QueryEx = "select * from SMS_R_User where SMS_R_User.UserGroupName = `"$NetBIOSName\\$GroupProd`""
                Add-CMUserCollectionQueryMembershipRule -CollectionName $AppName -QueryExpression $QueryEx -RuleName $GroupProd | Out-String | Write-Verbose
                write-output "Successfully added membership query to [$AppName]"         
            }
            Catch
            { 
                $errorcount++
                write-error "Failed to set membership query [$QueryEx] on collection [$AppName] because [$($_.Exception.Message)]"
            }
        }
    }
}


END {
    if ($errorcount -eq 0){ write-output "All done without any errors. Yey!" }
    else { write-output "We've encountered [$warningcount] warning(s) and [$errorcount] error(s). Go read the colorful text in your console to see what went wrong." }

    Set-Location $ScriptPath
    Stop-Transcript
    Pause
}

If you have any questions or comments, i’d be happy to hear them.