Public/Add-MSIXDesktop7Shortcut.ps1

function Add-MSIXDesktop7Shortcut {
<#
.SYNOPSIS
    Adds a working desktop7:Shortcut to an MSIX app: a physical .lnk that targets the
    app's native install path (the icon is taken from that executable) at a per-user
    location. -Location tab-completes the folders.
.PARAMETER MSIXFolder
    Expanded MSIX package folder.
.PARAMETER AppId
    Id of the Application the shortcut belongs to.
.PARAMETER Name
    Shortcut file name without extension. Defaults to the AppId.
.PARAMETER Location
    Target folder token, e.g. '[{Programs}]' (default) or '[{Desktop}]'. Use a per-user
    location; system locations ('[{Common Programs}]', '[{Common Desktop}]') do not render
    the shortcut icon. Tab-completes.
.PARAMETER Arguments
    Optional command-line arguments.
.PARAMETER Description
    Optional shortcut tooltip.
.PARAMETER PinToStartMenu
    When set, pins the shortcut to the Start menu (desktop7:Shortcut PinToStartMenu="true").
.PARAMETER IconSource
    Optional icon source (.ico/.exe/.dll). Omit to use the app's own executable. A file
    already inside the package is referenced in place; an external file is copied next to
    the app executable (a resolvable location). A .png under Assets\ does not render.
.EXAMPLE
    Add-MSIXDesktop7Shortcut -MSIXFolder $pkg -AppId 'CHROME' -Location '[{Desktop}]'
.NOTES
    Tim Mangan: https://www.tmurgent.com/TmBlog/?p=3857
    Andreas Nick, 2026
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [System.IO.DirectoryInfo] $MSIXFolder,

        [Parameter(Mandatory = $true, Position = 1)]
        [string] $AppId,

        [string] $Name,

        [ArgumentCompleter({
            '[{Programs}]', '[{Desktop}]',
            '[{Common Programs}]', '[{Common Desktop}]'
        })]
        [string] $Location = '[{Programs}]',

        [string] $Arguments   = '',
        [string] $Description = '',
        [string] $IconSource  = '',
        [switch] $PinToStartMenu
    )

    # Placement target must be a known location with a real VFS folder
    # ('[{Package}]' resolves to the root and is not a valid shortcut location).
    if (-not $ShortcutLocationTokens.Contains($Location) -or [string]::IsNullOrEmpty($ShortcutLocationTokens[$Location])) {
        $valid = ($ShortcutLocationTokens.GetEnumerator() | Where-Object { $_.Value } | ForEach-Object { $_.Key }) -join ', '
        Write-Error "Invalid Location token '$Location'. Use one of: $valid"
        return
    }

    # All-users locations deploy the .lnk but it does NOT appear under per-user MSIX
    # deployment. Only per-user locations ([{Programs}] / [{Desktop}]) render.
    if ($Location -eq '[{Common Programs}]' -or $Location -eq '[{Common Desktop}]') {
        Write-Warning "Location '$Location' is an all-users path; desktop7 shortcuts there do not appear under per-user MSIX deployment. Prefer '[{Programs}]' or '[{Desktop}]'."
    }

    $manifestPath = Join-Path $MSIXFolder.FullName 'AppxManifest.xml'
    if (-not (Test-Path $manifestPath)) {
        Write-Error "AppxManifest.xml not found in: $($MSIXFolder.FullName)"
        return
    }

    $desktop7Ns = $AppXNamespaces['desktop7']

    $manifest = New-Object System.Xml.XmlDocument
    $manifest.PreserveWhitespace = $false
    $manifest.Load($manifestPath)

    $nsmgr = New-Object System.Xml.XmlNamespaceManager($manifest.NameTable)
    $AppXNamespaces.GetEnumerator() | ForEach-Object { $null = $nsmgr.AddNamespace($_.Key, $_.Value) }

    $app = $manifest.SelectSingleNode("//ns:Package/ns:Applications/ns:Application[@Id='$AppId']", $nsmgr)
    if ($null -eq $app) {
        Write-Error "Application '$AppId' not found in manifest."
        return
    }

    $executable = $app.GetAttribute('Executable')
    if ([string]::IsNullOrEmpty($Name)) { $Name = $AppId }

    # The .lnk launch target and the default icon both come from the application
    # executable - verify it is present in the package.
    if (-not (Test-Path (Join-Path $MSIXFolder.FullName $executable))) {
        Write-Warning "Application executable not found in package: $executable. Launch target and default icon may not resolve."
    }

    # --- 1. Resolve the Icon reference -----------------------------------------
    # The shortcut icon is taken from the .lnk's target executable (see section 2). The
    # manifest Icon points at that same executable inside the package - the pattern used
    # by captured shortcuts (e.g. Icon="[{Package}]\bin\app.exe"). An explicit IconSource
    # may point at another file inside the package (.exe/.dll/.ico).
    if ([string]::IsNullOrEmpty($IconSource)) {
        $iconRef = '[{Package}]\' + $executable
    }
    else {
        if (-not (Test-Path $IconSource)) {
            Write-Error "Icon source not found: $IconSource"
            return
        }
        $iconExt = [System.IO.Path]::GetExtension($IconSource).ToLowerInvariant()
        if ($iconExt -notin @('.ico', '.exe', '.dll')) {
            Write-Warning "Icon source extension '$iconExt' may not render as a shortcut icon. Use .ico/.exe/.dll (a .png does not resolve for a desktop7 shortcut)."
        }
        $fullIcon = (Resolve-Path $IconSource).Path
        $pkgRoot  = $MSIXFolder.FullName.TrimEnd('\')
        if ($fullIcon -like "$pkgRoot\*") {
            # Already inside the package - reference it in place.
            $iconRef = '[{Package}]\' + $fullIcon.Substring($pkgRoot.Length + 1)
        }
        else {
            # External file: copy it next to the application executable - a per-user
            # resolvable VFS location (the only kind that renders). Icons under Assets\
            # do NOT resolve for a desktop7 shortcut.
            $exeRelDir = Split-Path $executable -Parent
            $destDir   = if ([string]::IsNullOrEmpty($exeRelDir)) { $pkgRoot } else { Join-Path $pkgRoot $exeRelDir }
            if (-not (Test-Path $destDir)) {
                Write-Error "Cannot copy icon: application folder '$exeRelDir' does not exist in the package."
                return
            }
            $iconLeaf = Split-Path $fullIcon -Leaf
            Copy-Item -LiteralPath $fullIcon -Destination (Join-Path $destDir $iconLeaf) -Force
            $iconRelPath = if ([string]::IsNullOrEmpty($exeRelDir)) { $iconLeaf } else { Join-Path $exeRelDir $iconLeaf }
            $iconRef = '[{Package}]\' + $iconRelPath
            Write-Verbose "Copied external icon into package: $iconRelPath"
        }
    }

    # --- 2. Build the native TargetPath: the app's original install path. The shortcut
    # icon is taken from this executable, so it must resolve on the deployed machine,
    # e.g. VFS\ProgramFilesX86\App\app.exe -> C:\Program Files (x86)\App\app.exe.
    $nativeTarget = $executable
    $nativeMap = @{
        'VFS\ProgramFilesX64\'    = ($env:ProgramW6432 + '\')
        'VFS\ProgramFilesX86\'    = (${env:ProgramFiles(x86)} + '\')
        'VFS\ProgramFilesCommonX64\' = ($env:CommonProgramW6432 + '\')
        'VFS\ProgramFilesCommonX86\' = (${env:CommonProgramFiles(x86)} + '\')
        'VFS\SystemX64\'          = ($env:windir + '\System32\')
        'VFS\SystemX86\'          = ($env:windir + '\SysWOW64\')
        'VFS\Windows\'            = ($env:windir + '\')
    }
    foreach ($k in $nativeMap.Keys) {
        if ($nativeTarget -like "$k*") {
            $nativeTarget = $nativeMap[$k] + $nativeTarget.Substring($k.Length)
            break
        }
    }
    if (-not [System.IO.Path]::IsPathRooted($nativeTarget)) {
        # Package-root-relative executable -> assume WindowsApps native location is unknown; keep a plausible path
        $nativeTarget = Join-Path $env:ProgramW6432 $nativeTarget
    }

    # --- 3. Write the physical .lnk into the package ---------------------------
    $vfsDir  = Join-Path $MSIXFolder.FullName $ShortcutLocationTokens[$Location]
    if (-not (Test-Path $vfsDir)) { $null = New-Item -ItemType Directory -Path $vfsDir -Force }
    $lnkPath = Join-Path $vfsDir ($Name + '.lnk')

    $wshShell = New-Object -ComObject WScript.Shell
    try {
        $lnk = $wshShell.CreateShortcut($lnkPath)
        $lnk.TargetPath = $nativeTarget
        if ($Arguments)   { $lnk.Arguments   = $Arguments }
        if ($Description) { $lnk.Description = $Description }
        $lnk.Save()
    }
    finally {
        $null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wshShell)
    }
    Write-Verbose "Wrote .lnk: $lnkPath (TargetPath=$nativeTarget)"

    # --- 4. Ensure desktop7 namespace, then add the shortcut extension ---------
    $root = $manifest.DocumentElement
    if (-not $root.HasAttribute('xmlns:desktop7')) {
        $null = $root.SetAttribute('xmlns:desktop7', $desktop7Ns)
        $ign = $root.GetAttribute('IgnorableNamespaces')
        if ($ign -notmatch '\bdesktop7\b') {
            $null = $root.SetAttribute('IgnorableNamespaces', ("$ign desktop7").Trim())
        }
    }

    $extensions = $app.SelectSingleNode('ns:Extensions', $nsmgr)
    if ($null -eq $extensions) {
        $extensions = $manifest.CreateElement('Extensions', $AppXNamespaces['ns'])
        $null = $app.AppendChild($extensions)
    }

    $ext = $manifest.CreateElement('desktop7:Extension', $desktop7Ns)
    $null = $ext.SetAttribute('Category', 'windows.shortcut')
    $sc  = $manifest.CreateElement('desktop7:Shortcut', $desktop7Ns)
    $null = $sc.SetAttribute('File', ($Location + '\' + $Name + '.lnk'))
    $null = $sc.SetAttribute('Icon', $iconRef)
    if ($Arguments)      { $null = $sc.SetAttribute('Arguments', $Arguments) }
    if ($Description)    { $null = $sc.SetAttribute('Description', $Description) }
    if ($PinToStartMenu) { $null = $sc.SetAttribute('PinToStartMenu', 'true') }
    $null = $ext.AppendChild($sc)
    $null = $extensions.AppendChild($ext)

    $manifest.Save($manifestPath)
    Write-Verbose "Added desktop7:Shortcut '$Name' for application '$AppId' at $Location."

    [PSCustomObject]@{
        ApplicationId  = $AppId
        Name           = $Name
        File           = ($Location + '\' + $Name + '.lnk')
        Icon           = $iconRef
        PinToStartMenu = [bool]$PinToStartMenu
        LnkPath        = $lnkPath
    }
}