Public/New-APFDeployment.ps1

function New-APFDeployment {
    <#
    .SYNOPSIS
        Creates an Application Packaging Framework (APF) deployment package for Intune.

    .DESCRIPTION
        The New-APFDeployment function creates a folder named after the application in DestinationFolder,
        copies the installer and any additional files into it, adds the APF installer scripts from the
        module's Application template and fills in config.installer.json and the detection script.
        Optionally it also wraps the folder into a .intunewin package with IntuneWinAppUtil.exe.

        For MSI files the name and version are read from the MSI when not supplied (Windows only). For
        EXE files the file name and file version are used.

        When the application folder already exists you are asked before it is deleted and recreated.

    .PARAMETER Name
        The name of the application. It is written into the exported configuration files and used as the
        folder name. When omitted it is read from the installer file.

    .PARAMETER Version
        The version of the application in the format x.x.x.x. When omitted it is read from the installer file.

    .PARAMETER Target
        The installation context: 'system' or 'user'. Default is 'system'.

    .PARAMETER InstallSwitches
        The command-line switches used to install the application.

    .PARAMETER UninstallSwitches
        The command-line switches used to uninstall the application.

    .PARAMETER UninstallPath
        The path to the uninstall executable or file.

    .PARAMETER Path
        The path to the installer file (.msi or .exe).

    .PARAMETER IncludedFiles
        Paths to additional files to include in the package.

    .PARAMETER DestinationFolder
        The folder in which the application folder is created. Default is the current directory.

    .PARAMETER CreateIntuneWinPackage
        Also creates a .intunewin package in DestinationFolder. IntuneWinAppUtil.exe is downloaded to the
        per-user tool folder (LocalApplicationData\tcs.intune.packaging) after confirmation when it is missing.

    .OUTPUTS
        System.String
        Messages that describe where the package was created and which install commands to use.

    .EXAMPLE
        New-APFDeployment -Path "C:\Installers\MyApp.msi" -Name "MyApp" -Version "1.0.0.0"

        Creates an APF deployment package for MyApp version 1.0.0.0 in the current directory.

    .EXAMPLE
        New-APFDeployment -Path "C:\Installers\Setup.exe" -InstallSwitches "/S" -UninstallSwitches "/U" -DestinationFolder C:\Packages -CreateIntuneWinPackage

        Creates an APF deployment package with custom install and uninstall switches and a .intunewin package.

    .NOTES
        Only MSI and EXE installer files are supported.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([string])]
    param(
        [Parameter(HelpMessage = "The name of the application. `nThis is written in to the exported configuration files. `nIf you do not provide a name, the script will attempt to extract it from the installer file.")]
        [Alias("ApplicationName", "AppName")]
        [string]$Name,

        [Parameter(HelpMessage = "The version of the application. `nThis is written in to the exported configuration files. `nThis should be in the format of x.x.x.x. `nIf you do not provide a version, the script will attempt to extract it from the installer file.")]
        [Alias("ApplicationVersion", "AppVersion")]
        [version]$Version,

        [Parameter(HelpMessage = "The target for the deployment, User context or System context. Default is 'system'.")]
        [ValidateSet("system", "user")]
        [string]$Target = "system",

        [Parameter(HelpMessage = "The switches to use when installing the application.")]
        [string]$InstallSwitches,

        [Parameter(HelpMessage = "The switches to use when uninstalling the application.")]
        [string]$UninstallSwitches,

        [Parameter(HelpMessage = "The path to the uninstall file.")]
        [string]$UninstallPath,

        [Parameter(Mandatory = $true)]
        [Alias("InstallerFile", "SourceFile")]
        [ValidateScript({
                if ($_ -notmatch "\.(msi|exe)$") {
                    throw "Please supply a valid installer file path. Only .msi and .exe files are supported."
                }
                if (-not (Test-Path -Path $_ -PathType Leaf)) {
                    throw "The file $_ does not exist."
                }
                $true
            })]
        [string]$Path,

        [Parameter(HelpMessage = "Paths to any additional files that need to be included in the installation.")]
        [ValidateScript({
                foreach ($file in $_) {
                    if (-not (Test-Path -Path $file)) {
                        throw "The file $file does not exist."
                    }
                }
                $true
            })]
        [string[]]$IncludedFiles,

        [Parameter(HelpMessage = "The folder where the files will be copied to. Default is the current directory.")]
        [ValidateScript({
                if (-not (Test-Path -Path $_ -PathType Container)) {
                    throw "The folder $_ does not exist."
                }
                $true
            })]
        [string]$DestinationFolder = $PWD.Path,

        [Parameter(HelpMessage = "Create a Intune package for the application. Default is false.")]
        [switch]$CreateIntuneWinPackage
    )
    process {
        $TelemetryArgs = @{
            ModuleName    = $MyInvocation.MyCommand.Module.Name
            ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
            CommandName   = $MyInvocation.MyCommand.Name
            ExecutionID   = [guid]::NewGuid().ToString()
        }
        Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer

        try {
            $InstallerFile = Get-Item -Path $Path -ErrorAction Stop
            if ($InstallerFile.Extension -eq '.msi') {
                if (-not $Name -or -not $Version) {
                    # Read the application name and version from the MSI file
                    $MSIProperties = Get-MSIProperty -Path $InstallerFile.FullName -ErrorAction Stop
                    if (-not $Name) {
                        $Name = $MSIProperties.ProductName
                    }
                    if (-not $Version) {
                        $Version = [version]$MSIProperties.ProductVersion
                    }
                }
            }
            else {
                if (-not $Name) {
                    $Name = $InstallerFile.BaseName
                }
                if (-not $Version) {
                    $FileVersion = $InstallerFile.VersionInfo.FileVersion
                    $ParsedVersion = $null
                    if ([string]::IsNullOrEmpty($FileVersion) -or -not [version]::TryParse(($FileVersion -replace ',\s*', '.' -replace '\s.*$', ''), [ref]$ParsedVersion)) {
                        throw "The version could not be read from '$($InstallerFile.Name)'. Specify it with -Version."
                    }
                    $Version = $ParsedVersion
                }
            }
            if ([string]::IsNullOrEmpty($Name)) {
                throw 'The application name could not be determined. Specify it with -Name.'
            }

            # Create the application folder
            $AppFolder = Join-Path -Path $DestinationFolder -ChildPath $Name
            if (-not $PSCmdlet.ShouldProcess($AppFolder, 'Create APF deployment package')) {
                Invoke-TelemetryCollection @TelemetryArgs -Stage End
                return
            }
            if (Test-Path -Path $AppFolder) {
                if (-not $PSCmdlet.ShouldContinue("Overwrite existing subfolder '$AppFolder' for the application? Warning: This will recursively delete all files in the folder.", "Confirm Overwrite")) {
                    Write-Warning "The folder '$AppFolder' already exists and was not changed."
                    Invoke-TelemetryCollection @TelemetryArgs -Stage End
                    return
                }
                Remove-Item -Path $AppFolder -Recurse -Force -ErrorAction Stop
            }
            $null = New-Item -Path $AppFolder -ItemType Directory -ErrorAction Stop

            # Copy the installer file and any additional files to the application folder
            Copy-Item -LiteralPath $InstallerFile.FullName -Destination $AppFolder -ErrorAction Stop
            foreach ($File in $IncludedFiles) {
                Copy-Item -Path $File -Destination $AppFolder -Recurse -ErrorAction Stop
            }
            # Copy the template files to the application folder
            $TemplateFolder = Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Templates') -ChildPath 'Application'
            Copy-Item -Path (Join-Path -Path $TemplateFolder -ChildPath '*') -Destination $AppFolder -Recurse -Exclude '*.md' -ErrorAction Stop

            # Update the template files with the application details
            $ConfigPath = Join-Path -Path $AppFolder -ChildPath 'config.installer.json'
            $MainConfig = Get-Content -Path $ConfigPath -Raw -ErrorAction Stop | ConvertFrom-Json
            $MainConfig.name = $Name
            $MainConfig.version = $Version.ToString()
            $MainConfig.filename = $InstallerFile.Name
            $MainConfig.target = $Target
            $MainConfig.installSwitches = [string]$InstallSwitches
            $MainConfig.uninstallSwitches = [string]$UninstallSwitches
            $MainConfig.uninstallPath = [string]$UninstallPath
            $MainConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -ErrorAction Stop

            $DetectionPath = Join-Path -Path $AppFolder -ChildPath 'Intune-D-AppDetection.ps1'
            $DetectionScript = Get-Content -Path $DetectionPath -Raw -ErrorAction Stop
            $DetectionScript = $DetectionScript.Replace('##NAME_TEMPLATE', $Name).Replace('##VERSION_TEMPLATE', $Version.ToString()).Replace('##FILENAME_TEMPLATE', $InstallerFile.Name)
            Set-Content -Path $DetectionPath -Value $DetectionScript -NoNewline -ErrorAction Stop

            Write-Output "The application '$Name' has been successfully packaged.`nThis can be found in the folder '$AppFolder'."

            if ($CreateIntuneWinPackage) {
                $Package = New-APFIntuneWinPackage -SourceFolder $AppFolder -SetupFile (Join-Path -Path $AppFolder -ChildPath $InstallerFile.Name) -OutputFolder $DestinationFolder -ErrorAction Stop
                if ($Package) {
                    Write-Output "The application '$Name' was also packaged to an intunewin file.`nThis can be found at '$($Package.FullName)'."
                }
            }
            Write-Output "When publishing the application to Intune, use`n'powershell.exe -ExecutionPolicy RemoteSigned -File Intune-I-MainInstaller.ps1' for the install Command and`n'powershell.exe -ExecutionPolicy RemoteSigned -File Intune-I-MainInstaller.ps1 -Uninstall' for the Uninstall Command."
            Invoke-TelemetryCollection @TelemetryArgs -Stage End
        }
        catch {
            Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
            Write-Error -Message "Failed to package application: $($_.Exception.Message)" -Exception $_.Exception
        }
    }
}