前言

由于工作等原因(借口),距离发布上一篇MDT系列的文章已经过去一年 ::twemoji:sweat::

上一章我记录了基于MDT如何使用一个Task即可实现制作Windows基础wim镜像+DIY基础软件+捕捉镜像传送门

有好多同学一直咨询在制作捕捉镜像的时候遇到的常见的2个问题:

  1. 如何彻底的移除掉Windows10中内置的应用;
  2. 手动移除掉一些应用后就无法执行sysprep该如何解决;

接下来我会分别用两篇文章去记录我使用的解决方法。本篇将介绍:如何按需移除掉Windows10中内置应用


环境准备

本文为 微风 原创文章.经实践,测试,整理发布.如需转载请联系作者获得授权,并注明转载地址.


操作说明

获取内置应用清单

微软在过去几年,几乎每个版本的Windows都会有新增一些新的内置应用。为了便于前后的效果对比,我这里也通过ISO安装一个默认系统。

默认系统安装完成后,手动更新系统补丁以及Windows Store应用。

基于微软介绍,Windows内置应用包含以下两种:

官方文档


  1. 预配应用:Provisioned Apps
  2. 系统应用:System Apps

然后以管理员身份执行powershell,并执行以下命令,即可将系统中所有预配应用的列表列出:

# 获取provisioned-apps
Get-AppxProvisionedPackage -Online | Format-Table DisplayName, PackageName

# 获取system-apps
Get-AppxPackage -PackageTypeFilter Main | ? { $_.SignatureKind -eq "System" } | Sort Name | Format-Table Name, InstallLocation

通过上述方法即可获取到当前系统的所有内置应用,然后将你需要保留的应用名称单独记录下来,后面通过脚本执行除去现有记录的应用外,其他所有内置应用执行卸载操作。

注意:因为不同的Windows版本其内置的应用清单会有所不同,所以我这里做一个需要保留的应用白名单,除白名单外的应用均执行卸载。


卸载脚本

<#
.SYNOPSIS
    Remove built-in apps (modern apps) from Windows 10.

.DESCRIPTION
    此脚本将删除所有未在此脚本的“白名单”中指定的预配包中内置应用程序。
    它支持MDT和ConfigMgr使用,但仅适用于在线场景,所以它不能在WinPE阶段执行。
    有关每个Windows 10版本中可用应用程序的更详细列表,请参阅此处的文档:
    https://docs.microsoft.com/en-us/windows/application-management/apps-in-windows-10

.EXAMPLE
    .\Invoke-RemoveBuiltinApps.ps1

