Public/New-APFConfigDeployment.ps1
|
function New-APFConfigDeployment { <# .SYNOPSIS Creates an APF (Application Packaging Framework) configuration deployment package for Intune. .DESCRIPTION The New-APFConfigDeployment function creates a folder named after the deployment in DestinationFolder, copies the matching APF installer templates into it and fills in config.installer.json and the detection script. Optionally it also wraps the folder into an Intune package ("<Name>.intunewin" in DestinationFolder). The parameters available depend on ConfigurationType (they are dynamic parameters): - All types: Name, Version, DestinationFolder, CreateIntuneWinPackage. - Registry: IncludedFiles, RegistryValue, Target. - PowerShellProfiles and Files: Files, FilesDirectoryName. - Script-OS and WindowsFeature: IncludedFiles. - StandAlone-Exe: Path, IncludedFiles, CLIApp. - Script-App and Script-User: Path (the deployment script), IncludedFiles. - Custom: IncludedFiles, Target. - Standalone-Application: Path, IncludedFiles, LauncherName, LauncherRelativePath. Dynamic parameters: Name The name of the deployment; written into the configuration files and used as the folder name. Version The version of the deployment (x.x.x.x). Path The main file (StandAlone-Exe), the file or folder of a standalone application (Standalone-Application) or the PowerShell deployment script (Script-App, Script-User). DestinationFolder Where the package folder is created. Default is the current directory. CreateIntuneWinPackage Also create a .intunewin package. IncludedFiles Additional files or folders to include in the package. RegistryValue One registry entry in the format of the registry CSV columns: "RegistryPath,KeyName,ValueName,ValueType,ValueData,State", for example "HKLM:\Software,MySoftware,MyValue,String,MyData,ADD". State is ADD, MODIFY or REMOVE. More entries can be added to the generated "<Name>_Registry.csv" file. Any failed entry fails the whole deployment. Target 'System' (default) or 'User'. LauncherName The file that launches a standalone application. LauncherRelativePath The relative path of the launcher inside the included files. CLIApp $true when a standalone executable is a command-line tool; it is then installed to the APF bin folder that is added to PATH. Files The files to deploy (PowerShellProfiles and Files). FilesDirectoryName The name of the directory the files are deployed to. When the package folder already exists you are asked before it is deleted and recreated; when you decline, that deployment is skipped with a warning. The package folder is always a subfolder of DestinationFolder: Name must not be '.' or '..', contain path separators, wildcard characters ([ ]) or characters that Windows does not allow in file names (< > : " | ? *), or end with a space or a dot. Name, Version and the other dynamic parameters can be bound from the pipeline by property name, one package per input object. Script-App, Script-User and Custom packages use the "script" template: the main installer runs the pre-install script, the deployment script and the post-install script (each with -Uninstall for the uninstall command) and saves the configuration that the detection script checks. - Script-App runs your script in the system context. - Script-User runs your script in the user context (assign the Intune app to run as user). - Custom adds a placeholder deployment script, Intune-Custom.ps1, for you to complete. .PARAMETER ConfigurationType The type of configuration package to create: Registry, PowerShellProfiles, Files, Script-OS, Script-App, Script-User, StandAlone-Exe, Standalone-Application, WindowsFeature or Custom. Script-App, Script-User and Custom build a script package; see the description. .OUTPUTS System.String Messages that describe where the package was created and which install commands to use. .EXAMPLE New-APFConfigDeployment -ConfigurationType Registry -Name "My Settings" -Version "1.0.0.0" -RegistryValue "HKLM:\Software,MySoftware,MyValue,String,MyData,ADD" Creates a registry deployment package with one registry entry. .EXAMPLE New-APFConfigDeployment -ConfigurationType Files -Name "My Files" -Version "1.0.0.0" -Files "C:\file1.txt", "C:\file2.txt" -FilesDirectoryName "MyFiles" Creates a package that deploys two files to the MyFiles directory. .EXAMPLE New-APFConfigDeployment -ConfigurationType StandAlone-Exe -Name "MyApp" -Version "1.2.3.4" -Path "C:\path\to\myapp.exe" -CLIApp $true Creates a package that installs myapp.exe as a command-line tool. .NOTES The generated installer scripts run on Windows devices managed by Intune. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([string])] param( [Parameter(Mandatory = $true, HelpMessage = "The type of configuration this command is for.")] [ValidateSet("Registry", "PowerShellProfiles", "Files", "Script-OS", "Script-App", "Script-User", "StandAlone-Exe", "Standalone-Application", "WindowsFeature", "Custom")] [string]$ConfigurationType ) DynamicParam { $paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary # Default Params - doing this so the only default choice is the Configuration Type $PathValidation = { foreach ($i in $_) { if (-not (Test-Path $i)) { throw "Path $($i) is invalid, double check and try again." } } return $true } $defaults = @( @{Name = "Name"; ParameterType = [string]; Mandatory = $true; Position = 1; ValueFromPipelineByPropertyName = $true; HelpMessage = "The name of the deployment. This is written in to the exported configuration files." } @{Name = "Version"; ParameterType = [version]; Mandatory = $true; Position = 2; ValueFromPipelineByPropertyName = $true; HelpMessage = "The version of the deployment in the format x.x.x.x. This is written in to the exported configuration files." } @{Name = "Path"; ParameterType = [string]; Mandatory = $true; Position = 3; ValueFromPipelineByPropertyName = $true; HelpMessage = "The Path or either the main file or directory of a standalone application"; ValidateScript = $PathValidation } @{Name = "DestinationFolder"; ParameterType = [string]; ValueFromPipelineByPropertyName = $true; HelpMessage = "The folder where the output will be created. Default is the current directory." } @{Name = "CreateIntuneWinPackage"; ParameterType = [switch]; ValueFromPipelineByPropertyName = $true; HelpMessage = "Create a Intune package for the application. Default is false." } ) foreach ($d in $defaults) { $param = $(New-DynamicParameter @d) $paramDictionary.Add($param.Name, $param.Parameter) } $variableParams = @( @{Name = "IncludedFiles"; ParameterType = [string[]]; ValueFromPipelineByPropertyName = $true; HelpMessage = "Additional files or folders to include in the package."; ValidateScript = $PathValidation } @{Name = "RegistryValue"; ParameterType = [string]; ValueFromPipelineByPropertyName = $true; HelpMessage = "One registry entry as 'RegistryPath,KeyName,ValueName,ValueType,ValueData,State', for example 'HKLM:\Software,MySoftware,MyValue,String,MyData,ADD'. State is ADD, MODIFY or REMOVE." } @{Name = "Target"; ParameterType = [string]; ValidateSet = "System", "User"; HelpMessage = "The target for the deployment, User context or System context. Default is 'System'." } @{Name = "LauncherName"; ParameterType = [string]; HelpMessage = "Name of the file to launch the application." } @{Name = "LauncherRelativePath"; ParameterType = [string]; HelpMessage = "The relative path of the launcher file in the included files directory path." } @{Name = "CLIApp"; ParameterType = [bool]; HelpMessage = "If the standalone-exe is a cli application, add it to the bin directory and ensure it's added to the users PATH." } @{Name = "Files"; ParameterType = [string[]]; HelpMessage = "List of files to include in the package."; ValidateScript = $PathValidation } @{Name = "FilesDirectoryName"; ParameterType = [string]; Mandatory = $true; HelpMessage = "Name of the directory where the files will be stored." } ) $selected = switch ($ConfigurationType) { 'Registry' { 0, 1, 2 } { $_ -in 'Files', 'PowerShellProfiles' } { 6, 7 } { $_ -in 'Script-OS', 'WindowsFeature' } { 0 } { $_ -in 'Script-App', 'Script-User' } { 0 } 'Custom' { 0, 2 } 'StandAlone-Exe' { 0, 5 } 'Standalone-Application' { 0, 3, 4 } default { $null } } if ($null -eq $selected) { return } foreach ($index in $selected) { $definition = $variableParams[$index] $param = $(New-DynamicParameter @definition) if (-not $paramDictionary.ContainsKey($param.Name)) { $paramDictionary.Add($param.Name, $param.Parameter) } } if ($ConfigurationType -notin 'StandAlone-Exe', 'Standalone-Application', 'Script-App', 'Script-User') { $null = $paramDictionary.Remove('Path') } return $paramDictionary } begin { $TemplateRoot = Join-Path -Path $PSScriptRoot -ChildPath 'Templates' $PackagedBy = [Environment]::UserName # Sets a value on the imported template configuration, adding the property when it is missing function Add-TemplateValue { param ($Config, [string]$Property, $Value) $Config | Add-Member -NotePropertyName $Property -NotePropertyValue $Value -Force } function Copy-IncludedItem { param ([string[]]$Item, [string]$Destination) $count = 0 foreach ($i in $Item) { $count++ Write-Verbose "Copying $i. [$count of $($Item.Count)]" Copy-Item -Path $i -Destination $Destination -Recurse -Force -ErrorAction Stop } } } 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 { # The dynamic parameters can be bound from the pipeline (ValueFromPipelineByPropertyName), # so they are read here, per input object, and not in begin $DestinationFolder = $PSBoundParameters['DestinationFolder'] if ([string]::IsNullOrEmpty($DestinationFolder)) { $DestinationFolder = $PWD.Path } $Name = $PSBoundParameters['Name'] $Version = $PSBoundParameters['Version'] $Path = $PSBoundParameters['Path'] $IncludedFiles = @($PSBoundParameters['IncludedFiles'] | Where-Object { $_ }) $CreateIntuneWinPackage = [bool]$PSBoundParameters['CreateIntuneWinPackage'] $RegistryValue = $PSBoundParameters['RegistryValue'] $Target = $PSBoundParameters['Target'] if ([string]::IsNullOrEmpty($Target)) { $Target = 'System' } $LauncherName = $PSBoundParameters['LauncherName'] $LauncherRelativePath = $PSBoundParameters['LauncherRelativePath'] $CLIApp = [bool]$PSBoundParameters['CLIApp'] $FilesDirectoryName = $PSBoundParameters['FilesDirectoryName'] $Files = @($PSBoundParameters['Files'] | Where-Object { $_ }) if (-not (Test-Path -LiteralPath $DestinationFolder -PathType Container)) { throw "The destination folder '$DestinationFolder' does not exist." } if ($ConfigurationType -in @('Script-App', 'Script-User') -and ([System.IO.Path]::GetExtension($Path) -ne '.ps1')) { throw "For $ConfigurationType, Path must be the PowerShell deployment script (.ps1); '$Path' is not." } # Validates Name and makes sure the package folder is a subfolder of the destination $PackageFolder = Get-PackageFolderPath -DestinationFolder $DestinationFolder -Name $Name if (-not $PSCmdlet.ShouldProcess($PackageFolder, "Create $ConfigurationType deployment package")) { Invoke-TelemetryCollection @TelemetryArgs -Stage End return } if (Test-Path -LiteralPath $PackageFolder) { if (-not (Confirm-FolderOverwrite -Path $PackageFolder)) { Write-Warning "The folder '$PackageFolder' already exists and was not changed." Invoke-TelemetryCollection @TelemetryArgs -Stage End return } Write-Verbose "Removing existing directory $PackageFolder and recreating it." Remove-Item -LiteralPath $PackageFolder -Recurse -Force -ErrorAction Stop } Write-Verbose "Creating package folder $PackageFolder" $null = New-Item -Path $PackageFolder -ItemType Directory -ErrorAction Stop $ConfigPath = Join-Path -Path $PackageFolder -ChildPath 'config.installer.json' $SetupFile = Join-Path -Path $PackageFolder -ChildPath 'Intune-I-MainInstaller.ps1' switch ($ConfigurationType) { "Registry" { $TemplateFolder = Join-Path -Path $TemplateRoot -ChildPath 'Registry' $RegistryFile = Join-Path -Path $PackageFolder -ChildPath "$($Name)_Registry.csv" Write-Verbose "Creating registry file at $RegistryFile" Copy-Item -Path (Join-Path -Path $TemplateFolder -ChildPath 'registry_entries.config.csv') -Destination $RegistryFile -Force -ErrorAction Stop if ($RegistryValue) { # Add the supplied entry to the registry CSV unless it is already there $Header = @((Get-Content -Path $RegistryFile -TotalCount 1).Split(',')) $Fields = @($RegistryValue.Split(',')) if ($Fields.Count -ne $Header.Count) { throw "RegistryValue must contain $($Header.Count) comma-separated values ($($Header -join ',')); '$RegistryValue' contains $($Fields.Count)." } $Entry = [ordered]@{} for ($i = 0; $i -lt $Header.Count; $i++) { $Entry[$Header[$i]] = $Fields[$i].Trim() } $Entry = [PSCustomObject]$Entry $Existing = @(Import-Csv -Path $RegistryFile -ErrorAction Stop) $Duplicate = $Existing | Where-Object { ($_.PSObject.Properties.Value -join ',') -eq ($Entry.PSObject.Properties.Value -join ',') } if ($Duplicate) { Write-Warning "The registry value '$RegistryValue' already exists in the file." } else { @($Existing) + $Entry | Export-Csv -Path $RegistryFile -NoTypeInformation -Force -ErrorAction Stop } } else { Write-Verbose "No registry keys were supplied." } $IncludeFolder = Join-Path -Path $PackageFolder -ChildPath "src" $null = New-Item -Path $IncludeFolder -ItemType Directory -Force -ErrorAction Stop Copy-IncludedItem -Item $IncludedFiles -Destination $PackageFolder Copy-APFTemplate -Template 'Registry' -Destination $PackageFolder -Exclude '*.md', '*config.*' $MainConfig = Get-Content -Path (Join-Path -Path $TemplateFolder -ChildPath 'config.installer.json') -Raw -ErrorAction Stop | ConvertFrom-Json Add-TemplateValue -Config $MainConfig -Property 'name' -Value $Name Add-TemplateValue -Config $MainConfig -Property 'version' -Value $Version.ToString() Add-TemplateValue -Config $MainConfig -Property 'target' -Value $Target Add-TemplateValue -Config $MainConfig -Property 'registryfile' -Value "$($Name)_Registry.csv" Add-TemplateValue -Config $MainConfig -Property 'packagedby' -Value $PackagedBy $MainConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -ErrorAction Stop Set-TemplateToken -Path (Join-Path -Path $PackageFolder -ChildPath 'Intune-D-RegistryDetection.ps1') -Values @{ NAME = $Name VERSION = $Version.ToString() } } { $_ -in "PowerShellProfiles", "Files" } { Write-Verbose "Copying template files to destination folder." Copy-APFTemplate -Template $(if ($ConfigurationType -eq 'Files') { 'Files' } else { 'PowerShellProfile' }) -Destination $PackageFolder Copy-IncludedItem -Item $Files -Destination $PackageFolder $FileNames = @($Files | ForEach-Object { Split-Path -Path $_ -Leaf }) -join ',' $MainConfig = Get-Content -Path $ConfigPath -Raw -ErrorAction Stop | ConvertFrom-Json Add-TemplateValue -Config $MainConfig -Property 'name' -Value $Name Add-TemplateValue -Config $MainConfig -Property 'version' -Value $Version.ToString() Add-TemplateValue -Config $MainConfig -Property 'target' -Value 'system' Add-TemplateValue -Config $MainConfig -Property 'directory' -Value $FilesDirectoryName Add-TemplateValue -Config $MainConfig -Property 'files' -Value $FileNames Add-TemplateValue -Config $MainConfig -Property 'packagedby' -Value $PackagedBy Write-Verbose "Modifying template files with template parameters." $MainConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -ErrorAction Stop Set-TemplateToken -Path (Join-Path -Path $PackageFolder -ChildPath 'Intune-D-AppDetection.ps1') -Values @{ NAME = $Name VERSION = $Version.ToString() FILES = $FileNames } } "StandAlone-Exe" { Copy-Item -Path $Path -Destination $PackageFolder -ErrorAction Stop Copy-IncludedItem -Item $IncludedFiles -Destination $PackageFolder Write-Verbose "Copying template files to destination folder." Copy-APFTemplate -Template 'standalone-exe' -Destination $PackageFolder $FileName = Split-Path -Path $Path -Leaf $MainConfig = Get-Content -Path $ConfigPath -Raw -ErrorAction Stop | ConvertFrom-Json Add-TemplateValue -Config $MainConfig -Property 'name' -Value $Name Add-TemplateValue -Config $MainConfig -Property 'version' -Value $Version.ToString() # The installer script reads the executable name from 'filename' Add-TemplateValue -Config $MainConfig -Property 'filename' -Value $FileName Add-TemplateValue -Config $MainConfig -Property 'includedfiles' -Value (@($IncludedFiles | ForEach-Object { Split-Path -Path $_ -Leaf }) -join ',') Add-TemplateValue -Config $MainConfig -Property 'cli' -Value $CLIApp Add-TemplateValue -Config $MainConfig -Property 'packagedby' -Value $PackagedBy Write-Verbose "Modifying template files with template parameters." $MainConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -ErrorAction Stop Set-TemplateToken -Path (Join-Path -Path $PackageFolder -ChildPath 'Intune-D-AppDetection.ps1') -Values @{ NAME = $Name VERSION = $Version.ToString() FILENAME = $FileName } } { $_ -in "Script-OS", "WindowsFeature" } { if ($ConfigurationType -eq 'Script-OS') { $TemplateName = 'script-os' $TemplateFolder = Join-Path -Path $TemplateRoot -ChildPath $TemplateName $EntriesTemplate = 'os-config_entries.config.csv' $DetectionFile = 'Intune-D-Detection.ps1' } else { $TemplateName = 'WindowsFeatures' $TemplateFolder = Join-Path -Path $TemplateRoot -ChildPath $TemplateName $EntriesTemplate = 'windowsfeatures_entries.config.csv' $DetectionFile = 'Intune-D-WindowsFeatureDetection.ps1' } # The installer scripts read "<Name>_config.csv" $ConfigFileName = "$($Name)_config.csv" $ConfigFile = Join-Path -Path $PackageFolder -ChildPath $ConfigFileName Write-Verbose "Creating configuration file at $ConfigFile" Copy-Item -Path (Join-Path -Path $TemplateFolder -ChildPath $EntriesTemplate) -Destination $ConfigFile -Force -ErrorAction Stop $IncludeFolder = Join-Path -Path $PackageFolder -ChildPath "src" $null = New-Item -Path $IncludeFolder -ItemType Directory -Force -ErrorAction Stop Copy-IncludedItem -Item $IncludedFiles -Destination $PackageFolder Copy-APFTemplate -Template $TemplateName -Destination $PackageFolder -Exclude '*.md', '*config.*' $MainConfig = Get-Content -Path (Join-Path -Path $TemplateFolder -ChildPath 'config.installer.json') -Raw -ErrorAction Stop | ConvertFrom-Json Add-TemplateValue -Config $MainConfig -Property 'name' -Value $Name Add-TemplateValue -Config $MainConfig -Property 'version' -Value $Version.ToString() Add-TemplateValue -Config $MainConfig -Property 'configfile' -Value $ConfigFileName Add-TemplateValue -Config $MainConfig -Property 'includedfiles' -Value (@($IncludedFiles | ForEach-Object { Split-Path -Path $_ -Leaf }) -join ',') Add-TemplateValue -Config $MainConfig -Property 'packagedby' -Value $PackagedBy $MainConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -ErrorAction Stop Set-TemplateToken -Path (Join-Path -Path $PackageFolder -ChildPath $DetectionFile) -Values @{ NAME = $Name VERSION = $Version.ToString() } } { $_ -in "Script-App", "Script-User", "Custom" } { $TemplateFolder = Join-Path -Path $TemplateRoot -ChildPath 'script' Write-Verbose "Copying template files to destination folder." Copy-APFTemplate -Template 'script' -Destination $PackageFolder -Exclude '*.md', 'Intune-Custom.ps1' if ($ConfigurationType -eq 'Custom') { Copy-Item -Path (Join-Path -Path $TemplateFolder -ChildPath 'Intune-Custom.ps1') -Destination $PackageFolder -ErrorAction Stop $ScriptFile = 'Intune-Custom.ps1' $ScriptTarget = $Target.ToLowerInvariant() } else { Copy-Item -Path $Path -Destination $PackageFolder -ErrorAction Stop $ScriptFile = Split-Path -Path $Path -Leaf $ScriptTarget = $(if ($ConfigurationType -eq 'Script-User') { 'user' } else { 'system' }) } Copy-IncludedItem -Item $IncludedFiles -Destination $PackageFolder $MainConfig = Get-Content -Path $ConfigPath -Raw -ErrorAction Stop | ConvertFrom-Json Add-TemplateValue -Config $MainConfig -Property 'name' -Value $Name Add-TemplateValue -Config $MainConfig -Property 'version' -Value $Version.ToString() Add-TemplateValue -Config $MainConfig -Property 'target' -Value $ScriptTarget Add-TemplateValue -Config $MainConfig -Property 'scriptfile' -Value $ScriptFile Add-TemplateValue -Config $MainConfig -Property 'includedfiles' -Value (@($IncludedFiles | ForEach-Object { Split-Path -Path $_ -Leaf }) -join ',') Add-TemplateValue -Config $MainConfig -Property 'packagedby' -Value $PackagedBy $MainConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -ErrorAction Stop Set-TemplateToken -Path (Join-Path -Path $PackageFolder -ChildPath 'Intune-D-Detection.ps1') -Values @{ NAME = $Name VERSION = $Version.ToString() } } "Standalone-Application" { Copy-Item -Path $Path -Destination $PackageFolder -Recurse -ErrorAction Stop Copy-IncludedItem -Item $IncludedFiles -Destination $PackageFolder Write-Verbose "Copying template files to destination folder." Copy-APFTemplate -Template 'standalone-application' -Destination $PackageFolder -Exclude '*.md' $MainConfig = Get-Content -Path $ConfigPath -Raw -ErrorAction Stop | ConvertFrom-Json Add-TemplateValue -Config $MainConfig -Property 'name' -Value $Name Add-TemplateValue -Config $MainConfig -Property 'version' -Value $Version.ToString() # The installer script reads the deployed item from 'filename' Add-TemplateValue -Config $MainConfig -Property 'filename' -Value (Split-Path -Path $Path -Leaf) Add-TemplateValue -Config $MainConfig -Property 'launchername' -Value ([string]$LauncherName) Add-TemplateValue -Config $MainConfig -Property 'launcherrelativepath' -Value ([string]$LauncherRelativePath) Add-TemplateValue -Config $MainConfig -Property 'includedfiles' -Value (@($IncludedFiles | ForEach-Object { Split-Path -Path $_ -Leaf }) -join ',') Write-Verbose "Modifying template files with template parameters." $MainConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -ErrorAction Stop Set-TemplateToken -Path (Join-Path -Path $PackageFolder -ChildPath 'Intune-D-AppDetection.ps1') -Values @{ NAME = $Name VERSION = $Version.ToString() LAUNCHERNAME = $LauncherName LAUNCHERRELATIVEPATH = $LauncherRelativePath } } } Write-Output "The deployment '$Name' has been successfully packaged.`nThis can be found in the folder '$PackageFolder'." if ($CreateIntuneWinPackage) { $Package = New-APFIntuneWinPackage -SourceFolder $PackageFolder -SetupFile $SetupFile -OutputFolder $DestinationFolder -PackageName $Name -ErrorAction Stop if ($Package) { Write-Output "The deployment '$Name' was also packaged to an intunewin file.`nThis can be found at '$($Package.FullName)'." } } $CommandLine = Get-APFCommandLine Write-Output "When publishing the deployment to Intune, use`n'$($CommandLine.InstallCommand)' for the install command and`n'$($CommandLine.UninstallCommand)' for the uninstall command." Invoke-TelemetryCollection @TelemetryArgs -Stage End } catch { Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_ Write-Error -Message "Failed to create the $ConfigurationType deployment package: $($_.Exception.Message)" -Exception $_.Exception } } } |