Public/New-IntuneApplication.ps1

function New-IntuneApplication {
    <#
    .SYNOPSIS
        Creates a new Intune application package with all necessary files and configurations.

    .DESCRIPTION
        The New-IntuneApplication function creates a complete Intune application package by generating
        the required folder structure, copying source files, creating detection scripts, and preparing
        all necessary configuration files for deployment.

    .PARAMETER ApplicationName
        The name of the application to be packaged.

    .PARAMETER SourceFiles
        The source files for the package: either one folder that contains all files, or one or more files.
        When several files are given they are copied to "<OutputFolder>/<ApplicationName>.<Version>" first.

    .PARAMETER MainInstallerFileName
        The file name of the main installer (for example setup.exe or installer.msi). It must be one of
        the source files, or be inside the source folder.

    .PARAMETER OutputFolder
        The existing folder where the JSON and .intunewin files are created. Default is the current directory.

    .PARAMETER Description
        A description of the application. Default is a Markdown summary of ApplicationName, Publisher,
        Version, Developer and Notes.

    .PARAMETER Publisher
        The publisher of the application. Default is the current user name.

    .PARAMETER Version
        The version of the application. Default is "1.0".

    .PARAMETER Developer
        The developer of the application. Default is the current user name.

    .PARAMETER Owner
        The owner of the application.

    .PARAMETER Notes
        Additional notes about the application.

    .PARAMETER LogoPath
        Path to the application logo: a PNG or JPG image of at most 256x256 pixels.

    .PARAMETER InstallFor
        The installation context: "User" or "System". Default is "System".

    .PARAMETER RestartBehavior
        The device restart behaviour: basedOnReturnCode (default), allow, suppress or force.

    .PARAMETER IsFeatured
        Whether the application is featured in the Company Portal. Default is $false.

    .PARAMETER InstallCommand
        The command line that installs the application.

    .PARAMETER UninstallCommand
        The command line that uninstalls the application.

    .PARAMETER RequirementRuleConfig
        A hashtable that describes the requirement rules; written to the JSON file.

    .PARAMETER DetectionRuleConfig
        A hashtable that describes the detection rules (for example from New-IntuneWin32Rule); written to the JSON file.

    .PARAMETER AssignmentType
        How the application is assigned: User-Group, Device-Group, All-Users or All-Devices.

    .PARAMETER AssignmentGroup
        The group the application is assigned to, for the User-Group and Device-Group assignment types.

    .PARAMETER FilterRuleType
        Whether the assignment filter includes or excludes devices: Include or Exclude.

    .PARAMETER FilterRule
        The assignment filter rule.

    .PARAMETER Publish
        Publishes the package to Intune with Publish-IntuneAppPackage after creating it. DetectionRuleConfig
        (and RequirementRuleConfig) must then be rules created with New-IntuneWin32Rule. With -Overwrite an
        existing app with the same name gets the package as a new content version.

    .PARAMETER IntuneToolsPath
        The path to IntuneWinAppUtil.exe. When the file does not exist, release v1.8.6 of the tool is
        downloaded to the per-user tool folder (LocalApplicationData\tcs.intune.packaging) and used.

    .PARAMETER Overwrite
        Overwrite existing JSON and .intunewin files in OutputFolder.

    .PARAMETER NoJson
        Do not create the JSON configuration file.

    .PARAMETER NoIntuneWin
        Do not create the .intunewin package.

    .PARAMETER NoCleanUp
        Keep the JSON file and .intunewin package after a successful -Publish (they are removed by default).

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        JsonPath and IntuneWinPath of the created files ($null for files that were not created or were
        removed after publishing) and App, the published app when -Publish is used.

    .EXAMPLE
        New-IntuneApplication -ApplicationName "MyApp" -SourceFiles "C:\Source\MyApp" -MainInstallerFileName "setup.exe" -InstallCommand "setup.exe /S" -UninstallCommand "setup.exe /U" -DetectionRuleConfig @{ Type = 'File'; Path = 'C:\Program Files\MyApp' } -AssignmentType All-Devices

        Creates MyApp.1.0.json and setup.intunewin in the current directory.

    .EXAMPLE
        New-IntuneApplication -ApplicationName "MyApp" -SourceFiles "C:\Source\installer.msi", "C:\Source\config.xml" -MainInstallerFileName "installer.msi" -OutputFolder "C:\Packages" -Version "2.1" -InstallCommand "msiexec /i installer.msi /qn" -UninstallCommand "msiexec /x installer.msi /qn" -DetectionRuleConfig @{ Type = 'MSI' } -AssignmentType User-Group -AssignmentGroup 'Intune-AG-MyApp-Available' -NoIntuneWin

        Creates only the JSON configuration file for version 2.1.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$SourceFiles,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$MainInstallerFileName,

        [string]$OutputFolder = $($PWD.Path),

        [string]$Description,

        [ValidateNotNullOrEmpty()]
        [string]$Publisher = [Environment]::UserName,

        [string]$Version = "1.0",

        [string]$Developer = [Environment]::UserName,

        [string]$Owner = "",

        [string]$Notes = "",

        [ValidateScript({
                if (-not (Test-Path -Path $_ -PathType Leaf)) {
                    throw "The LogoPath path does not exist."
                }
                if ([System.IO.Path]::GetExtension($_) -notin @('.png', '.jpg', '.jpeg')) {
                    throw "The LogoPath must be a PNG or JPG file."
                }
                $true
            })]
        [string]$LogoPath,

        [ValidateSet("User", "System")]
        [string]$InstallFor = "System",

        [ValidateSet("basedOnReturnCode", "allow", "suppress", "force")]
        [string]$RestartBehavior = "basedOnReturnCode",

        [bool]$IsFeatured = $false,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$InstallCommand,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$UninstallCommand,

        [hashtable]$RequirementRuleConfig,

        [Parameter(Mandatory = $true)]
        [hashtable]$DetectionRuleConfig,

        [Parameter(Mandatory = $true)]
        [ValidateSet('User-Group', 'Device-Group', 'All-Users', 'All-Devices')]
        [string]$AssignmentType,

        [string]$AssignmentGroup,

        [ValidateSet('Include', 'Exclude')]
        [string]$FilterRuleType,

        [string]$FilterRule,

        [switch]$Publish,

        [ValidateNotNullOrEmpty()]
        [string]$IntuneToolsPath = (Get-IntuneWinAppUtilPath),

        [switch]$Overwrite,

        [switch]$NoJson,

        [switch]$NoIntuneWin,

        [switch]$NoCleanUp
    )

    begin {
        $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
        $TelemetryFailed = $false
        try {
            #region PathValidation
            if ($LogoPath) {
                if (-not (Test-IntuneLogoImage -Path $LogoPath -ErrorAction Stop)) {
                    throw "The logo '$LogoPath' is not a valid Intune logo image."
                }
            }
            foreach ($File in $SourceFiles) {
                if (-not (Test-Path -Path $File)) {
                    throw "The Source File path '$File' does not exist."
                }
            }
            if (-not (Test-Path -Path $OutputFolder -PathType Container)) {
                throw "The Output Folder path '$OutputFolder' does not exist."
            }
            #endregion PathValidation

            #region SourceFilesValidation
            # Find the main installer in the source files (or in the single source folder)
            if (Test-Path -Path $MainInstallerFileName -PathType Leaf) {
                $MainInstallerFileName = Split-Path -Path $MainInstallerFileName -Leaf
            }
            $MainInstallerFilePath = $null
            if ($SourceFiles.Count -eq 1 -and (Test-Path -Path $SourceFiles[0] -PathType Container)) {
                $Found = Get-ChildItem -Path $SourceFiles[0] -Recurse -File | Where-Object { $_.Name -eq $MainInstallerFileName } | Select-Object -First 1
                if ($Found) {
                    $MainInstallerFilePath = $Found.DirectoryName
                }
            }
            else {
                foreach ($File in $SourceFiles) {
                    if ((Split-Path -Path $File -Leaf) -eq $MainInstallerFileName) {
                        $MainInstallerFilePath = Split-Path -Path (Resolve-Path -Path $File).ProviderPath -Parent
                        break
                    }
                }
            }
            if (-not $MainInstallerFilePath) {
                throw "The MainInstallerFileName '$MainInstallerFileName' does not exist in the SourceFiles list."
            }
            #endregion SourceFilesValidation

            if (-not $PSBoundParameters.ContainsKey('Description')) {
                $Description = "# $ApplicationName`nPublisher: $Publisher`nVersion: $Version`nDeveloper: $Developer`n`n$Notes"
            }

            #region ParameterSplat
            # Using the parameters passed to the function (and the defaults), create a splat for the JSON file
            $CommonParameters = [System.Management.Automation.PSCmdlet]::CommonParameters + [System.Management.Automation.PSCmdlet]::OptionalCommonParameters
            $ParameterSplat = @{}
            foreach ($P in $PSBoundParameters.Keys) {
                if ($P -notin $CommonParameters) {
                    $ParameterSplat[$P] = $PSBoundParameters[$P]
                }
            }
            $ParameterSplat['MainInstallerFileName'] = $MainInstallerFileName
            $ParameterSplat['Description'] = $Description
            $ParameterSplat['Publisher'] = $Publisher
            $ParameterSplat['Version'] = $Version
            $ParameterSplat['Developer'] = $Developer
            $ParameterSplat['InstallFor'] = $InstallFor
            $ParameterSplat['RestartBehavior'] = $RestartBehavior
            $ParameterSplat['IsFeatured'] = $IsFeatured
            $ParameterSplat['OutputFolder'] = $OutputFolder
            Write-Verbose ("`n" + (($ParameterSplat.GetEnumerator() | ForEach-Object { "$($_.Key): $($_.Value)" }) -join "`n"))
            #endregion ParameterSplat
        }
        catch {
            $TelemetryFailed = $true
            Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
            throw
        }
    }
    process {
        try {
            $JSONOutputPath = $null
            $IntunewinFullPath = $null
            $StagingFolder = $null

            #region CreateJSON
            if (-not $NoJson) {
                Write-Verbose "Creating JSON file for $ApplicationName"
                $JSONOutputPath = Join-Path -Path $OutputFolder -ChildPath "$ApplicationName.$Version.json"
                if ((Test-Path -Path $JSONOutputPath) -and -not $Overwrite) {
                    throw "The JSON file '$JSONOutputPath' already exists, use -Overwrite to replace it."
                }
                if ($PSCmdlet.ShouldProcess($JSONOutputPath, 'Write application JSON')) {
                    New-IntuneAppJSON -AppParams $ParameterSplat | Set-Content -Path $JSONOutputPath -Force -ErrorAction Stop
                }
            }
            #endregion CreateJSON

            #region CreateIntuneWin
            if (-not $NoIntuneWin) {
                Write-Verbose "Creating .intunewin file for $ApplicationName"
                $MainInstallerFileFullPath = Join-Path -Path $MainInstallerFilePath -ChildPath $MainInstallerFileName
                $IntunewinFullPath = Join-Path -Path $OutputFolder -ChildPath "$([System.IO.Path]::GetFileNameWithoutExtension($MainInstallerFileName)).intunewin"
                if ((Test-Path -Path $IntunewinFullPath) -and -not $Overwrite) {
                    throw "The .intunewin file '$IntunewinFullPath' already exists, use -Overwrite to replace it."
                }
                if ($PSCmdlet.ShouldProcess($IntunewinFullPath, 'Create .intunewin package')) {
                    # IntuneWinAppUtil.exe needs one source folder: copy multiple source files to a staging folder
                    if ($SourceFiles.Count -gt 1) {
                        $StagingFolder = New-Item -Path (Join-Path -Path $OutputFolder -ChildPath "$ApplicationName.$Version") -ItemType Directory -Force -ErrorAction Stop
                        Copy-Item -Path $SourceFiles -Destination $StagingFolder.FullName -Force -ErrorAction Stop
                        $SourceFolder = $StagingFolder.FullName
                        $MainInstallerFileFullPath = Join-Path -Path $SourceFolder -ChildPath $MainInstallerFileName
                    }
                    elseif (Test-Path -Path $SourceFiles[0] -PathType Container) {
                        $SourceFolder = (Resolve-Path -Path $SourceFiles[0]).ProviderPath
                    }
                    else {
                        $SourceFolder = $MainInstallerFilePath
                    }

                    if (-not (Test-Path -Path $IntuneToolsPath -PathType Leaf)) {
                        # Pinned to v1.8.6 as a known stable release
                        $IntuneToolsPath = (Get-IntunePackagingTool -Path (Split-Path -Path (Get-IntuneWinAppUtilPath) -Parent) -Force -DownloadTag 'v1.8.6' -ErrorAction Stop).FullName
                    }
                    if (Test-Path -Path $IntunewinFullPath) {
                        Remove-Item -Path $IntunewinFullPath -Force -ErrorAction Stop
                    }
                    $Result = Invoke-Executable -FilePath $IntuneToolsPath -Arguments "-c `"$SourceFolder`" -s `"$MainInstallerFileFullPath`" -o `"$OutputFolder`" -q"
                    if ($Result.ExitCode -ne 0 -or -not (Test-Path -Path $IntunewinFullPath)) {
                        throw "IntuneWinAppUtil.exe did not create '$IntunewinFullPath' (exit code $($Result.ExitCode)). $($Result.StandardError)"
                    }
                }
            }
            #endregion CreateIntuneWin

            $App = $null
            if ($Publish) {
                if ($NoJson -or $NoIntuneWin) {
                    throw 'Publish needs both the JSON file and the .intunewin package; do not combine it with -NoJson or -NoIntuneWin.'
                }
                if ($PSCmdlet.ShouldProcess($ApplicationName, 'Publish to Intune')) {
                    $App = Publish-IntuneAppPackage -IntuneAppJSONPath $JSONOutputPath -IntuneWinPath $IntunewinFullPath -Force:$Overwrite -ErrorAction Stop
                    if (-not $NoCleanUp) {
                        Write-Verbose 'Removing the published JSON file and .intunewin package.'
                        Remove-Item -Path $JSONOutputPath, $IntunewinFullPath -Force -ErrorAction SilentlyContinue
                        if ($StagingFolder) {
                            Remove-Item -Path $StagingFolder.FullName -Recurse -Force -ErrorAction SilentlyContinue
                        }
                        $JSONOutputPath = $null
                        $IntunewinFullPath = $null
                    }
                }
            }

            [PSCustomObject]@{
                JsonPath      = $JSONOutputPath
                IntuneWinPath = $IntunewinFullPath
                App           = $App
            }
        }
        catch {
            if (-not $TelemetryFailed) {
                $TelemetryFailed = $true
                Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
            }
            throw
        }
    }
    end {
        if (-not $TelemetryFailed) {
            Invoke-TelemetryCollection @TelemetryArgs -Stage End
        }
    }
}