#>
Begin {
    # White list of Features On Demand V2 packages
    $WhiteListOnDemand = "NetFX3|DirectX|Tools.DeveloperMode.Core|Language|InternetExplorer|ContactSupport|OneCoreUAP|WindowsMediaPlayer|Hello.Face|Notepad|MSPaint|PowerShell.ISE|ShellComponents"

    # White list of appx packages to keep installed
    $WhiteListedApps = New-Object -TypeName System.Collections.ArrayList
    $WhiteListedApps.AddRange(@(
            "Microsoft.DesktopAppInstaller",
            "Microsoft.Office.OneNote",
            "Microsoft.Messaging", 
            "Microsoft.MSPaint",
            "Microsoft.Windows.Photos",
            "Microsoft.StorePurchaseApp",
            "Microsoft.MicrosoftOfficeHub",
            "Microsoft.MicrosoftStickyNotes",
            "Microsoft.WindowsAlarms",
            "Microsoft.WindowsCalculator", 
            "Microsoft.WindowsCommunicationsApps", # Mail, Calendar etc
            "Microsoft.WindowsSoundRecorder", 
            "Microsoft.WindowsStore"
        ))

    # Windows 10 version 1809
    $WhiteListedApps.AddRange(@(
            "Microsoft.ScreenSketch",
            "Microsoft.HEIFImageExtension",
            "Microsoft.VP9VideoExtensions",
            "Microsoft.WebMediaExtensions",
            "Microsoft.WebpImageExtension"
        ))

    # Windows 10 version 1909
    $WhiteListedApps.AddRange(@(
            "Microsoft.Outlook.DesktopIntegrationServicess"
        ))

    # Windows 10 version 2004
    $WhiteListedApps.AddRange(@(
            "Microsoft.VCLibs.140.00"
        ))

    # Windows 10 version 20H2
    $WhiteListedApps.AddRange(@(
            "Microsoft.MicrosoftEdge.Stable"
        ))

    # Windows 10 version 2H2
    $WhiteListedApps.AddRange(@(
            "Microsoft.WindowsCamera",
            "Microsoft.YourPhone",
            "Microsoft.ZuneMusic",
            "Microsoft.ZuneVideo",
            "Microsoft.Microsoft3DViewer"
            # Add the appname which you want keep here.
        ))
}
Process {
    # Functions
    function Write-LogEntry {
        param(
            [parameter(Mandatory = $true, HelpMessage = "Value added to the RemovedApps.log file.")]
            [ValidateNotNullOrEmpty()]
            [string]$Value,

            [parameter(Mandatory = $false, HelpMessage = "Name of the log file that the entry will written to.")]
            [ValidateNotNullOrEmpty()]
            [string]$FileName = "RemovedApps.log"
        )
        # Determine log file location
        $LogFilePath = Join-Path -Path $env:windir -ChildPath "Temp\$($FileName)"

        # Add value to log file
        try {
            Out-File -InputObject $Value -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop
        }
        catch [System.Exception] {
            Write-Warning -Message "Unable to append log entry to $($FileName) file"
        }
    }

    # Initial logging
    Write-LogEntry -Value "Starting built-in AppxPackage, AppxProvisioningPackage and Feature on Demand V2 removal process"

    # Determine provisioned apps
    $AppArrayList = Get-AppxProvisionedPackage -Online | Select-Object -ExpandProperty DisplayName

    # Loop through the list of appx packages
    foreach ($App in $AppArrayList) {
        Write-LogEntry -Value "Processing appx package: $($App)"

        # If application name not in appx package white list, remove AppxPackage and AppxProvisioningPackage
        if (($App -in $WhiteListedApps)) {
            Write-LogEntry -Value "Skipping excluded application package: $($App)"
        }
        else {
            # Gather package names
            $AppPackageFullName = Get-AppxPackage -Name $App | Select-Object -ExpandProperty PackageFullName -First 1
            $AppProvisioningPackageName = Get-AppxProvisionedPackage -Online | Where-Object { $_.DisplayName -like $App } | Select-Object -ExpandProperty PackageName -First 1

            # Attempt to remove AppxPackage
            if ($AppPackageFullName -ne $null) {
                try {
                    Write-LogEntry -Value "Removing AppxPackage: $($AppPackageFullName)"
                    Remove-AppxPackage -Package $AppPackageFullName -ErrorAction Stop | Out-Null
                }
                catch [System.Exception] {
                    Write-LogEntry -Value "Removing AppxPackage '$($AppPackageFullName)' failed: $($_.Exception.Message)"
                }
            }
            else {
                Write-LogEntry -Value "Unable to locate AppxPackage for current app: $($App)"
            }

            # Attempt to remove AppxProvisioningPackage
            if ($AppProvisioningPackageName -ne $null) {
                try {
                    Write-LogEntry -Value "Removing AppxProvisioningPackage: $($AppProvisioningPackageName)"
                    Remove-AppxProvisionedPackage -PackageName $AppProvisioningPackageName -Online -ErrorAction Stop | Out-Null
                }
                catch [System.Exception] {
                    Write-LogEntry -Value "Removing AppxProvisioningPackage '$($AppProvisioningPackageName)' failed: $($_.Exception.Message)"
                }
            }
            else {
                Write-LogEntry -Value "Unable to locate AppxProvisioningPackage for current app: $($App)"
            }
        }
    }

    # Complete
    Write-LogEntry -Value "Completed built-in AppxPackage, AppxProvisioningPackage and Feature on Demand V2 removal process"
}



脚本用法

将上述脚本复制并保存到MDT的Scripts目录中,我的存放在%SCRIPTROOT%\Custom\Remove-Builtin-Apps\Invoke-RemoveBuiltinApps.ps1


该脚本也可以用在SCCM的Task中,具体使用方法请参考下图

MDT部署Windows系列 (十二): 进阶篇—制作完美的Win10 22H2系统镜像模板之移除Windows内置应用_MDT


执行效果

本篇不再介绍如何捕捉镜像以及过程中创建暂停任务功能,如需可参考上一篇文档。

执行前应用的数量:

MDT部署Windows系列 (十二): 进阶篇—制作完美的Win10 22H2系统镜像模板之移除Windows内置应用_MDT_02

任务执行状态:

执行后应用的数量:

MDT部署Windows系列 (十二): 进阶篇—制作完美的Win10 22H2系统镜像模板之移除Windows内置应用_MDT_03

日志文件:

MDT部署Windows系列 (十二): 进阶篇—制作完美的Win10 22H2系统镜像模板之移除Windows内置应用_MDT部署Windows_04


后续

至此,已经完成在制作捕捉镜像时移除不需要的系统内置应用的需求。

但如果不出意外(你的环境中服务器、客户端能否访问互联网并且执行了系统更新时)你将会在Sysprep过程中遇到如下错误:(

MDT部署Windows系列 (十二): 进阶篇—制作完美的Win10 22H2系统镜像模板之移除Windows内置应用_MDT部署Windows_05

Error SYSPRP Package <PackageFullName> was installed for a user, but not provisioned for all users. This package will not function properly in the sysprep image.
<Date> <Time>, Error SYSPRP Failed to remove apps for the current user: 0x80073cf2.
<Date> <Time>, Error SYSPRP Exit code of RemoveAllApps thread was 0x3cf2.
....

下一篇我将介绍使用多种方式解决因删除或更新包含内置Windows映像的Microsoft Store应用后Sysprep失败的问题。


Enjoy ~~ :)