Compare commits

..
13 Commits
Author SHA1 Message Date
Kobi HikriandGitHub 5f37ef4a80 ci: read the PR body from env instead of interpolating it into the shell (#4880) 2026-07-28 14:11:17 -05:00
OmarandGitHub d790118546 Add DNS-over-HTTPS (DoH) support (#4856)
* feat: Add DoH templates for DNS providers in configuration

* feat: Implement DoH support for DNS configuration and management

* feat(pester): enhance DNS tests to include DoH server address handling and template application

* fix: address codex feedback

* fix: update existing DoH server entries

* fix: enable DoH auto-upgrade

* fix: handle DNS setup failures

* fix: remove DoH state on DHCP reset
2026-07-28 14:09:44 -05:00
OmarGitHubChris Tituscoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
5ebb24b107 Refactor Win11 Creator ISO workflow (#4862)
* refactor: simplify Win11 Creator ISO workflow

Keep no-driver ISO generation on the fast copy-only path.

Inject exported drivers with one selected-index WIM mount, one Add-Driver pass, and one commit.

Limit WinPE staging to boot-storage drivers and improve workflow validation coverage.

* refactor: stage Win11 setup script fallback

Copy the prepared setup script payloads into sources/$//Setup/Scripts so setup media does not depend solely on answer-file extension extraction.

Keep the existing autounattend execution path unchanged and cover the fallback files in the Win11 Creator tests.

* refactor: address codex feedback

* refactor: run ISO verification in runspace

Rename the ISO logger to Write-WinUtilISOLog so the shared runspace pool imports it automatically. Run Mount & Verify off the WPF thread with dispatcher-safe UI updates, and add Pester coverage for the runspace and logger contract.

* refactor: address ISO review feedback

Keep the selected ISO stable during background verification. Apply ContentDeliveryManager settings to both the first account and the default profile. Block FAT32 USB creation when install.esd exceeds the supported file size.

* refactor: address ISO setup edge cases

Set BypassNRO before OOBE so local account setup is available during Windows Setup.

Detect registered DISM mounts during driver-injection cleanup so partial mount failures are discarded.

* refactor: address ISO review edge cases

Use the actual FAT32 file limit for install.esd USB checks.

Disable ISO modification while mount verification is running.

Apply unsupported-hardware notice suppression to the first user profile.

* refactor: stage ISO setup safeguards earlier

Set device encryption and reserved-storage registry guards before OOBE.

Enable the OEM configuration-set fallback when setup scripts are staged there.

* Update functions/private/Invoke-WinUtilISOUSB.ps1

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Chris Titus <contact@christitus.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-28 14:08:56 -05:00
Chris TitusandGitHub fae58d73c4 Update sponsors in README (#4881) 2026-07-28 11:10:13 -05:00
Chris TitusandGitHub 9e2989964e Update sponsors in README (#4878) 2026-07-27 11:14:18 -05:00
OmarandGitHub 50be7390e5 fix: restore WindowStyle to SingleBorderWindow for proper window framing (#4868) 2026-07-22 12:42:25 -05:00
Chris TitusandGitHub 8ab02ae756 chore: Update generated dev docs (#4873) 2026-07-22 12:42:13 -05:00
OmarandGitHub 7d1a03dcc9 bugfix(tweaks): fix OneDrive uninstall path (#4864)
* bugfix(tweaks): fix OneDrive uninstall path

* bugfix(tweaks): use SystemRoot for OneDrive path
2026-07-22 12:41:48 -05:00
Paweł GwozdowskiandGitHub 5a4c9c883d Revert "Rework contributing.md (#4567)" (#4855) 2026-07-22 12:34:14 -05:00
Chris Titus 5c104d02e2 Update automated update PR workflows and filter release notes 2026-07-16 13:39:33 -05:00
Chris TitusandGitHub 06f33e88ba Fix Show Installed Apps reliability and selection (#4850)
* Fix Windows PowerShell runspace cleanup callback

* Fix installed app package ID matching

* Synchronize installed app selection UI

* Streamline installed app detection

* Avoid installed app UI runspace deadlock
2026-07-16 13:30:12 -05:00
b9dee86694 refactor: style and align Install tab filter chips (#4848)
* refactor: implement FilterChipStyle for Install tab category filters

- Created a dedicated FilterChipStyle style block with custom padding, cursor, and height definitions

- Applied FilterChipStyle to all category filter buttons

- Added a new 'Filters' TextBlock section title aligned with the sidebar actions

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Chris Titus <contact@christitus.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-16 10:37:25 -05:00
OmarandGitHub 3d1904ebbe fix: wrap Tweaks header buttons to prevent layout clipping (#4847) 2026-07-16 10:29:16 -05:00
33 changed files with 1619 additions and 1079 deletions
+88 -114
View File
@@ -1,149 +1,123 @@
## Contributing Code # How to Contribute?
### Before You Start ## Testing
- Keep each pull request focused on a single feature or fix. * Test the latest changes to WinUtil by running the pre-release and reporting issues you are encountering to help us continually improve WinUtil!
- Avoid unnecessary formatting changes or large unrelated edits.
- Document what changed and why in your PR description.
--- #### **Run the latest pre-release**
```ps1
irm https://christitus.com/windev | iex
```
## Basic Git Workflow !!! bug "Keep in mind"
### 1. Fork the Repository This is a pre-release and should be treated as such. It exists for developers to test the utility and report or fix bugs before they get added to the stable release. Don't use it in production!
Go to the ChrisTitusTech/winutil repository on GitHub and click the Fork button in the top right corner. ## Issues
<img width="171" height="50" alt="{650A4723-F38A-44A4-9820-D232BC87C8A0}" src="https://github.com/user-attachments/assets/a214f27c-2fee-444a-920f-d87b14f5896f" /> * If you encounter any challenges or problems with the script, I kindly request that you submit them via the "Issues" tab on the GitHub repository. By filling out the provided template, you can provide specific details about the issue, allowing me (and others in the community) to promptly address any bugs or consider feature requests.
--- ## Contribute Code
### 2. Clone Your Fork * Pull requests are now handled directly on the **MAIN branch**. This was done since we can now select specific releases to launch via releases in GitHub.
```powershell * If you're doing code changes, then you can submit a PR to the `main` branch.
git clone https://github.com/YOUR_USERNAME/winutil.git
cd winutil !!! warning "Important"
Do not use a code formatter, make massive amounts of line changes, or make multiple feature changes. EACH FEATURE CHANGE SHOULD BE IT'S OWN PULL REQUEST!
Do not open a pull request that adds support for other languages to WinUtil for now, until we decide how we want to move forward with language support.
* When creating pull requests, it is essential to thoroughly document all changes made. This includes, but is not limited to, documenting any additions made to the `tweaks` section and corresponding `undo tweak`, so users are able to remove the newly added tweaks if necessary, and comprehensive documentation is required for all code changes. Document your changes and briefly explain why you made your changes in your Pull Request Description. Failure to adhere to this format may result in the denial of the pull request. Additionally, any code lacking sufficient documentation may also be denied.
* By following these guidelines, we can maintain a high standard of quality and ensure that the codebase remains organized and well-documented.
!!! note
When creating a function, please include "WPF" or "WinUtil" in the file name so it can be loaded into the runspace.
## Walk through
* This is a guide for beginners. If you are still having issues, look at the following official GitHub documentation:
* [Commit through WEB](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/about-commits)
* [Commit through GitHub Desktop](https://docs.github.com/en/desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project-in-github-desktop#about-commits)
* [Create a Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)
### Overview
``` mermaid
%%{init: {"flowchart": {"curve": "cardinal"}} }%%
graph TD
  A[Fork Project] --> B[Clone Repository];
  B --> C[Create New Branch];
  C --> D[Make Changes];
  D --> G[Test Changes];
  G --> H{Tests Passed?};
  H -->|Yes| E[Commit Changes];
  H -->|No| J[Fix Issues];
  J --> G;
  E --> F[Push Branch];
  F --> K[Create Pull Request];
  K --> L[Fill out PR template];
classDef default stroke:#333,stroke-width:4px,font-size:12pt;
``` ```
!!! info
--- This is a diagram to guide you through the process. It may vary depending on the type of change you're making.
### 3. Create a Branch ### Fork the Repo
* Fork the WinUtil Repository [here](https://github.com/ChrisTitusTech/winutil) to create a copy that will be available in your repository list.
Never work directly on `main`. ![Fork Image](/docs/assets/images/Fork-Button-Dark.png#gh-dark-mode-only)
Create a branch related to your change: ![Fork Image](/docs/assets/images/Fork-Button-Light.png#only-light#gh-light-mode-only)
```powershell ### Clone the Fork
git checkout -b feature-name !!! tip
```
Example: While you can make your changes directly through the Web, we recommend cloning the repo to your device using the application GitHub Desktop (available in WinUtil) to test your fork easily.
```powershell * Install GitHub Desktop if it is not already installed.
git checkout -b add-firefox-tweak * Log in using the same GitHub account you used to fork WinUtil.
``` * Choose the fork under "Your Repositories" and press "clone {repo name}"
* Create a new branch and name it something relatable to your changes.
--- * Now you can modify WinUtil to your liking using your preferred text editor.
### 4. Edit the Code
Open the project in your preferred text editor and make your changes. ### Testing your changes
Keep changes small and focused. * To test to see if your changes work as intended, run the following commands in a PowerShell terminal as admin:
--- * Change the directory where you are running the commands to the forked project.
* `cd {path to the folder with the compile.ps1}`
* Run the following command to compile and run WinUtil:
* `.\Compile.ps1 -run`
### 5. Test Your Changes ![Compile](/docs/assets/images/Complie.png)
Open Powershell as Administrator.
Go to the project folder: Open PowerShell as Administrator.
```powershell * After seeing that your changes work properly, feel free to commit the changes to the repository and make a PR. For help on that, follow the documentation below.
cd path\to\winutil
```
Run:
```powershell ### Committing the changes
.\Compile.ps1 -Run * Before committing your changes, please discard changes made to the `winutil.ps1` file, like the following:
```
Verify: ![Push Commit Image](/docs/assets/images/Discard-GHD.png)
- WinUtil launches correctly * Now, commit your changes once you are happy with the result.
- Your feature works
- Nothing else breaks
If something fails, fix it before committing. ![Commit Image](/docs/assets/images/Commit-GHD.png)
--- * Push the changes to upload them to your fork on github.com.
### 6. Review Your Changes ![Push Commit Image](/docs/assets/images/Push-Commit.png)
Check what changed: ### Making a PR
* To make a PR on your repo under a new branch linking to the main branch, a button will show and say Preview and Create pull request. Click that button and fill in all the information that is provided on the template. Once all the information is filled in correctly, check your PR to make sure there is no WinUtil.ps1 file attached to the PR. Once everything is good, make the PR and wait for Chris (the maintainer) to accept or deny your PR. Once it is accepted by Chris, you will be able to see your changes in the "/windev" build.
```powershell * If you do not see your feature in the main "/win" build, that is fine. All new changes go into the /windev build to make sure everything is working OK before going fully public.
git status * Congratulations! You just submitted your first PR. Thank you so much for contributing to WinUtil.
```
Review the diff:
```powershell
git diff
```
Make sure you did not accidentally modify unrelated files.
---
### 7. Commit Your Changes
Stage files:
```powershell
git add .
```
Commit them:
```powershell
git commit -m "Add feature description"
```
Example:
```powershell
git commit -m "Add Firefox package tweak"
```
---
### 8. Push Your Branch
```powershell
git push origin branch-name
```
Example:
```powershell
git push origin add-firefox-tweak
```
---
### 9. Open a Pull Request
Go to your fork on GitHub.
GitHub will show a button to create a pull request.
<img width="1009" height="71" alt="{C8C6A3CC-79D4-44FD-A54C-4C5717F12730}" src="https://github.com/user-attachments/assets/0419d193-d4e7-47c0-87cf-b986742201a0" />
Before submitting:
- Explain what changed
- Explain why you changed it
- Make sure unrelated files are not included
Once submitted, maintainers will review your PR.
+2 -2
View File
@@ -1,4 +1,4 @@
name: Auto-merge Docs PRs name: Auto-merge Automated Update PRs
on: on:
pull_request: pull_request:
@@ -8,7 +8,7 @@ on:
jobs: jobs:
auto-merge: auto-merge:
if: github.event.pull_request.head.ref == 'docs-update' && (github.event.pull_request.user.login == 'ChrisTitusTech' || github.event.pull_request.user.login == 'github-actions[bot]') if: (github.event.pull_request.head.ref == 'docs-update' || github.event.pull_request.head.ref == 'sponsors-update') && (github.event.pull_request.user.login == 'ChrisTitusTech' || github.event.pull_request.user.login == 'github-actions[bot]')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
pull-requests: write pull-requests: write
@@ -18,10 +18,11 @@ jobs:
- name: Extract Discussion Number & Close If any Were Found - name: Extract Discussion Number & Close If any Were Found
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_BODY: ${{ github.event.pull_request.body }}
if: github.event.pull_request.merged == true if: github.event.pull_request.merged == true
id: extract-discussion id: extract-discussion
run: | run: |
pr_body="${{ github.event.pull_request.body }}" pr_body="$PR_BODY"
discussion_ids=$(echo "$pr_body" | grep -oP '(?i)(resolve|fix|close)[s|d]? #\K[0-9]+') discussion_ids=$(echo "$pr_body" | grep -oP '(?i)(resolve|fix|close)[s|d]? #\K[0-9]+')
if [ -z "$discussion_ids" ]; then if [ -z "$discussion_ids" ]; then
+16 -1
View File
@@ -109,6 +109,21 @@ jobs:
config-name: release-drafter.yml config-name: release-drafter.yml
version: ${{ env.VERSION }} version: ${{ env.VERSION }}
- name: Remove Automated Updates from Release Notes
id: filter_notes
shell: pwsh
env:
RELEASE_NOTES: ${{ steps.generate_notes.outputs.body }}
run: |
$filteredNotes = ($env:RELEASE_NOTES -split '\r?\n' | Where-Object {
$_ -notmatch '(?i)Update (Generated Dev Docs|Sponsors)'
}) -join "`n"
$delimiter = [guid]::NewGuid().ToString()
"body<<$delimiter" >> $env:GITHUB_OUTPUT
$filteredNotes >> $env:GITHUB_OUTPUT
$delimiter >> $env:GITHUB_OUTPUT
- name: Create and Upload Release - name: Create and Upload Release
id: create_release id: create_release
uses: softprops/action-gh-release@v3 uses: softprops/action-gh-release@v3
@@ -116,7 +131,7 @@ jobs:
tag_name: ${{ env.VERSION }} tag_name: ${{ env.VERSION }}
name: Pre-Release ${{ env.VERSION }} name: Pre-Release ${{ env.VERSION }}
body: | body: |
${{ steps.generate_notes.outputs.body }} ${{ steps.filter_notes.outputs.body }}
![GitHub Downloads (specific asset, specific tag)](https://img.shields.io/github/downloads/ChrisTitusTech/winutil/${{ env.VERSION }}/winutil.ps1) ![GitHub Downloads (specific asset, specific tag)](https://img.shields.io/github/downloads/ChrisTitusTech/winutil/${{ env.VERSION }}/winutil.ps1)
append_body: false append_body: false
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
id: cpr id: cpr
uses: peter-evans/create-pull-request@v8 uses: peter-evans/create-pull-request@v8
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.AUTO_MERGE }}
commit-message: 'Update sponsors in README' commit-message: 'Update sponsors in README'
title: 'chore: Update Sponsors README' title: 'chore: Update Sponsors README'
body: 'Automated update of sponsors section' body: 'Automated update of sponsors section'
+1
View File
@@ -189,5 +189,6 @@ When the user corrects an agent approach, add or tighten one concrete rule here
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic. - When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity. - Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media. - For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
- For Win11 Creator driver injection, keep offline WIM servicing to one mount, one `/Add-Driver`, and one commit; do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export.
- For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives. - For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives.
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP. - For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
+1 -1
View File
@@ -77,7 +77,7 @@ See https://github.com/ChrisTitusTech/winutil/blob/main/.github/CONTRIBUTING.md
These are the sponsors that help keep this project alive with monthly contributions. These are the sponsors that help keep this project alive with monthly contributions.
<!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https:&#x2F;&#x2F;github.com&#x2F;ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https:&#x2F;&#x2F;github.com&#x2F;dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https:&#x2F;&#x2F;github.com&#x2F;mews-se.png" width="60px" alt="User avatar: Martin Stockzell" /></a><a href="https://github.com/jdiegmueller"><img src="https:&#x2F;&#x2F;github.com&#x2F;jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https:&#x2F;&#x2F;github.com&#x2F;robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https:&#x2F;&#x2F;github.com&#x2F;paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https:&#x2F;&#x2F;github.com&#x2F;djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https:&#x2F;&#x2F;github.com&#x2F;anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https:&#x2F;&#x2F;github.com&#x2F;FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https:&#x2F;&#x2F;github.com&#x2F;DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https:&#x2F;&#x2F;github.com&#x2F;DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https:&#x2F;&#x2F;github.com&#x2F;KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/andrewpayne68"><img src="https:&#x2F;&#x2F;github.com&#x2F;andrewpayne68.png" width="60px" alt="User avatar: Andrew P" /></a><a href="https://github.com/johanwildeboer"><img src="https:&#x2F;&#x2F;github.com&#x2F;johanwildeboer.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/lukas346"><img src="https:&#x2F;&#x2F;github.com&#x2F;lukas346.png" width="60px" alt="User avatar: Wook" /></a><a href="https://github.com/tsv31"><img src="https:&#x2F;&#x2F;github.com&#x2F;tsv31.png" width="60px" alt="User avatar: Sorin" /></a><a href="https://github.com/seanh1995"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanh1995.png" width="60px" alt="User avatar: Sean (ANGRYxScotsman)" /></a><!-- sponsors --> <!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https:&#x2F;&#x2F;github.com&#x2F;ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https:&#x2F;&#x2F;github.com&#x2F;dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https:&#x2F;&#x2F;github.com&#x2F;mews-se.png" width="60px" alt="User avatar: Martin Stockzell" /></a><a href="https://github.com/jdiegmueller"><img src="https:&#x2F;&#x2F;github.com&#x2F;jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https:&#x2F;&#x2F;github.com&#x2F;robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https:&#x2F;&#x2F;github.com&#x2F;paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https:&#x2F;&#x2F;github.com&#x2F;djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https:&#x2F;&#x2F;github.com&#x2F;anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https:&#x2F;&#x2F;github.com&#x2F;FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https:&#x2F;&#x2F;github.com&#x2F;DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https:&#x2F;&#x2F;github.com&#x2F;DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https:&#x2F;&#x2F;github.com&#x2F;KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/andrewpayne68"><img src="https:&#x2F;&#x2F;github.com&#x2F;andrewpayne68.png" width="60px" alt="User avatar: Andrew P" /></a><a href="https://github.com/seanh1995"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanh1995.png" width="60px" alt="User avatar: Sean (ANGRYxScotsman)" /></a><a href="https://github.com/Abs313a"><img src="https:&#x2F;&#x2F;github.com&#x2F;Abs313a.png" width="60px" alt="User avatar: Abs" /></a><!-- sponsors -->
--- ---
+16 -8
View File
@@ -3,48 +3,56 @@
"Primary": "8.8.8.8", "Primary": "8.8.8.8",
"Secondary": "8.8.4.4", "Secondary": "8.8.4.4",
"Primary6": "2001:4860:4860::8888", "Primary6": "2001:4860:4860::8888",
"Secondary6": "2001:4860:4860::8844" "Secondary6": "2001:4860:4860::8844",
"DohTemplate": "https://dns.google/dns-query"
}, },
"Cloudflare":{ "Cloudflare":{
"Primary": "1.1.1.1", "Primary": "1.1.1.1",
"Secondary": "1.0.0.1", "Secondary": "1.0.0.1",
"Primary6": "2606:4700:4700::1111", "Primary6": "2606:4700:4700::1111",
"Secondary6": "2606:4700:4700::1001" "Secondary6": "2606:4700:4700::1001",
"DohTemplate": "https://cloudflare-dns.com/dns-query"
}, },
"Cloudflare_Malware":{ "Cloudflare_Malware":{
"Primary": "1.1.1.2", "Primary": "1.1.1.2",
"Secondary": "1.0.0.2", "Secondary": "1.0.0.2",
"Primary6": "2606:4700:4700::1112", "Primary6": "2606:4700:4700::1112",
"Secondary6": "2606:4700:4700::1002" "Secondary6": "2606:4700:4700::1002",
"DohTemplate": "https://security.cloudflare-dns.com/dns-query"
}, },
"Cloudflare_Malware_Adult":{ "Cloudflare_Malware_Adult":{
"Primary": "1.1.1.3", "Primary": "1.1.1.3",
"Secondary": "1.0.0.3", "Secondary": "1.0.0.3",
"Primary6": "2606:4700:4700::1113", "Primary6": "2606:4700:4700::1113",
"Secondary6": "2606:4700:4700::1003" "Secondary6": "2606:4700:4700::1003",
"DohTemplate": "https://family.cloudflare-dns.com/dns-query"
}, },
"Open_DNS":{ "Open_DNS":{
"Primary": "208.67.222.222", "Primary": "208.67.222.222",
"Secondary": "208.67.220.220", "Secondary": "208.67.220.220",
"Primary6": "2620:119:35::35", "Primary6": "2620:119:35::35",
"Secondary6": "2620:119:53::53" "Secondary6": "2620:119:53::53",
"DohTemplate": "https://doh.opendns.com/dns-query"
}, },
"Quad9":{ "Quad9":{
"Primary": "9.9.9.9", "Primary": "9.9.9.9",
"Secondary": "149.112.112.112", "Secondary": "149.112.112.112",
"Primary6": "2620:fe::fe", "Primary6": "2620:fe::fe",
"Secondary6": "2620:fe::9" "Secondary6": "2620:fe::9",
"DohTemplate": "https://dns.quad9.net/dns-query"
}, },
"AdGuard_Ads_Trackers":{ "AdGuard_Ads_Trackers":{
"Primary": "94.140.14.14", "Primary": "94.140.14.14",
"Secondary": "94.140.15.15", "Secondary": "94.140.15.15",
"Primary6": "2a10:50c0::ad1:ff", "Primary6": "2a10:50c0::ad1:ff",
"Secondary6": "2a10:50c0::ad2:ff" "Secondary6": "2a10:50c0::ad2:ff",
"DohTemplate": "https://dns.adguard-dns.com/dns-query"
}, },
"AdGuard_Ads_Trackers_Malware_Adult":{ "AdGuard_Ads_Trackers_Malware_Adult":{
"Primary": "94.140.14.15", "Primary": "94.140.14.15",
"Secondary": "94.140.15.16", "Secondary": "94.140.15.16",
"Primary6": "2a10:50c0::bad1:ff", "Primary6": "2a10:50c0::bad1:ff",
"Secondary6": "2a10:50c0::bad2:ff" "Secondary6": "2a10:50c0::bad2:ff",
"DohTemplate": "https://family.adguard-dns.com/dns-query"
} }
} }
+1 -1
View File
@@ -650,7 +650,7 @@
icacls $Env:OneDrive /deny \"Administrators:(D,DC)\" icacls $Env:OneDrive /deny \"Administrators:(D,DC)\"
Write-Host \"Uninstalling OneDrive...\" Write-Host \"Uninstalling OneDrive...\"
Start-Process '$Env:SystemRoot\\System32\\OneDriveSetup.exe' -ArgumentList '/uninstall' -Wait Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
# Some of OneDrive files use explorer, and OneDrive uses FileCoAuth # Some of OneDrive files use explorer, and OneDrive uses FileCoAuth
Write-Host \"Removing leftover OneDrive Files...\" Write-Host \"Removing leftover OneDrive Files...\"
Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

+88 -119
View File
@@ -2,152 +2,121 @@
toc: true toc: true
--- ---
## Contributing Code # How to Contribute?
### Before You Start ## Testing
- Keep each pull request focused on a single feature or fix. * Test the latest changes to WinUtil by running the pre-release and reporting issues you are encountering to help us continually improve WinUtil!
- Avoid unnecessary formatting changes or large unrelated edits.
- Document what changed and why in your PR description.
--- #### Run the latest pre-release
## Basic Git Workflow ```ps1
irm https://christitus.com/windev | iex
### 1. Fork the Repository
Go to the ChrisTitusTech/winutil repository on GitHub and click the Fork button in the top right corner.
<img width="171" height="50" alt="{650A4723-F38A-44A4-9820-D232BC87C8A0}" src="https://github.com/user-attachments/assets/a214f27c-2fee-444a-920f-d87b14f5896f" />
---
### 2. Clone Your Fork
```bash
git clone https://github.com/YOUR_USERNAME/winutil.git
cd winutil
``` ```
--- > [!IMPORTANT]
> **Keep in mind** That this is a pre-release and should be treated as such. It exists for developers to test the utility and report or fix bugs before they get added to the stable release. Don't use it in production!
### 3. Create a Branch ## Issues
Never work directly on `main`. * If you encounter any challenges or problems with the script, I kindly request that you submit them via the "Issues" tab on the GitHub repository. By filling out the provided template, you can provide specific details about the issue, allowing me (and others in the community) to promptly address any bugs or consider feature requests.
Create a branch related to your change: ## Contribute Code
```bash * Pull requests are now handled directly on the **MAIN branch**. This was done since we can now select specific releases to launch via releases in GitHub.
git checkout -b feature-name
* If you're doing code changes, then you can submit a PR to the `main` branch.
> [!IMPORTANT]
> Do not use a code formatter, make massive amounts of line changes, or make multiple feature changes. EACH FEATURE CHANGE SHOULD BE IT'S OWN PULL REQUEST!
> [!IMPORTANT]
> Do not open a pull request that adds support for other languages to WinUtil for now, until we decide how we want to move forward with language support.
* When creating pull requests, it is essential to thoroughly document all changes made. This includes, but is not limited to, documenting any additions made to the `tweaks` section and corresponding `undo tweak`, so users are able to remove the newly added tweaks if necessary, and comprehensive documentation is required for all code changes. Document your changes and briefly explain why you made your changes in your Pull Request Description. Failure to adhere to this format may result in the denial of the pull request. Additionally, any code lacking sufficient documentation may also be denied.
* By following these guidelines, we can maintain a high standard of quality and ensure that the codebase remains organized and well-documented.
> [!NOTE]
>
> When creating a function, please include "WPF" or "WinUtil" in the file name so it can be loaded into the runspace.
## Walk through
* This is a guide for beginners. If you are still having issues, look at the following official GitHub documentation:
* [Commit through WEB](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/about-commits)
* [Commit through GitHub Desktop](https://docs.github.com/en/desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project-in-github-desktop#about-commits)
* [Create a Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)
### Overview
```mermaid
%%{init: {"flowchart": {"curve": "cardinal"}} }%%
graph TD
A[Fork Project] --> B[Clone Repository];
B --> C[Create New Branch];
C --> D[Make Changes];
D --> G[Test Changes];
G --> H{Tests Passed?};
H -->|Yes| E[Commit Changes];
H -->|No| J[Fix Issues];
J --> G;
E --> F[Push Branch];
F --> K[Create Pull Request];
K --> L[Fill out PR template];
classDef default stroke:#333,stroke-width:4px,font-size:12pt;
``` ```
Example: > [!NOTE]
>
> This is a diagram to guide you through the process. It may vary depending on the type of change you're making.
```bash ### Fork the Repo
git checkout -b add-firefox-tweak * Fork the WinUtil Repository [here](https://github.com/ChrisTitusTech/winutil) to create a copy that will be available in your repository list.
```
--- {{< image src="images/Fork-Button" alt="Fork Image" >}}
### 4. Edit the Code ### Clone the Fork
> [!TIP]
>
> While you can make your changes directly through the Web, we recommend cloning the repo to your device using the application GitHub Desktop (available in WinUtil) to test your fork easily.
Open the project in your preferred text editor and make your changes. * Install GitHub Desktop if it is not already installed.
* Log in using the same GitHub account you used to fork WinUtil.
* Choose the fork under "Your Repositories" and press "clone {repo name}"
* Create a new branch and name it something relatable to your changes.
Keep changes small and focused. * Now you can modify WinUtil to your liking using your preferred text editor.
---
### 5. Test Your Changes ### Testing your changes
Open PowerShell as Administrator. * To test to see if your changes work as intended, run the following commands in a PowerShell terminal as admin:
Go to the project folder: * Change the directory where you are running the commands to the forked project.
* `cd {path to the folder with the compile.ps1}`
* Run the following command to compile and run WinUtil:
* `.\Compile.ps1 -run`
```powershell {{< image src="images/Complie" alt="Compile" >}}
cd path\to\winutil
```
Run: * After seeing that your changes work properly, feel free to commit the changes to the repository and make a PR. For help on that, follow the documentation below.
```powershell ### Committing the changes
.\Compile.ps1 -Run * Before committing your changes, please discard changes made to the `winutil.ps1` file, like the following:
```
Verify: {{< image src="images/Discard-GHD" alt="Push Commit Image" >}}
- WinUtil launches correctly * Now, commit your changes once you are happy with the result.
- Your feature works
- Nothing else breaks
If something fails, fix it before committing. {{< image src="images/Commit-GHD" alt="Commit Image" >}}
--- * Push the changes to upload them to your fork on github.com.
### 6. Review Your Changes {{< image src="images/Push-Commit" alt="Push Commit Image" >}}
Check what changed: ### Making a PR
* To make a PR on your repo under a new branch linking to the main branch, a button will show and say Preview and Create pull request. Click that button and fill in all the information that is provided on the template. Once all the information is filled in correctly, check your PR to make sure there is no WinUtil.ps1 file attached to the PR. Once everything is good, make the PR and wait for Chris (the maintainer) to accept or deny your PR. Once it is accepted by Chris, you will be able to see your changes in the "/windev" build.
```bash * If you do not see your feature in the main "/win" build, that is fine. All new changes go into the /windev build to make sure everything is working OK before going fully public.
git status * Congratulations! You just submitted your first PR. Thank you so much for contributing to WinUtil.
```
Review the diff:
```bash
git diff
```
Make sure you did not accidentally modify unrelated files.
---
### 7. Commit Your Changes
Stage files:
```bash
git add .
```
Commit them:
```bash
git commit -m "Add feature description"
```
Example:
```bash
git commit -m "Add Firefox package tweak"
```
---
### 8. Push Your Branch
```bash
git push origin branch-name
```
Example:
```bash
git push origin add-firefox-tweak
```
---
### 9. Open a Pull Request
Go to your fork on GitHub.
GitHub will show a button to create a pull request.
<img width="1009" height="71" alt="{C8C6A3CC-79D4-44FD-A54C-4C5717F12730}" src="https://github.com/user-attachments/assets/0419d193-d4e7-47c0-87cf-b986742201a0" />
Before submitting:
- Explain what changed
- Explain why you changed it
- Make sure unrelated files are not included
Once submitted, maintainers will review your PR.
@@ -15,7 +15,7 @@ description: ""
icacls $Env:OneDrive /deny \"Administrators:(D,DC)\" icacls $Env:OneDrive /deny \"Administrators:(D,DC)\"
Write-Host \"Uninstalling OneDrive...\" Write-Host \"Uninstalling OneDrive...\"
Start-Process '$Env:SystemRoot\\System32\\OneDriveSetup.exe' -ArgumentList '/uninstall' -Wait Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
# Some of OneDrive files use explorer, and OneDrive uses FileCoAuth # Some of OneDrive files use explorer, and OneDrive uses FileCoAuth
Write-Host \"Removing leftover OneDrive Files...\" Write-Host \"Removing leftover OneDrive Files...\"
@@ -15,32 +15,36 @@ Function Invoke-WinUtilCurrentSystem {
) )
if ($CheckBox -eq "choco") { if ($CheckBox -eq "choco") {
$apps = (choco list | Select-String -Pattern "^\S+").Matches.Value $apps = (choco list | Select-String -Pattern "^\S+").Matches.Value
$filter = Get-WinUtilVariables -Type Checkbox | Where-Object {$psitem -like "WPFInstall*"} $sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object {
$sync.GetEnumerator() | Where-Object {$psitem.Key -in $filter} | ForEach-Object { $packageId = ($_.Value.choco -split ";")[-1].Trim()
$dependencies = @($sync.configs.applications.$($psitem.Key).choco -split ";") if ($packageId -ne "na" -and $packageId -in $apps) {
if ($dependencies -in $apps) { Write-Output $_.Key
Write-Output $psitem.name
} }
} }
} }
if ($checkbox -eq "winget") { if ($checkbox -eq "winget") {
$originalEncoding = [Console]::OutputEncoding $originalEncoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() try {
$Sync.InstalledPrograms = @("winget", "msstore") | ForEach-Object { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
winget list -s $psitem | Select-Object -skip 3 | ConvertFrom-String -PropertyNames "Name", "Id", "Version", "Available" -Delimiter '\s{2,}' $installedProgramOutput = @(winget list --accept-source-agreements --disable-interactivity 2>&1)
if ($LASTEXITCODE -ne 0) {
throw "winget list failed with exit code $LASTEXITCODE."
}
} finally {
[Console]::OutputEncoding = $originalEncoding
} }
[Console]::OutputEncoding = $originalEncoding $installedProgramText = $installedProgramOutput -join "`n"
$filter = Get-WinUtilVariables -Type Checkbox | Where-Object {$psitem -like "WPFInstall*"} $sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object {
$sync.GetEnumerator() | Where-Object {$psitem.Key -in $filter} | ForEach-Object { $packageId = (($_.Value.winget -split ";")[-1] -replace "^msstore:", "").Trim()
$dependencies = @($sync.configs.applications.$($psitem.Key).winget -split ";") | ForEach-Object { if ([string]::IsNullOrWhiteSpace($packageId) -or $packageId -eq "na") {
$psitem -replace "^msstore:", "" return
} }
if ($dependencies[-1] -in $sync.InstalledPrograms.Id) { $packagePattern = "(?im)[^\S\r\n]{2,}$([regex]::Escape($packageId))(?=[^\S\r\n]{2,}|$)"
Write-Output $psitem.name if ($installedProgramText -match $packagePattern) {
Write-Output $_.Key
} }
} }
} }
+115 -244
View File
@@ -1,4 +1,4 @@
function Write-Win11ISOLog { function Write-WinUtilISOLog {
param([string]$Message) param([string]$Message)
$ts = (Get-Date).ToString("HH:mm:ss") $ts = (Get-Date).ToString("HH:mm:ss")
$logLine = "[$ts] $Message" $logLine = "[$ts] $Message"
@@ -35,7 +35,7 @@ function Invoke-WinUtilISOBrowse {
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed" $sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
$sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed" $sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed"
Write-Win11ISOLog "ISO selected: $isoPath ($fileSizeGB GB)" Write-WinUtilISOLog "ISO selected: $isoPath ($fileSizeGB GB)"
} }
function Invoke-WinUtilISOMountAndVerify { function Invoke-WinUtilISOMountAndVerify {
@@ -46,84 +46,102 @@ function Invoke-WinUtilISOMountAndVerify {
return return
} }
Write-Win11ISOLog "Mounting ISO: $isoPath" Write-WinUtilISOLog "Mounting ISO: $isoPath"
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Mounting ISO..." -Percent 10 Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Mounting ISO..." -Percent 10
$sync["WPFWin11ISOBrowseButton"].IsEnabled = $false
$sync["WPFWin11ISOMountButton"].IsEnabled = $false
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false
$sync["Win11ISOProcessRunning"] = $true
try { Invoke-WPFRunspace -ParameterList @(,('isoPath', $isoPath)) -ScriptBlock {
Mount-DiskImage -ImagePath $isoPath param($isoPath)
do { try {
Start-Sleep -Milliseconds 500 Mount-DiskImage -ImagePath $isoPath
} until ((Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter)
$driveLetter = (Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter + ":" do {
Write-Win11ISOLog "Mounted at drive $driveLetter" Start-Sleep -Milliseconds 500
} until ((Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter)
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Verifying ISO contents..." -Percent 30 $driveLetter = (Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter + ":"
Write-WinUtilISOLog "Mounted at drive $driveLetter"
$wimPath = Join-Path $driveLetter "sources\install.wim" Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Verifying ISO contents..." -Percent 30
$esdPath = Join-Path $driveLetter "sources\install.esd"
if (-not (Test-Path $wimPath) -and -not (Test-Path $esdPath)) { $wimPath = Join-Path $driveLetter "sources\install.wim"
Dismount-DiskImage -ImagePath $isoPath $esdPath = Join-Path $driveLetter "sources\install.esd"
Write-Win11ISOLog "ERROR: install.wim/install.esd not found - not a valid Windows ISO."
[System.Windows.MessageBox]::Show(
"This does not appear to be a valid Windows ISO.`n`ninstall.wim / install.esd was not found.",
"Invalid ISO", "OK", "Error")
Set-WinUtilTweaksProgressIndicator -Visible $false
return
}
$activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath } if (-not (Test-Path $wimPath) -and -not (Test-Path $esdPath)) {
Dismount-DiskImage -ImagePath $isoPath
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Reading image metadata..." -Percent 55 Write-WinUtilISOLog "ERROR: install.wim/install.esd not found - not a valid Windows ISO."
$imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName Invoke-WPFUIThread {
[System.Windows.MessageBox]::Show(
if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) { "This does not appear to be a valid Windows ISO.`n`ninstall.wim / install.esd was not found.",
Dismount-DiskImage -ImagePath $isoPath "Invalid ISO", "OK", "Error")
Write-Win11ISOLog "ERROR: No 'Windows 11' edition found in the image."
[System.Windows.MessageBox]::Show(
"No Windows 11 edition was found in this ISO.`n`nOnly official Windows 11 ISOs are supported.",
"Not a Windows 11 ISO", "OK", "Error")
Set-WinUtilTweaksProgressIndicator -Visible $false
return
}
$sync["Win11ISOImageInfo"] = $imageInfo
$sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $driveLetter | Image file: $(Split-Path $activeWim -Leaf)"
$sync["WPFWin11ISOEditionComboBox"].Dispatcher.Invoke([action]{
$sync["WPFWin11ISOEditionComboBox"].Items.Clear()
foreach ($img in $imageInfo) {
[void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)")
}
if ($sync["WPFWin11ISOEditionComboBox"].Items.Count -gt 0) {
$proIndex = -1
for ($i = 0; $i -lt $sync["WPFWin11ISOEditionComboBox"].Items.Count; $i++) {
if ($sync["WPFWin11ISOEditionComboBox"].Items[$i] -match "Windows 11 Pro(?![\w ])") {
$proIndex = $i; break
}
} }
$sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 } return
} }
})
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible"
$sync["Win11ISODriveLetter"] = $driveLetter $activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath }
$sync["Win11ISOWimPath"] = $activeWim
$sync["Win11ISOImagePath"] = $isoPath
$sync["WPFWin11ISOModifySection"].Visibility = "Visible"
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "ISO verified" -Percent 100 Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Reading image metadata..." -Percent 55
Write-Win11ISOLog "ISO verified OK. Editions found: $($imageInfo.Count)" $imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName
} catch {
Write-Win11ISOLog "ERROR during mount/verify: $_" if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) {
[System.Windows.MessageBox]::Show( Dismount-DiskImage -ImagePath $isoPath
"An error occurred while mounting or verifying the ISO:`n`n$_", Write-WinUtilISOLog "ERROR: No 'Windows 11' edition found in the image."
"Error", "OK", "Error") Invoke-WPFUIThread {
} finally { [System.Windows.MessageBox]::Show(
Start-Sleep -Milliseconds 800 "No Windows 11 edition was found in this ISO.`n`nOnly official Windows 11 ISOs are supported.",
Set-WinUtilTweaksProgressIndicator -Visible $false "Not a Windows 11 ISO", "OK", "Error")
}
return
}
$sync["Win11ISOImageInfo"] = $imageInfo
$sync["Win11ISODriveLetter"] = $driveLetter
$sync["Win11ISOWimPath"] = $activeWim
$sync["Win11ISOImagePath"] = $isoPath
Invoke-WPFUIThread {
$sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $driveLetter | Image file: $(Split-Path $activeWim -Leaf)"
$sync["WPFWin11ISOEditionComboBox"].Items.Clear()
foreach ($img in $imageInfo) {
[void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)")
}
if ($sync["WPFWin11ISOEditionComboBox"].Items.Count -gt 0) {
$proIndex = -1
for ($i = 0; $i -lt $sync["WPFWin11ISOEditionComboBox"].Items.Count; $i++) {
if ($sync["WPFWin11ISOEditionComboBox"].Items[$i] -match "Windows 11 Pro(?![\w ])") {
$proIndex = $i; break
}
}
$sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 }
}
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible"
$sync["WPFWin11ISOModifySection"].Visibility = "Visible"
$sync["WPFWin11ISOModifyButton"].IsEnabled = $true
}
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "ISO verified" -Percent 100
Write-WinUtilISOLog "ISO verified OK. Editions found: $($imageInfo.Count)"
} catch {
$errorMessage = $_
Write-WinUtilISOLog "ERROR during mount/verify: $errorMessage"
Invoke-WPFUIThread {
[System.Windows.MessageBox]::Show(
"An error occurred while mounting or verifying the ISO:`n`n$errorMessage",
"Error", "OK", "Error")
}
} finally {
Start-Sleep -Milliseconds 800
Set-WinUtilTweaksProgressIndicator -Visible $false
Invoke-WPFUIThread {
$sync["WPFWin11ISOBrowseButton"].IsEnabled = $true
$sync["WPFWin11ISOMountButton"].IsEnabled = $true
$sync["Win11ISOProcessRunning"] = $false
}
}
} }
} }
@@ -147,7 +165,7 @@ function Invoke-WinUtilISOModify {
$selectedWimIndex = $sync["Win11ISOImageInfo"][0].ImageIndex $selectedWimIndex = $sync["Win11ISOImageInfo"][0].ImageIndex
} }
$selectedEditionName = if ($selectedItem) { ($selectedItem -replace '^\d+:\s*', '') } else { "Unknown" } $selectedEditionName = if ($selectedItem) { ($selectedItem -replace '^\d+:\s*', '') } else { "Unknown" }
Write-Win11ISOLog "Selected edition: $selectedEditionName (Index $selectedWimIndex)" Write-WinUtilISOLog "Selected edition: $selectedEditionName (Index $selectedWimIndex)"
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false $sync["WPFWin11ISOModifyButton"].IsEnabled = $false
$sync["Win11ISOModifying"] = $true $sync["Win11ISOModifying"] = $true
@@ -170,7 +188,6 @@ function Invoke-WinUtilISOModify {
$runspace.ThreadOptions = "ReuseThread" $runspace.ThreadOptions = "ReuseThread"
$runspace.Open() $runspace.Open()
$injectDrivers = $sync["WPFWin11ISOInjectDrivers"].IsChecked -eq $true $injectDrivers = $sync["WPFWin11ISOInjectDrivers"].IsChecked -eq $true
$runspace.SessionStateProxy.SetVariable("sync", $sync) $runspace.SessionStateProxy.SetVariable("sync", $sync)
$runspace.SessionStateProxy.SetVariable("isoPath", $isoPath) $runspace.SessionStateProxy.SetVariable("isoPath", $isoPath)
$runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter) $runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter)
@@ -182,7 +199,7 @@ function Invoke-WinUtilISOModify {
$runspace.SessionStateProxy.SetVariable("injectDrivers", $injectDrivers) $runspace.SessionStateProxy.SetVariable("injectDrivers", $injectDrivers)
$isoScriptFuncDef = "function Invoke-WinUtilISOScript {`n" + ${function:Invoke-WinUtilISOScript}.ToString() + "`n}" $isoScriptFuncDef = "function Invoke-WinUtilISOScript {`n" + ${function:Invoke-WinUtilISOScript}.ToString() + "`n}"
$win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + ${function:Write-Win11ISOLog}.ToString() + "`n}" $win11ISOLogFuncDef = "function Write-WinUtilISOLog {`n" + ${function:Write-WinUtilISOLog}.ToString() + "`n}"
$runspace.SessionStateProxy.SetVariable("isoScriptFuncDef", $isoScriptFuncDef) $runspace.SessionStateProxy.SetVariable("isoScriptFuncDef", $isoScriptFuncDef)
$runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef) $runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef)
@@ -235,121 +252,6 @@ function Invoke-WinUtilISOModify {
} }
} }
function Get-WinUtilMountedImageEditionId {
param(
[Parameter(Mandatory)][string]$MountDir,
[string]$EditionName,
[scriptblock]$Logger
)
try {
$dismOutput = & dism /English "/Image:$MountDir" /Get-CurrentEdition 2>&1
foreach ($line in $dismOutput) {
if ($line -match '^\s*Current Edition\s*:\s*(.+?)\s*$') {
$editionId = $Matches[1].Trim()
if ($editionId) {
if ($Logger) { $null = $Logger.Invoke("Detected mounted image EditionID: $editionId") }
return $editionId
}
}
}
} catch {
if ($Logger) { $null = $Logger.Invoke("Warning: could not detect mounted image EditionID with DISM: $_") }
}
$fallbackEditionId = Get-WinUtilEditionIdFromName -EditionName $EditionName
if ($fallbackEditionId -and $Logger) {
$null = $Logger.Invoke("Using fallback EditionID '$fallbackEditionId' from selected edition name.")
}
return $fallbackEditionId
}
function Get-DismImageInfoMap {
param(
[Parameter(Mandatory)][string]$ImagePath,
[int]$Index = 1
)
$map = @{}
$lines = & dism /English "/Get-ImageInfo" "/ImageFile:$ImagePath" "/Index:$Index"
foreach ($line in $lines) {
if ($line -match '^\s*([^:]+?)\s*:\s*(.*)$') {
$key = $Matches[1].Trim()
$val = $Matches[2].Trim()
if (-not $map.ContainsKey($key)) {
$map[$key] = $val
}
}
}
return $map
}
function Invoke-WinUtilWimMetadataHydration {
param(
[Parameter(Mandatory)][string]$ImagePath,
[Parameter(Mandatory)][string]$EditionName,
[scriptblock]$Logger
)
$metadataLogger = $Logger
function LogMeta([string]$Message) {
if ($metadataLogger) {
$null = $metadataLogger.Invoke($Message)
}
}
$before = Get-DismImageInfoMap -ImagePath $ImagePath -Index 1
$undefinedBefore = @($before.GetEnumerator() | Where-Object { $_.Value -eq '<undefined>' } | ForEach-Object { $_.Key })
if ($undefinedBefore.Count -eq 0) {
LogMeta "Metadata check: no undefined DISM fields detected."
return
}
LogMeta "Metadata check: undefined DISM fields detected: $($undefinedBefore -join ', ')"
LogMeta "Attempting best-effort metadata hydration for install.wim..."
$setImage = Get-Command Set-WindowsImage -ErrorAction SilentlyContinue
if (-not $setImage) {
LogMeta "Set-WindowsImage is unavailable on this host; cannot write additional WIM metadata fields."
return
}
$targetName = if ($EditionName -and $EditionName -ne 'Unknown') { $EditionName } else { $before['Name'] }
if (-not $targetName) { $targetName = 'Windows 11' }
$targetDescription = if ($before['Description'] -and $before['Description'] -ne '<undefined>') {
$before['Description']
} else {
$targetName
}
$setArgs = @{
ImagePath = $ImagePath
Index = 1
Name = $targetName
Description = $targetDescription
ErrorAction = 'Stop'
}
try {
Set-WindowsImage @setArgs | Out-Null
LogMeta "Applied Set-WindowsImage metadata updates (Name/Description)."
} catch {
LogMeta "Warning: Set-WindowsImage metadata update failed: $_"
}
$after = Get-DismImageInfoMap -ImagePath $ImagePath -Index 1
$undefinedAfter = @($after.GetEnumerator() | Where-Object { $_.Value -eq '<undefined>' } | ForEach-Object { $_.Key })
if ($undefinedAfter.Count -eq 0) {
LogMeta "Metadata hydration complete: no undefined DISM fields remain."
} else {
LogMeta "Metadata hydration complete. Remaining undefined DISM fields: $($undefinedAfter -join ', ')"
LogMeta "Note: some DISM metadata fields are read-only and come from Microsoft image internals."
}
}
try { try {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed" $sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
@@ -359,51 +261,30 @@ function Invoke-WinUtilISOModify {
Log "Creating working directory: $workDir" Log "Creating working directory: $workDir"
$isoContents = Join-Path $workDir "iso_contents" $isoContents = Join-Path $workDir "iso_contents"
$mountDir = Join-Path $workDir "wim_mount" New-Item -ItemType Directory -Path $isoContents -Force
New-Item -ItemType Directory -Path $isoContents, $mountDir -Force
SetProgress "Copying ISO contents..." 10 SetProgress "Copying ISO contents..." 10
Log "Copying ISO contents from $driveLetter to $isoContents..." Log "Copying ISO contents from $driveLetter to $isoContents..."
& robocopy $driveLetter $isoContents /E /NFL /NDL /NJH /NJS & robocopy $driveLetter $isoContents /E /NFL /NDL /NJH /NJS
Log "ISO contents copied." Log "ISO contents copied."
SetProgress "Mounting install.wim..." 25 SetProgress "Preparing setup media..." 25
$sourceImageFileName = Split-Path $wimPath -Leaf $sourceImageFileName = Split-Path $wimPath -Leaf
$localWim = Join-Path $isoContents "sources\$sourceImageFileName" $localWim = Join-Path $isoContents "sources\$sourceImageFileName"
if (-not (Test-Path $localWim)) { if (-not (Test-Path $localWim)) {
throw "Copied ISO image file not found: sources\$sourceImageFileName" throw "Copied ISO image file not found: sources\$sourceImageFileName"
} }
Set-ItemProperty -Path $localWim -Name IsReadOnly -Value $false $selectedEditionId = Get-WinUtilEditionIdFromName -EditionName $selectedEditionName
Log "Mounting install.wim (Index ${selectedWimIndex}: $selectedEditionName) at $mountDir..." Log "Writing autounattend.xml and edition selection..."
Mount-WindowsImage -ImagePath $localWim -Index $selectedWimIndex -Path $mountDir Invoke-WinUtilISOScript -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallImagePath $localWim -InstallImageIndex $selectedWimIndex -InstallEditionId $selectedEditionId -Log { param($m) Log $m }
SetProgress "Modifying install.wim..." 45
$selectedEditionId = Get-WinUtilMountedImageEditionId -MountDir $mountDir -EditionName $selectedEditionName -Logger ${function:Log}
Log "Applying WinUtil modifications to install.wim..." SetProgress "Preserving install image..." 70
Invoke-WinUtilISOScript -ScratchDir $mountDir -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallEditionId $selectedEditionId -InstallImageIndex 1 -Log { param($m) Log $m } if ($injectDrivers) {
Log "Added current-system drivers to $sourceImageFileName index $selectedWimIndex with one mount and commit."
SetProgress "Cleaning up component store (WinSxS)..." 56 } else {
Log "Running DISM component store cleanup (/ResetBase)..." Log "Preserved the original $sourceImageFileName without mounting, exporting, or modifying it."
& dism /English "/image:$mountDir" /Cleanup-Image /StartComponentCleanup /ResetBase | ForEach-Object { Log $_ } }
Log "Component store cleanup complete."
SetProgress "Saving modified install.wim..." 65
Log "Dismounting and saving install.wim. This will take several minutes..."
Dismount-WindowsImage -Path $mountDir -Save
Log "install.wim saved."
SetProgress "Removing unused editions from install.wim..." 70
Log "Exporting edition '$selectedEditionName' (Index $selectedWimIndex) to a single-edition install.wim..."
$exportWim = Join-Path $isoContents "sources\install_export.wim"
Export-WindowsImage -SourceImagePath $localWim -SourceIndex $selectedWimIndex -DestinationImagePath $exportWim
Remove-Item -Path $localWim -Force
Rename-Item -Path $exportWim -NewName "install.wim" -Force
$localWim = Join-Path $isoContents "sources\install.wim"
Log "Unused editions removed. install.wim now contains only '$selectedEditionName'."
SetProgress "Hydrating WIM metadata..." 76
Invoke-WinUtilWimMetadataHydration -ImagePath $localWim -EditionName $selectedEditionName -Logger ${function:Log}
SetProgress "Dismounting source ISO..." 80 SetProgress "Dismounting source ISO..." 80
Log "Dismounting original ISO..." Log "Dismounting original ISO..."
@@ -421,16 +302,6 @@ function Invoke-WinUtilISOModify {
} catch { } catch {
Log "ERROR during modification: $_" Log "ERROR during modification: $_"
try {
if (Test-Path $mountDir) {
$mountedImages = Get-WindowsImage -Mounted | Where-Object { $_.Path -eq $mountDir }
if ($mountedImages) {
Log "Cleaning up: dismounting install.wim (discarding changes)..."
Dismount-WindowsImage -Path $mountDir -Discard
}
}
} catch { Log "Warning: could not dismount install.wim during cleanup: $_" }
try { try {
$mountedISO = Get-DiskImage -ImagePath $isoPath $mountedISO = Get-DiskImage -ImagePath $isoPath
if ($mountedISO -and $mountedISO.Attached) { if ($mountedISO -and $mountedISO.Attached) {
@@ -498,9 +369,9 @@ function Invoke-WinUtilISOCheckExistingWork {
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible" $sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
$modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm") $modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
Write-Win11ISOLog "Existing working directory found: $($existingWorkDir.FullName)" Write-WinUtilISOLog "Existing working directory found: $($existingWorkDir.FullName)"
Write-Win11ISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4." Write-WinUtilISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4."
Write-Win11ISOLog "Click 'Clean & Reset' if you want to start over with a new ISO." Write-WinUtilISOLog "Click 'Clean & Reset' if you want to start over with a new ISO."
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
"A previous WinUtil ISO working directory was found:`n`n$($existingWorkDir.FullName)`n`n(Last modified: $modified)`n`nStep 4 (output options) has been restored so you can save the already-modified image.`n`nClick 'Clean & Reset' in Step 4 if you want to start over.", "A previous WinUtil ISO working directory was found:`n`n$($existingWorkDir.FullName)`n`n(Last modified: $modified)`n`nStep 4 (output options) has been restored so you can save the already-modified image.`n`nClick 'Clean & Reset' in Step 4 if you want to start over.",
@@ -690,29 +561,29 @@ function Invoke-WinUtilISOExport {
} }
if (-not $oscdimg) { if (-not $oscdimg) {
Write-Win11ISOLog "oscdimg.exe not found. Attempting to install via winget..." Write-WinUtilISOLog "oscdimg.exe not found. Attempting to install via winget..."
try { try {
# First ensure winget is installed and operational # First ensure winget is installed and operational
Install-WinUtilWinget Install-WinUtilWinget
$winget = Get-Command winget $winget = Get-Command winget
$result = & $winget install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements $result = & $winget install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements
Write-Win11ISOLog "winget output: $result" Write-WinUtilISOLog "winget output: $result"
$oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" | $oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" |
Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } | Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } |
Select-Object -First 1 -ExpandProperty FullName Select-Object -First 1 -ExpandProperty FullName
} catch { } catch {
Write-Win11ISOLog "winget not available or install failed: $_" Write-WinUtilISOLog "winget not available or install failed: $_"
} }
if (-not $oscdimg) { if (-not $oscdimg) {
Write-Win11ISOLog "oscdimg.exe still not found after install attempt." Write-WinUtilISOLog "oscdimg.exe still not found after install attempt."
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
"oscdimg.exe could not be found or installed automatically.`n`nPlease install it manually:`n winget install -e --id Microsoft.OSCDIMG`n`nOr install the Windows ADK from:`nhttps://learn.microsoft.com/windows-hardware/get-started/adk-install", "oscdimg.exe could not be found or installed automatically.`n`nPlease install it manually:`n winget install -e --id Microsoft.OSCDIMG`n`nOr install the Windows ADK from:`nhttps://learn.microsoft.com/windows-hardware/get-started/adk-install",
"oscdimg Not Found", "OK", "Warning") "oscdimg Not Found", "OK", "Warning")
return return
} }
Write-Win11ISOLog "oscdimg.exe installed successfully." Write-WinUtilISOLog "oscdimg.exe installed successfully."
} }
$sync["WPFWin11ISOChooseISOButton"].IsEnabled = $false $sync["WPFWin11ISOChooseISOButton"].IsEnabled = $false
@@ -727,7 +598,7 @@ function Invoke-WinUtilISOExport {
$runspace.SessionStateProxy.SetVariable("outputISO", $outputISO) $runspace.SessionStateProxy.SetVariable("outputISO", $outputISO)
$runspace.SessionStateProxy.SetVariable("oscdimg", $oscdimg) $runspace.SessionStateProxy.SetVariable("oscdimg", $oscdimg)
$win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + ${function:Write-Win11ISOLog}.ToString() + "`n}" $win11ISOLogFuncDef = "function Write-WinUtilISOLog {`n" + ${function:Write-WinUtilISOLog}.ToString() + "`n}"
$runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef) $runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef)
$script = [Management.Automation.PowerShell]::Create() $script = [Management.Automation.PowerShell]::Create()
@@ -745,13 +616,13 @@ function Invoke-WinUtilISOExport {
} }
try { try {
Write-Win11ISOLog "Exporting to ISO: $outputISO" Write-WinUtilISOLog "Exporting to ISO: $outputISO"
SetProgress "Building ISO..." 10 SetProgress "Building ISO..." 10
$bootData = "2#p0,e,b`"$contentsDir\boot\etfsboot.com`"#pEF,e,b`"$contentsDir\efi\microsoft\boot\efisys.bin`"" $bootData = "2#p0,e,b`"$contentsDir\boot\etfsboot.com`"#pEF,e,b`"$contentsDir\efi\microsoft\boot\efisys.bin`""
$oscdimgArgs = @("-m", "-o", "-u2", "-udfver102", "-bootdata:$bootData", "-l`"CTOS_MODIFIED`"", "`"$contentsDir`"", "`"$outputISO`"") $oscdimgArgs = @("-m", "-o", "-u2", "-udfver102", "-bootdata:$bootData", "-l`"CTOS_MODIFIED`"", "`"$contentsDir`"", "`"$outputISO`"")
Write-Win11ISOLog "Running oscdimg..." Write-WinUtilISOLog "Running oscdimg..."
$psi = [System.Diagnostics.ProcessStartInfo]::new() $psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = $oscdimg $psi.FileName = $oscdimg
@@ -768,7 +639,7 @@ function Invoke-WinUtilISOExport {
# Stream stdout line-by-line as oscdimg runs # Stream stdout line-by-line as oscdimg runs
while (-not $proc.StandardOutput.EndOfStream) { while (-not $proc.StandardOutput.EndOfStream) {
$line = $proc.StandardOutput.ReadLine() $line = $proc.StandardOutput.ReadLine()
if ($line.Trim()) { Write-Win11ISOLog $line } if ($line.Trim()) { Write-WinUtilISOLog $line }
} }
$proc.WaitForExit() $proc.WaitForExit()
@@ -776,17 +647,17 @@ function Invoke-WinUtilISOExport {
# Flush any stderr after process exits # Flush any stderr after process exits
$stderr = $proc.StandardError.ReadToEnd() $stderr = $proc.StandardError.ReadToEnd()
foreach ($line in ($stderr -split "`r?`n")) { foreach ($line in ($stderr -split "`r?`n")) {
if ($line.Trim()) { Write-Win11ISOLog "[stderr]$line" } if ($line.Trim()) { Write-WinUtilISOLog "[stderr]$line" }
} }
if ($proc.ExitCode -eq 0) { if ($proc.ExitCode -eq 0) {
SetProgress "ISO exported" 100 SetProgress "ISO exported" 100
Write-Win11ISOLog "ISO exported successfully: $outputISO" Write-WinUtilISOLog "ISO exported successfully: $outputISO"
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show("ISO exported successfully!`n`n$outputISO", "Export Complete", "OK", "Info") [System.Windows.MessageBox]::Show("ISO exported successfully!`n`n$outputISO", "Export Complete", "OK", "Info")
}) })
} else { } else {
Write-Win11ISOLog "oscdimg exited with code $($proc.ExitCode)." Write-WinUtilISOLog "oscdimg exited with code $($proc.ExitCode)."
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show( [System.Windows.MessageBox]::Show(
"oscdimg exited with code $($proc.ExitCode).`nCheck the status log for details.", "oscdimg exited with code $($proc.ExitCode).`nCheck the status log for details.",
@@ -794,7 +665,7 @@ function Invoke-WinUtilISOExport {
}) })
} }
} catch { } catch {
Write-Win11ISOLog "ERROR during ISO export: $_" Write-WinUtilISOLog "ERROR during ISO export: $_"
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error") [System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error")
}) })
+462 -430
View File
@@ -1,220 +1,228 @@
function Invoke-WinUtilISOScript { function Invoke-WinUtilISOScript {
<# <#
.SYNOPSIS .SYNOPSIS
Applies WinUtil modifications to a mounted Windows 11 install.wim image. Prepares copied Windows setup media without modifying its install image.
.DESCRIPTION .DESCRIPTION
Removes AppX bloatware and OneDrive, optionally injects all drivers exported from Stages WinUtil's AppX removal, registry tweaks, and scheduled-task cleanup
the running system into install.wim and boot.wim (controlled by the in the answer file for first logon, writes sources\ei.cfg for the selected
-InjectCurrentSystemDrivers switch), applies offline registry tweaks (hardware edition, and optionally adds current-system drivers to one install.wim index.
bypass, privacy, OOBE, telemetry, update suppression), deletes CEIP/WU
scheduled-task definition files, and optionally writes autounattend.xml to the ISO
root and removes the support\ folder from the ISO contents directory.
All setup scripts embedded in the autounattend.xml <Extensions><File> nodes are
written directly into the WIM at their target paths under C:\Windows\Setup\Scripts\
to ensure they survive Windows Setup stripping unrecognised-namespace XML elements
from the Panther copy of the answer file.
Mounting/dismounting the WIM is the caller's responsibility (e.g. Invoke-WinUtilISO).
.PARAMETER ScratchDir
Mandatory. Full path to the directory where the Windows image is currently mounted.
.PARAMETER ISOContentsDir .PARAMETER ISOContentsDir
Optional. Root directory of the extracted ISO contents. When supplied, Root directory of the copied ISO contents.
autounattend.xml is written here and the support\ folder is removed.
.PARAMETER AutoUnattendXml .PARAMETER AutoUnattendXml
Optional. Full XML content for autounattend.xml. If empty, the OOBE bypass Full XML content for autounattend.xml.
file is skipped and a warning is logged.
.PARAMETER InjectCurrentSystemDrivers
Optional. When $true, exports all drivers from the running system and injects
them into install.wim and boot.wim index 2 (Windows Setup PE).
Defaults to $false.
.PARAMETER InstallEditionId .PARAMETER InstallEditionId
Optional. Windows edition ID for the selected image, for example Professional Windows setup EditionID for sources\ei.cfg, for example Professional or Core.
or Core. Used to write sources\ei.cfg so setup does not fall back to an
embedded firmware product key for a different edition. .PARAMETER InstallImagePath
Copied install.wim to service when current-system driver injection is enabled.
.PARAMETER InstallImageIndex .PARAMETER InstallImageIndex
Optional. Image index that setup should install from the final install.wim. Selected edition index in install.wim.
Win11 Creator exports the selected edition to a single-image WIM, so this
defaults to 1.
.PARAMETER Log .PARAMETER Log
Optional ScriptBlock for progress/status logging. Receives a single [string] argument. Optional ScriptBlock for progress/status logging. Receives a single [string] argument.
.EXAMPLE
Invoke-WinUtilISOScript -ScratchDir "C:\Temp\wim_mount"
.EXAMPLE
Invoke-WinUtilISOScript `
-ScratchDir $mountDir `
-ISOContentsDir $isoRoot `
-AutoUnattendXml (Get-Content .\tools\autounattend.xml -Raw) `
-Log { param($m) Write-Host $m }
.NOTES
Author : Chris Titus @christitustech
GitHub : https://github.com/ChrisTitusTech
#> #>
param ( param (
[Parameter(Mandatory)][string]$ScratchDir, [Parameter(Mandatory)][string]$ISOContentsDir,
[string]$ISOContentsDir = "",
[string]$AutoUnattendXml = "", [string]$AutoUnattendXml = "",
[bool]$InjectCurrentSystemDrivers = $false, [bool]$InjectCurrentSystemDrivers = $false,
[string]$InstallEditionId = "", [string]$InstallEditionId = "",
[string]$InstallImagePath = "",
[int]$InstallImageIndex = 1, [int]$InstallImageIndex = 1,
[scriptblock]$Log = { param($m) Write-Output $m } [scriptblock]$Log = { param($m) Write-Output $m }
) )
function Set-ISOScriptReg {
param ([string]$Path, [string]$Name, [string]$Type, [string]$Value) function Add-WinUtilISOStagedDrivers {
try { param (
& reg add $Path /v $Name /t $Type /d $Value /f [Parameter(Mandatory)][string]$ContentRoot,
& $Log "Set registry value: $Path\$Name" [Parameter(Mandatory)][string]$InstallImagePath,
} catch { [Parameter(Mandatory)][int]$InstallImageIndex,
& $Log "Error setting registry value: $_" [scriptblock]$Logger
)
function Copy-WinUtilISODriverFolder {
param (
[Parameter(Mandatory)][string]$Source,
[Parameter(Mandatory)][string]$Destination
)
$folderName = Split-Path $Source -Leaf
$targetPath = Join-Path $Destination $folderName
$suffix = 1
while (Test-Path -LiteralPath $targetPath) {
$targetPath = Join-Path $Destination "${folderName}_$suffix"
$suffix++
}
Copy-Item -LiteralPath $Source -Destination $targetPath -Recurse -Force -ErrorAction Stop
return $targetPath
} }
}
function Remove-ISOScriptReg { function Test-WinUtilISOStorageDriver {
param ([string]$path) param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)
try {
& reg delete $path /f if ($InfFile.BaseName -match '(?i)(iaahci|iastor|vmd|irst|rst)') {
& $Log "Removed registry key: $path" return $true
} catch { }
& $Log "Error removing registry key: $_"
try {
return (Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop) -match '(?im)^\s*Class\s*=\s*(SCSIAdapter|HDC)\s*(?:;.*)?$'
} catch {
& $Logger "Warning: could not classify storage driver '$($InfFile.FullName)': $_"
return $false
}
} }
}
function Add-DriversToImage { function Invoke-WinUtilISODism {
param ([string]$MountPath, [string]$DriverDir, [string]$Label = "image", [scriptblock]$Logger) param (
& dism /English "/image:$MountPath" /Add-Driver "/Driver:$DriverDir" /Recurse | [Parameter(Mandatory)][string[]]$Arguments,
ForEach-Object { & $Logger " dism[$Label]: $_" } [Parameter(Mandatory)][string]$Operation
} )
$output = @(& dism.exe @Arguments 2>&1)
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
foreach ($line in @($output | Select-Object -Last 20)) {
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
& $Logger " dism[$Operation]: $line"
}
}
throw "DISM $Operation failed with exit code $exitCode."
}
if ($Operation -ne 'metadata') {
& $Logger "DISM $Operation completed."
}
return $output
}
function Get-WinUtilISOWimMetadata {
param ([Parameter(Mandatory)][string]$ImagePath, [Parameter(Mandatory)][int]$Index)
$metadata = @{}
$output = Invoke-WinUtilISODism -Arguments @('/English', '/Get-WimInfo', "/WimFile:$ImagePath", "/Index:$Index") -Operation 'metadata'
foreach ($line in $output) {
if ([string]$line -match '^\s*([^:]+?)\s*:\s*(.*?)\s*$') {
$metadata[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $metadata
}
function Assert-WinUtilISOWimMetadata {
param (
[Parameter(Mandatory)][hashtable]$Before,
[hashtable]$After
)
foreach ($key in 'Languages', 'Installation', 'Edition', 'ProductSuite', 'ProductType') {
$beforeValue = [string]$Before[$key]
if ($beforeValue -eq '<undefined>' -or ($key -in 'Installation', 'Edition', 'ProductType' -and [string]::IsNullOrWhiteSpace($beforeValue))) {
throw "install.wim metadata is already invalid: $key is undefined. Driver injection was not attempted."
}
if ($After) {
$afterValue = [string]$After[$key]
if ($afterValue -eq '<undefined>' -or ($beforeValue -and $afterValue -ne $beforeValue)) {
throw "install.wim metadata validation failed after driver injection: $key changed from '$beforeValue' to '$afterValue'."
}
}
}
}
function Test-WinUtilISOMountedImage {
param ([Parameter(Mandatory)][string]$Path)
return @(& dism.exe /English /Get-MountedImageInfo 2>$null) -match [regex]::Escape($Path)
}
if ([IO.Path]::GetExtension($InstallImagePath) -ne '.wim') {
throw 'Current-system driver injection requires install.wim; install.esd cannot be serviced in place.'
}
if (-not (Test-Path -LiteralPath $InstallImagePath)) {
throw "install.wim was not found: $InstallImagePath"
}
if ($InstallImageIndex -lt 1) {
throw 'Current-system driver injection requires a valid install.wim image index.'
}
$driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss')_$(([guid]::NewGuid()).ToString('N').Substring(0, 8))"
$mountDir = Join-Path (Split-Path -Path $ContentRoot -Parent) 'wim_mount'
New-Item -Path $driverExportRoot -ItemType Directory -Force | Out-Null
$imageMounted = $false
function Invoke-BootWimInject {
param ([string]$BootWimPath, [string]$DriverDir, [scriptblock]$Logger)
Set-ItemProperty -Path $BootWimPath -Name IsReadOnly -Value $false
$mountDir = Join-Path $env:TEMP "WinUtil_BootMount_$(Get-Random)"
New-Item -Path $mountDir -ItemType Directory -Force
try { try {
& $Logger "Mounting boot.wim (index 2) for driver injection..." & $Logger "Exporting current system drivers before modifying install.wim..."
Mount-WindowsImage -ImagePath $BootWimPath -Index 2 -Path $mountDir $dismLog = Join-Path $env:TEMP "WinUtil_DismDriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
Add-DriversToImage -MountPath $mountDir -DriverDir $DriverDir -Label "boot" -Logger $Logger $dismProcess = Start-Process -FilePath "dism.exe" -ArgumentList "/online /export-driver /destination:`"$driverExportRoot`" /LogPath:`"$dismLog`"" -Wait -NoNewWindow -PassThru
& $Logger "Saving boot.wim..." if ($dismProcess.ExitCode -ne 0) {
Dismount-WindowsImage -Path $mountDir -Save throw "dism.exe driver export failed with exit code $($dismProcess.ExitCode)."
& $Logger "boot.wim driver injection complete." }
} catch {
& $Logger "Warning: boot.wim driver injection failed: $_" $driverInfs = @(Get-ChildItem -Path $driverExportRoot -Filter '*.inf' -Recurse -File)
try { Dismount-WindowsImage -Path $mountDir -Discard } catch { & $Logger "Warning: could not discard boot.wim mount: $_" } if ($driverInfs.Count -eq 0) {
throw 'DISM exported no driver INF files.'
}
$driverFolders = @($driverInfs | Group-Object { $_.Directory.FullName })
$winpeDriverDir = Join-Path $ContentRoot '$WinpeDriver$'
$storageCount = 0
$copyFailures = 0
foreach ($driverFolderGroup in $driverFolders) {
$driverFolder = [string]$driverFolderGroup.Name
$storageInfs = @($driverFolderGroup.Group | Where-Object { Test-WinUtilISOStorageDriver -InfFile $_ })
if ($storageInfs.Count -eq 0) {
continue
}
try {
New-Item -Path $winpeDriverDir -ItemType Directory -Force | Out-Null
$winpeTarget = Copy-WinUtilISODriverFolder -Source $driverFolder -Destination $winpeDriverDir
$storageCount++
& $Logger "Staged boot-storage package '$driverFolder' for WinPE as '$winpeTarget'."
} catch {
$copyFailures++
& $Logger "Warning: failed to stage boot-storage package '$driverFolder': $_"
}
}
if ($copyFailures -gt 0) {
throw "Failed to stage $copyFailures boot-storage driver package folders."
}
& $Logger "Exported $($driverInfs.Count) driver INF files across $($driverFolders.Count) package folders; staged $storageCount boot-storage packages for WinPE."
$metadataBefore = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore
Set-ItemProperty -LiteralPath $InstallImagePath -Name IsReadOnly -Value $false
New-Item -Path $mountDir -ItemType Directory -Force | Out-Null
& $Logger "Mounting install.wim index $InstallImageIndex once for driver injection..."
Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$InstallImagePath", "/Index:$InstallImageIndex", "/MountDir:$mountDir") -Operation 'mount' | Out-Null
$imageMounted = $true
& $Logger "Adding all exported drivers to the selected Windows image in one DISM operation..."
Invoke-WinUtilISODism -Arguments @('/English', "/Image:$mountDir", '/Add-Driver', "/Driver:$driverExportRoot", '/Recurse') -Operation 'add-driver' | Out-Null
& $Logger 'Committing the driver-only install.wim change...'
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Commit') -Operation 'commit' | Out-Null
$imageMounted = $false
$metadataAfter = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore -After $metadataAfter
& $Logger 'Driver injection complete; install.wim metadata validation passed.'
} finally { } finally {
Remove-Item -Path $mountDir -Recurse -Force if ($imageMounted -or (Test-WinUtilISOMountedImage -Path $mountDir)) {
} try {
} Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null
} catch {
function Get-WinUtilISOScriptChildElement { & $Logger "Warning: could not discard the failed install.wim mount: $_"
param ( }
[Parameter(Mandatory)][System.Xml.XmlElement]$Parent,
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$NamespaceUri
)
foreach ($childNode in $Parent.ChildNodes) {
if ($childNode.NodeType -eq [System.Xml.XmlNodeType]::Element -and
$childNode.LocalName -eq $Name -and
$childNode.NamespaceURI -eq $NamespaceUri) {
return [System.Xml.XmlElement]$childNode
} }
Remove-Item -Path $mountDir -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue
} }
$childElement = $Parent.OwnerDocument.CreateElement($Name, $NamespaceUri)
[void]$Parent.AppendChild($childElement)
return $childElement
}
function ConvertTo-WinUtilISOAnswerFile {
param (
[Parameter(Mandatory)][string]$XmlContent,
[int]$ImageIndex = 1
)
if ($ImageIndex -lt 1) { $ImageIndex = 1 }
$unattendNs = "urn:schemas-microsoft-com:unattend"
$wcmNs = "http://schemas.microsoft.com/WMIConfig/2002/State"
$xmlDoc = [xml]::new()
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.LoadXml($XmlContent)
if ($xmlDoc.DocumentElement.NamespaceURI -ne $unattendNs) {
throw "Unexpected autounattend.xml namespace: $($xmlDoc.DocumentElement.NamespaceURI)"
}
if (-not $xmlDoc.DocumentElement.HasAttribute("xmlns:wcm")) {
$xmlDoc.DocumentElement.SetAttribute("wcm", "http://www.w3.org/2000/xmlns/", $wcmNs)
}
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("u", $unattendNs)
$windowsPESettings = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]', $nsMgr)
if (-not $windowsPESettings) {
$windowsPESettings = $xmlDoc.CreateElement("settings", $unattendNs)
$windowsPESettings.SetAttribute("pass", "windowsPE")
[void]$xmlDoc.DocumentElement.PrependChild($windowsPESettings)
}
$setupComponent = $windowsPESettings.SelectSingleNode('u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
if (-not $setupComponent) {
$setupComponent = $xmlDoc.CreateElement("component", $unattendNs)
$setupComponent.SetAttribute("name", "Microsoft-Windows-Setup")
$setupComponent.SetAttribute("processorArchitecture", "amd64")
$setupComponent.SetAttribute("publicKeyToken", "31bf3856ad364e35")
$setupComponent.SetAttribute("language", "neutral")
$setupComponent.SetAttribute("versionScope", "nonSxS")
[void]$windowsPESettings.AppendChild($setupComponent)
}
$productKeyNodes = @($setupComponent.SelectNodes("u:UserData/u:ProductKey", $nsMgr))
foreach ($productKeyNode in $productKeyNodes) {
$keyNode = $productKeyNode.SelectSingleNode("u:Key", $nsMgr)
$keyValue = if ($keyNode) { $keyNode.InnerText.Trim() } else { "" }
if ([string]::IsNullOrWhiteSpace($keyValue) -or $keyValue -eq "00000-00000-00000-00000-00000") {
[void]$productKeyNode.ParentNode.RemoveChild($productKeyNode)
}
}
$imageInstall = Get-WinUtilISOScriptChildElement -Parent $setupComponent -Name "ImageInstall" -NamespaceUri $unattendNs
$osImage = Get-WinUtilISOScriptChildElement -Parent $imageInstall -Name "OSImage" -NamespaceUri $unattendNs
$installFrom = Get-WinUtilISOScriptChildElement -Parent $osImage -Name "InstallFrom" -NamespaceUri $unattendNs
$existingMetadataNodes = @($installFrom.SelectNodes("u:MetaData", $nsMgr))
foreach ($metadataNode in $existingMetadataNodes) {
[void]$installFrom.RemoveChild($metadataNode)
}
$metadata = $xmlDoc.CreateElement("MetaData", $unattendNs)
$actionAttribute = $xmlDoc.CreateAttribute("wcm", "action", $wcmNs)
$actionAttribute.Value = "add"
[void]$metadata.Attributes.Append($actionAttribute)
$keyElement = $xmlDoc.CreateElement("Key", $unattendNs)
$keyElement.InnerText = "/IMAGE/INDEX"
[void]$metadata.AppendChild($keyElement)
$valueElement = $xmlDoc.CreateElement("Value", $unattendNs)
$valueElement.InnerText = [string]$ImageIndex
[void]$metadata.AppendChild($valueElement)
[void]$installFrom.AppendChild($metadata)
return $xmlDoc.OuterXml
} }
function Write-WinUtilISOEditionConfig { function Write-WinUtilISOEditionConfig {
@@ -224,10 +232,6 @@ function Invoke-WinUtilISOScript {
[scriptblock]$Logger [scriptblock]$Logger
) )
if (-not (Test-Path $ContentRoot)) {
return
}
$sourcesDir = Join-Path $ContentRoot "sources" $sourcesDir = Join-Path $ContentRoot "sources"
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
@@ -256,253 +260,281 @@ Retail
& $Logger "Written sources\ei.cfg for EditionID '$EditionId'." & $Logger "Written sources\ei.cfg for EditionID '$EditionId'."
} }
# -- 1. Remove provisioned AppX packages ---------------------------------- function Add-WinUtilISOSetupCustomizations {
& $Log "Removing provisioned AppX packages..." param (
[Parameter(Mandatory)][string]$XmlContent,
[Parameter(Mandatory)][int]$InstallImageIndex,
[scriptblock]$Logger
)
$packages = & dism /English "/image:$ScratchDir" /Get-ProvisionedAppxPackages | $appxPackages = @(
ForEach-Object { if ($_ -match 'PackageName : (.*)') { $matches[1] } } 'Clipchamp.Clipchamp', 'Microsoft.BingNews', 'Microsoft.BingSearch',
'Microsoft.BingWeather', 'Microsoft.GetHelp', 'Microsoft.MicrosoftOfficeHub',
'Microsoft.MicrosoftSolitaireCollection', 'Microsoft.MicrosoftStickyNotes',
'Microsoft.OutlookForWindows', 'Microsoft.Paint', 'Microsoft.PowerAutomateDesktop',
'Microsoft.StartExperiencesApp', 'Microsoft.Todos', 'Microsoft.Windows.DevHome',
'Microsoft.WindowsFeedbackHub', 'Microsoft.WindowsSoundRecorder',
'Microsoft.ZuneMusic', 'MicrosoftCorporationII.QuickAssist', 'MSTeams'
)
$packagePrefixes = @( $appxList = ($appxPackages | ForEach-Object { " '$_'" }) -join "`r`n"
'Clipchamp.Clipchamp', $postInstallScript = @"
'Microsoft.BingNews', `$ErrorActionPreference = 'Continue'
'Microsoft.BingSearch', `$logPath = 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.log'
'Microsoft.BingWeather', Start-Transcript -Path `$logPath -Append -ErrorAction SilentlyContinue
'Microsoft.GetHelp',
'Microsoft.MicrosoftOfficeHub', try {
'Microsoft.MicrosoftSolitaireCollection', Write-Host 'WinUtil: Removing provisioned AppX packages...'
'Microsoft.MicrosoftStickyNotes', `$packages = @(
'Microsoft.OutlookForWindows', $appxList
'Microsoft.Paint',
'Microsoft.PowerAutomateDesktop',
'Microsoft.StartExperiencesApp',
'Microsoft.Todos',
'Microsoft.Windows.DevHome',
'Microsoft.WindowsFeedbackHub',
'Microsoft.WindowsSoundRecorder',
'Microsoft.ZuneMusic',
'MicrosoftCorporationII.QuickAssist',
'MSTeams'
) )
foreach (`$package in `$packages) {
Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue |
Where-Object { `$_.DisplayName -like "*`$package*" } |
ForEach-Object { Remove-AppxProvisionedPackage -Online -PackageName `$_.PackageName -ErrorAction SilentlyContinue | Out-Null }
Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue |
Where-Object { `$_.Name -like "*`$package*" } |
ForEach-Object { Remove-AppxPackage -AllUsers -Package `$_.PackageFullName -ErrorAction SilentlyContinue | Out-Null }
}
$packages | Where-Object { $pkg = $_; $packagePrefixes | Where-Object { $pkg -like "*$_*" } } | function Set-WinUtilRegistryValue([string]`$Path, [string]`$Name, [string]`$Type, [string]`$Value) {
ForEach-Object { & dism /English "/image:$ScratchDir" /Remove-ProvisionedAppxPackage "/PackageName:$_" } reg.exe add `$Path /v `$Name /t `$Type /d `$Value /f 2>&1 | Out-Null
}
function Set-WinUtilContentDeliveryManagerValues([string]`$HiveRoot) {
`$contentDeliveryManager = "`$HiveRoot\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager"
Set-WinUtilRegistryValue `$contentDeliveryManager 'OemPreInstalledAppsEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'PreInstalledAppsEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SilentInstalledAppsEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'ContentDeliveryAllowed' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'FeatureManagementEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'PreInstalledAppsEverEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SoftLandingEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContentEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-310093Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338388Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338389Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338393Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-353694Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-353696Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SystemPaneSuggestionsEnabled' 'REG_DWORD' '0'
reg.exe delete "`$contentDeliveryManager\Subscriptions" /f 2>&1 | Out-Null
reg.exe delete "`$contentDeliveryManager\SuggestedApps" /f 2>&1 | Out-Null
}
Write-Host 'WinUtil: Applying registry tweaks...'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager' 'ShippedWithReserves' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Control\BitLocker' 'PreventDeviceEncryption' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Chat' 'ChatIcon' 'REG_DWORD' '3'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OneDrive' 'DisableFileSyncNGSC' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection' 'AllowTelemetry' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\dmwappushservice' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' 'TurnOffWindowsCopilot' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Edge' 'HubsSidebarEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer' 'DisableSearchBoxSuggestions' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Teams' 'DisableInstallation' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Mail' 'PreventRun' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableWindowsConsumerFeatures' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableConsumerAccountStateContent' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableCloudOptimizedContent' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start' 'ConfigureStartPins' 'REG_SZ' '{"pinnedList": [{}]}'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' 'BypassNRO' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassCPUCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassRAMCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassSecureBootCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassStorageCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassTPMCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\MoSetup' 'AllowUpgradesWithUnsupportedTPMOrCPU' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\PushToInstall' 'DisablePushToInstall' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\MRT' 'DontOfferThroughWUAU' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' 'workCompleted' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate' 'workCompleted' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\DevHomeUpdate' 'workCompleted' 'REG_DWORD' '1'
reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' /f 2>&1 | Out-Null
reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate' /f 2>&1 | Out-Null
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'NoAutoUpdate' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'AUOptions' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'UseWUServer' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'DisableWindowsUpdateAccess' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'WUServer' 'REG_SZ' 'http://localhost:8080'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'WUStatusServer' 'REG_SZ' 'http://localhost:8080'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' 'workCompleted' 'REG_DWORD' '1'
reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' /f 2>&1 | Out-Null
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config' 'DODownloadMode' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\BITS' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\wuauserv' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\UsoSvc' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' 'Start' 'REG_DWORD' '4'
`$defaultHive = 'HKU\WinUtilDefault'
reg.exe load `$defaultHive 'C:\Users\Default\NTUSER.DAT' 2>&1 | Out-Null
if (`$LASTEXITCODE -eq 0) {
Set-WinUtilRegistryValue "`$defaultHive\Control Panel\UnsupportedHardwareNotificationCache" 'SV1' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Control Panel\UnsupportedHardwareNotificationCache" 'SV2' 'REG_DWORD' '0'
Set-WinUtilContentDeliveryManagerValues `$defaultHive
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo" 'Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Windows\CurrentVersion\Privacy" 'TailoredExperiencesWithDiagnosticDataEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy" 'HasAccepted' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Input\TIPC" 'Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization" 'RestrictImplicitInkCollection' 'REG_DWORD' '1'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization" 'RestrictImplicitTextCollection' 'REG_DWORD' '1'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization\TrainedDataStore" 'HarvestContacts' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Personalization\Settings" 'AcceptedPrivacyPolicy' 'REG_DWORD' '0'
reg.exe unload `$defaultHive 2>&1 | Out-Null
}
Set-WinUtilContentDeliveryManagerValues 'HKCU'
Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV1' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV2' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' 'TaskbarMn' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo' 'Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Windows\CurrentVersion\Privacy' 'TailoredExperiencesWithDiagnosticDataEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy' 'HasAccepted' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Input\TIPC' 'Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization' 'RestrictImplicitInkCollection' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization' 'RestrictImplicitTextCollection' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization\TrainedDataStore' 'HarvestContacts' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Personalization\Settings' 'AcceptedPrivacyPolicy' 'REG_DWORD' '0'
Write-Host 'WinUtil: Removing scheduled task definitions...'
`$taskPaths = @(
'C:\Windows\System32\Tasks\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser',
'C:\Windows\System32\Tasks\Microsoft\Windows\Customer Experience Improvement Program',
'C:\Windows\System32\Tasks\Microsoft\Windows\Application Experience\ProgramDataUpdater',
'C:\Windows\System32\Tasks\Microsoft\Windows\Chkdsk\Proxy',
'C:\Windows\System32\Tasks\Microsoft\Windows\Windows Error Reporting\QueueReporting',
'C:\Windows\System32\Tasks\Microsoft\Windows\InstallService',
'C:\Windows\System32\Tasks\Microsoft\Windows\UpdateOrchestrator',
'C:\Windows\System32\Tasks\Microsoft\Windows\UpdateAssistant',
'C:\Windows\System32\Tasks\Microsoft\Windows\WaaSMedic',
'C:\Windows\System32\Tasks\Microsoft\Windows\WindowsUpdate',
'C:\Windows\System32\Tasks\Microsoft\WindowsUpdate'
)
foreach (`$taskPath in `$taskPaths) { Remove-Item -LiteralPath `$taskPath -Recurse -Force -ErrorAction SilentlyContinue }
Start-Process -FilePath 'C:\Windows\System32\OneDriveSetup.exe' -ArgumentList '/uninstall' -Wait -ErrorAction SilentlyContinue
Write-Host 'WinUtil: Post-install customization complete.'
} finally {
Stop-Transcript -ErrorAction SilentlyContinue
}
"@
$xmlDoc = [xml]::new()
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.LoadXml($XmlContent)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$setupComponent = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
$extensions = $xmlDoc.SelectSingleNode('//sg:Extensions', $nsMgr)
$firstLogonFile = $xmlDoc.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\FirstLogon.ps1"]', $nsMgr)
if (-not $setupComponent -or -not $extensions -or -not $firstLogonFile) {
throw 'autounattend.xml is missing a required Windows Setup, Extensions, or FirstLogon.ps1 node.'
}
$imageInstall = $setupComponent.SelectSingleNode('u:ImageInstall', $nsMgr)
if (-not $imageInstall) {
$imageInstall = $xmlDoc.CreateElement('ImageInstall', $setupComponent.NamespaceURI)
[void]$setupComponent.AppendChild($imageInstall)
}
$osImage = $imageInstall.SelectSingleNode('u:OSImage', $nsMgr)
if (-not $osImage) {
$osImage = $xmlDoc.CreateElement('OSImage', $setupComponent.NamespaceURI)
[void]$imageInstall.AppendChild($osImage)
}
$installFrom = $osImage.SelectSingleNode('u:InstallFrom', $nsMgr)
if (-not $installFrom) {
$installFrom = $xmlDoc.CreateElement('InstallFrom', $setupComponent.NamespaceURI)
[void]$osImage.AppendChild($installFrom)
}
foreach ($existingMetadata in @($installFrom.SelectNodes('u:MetaData', $nsMgr))) {
[void]$installFrom.RemoveChild($existingMetadata)
}
$metadata = $xmlDoc.CreateElement('MetaData', $setupComponent.NamespaceURI)
$action = $xmlDoc.CreateAttribute('wcm', 'action', 'http://schemas.microsoft.com/WMIConfig/2002/State')
$action.Value = 'add'
[void]$metadata.Attributes.Append($action)
$key = $xmlDoc.CreateElement('Key', $setupComponent.NamespaceURI)
$key.InnerText = '/IMAGE/INDEX'
[void]$metadata.AppendChild($key)
$value = $xmlDoc.CreateElement('Value', $setupComponent.NamespaceURI)
$value.InnerText = [string]$InstallImageIndex
[void]$metadata.AppendChild($value)
[void]$installFrom.AppendChild($metadata)
$postInstallFile = $xmlDoc.CreateElement('File', $extensions.NamespaceURI)
$postInstallFile.SetAttribute('path', 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1')
$postInstallFile.InnerText = $postInstallScript
[void]$extensions.AppendChild($postInstallFile)
$firstLogonFile.InnerText = "& 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1';`r`n`r`n$($firstLogonFile.InnerText.Trim())"
$null = & $Logger 'Added WinUtil post-install AppX, registry, and scheduled-task customizations to autounattend.xml.'
return $xmlDoc.OuterXml
}
function Add-WinUtilISOSetupScriptFallback {
param (
[Parameter(Mandatory)][string]$ContentRoot,
[Parameter(Mandatory)][string]$XmlContent,
[scriptblock]$Logger
)
$xmlDoc = [xml]::new()
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.LoadXml($XmlContent)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$setupScriptsRoot = Join-Path $ContentRoot 'sources\$OEM$\$$\Setup\Scripts'
$stagedCount = 0
foreach ($file in $xmlDoc.SelectNodes('//sg:File', $nsMgr)) {
$path = $file.GetAttribute('path')
if (-not $path.StartsWith('C:\Windows\Setup\Scripts\', [System.StringComparison]::OrdinalIgnoreCase)) {
continue
}
$relativePath = $path.Substring('C:\Windows\Setup\Scripts\'.Length)
$targetPath = Join-Path $setupScriptsRoot $relativePath
New-Item -Path (Split-Path $targetPath -Parent) -ItemType Directory -Force | Out-Null
$encoding = switch ([System.IO.Path]::GetExtension($targetPath)) {
{ $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8; break }
{ $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new($false, $true); break }
default { [System.Text.Encoding]::Default }
}
$bytes = $encoding.GetPreamble() + $encoding.GetBytes($file.InnerText.Trim())
[System.IO.File]::WriteAllBytes($targetPath, $bytes)
$stagedCount++
}
$useConfigurationSet = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:UseConfigurationSet', $nsMgr)
if ($useConfigurationSet) {
$useConfigurationSet.InnerText = 'true'
[System.IO.File]::WriteAllText((Join-Path $ContentRoot 'autounattend.xml'), $xmlDoc.OuterXml, [System.Text.UTF8Encoding]::new($false))
}
& $Logger "Staged $stagedCount WinUtil setup script fallback files at '$setupScriptsRoot'."
}
if (-not (Test-Path $ISOContentsDir)) {
throw "ISO contents directory does not exist: $ISOContentsDir"
}
if ([string]::IsNullOrWhiteSpace($AutoUnattendXml)) {
throw "autounattend.xml content is required to prepare setup media."
}
$preparedAutoUnattendXml = Add-WinUtilISOSetupCustomizations -XmlContent $AutoUnattendXml -InstallImageIndex $InstallImageIndex -Logger $Log
$unattendPath = Join-Path $ISOContentsDir "autounattend.xml"
[System.IO.File]::WriteAllText($unattendPath, $preparedAutoUnattendXml, [System.Text.UTF8Encoding]::new($false))
& $Log "Written autounattend.xml with WinUtil setup customizations to ISO root ($unattendPath)."
Add-WinUtilISOSetupScriptFallback -ContentRoot $ISOContentsDir -XmlContent $preparedAutoUnattendXml -Logger $Log
Write-WinUtilISOEditionConfig -ContentRoot $ISOContentsDir -EditionId $InstallEditionId -Logger $Log
# -- 2. Inject current system drivers (optional) ---------------------------
if ($InjectCurrentSystemDrivers) { if ($InjectCurrentSystemDrivers) {
& $Log "Exporting all drivers from running system..." Add-WinUtilISOStagedDrivers -ContentRoot $ISOContentsDir -Logger $Log -InstallImagePath $InstallImagePath -InstallImageIndex $InstallImageIndex
$driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Random)"
New-Item -Path $driverExportRoot -ItemType Directory -Force
try {
Export-WindowsDriver -Online -Destination $driverExportRoot
& $Log "Injecting current system drivers into install.wim..."
Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log
& $Log "install.wim driver injection complete."
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$bootWim = Join-Path $ISOContentsDir "sources\boot.wim"
if (Test-Path $bootWim) {
& $Log "Injecting current system drivers into boot.wim..."
Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log
} else {
& $Log "Warning: boot.wim not found - skipping boot.wim driver injection."
}
}
} catch {
& $Log "Error during driver export/injection: $_"
} finally {
Remove-Item -Path $driverExportRoot -Recurse -Force
}
} else {
& $Log "Driver injection skipped."
}
# -- 3. Registry tweaks ----------------------------------------------------
& $Log "Loading offline registry hives..."
reg load HKLM\zCOMPONENTS "$ScratchDir\Windows\System32\config\COMPONENTS"
reg load HKLM\zDEFAULT "$ScratchDir\Windows\System32\config\default"
reg load HKLM\zNTUSER "$ScratchDir\Users\Default\ntuser.dat"
reg load HKLM\zSOFTWARE "$ScratchDir\Windows\System32\config\SOFTWARE"
reg load HKLM\zSYSTEM "$ScratchDir\Windows\System32\config\SYSTEM"
& $Log "Bypassing system requirements..."
Set-ISOScriptReg -Path 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV1' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV2' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV1' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV2' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassCPUCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassRAMCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassSecureBootCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassStorageCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassTPMCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\MoSetup' -Name 'AllowUpgradesWithUnsupportedTPMOrCPU' -Type 'REG_DWORD' -Value '1'
& $Log "Disabling sponsored apps..."
Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'OemPreInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'PreInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SilentInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableWindowsConsumerFeatures' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'ContentDeliveryAllowed' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\PolicyManager\current\device\Start' -Name 'ConfigureStartPins' -Type 'REG_SZ' -Value '{"pinnedList": [{}]}'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'FeatureManagementEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'PreInstalledAppsEverEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SoftLandingEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContentEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-310093Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338388Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338389Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338393Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-353694Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-353696Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SystemPaneSuggestionsEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\PushToInstall' -Name 'DisablePushToInstall' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\MRT' -Name 'DontOfferThroughWUAU' -Type 'REG_DWORD' -Value '1'
Remove-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\Subscriptions'
Remove-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\SuggestedApps'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableConsumerAccountStateContent' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableCloudOptimizedContent' -Type 'REG_DWORD' -Value '1'
& $Log "Enabling local accounts on OOBE..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' -Name 'BypassNRO' -Type 'REG_DWORD' -Value '1'
if ($AutoUnattendXml) {
$preparedAutoUnattendXml = $AutoUnattendXml
try {
$preparedAutoUnattendXml = ConvertTo-WinUtilISOAnswerFile -XmlContent $AutoUnattendXml -ImageIndex $InstallImageIndex
& $Log "Prepared autounattend.xml to install image index $InstallImageIndex without forcing a product key."
} catch {
& $Log "Warning: could not prepare autounattend.xml image selection: $_"
}
try {
$xmlDoc = [xml]::new()
$xmlDoc.LoadXml($preparedAutoUnattendXml)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$fileNodes = $xmlDoc.SelectNodes("//sg:File", $nsMgr)
if ($fileNodes -and $fileNodes.Count -gt 0) {
foreach ($fileNode in $fileNodes) {
$absPath = $fileNode.GetAttribute("path")
$relPath = $absPath -replace '^[A-Za-z]:[/\\]', ''
$destPath = Join-Path $ScratchDir $relPath
New-Item -Path (Split-Path $destPath -Parent) -ItemType Directory -Force
$ext = [IO.Path]::GetExtension($destPath).ToLower()
$encoding = switch ($ext) {
{ $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8 }
{ $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new($false, $true) }
default { [System.Text.Encoding]::Default }
}
[System.IO.File]::WriteAllBytes($destPath, ($encoding.GetPreamble() + $encoding.GetBytes($fileNode.InnerText.Trim())))
& $Log "Pre-staged setup script: $relPath"
}
} else {
& $Log "Warning: no <Extensions><File> nodes found in autounattend.xml - setup scripts not pre-staged."
}
} catch {
& $Log "Warning: could not pre-stage setup scripts from autounattend.xml: $_"
}
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$isoDest = Join-Path $ISOContentsDir "autounattend.xml"
Set-Content -Path $isoDest -Value $preparedAutoUnattendXml -Encoding UTF8 -Force
& $Log "Written autounattend.xml to ISO root ($isoDest)."
}
} else {
& $Log "Warning: autounattend.xml content is empty - skipping OOBE bypass file."
}
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
Write-WinUtilISOEditionConfig -ContentRoot $ISOContentsDir -EditionId $InstallEditionId -Logger $Log
}
& $Log "Disabling reserved storage..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager' -Name 'ShippedWithReserves' -Type 'REG_DWORD' -Value '0'
& $Log "Disabling BitLocker device encryption..."
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Control\BitLocker' -Name 'PreventDeviceEncryption' -Type 'REG_DWORD' -Value '1'
& $Log "Disabling Chat icon..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Chat' -Name 'ChatIcon' -Type 'REG_DWORD' -Value '3'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name 'TaskbarMn' -Type 'REG_DWORD' -Value '0'
& $Log "Disabling OneDrive folder backup..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\OneDrive' -Name 'DisableFileSyncNGSC' -Type 'REG_DWORD' -Value '1'
& $Log "Disabling telemetry..."
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo' -Name 'Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\Privacy' -Name 'TailoredExperiencesWithDiagnosticDataEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy' -Name 'HasAccepted' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Input\TIPC' -Name 'Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization' -Name 'RestrictImplicitInkCollection' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization' -Name 'RestrictImplicitTextCollection' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization\TrainedDataStore' -Name 'HarvestContacts' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Personalization\Settings' -Name 'AcceptedPrivacyPolicy' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\DataCollection' -Name 'AllowTelemetry' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\dmwappushservice' -Name 'Start' -Type 'REG_DWORD' -Value '4'
& $Log "Preventing installation of DevHome and Outlook..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\DevHomeUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1'
Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate'
Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate'
& $Log "Disabling Copilot..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' -Name 'TurnOffWindowsCopilot' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Edge' -Name 'HubsSidebarEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Explorer' -Name 'DisableSearchBoxSuggestions' -Type 'REG_DWORD' -Value '1'
& $Log "Disabling Windows Update during OOBE (re-enabled on first logon via FirstLogon.ps1)..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'NoAutoUpdate' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'AUOptions' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'UseWUServer' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'DisableWindowsUpdateAccess' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'WUServer' -Type 'REG_SZ' -Value 'http://localhost:8080'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'WUStatusServer' -Type 'REG_SZ' -Value 'http://localhost:8080'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1'
Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config' -Name 'DODownloadMode' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\BITS' -Name 'Start' -Type 'REG_DWORD' -Value '4'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\wuauserv' -Name 'Start' -Type 'REG_DWORD' -Value '4'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\UsoSvc' -Name 'Start' -Type 'REG_DWORD' -Value '4'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\WaaSMedicSvc' -Name 'Start' -Type 'REG_DWORD' -Value '4'
& $Log "Preventing installation of Teams..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Teams' -Name 'DisableInstallation' -Type 'REG_DWORD' -Value '1'
& $Log "Preventing installation of new Outlook..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Mail' -Name 'PreventRun' -Type 'REG_DWORD' -Value '1'
& $Log "Unloading offline registry hives..."
reg unload HKLM\zCOMPONENTS
reg unload HKLM\zDEFAULT
reg unload HKLM\zNTUSER
reg unload HKLM\zSOFTWARE
reg unload HKLM\zSYSTEM
# -- 4. Delete scheduled task definition files -----------------------------
& $Log "Deleting scheduled task definition files..."
$tasksPath = "$ScratchDir\Windows\System32\Tasks"
Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" -Force
Remove-Item "$tasksPath\Microsoft\Windows\Customer Experience Improvement Program" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\ProgramDataUpdater" -Force
Remove-Item "$tasksPath\Microsoft\Windows\Chkdsk\Proxy" -Force
Remove-Item "$tasksPath\Microsoft\Windows\Windows Error Reporting\QueueReporting" -Force
Remove-Item "$tasksPath\Microsoft\Windows\InstallService" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\UpdateOrchestrator" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\UpdateAssistant" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\WaaSMedic" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\WindowsUpdate" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\WindowsUpdate" -Recurse -Force
& $Log "Scheduled task files deleted."
# -- 5. Remove ISO support folder -----------------------------------------
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
& $Log "Removing ISO support\ folder..."
Remove-Item -Path (Join-Path $ISOContentsDir "support") -Recurse -Force
& $Log "ISO support\ folder removed."
} }
} }
+18 -4
View File
@@ -8,7 +8,7 @@ function Invoke-WinUtilISORefreshUSBDrives {
$combo.Items.Add("No USB drives detected.") $combo.Items.Add("No USB drives detected.")
$combo.SelectedIndex = 0 $combo.SelectedIndex = 0
$sync["Win11ISOUSBDisks"] = @() $sync["Win11ISOUSBDisks"] = @()
Write-Win11ISOLog "No USB drives detected." Write-WinUtilISOLog "No USB drives detected."
return return
} }
@@ -17,7 +17,7 @@ function Invoke-WinUtilISORefreshUSBDrives {
$combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)") $combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)")
} }
$combo.SelectedIndex = 0 $combo.SelectedIndex = 0
Write-Win11ISOLog "Found $($removable.Count) USB drive(s)." Write-WinUtilISOLog "Found $($removable.Count) USB drive(s)."
$sync["Win11ISOUSBDisks"] = $removable $sync["Win11ISOUSBDisks"] = $removable
} }
@@ -30,6 +30,20 @@ function Invoke-WinUtilISOWriteUSB {
return return
} }
$installWim = Join-Path $contentsDir "sources\install.wim"
$installEsd = Join-Path $contentsDir "sources\install.esd"
if (Test-Path $installEsd) {
$installEsdFile = Get-Item $installEsd
$esdSizeBytes = $installEsdFile.Length
$esdSizeMB = [math]::Ceiling($esdSizeBytes / 1MB)
if ($esdSizeBytes -ge 4GB) {
[System.Windows.MessageBox]::Show(
"This ISO uses an install.esd file that is $esdSizeMB MB. WinUtil's FAT32 USB format cannot store files larger than 4 GB.`n`nExport an ISO instead or use media with install.wim.",
"USB Creation Not Supported", "OK", "Warning")
return
}
}
$combo = $sync["WPFWin11ISOUSBDriveComboBox"] $combo = $sync["WPFWin11ISOUSBDriveComboBox"]
$selectedIndex = $combo.SelectedIndex $selectedIndex = $combo.SelectedIndex
$selectedItemText = [string]$combo.SelectedItem $selectedItemText = [string]$combo.SelectedItem
@@ -56,13 +70,13 @@ function Invoke-WinUtilISOWriteUSB {
"Confirm USB Erase", "YesNo", "Warning") "Confirm USB Erase", "YesNo", "Warning")
if ($confirm -ne "Yes") { if ($confirm -ne "Yes") {
Write-Win11ISOLog "USB write cancelled by user." Write-WinUtilISOLog "USB write cancelled by user."
return return
} }
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false $sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false
$sync["Win11ISOProcessRunning"] = $true $sync["Win11ISOProcessRunning"] = $true
Write-Win11ISOLog "Starting USB write to Disk $diskNum..." Write-WinUtilISOLog "Starting USB write to Disk $diskNum..."
$runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() $runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA" $runspace.ApartmentState = "STA"
+51 -3
View File
@@ -33,23 +33,71 @@ function Set-WinUtilDNS {
} }
} }
$dohSupported = [bool](Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue)
$dnscacheBase = "HKLM:\System\CurrentControlSet\Services\Dnscache\InterfaceSpecificParameters"
Foreach ($Adapter in $Adapters) { Foreach ($Adapter in $Adapters) {
$interfaceParams = "$dnscacheBase\$($Adapter.InterfaceGuid)"
if($DNSProvider -eq "DHCP") { if($DNSProvider -eq "DHCP") {
Write-WinUtilLog -Component "DNS" -Message "Resetting DNS to DHCP on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex))." Write-WinUtilLog -Component "DNS" -Message "Resetting DNS to DHCP on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex))."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ResetServerAddresses Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ResetServerAddresses
netsh interface ip set dnsservers name="$($Adapter.Name)" source=dhcp netsh interface ip set dnsservers name="$($Adapter.Name)" source=dhcp
netsh interface ipv6 set dnsservers name="$($Adapter.Name)" source=dhcp netsh interface ipv6 set dnsservers name="$($Adapter.Name)" source=dhcp
$dohInterfaceSettings = "$interfaceParams\DohInterfaceSettings"
if (Test-Path $dohInterfaceSettings) {
if ($dohSupported) {
$dohServerAddresses = @(
Get-ChildItem -Path "$dohInterfaceSettings\Doh" -ErrorAction SilentlyContinue
Get-ChildItem -Path "$dohInterfaceSettings\Doh6" -ErrorAction SilentlyContinue
) | Select-Object -ExpandProperty PSChildName -Unique
foreach ($ip in $dohServerAddresses) {
if (Get-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction SilentlyContinue) {
Write-WinUtilLog -Component "DNS" -Message "Removing DoH registration for $ip."
Remove-DnsClientDohServerAddress -ServerAddress $ip -Confirm:$false -ErrorAction Stop
}
}
}
Remove-Item -Path $dohInterfaceSettings -Recurse -Force -ErrorAction SilentlyContinue
}
} else { } else {
Write-WinUtilLog -Component "DNS" -Message "Setting IPv4 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary), $($dns.Secondary)." Write-WinUtilLog -Component "DNS" -Message "Setting IPv4 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary), $($dns.Secondary)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ($dns.Primary, $dns.Secondary) Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ($dns.Primary, $dns.Secondary)
Write-WinUtilLog -Component "DNS" -Message "Setting IPv6 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary6), $($dns.Secondary6)." Write-WinUtilLog -Component "DNS" -Message "Setting IPv6 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary6), $($dns.Secondary6)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ($dns.Primary6, $dns.Secondary6) Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ($dns.Primary6, $dns.Secondary6)
if ($dohSupported -and $dns.DohTemplate) {
$ips = @($dns.Primary, $dns.Secondary, $dns.Primary6, $dns.Secondary6) | Where-Object { $_ }
foreach ($ip in $ips) {
$existing = Get-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction SilentlyContinue
if ($existing) {
Set-DnsClientDohServerAddress -ServerAddress $ip -DohTemplate $dns.DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true -ErrorAction Stop
} else {
Write-WinUtilLog -Component "DNS" -Message "Registering DoH template for $ip."
Add-DnsClientDohServerAddress -ServerAddress $ip -DohTemplate $dns.DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true -ErrorAction Stop
}
$leaf = if ($ip.Contains(':')) { 'Doh6' } else { 'Doh' }
$regPath = "$interfaceParams\DohInterfaceSettings\$leaf\$ip"
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force -ErrorAction Stop | Out-Null
}
New-ItemProperty -Path $regPath -Name "DohFlags" -Value 1 -PropertyType QWord -Force -ErrorAction Stop | Out-Null
}
}
} }
} }
if ($DNSProvider -ne "DHCP" -and $dohSupported -and $dns.DohTemplate) {
Clear-DnsClientCache
}
Write-WinUtilLog -Component "DNS" -Message "DNS provider change completed: $DNSProvider" Write-WinUtilLog -Component "DNS" -Message "DNS provider change completed: $DNSProvider"
} catch { } catch {
Write-Warning "Unable to set DNS Provider due to an unhandled exception." Write-Warning "DNS provider $DNSProvider was not completed because an error occurred."
Write-Warning $psitem.Exception.StackTrace Write-Warning $psitem.Exception.Message
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "Unable to set DNS provider $DNSProvider`: $($psitem.Exception.Message)" Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "DNS provider $DNSProvider was not completed: $($psitem.Exception.Message)"
} }
} }
+63 -27
View File
@@ -1,6 +1,5 @@
function Invoke-WPFGetInstalled { function Invoke-WPFGetInstalled {
<# <#
TODO: Add the Option to use Chocolatey as Engine
.SYNOPSIS .SYNOPSIS
Invokes the function that gets the checkboxes to check in a new runspace Invokes the function that gets the checkboxes to check in a new runspace
@@ -19,35 +18,72 @@ function Invoke-WPFGetInstalled {
return return
} }
$managerPreference = $sync.preferences.packagemanager $managerPreference = $sync.preferences.packagemanager
$operation = [Hashtable]::Synchronized(@{
Invoke-WPFRunspace -ParameterList @(("managerPreference", $managerPreference),("checkbox", $checkbox)) -ScriptBlock { Checkboxes = @()
param ( Error = $null
[string]$checkbox, })
[string]$managerPreference $completeAction = [Action[hashtable, string]]{
param(
[hashtable]$completedOperation,
[string]$completedCheckbox
) )
$sync.ProcessRunning = $true try {
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" } if ($completedOperation.Error) {
Write-WinUtilLog -Level "ERROR" -Component "Install" -Message "Get installed state failed: $($completedOperation.Error)"
Write-Warning "Unable to get installed state: $($completedOperation.Error)"
return
}
if ($checkbox -eq "winget") { if ($completedCheckbox -eq "winget") {
Write-Host "Getting Installed Programs..." foreach ($checkboxName in $completedOperation.Checkboxes) {
switch ($managerPreference) { if (-not $sync.selectedApps.Contains($checkboxName)) {
"Choco"{$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox "choco"; break} $sync.selectedApps.Add($checkboxName)
"Winget"{$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox $checkbox; break} }
}
Reset-WPFCheckBoxes -checkboxfilterpattern "WPFInstall*"
} else {
foreach ($checkboxName in $completedOperation.Checkboxes) {
$sync.$checkboxName.ischecked = $True
}
}
} finally {
$sync.ProcessRunning = $false
Set-WinUtilTaskbaritem -state "None"
}
}
$sync.ProcessRunning = $true
Set-WinUtilTaskbaritem -state "Indeterminate"
try {
Invoke-WPFRunspace -ParameterList @(
("managerPreference", $managerPreference),
("checkbox", $checkbox),
("operation", $operation),
("completeAction", $completeAction)
) -ScriptBlock {
param (
[string]$checkbox,
[string]$managerPreference,
[hashtable]$operation,
[Action[hashtable, string]]$completeAction
)
try {
if ($checkbox -eq "winget") {
switch ($managerPreference) {
"Choco" { $operation.Checkboxes = @(Invoke-WinUtilCurrentSystem -CheckBox "choco"); break }
"Winget" { $operation.Checkboxes = @(Invoke-WinUtilCurrentSystem -CheckBox $checkbox); break }
}
} elseif ($checkbox -eq "tweaks") {
$operation.Checkboxes = @(Invoke-WinUtilCurrentSystem -CheckBox $checkbox)
}
} catch {
$operation.Error = $_.Exception.Message
} finally {
$sync.Form.Dispatcher.BeginInvoke($completeAction, [object[]]@($operation, $checkbox)) | Out-Null
} }
} }
elseif ($checkbox -eq "tweaks") { } catch {
Write-Host "Getting Installed Tweaks..." $operation.Error = $_.Exception.Message
$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox $checkbox $completeAction.Invoke($operation, $checkbox)
}
$sync.form.Dispatcher.invoke({
foreach ($checkbox in $Checkboxes) {
$sync.$checkbox.ischecked = $True
}
})
Write-Host "Done..."
$sync.ProcessRunning = $false
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" }
} }
} }
+3 -2
View File
@@ -44,6 +44,8 @@ public sealed class WinUtilRunspaceCleanupState
public static class WinUtilRunspaceCleanup public static class WinUtilRunspaceCleanup
{ {
public static readonly System.Threading.WaitOrTimerCallback Callback = Cleanup;
public static void Cleanup(object state, bool timedOut) public static void Cleanup(object state, bool timedOut)
{ {
var cleanupState = state as WinUtilRunspaceCleanupState; var cleanupState = state as WinUtilRunspaceCleanupState;
@@ -89,8 +91,7 @@ public static class WinUtilRunspaceCleanup
$cleanupState = [WinUtilRunspaceCleanupState]::new() $cleanupState = [WinUtilRunspaceCleanupState]::new()
$cleanupState.PowerShell = $powershell $cleanupState.PowerShell = $powershell
$cleanupState.Handle = $handle $cleanupState.Handle = $handle
$cleanupCallback = [System.Threading.WaitOrTimerCallback][WinUtilRunspaceCleanup]::Cleanup [System.Threading.ThreadPool]::RegisterWaitForSingleObject($handle.AsyncWaitHandle, [WinUtilRunspaceCleanup]::Callback, $cleanupState, -1, $true) | Out-Null
[System.Threading.ThreadPool]::RegisterWaitForSingleObject($handle.AsyncWaitHandle, $cleanupCallback, $cleanupState, -1, $true) | Out-Null
# Return the handle # Return the handle
return $handle return $handle
@@ -7,11 +7,21 @@ function Invoke-WPFSelectedCheckboxesUpdate ($type, $checkboxName) {
'^WPFAppx' { 'selectedAppx' } '^WPFAppx' { 'selectedAppx' }
} }
$selectionChanged = $false
if ($type -eq "Add") { if ($type -eq "Add") {
if (-not $sync.$listName.Contains($checkboxName)) { if (-not $sync.$listName.Contains($checkboxName)) {
$sync.$listName.Add($checkboxName) $sync.$listName.Add($checkboxName)
$selectionChanged = $true
} }
} else { } else {
$sync.$listName.Remove($checkboxName) $selectionChanged = $sync.$listName.Remove($checkboxName)
}
if ($listName -eq "selectedApps" -and $selectionChanged) {
$sync.WPFselectedAppsButton.Content = "Selected Apps: $($sync.selectedApps.Count)"
$sync.selectedAppsstackPanel.Children.Clear()
$sync.selectedApps | Sort-Object | ForEach-Object {
Add-SelectedAppsMenuItem -name $sync.configs.applicationsHashtable.$_.Content -key $_
}
} }
} }
+77 -3
View File
@@ -23,6 +23,20 @@ BeforeAll {
param($Message, $Level, $Component) param($Message, $Level, $Component)
} }
function Add-DnsClientDohServerAddress {
param($ServerAddress, $DohTemplate, $AllowFallbackToUdp, $AutoUpgrade, $ErrorAction)
}
function Set-DnsClientDohServerAddress {
param($ServerAddress, $DohTemplate, $AllowFallbackToUdp, $AutoUpgrade, $ErrorAction)
}
function Get-DnsClientDohServerAddress {
param($ServerAddress, $ErrorAction)
}
function Remove-DnsClientDohServerAddress {
param($ServerAddress, $Confirm, $ErrorAction)
}
function Clear-DnsClientCache { }
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilDNS.ps1") . (Join-Path $script:repoRoot "functions\private\Set-WinUtilDNS.ps1")
} }
@@ -36,6 +50,7 @@ Describe "Set-WinUtilDNS" {
Secondary = "1.0.0.1" Secondary = "1.0.0.1"
Primary6 = "2606:4700:4700::1111" Primary6 = "2606:4700:4700::1111"
Secondary6 = "2606:4700:4700::1001" Secondary6 = "2606:4700:4700::1001"
DohTemplate = "https://cloudflare-dns.com/dns-query"
} }
} }
} }
@@ -46,6 +61,7 @@ Describe "Set-WinUtilDNS" {
Name = "Ethernet" Name = "Ethernet"
Status = "Up" Status = "Up"
ifIndex = 7 ifIndex = 7
InterfaceGuid = "{1234-5678-90AB-CDEF}"
} }
} }
Mock Set-DnsClientServerAddress { } Mock Set-DnsClientServerAddress { }
@@ -53,13 +69,25 @@ Describe "Set-WinUtilDNS" {
Mock Write-WinUtilLog { } Mock Write-WinUtilLog { }
Mock Write-Warning { } Mock Write-Warning { }
Mock Write-Host { } Mock Write-Host { }
Mock Get-Command { return $true } -ParameterFilter { $Name -eq "Add-DnsClientDohServerAddress" }
Mock Add-DnsClientDohServerAddress { }
Mock Set-DnsClientDohServerAddress { }
Mock Get-DnsClientDohServerAddress { return $null }
Mock Remove-DnsClientDohServerAddress { }
Mock Test-Path { return $false }
Mock Get-ChildItem { }
Mock New-Item { }
Mock New-ItemProperty { }
Mock Remove-Item { }
Mock Clear-DnsClientCache { }
} }
AfterEach { AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
} }
It "sets IPv4 and IPv6 DNS server addresses separately" { It "sets IPv4 and IPv6 DNS server addresses separately and applies DoH templates" {
Set-WinUtilDNS -DNSProvider "Cloudflare" Set-WinUtilDNS -DNSProvider "Cloudflare"
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter { Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter {
@@ -77,9 +105,51 @@ Describe "Set-WinUtilDNS" {
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 0 -Exactly -ParameterFilter { Should -Invoke -CommandName Set-DnsClientServerAddress -Times 0 -Exactly -ParameterFilter {
$ServerAddresses.Count -eq 4 $ServerAddresses.Count -eq 4
} }
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 4 -Exactly
Should -Invoke -CommandName New-ItemProperty -Times 4 -ParameterFilter {
$Name -eq "DohFlags" -and $Value -eq 1
}
Should -Invoke -CommandName Clear-DnsClientCache -Times 1 -Exactly
} }
It "resets DNS to DHCP for IPv4 and IPv6" { It "updates an existing DoH entry with the selected provider settings" {
Mock Get-DnsClientDohServerAddress {
if ($ServerAddress -eq "1.1.1.1") {
return [pscustomobject]@{ ServerAddress = $ServerAddress }
}
return $null
}
Set-WinUtilDNS -DNSProvider "Cloudflare"
Should -Invoke -CommandName Set-DnsClientDohServerAddress -Times 1 -Exactly -ParameterFilter {
$ServerAddress -eq "1.1.1.1" -and
$DohTemplate -eq "https://cloudflare-dns.com/dns-query" -and
$AllowFallbackToUdp -eq $false -and
$AutoUpgrade -eq $true
}
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 3 -Exactly
}
It "resets DNS to DHCP and removes the applied DoH configuration" {
Mock Test-Path { return $true } -ParameterFilter { $Path -like "*DohInterfaceSettings*" }
Mock Get-ChildItem {
if ($Path -like "*\Doh6") {
return @(
[pscustomobject]@{ PSChildName = "2606:4700:4700::1111" }
[pscustomobject]@{ PSChildName = "2606:4700:4700::1001" }
)
}
return @(
[pscustomobject]@{ PSChildName = "1.1.1.1" }
[pscustomobject]@{ PSChildName = "1.0.0.1" }
)
}
Mock Get-DnsClientDohServerAddress {
[pscustomobject]@{ ServerAddress = $ServerAddress }
}
Set-WinUtilDNS -DNSProvider "DHCP" Set-WinUtilDNS -DNSProvider "DHCP"
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter { Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter {
@@ -102,6 +172,10 @@ Describe "Set-WinUtilDNS" {
$Arguments[4] -eq "name=Ethernet" -and $Arguments[4] -eq "name=Ethernet" -and
$Arguments[5] -eq "source=dhcp" $Arguments[5] -eq "source=dhcp"
} }
Should -Invoke -CommandName Remove-Item -Times 1 -Exactly -ParameterFilter {
$Path -like "*DohInterfaceSettings*" -and $Recurse -eq $true -and $Force -eq $true
}
Should -Invoke -CommandName Remove-DnsClientDohServerAddress -Times 4 -Exactly
} }
It "catches DNS setter failures so the tweak runspace can continue" { It "catches DNS setter failures so the tweak runspace can continue" {
@@ -112,7 +186,7 @@ Describe "Set-WinUtilDNS" {
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter { Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and $Level -eq "ERROR" -and
$Component -eq "DNS" -and $Component -eq "DNS" -and
$Message -like "Unable to set DNS provider Cloudflare*" $Message -like "DNS provider Cloudflare was not completed: *"
} }
} }
} }
+4
View File
@@ -140,6 +140,10 @@ Describe "Invoke-WPFRunspace behavior" {
$runspaceScript | Should -Not -Match '\$script:powershell' $runspaceScript | Should -Not -Match '\$script:powershell'
$runspaceScript | Should -Not -Match '\$script:handle' $runspaceScript | Should -Not -Match '\$script:handle'
} }
It "exposes a strongly typed cleanup callback" {
([WinUtilRunspaceCleanup]::Callback -is [System.Threading.WaitOrTimerCallback]) | Should -BeTrue
}
} }
Describe "Public runspace callers" { Describe "Public runspace callers" {
+70
View File
@@ -6,12 +6,82 @@ $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
BeforeAll { BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilCurrentSystem.ps1")
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilRegistry.ps1") . (Join-Path $script:repoRoot "functions\private\Set-WinUtilRegistry.ps1")
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilService.ps1") . (Join-Path $script:repoRoot "functions\private\Set-WinUtilService.ps1")
function winget {
param([Parameter(ValueFromRemainingArguments = $true)]$Arguments)
}
function choco {
param([Parameter(ValueFromRemainingArguments = $true)]$Arguments)
}
function Write-WinUtilLog { } function Write-WinUtilLog { }
} }
Describe "Invoke-WinUtilCurrentSystem installed apps" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
configs = [pscustomobject]@{
applicationsHashtable = @{
WPFInstallGit = [pscustomobject]@{ winget = "Git.Git"; choco = "git" }
WPFInstallChatGPT = [pscustomobject]@{ winget = "msstore:9NT1R1C2HH7J"; choco = "na" }
WPFInstallMissing = [pscustomobject]@{ winget = "Git"; choco = "missing" }
}
}
})
Mock winget {
$global:LASTEXITCODE = 0
$script:wingetArguments = @($Arguments)
@(
"Name Id Version Source",
"--------------------------------",
"Git Git.Git 2.0 winget",
"ChatGPT 9NT1R1C2HH7J 1.0 msstore"
)
}
Mock choco {
$script:chocoArguments = @($Arguments)
@("Chocolatey v2", "git 2.0", "2 packages installed.")
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name wingetArguments -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name chocoArguments -Scope Script -ErrorAction SilentlyContinue
}
It "matches single standard and Microsoft Store package IDs" {
$result = @(Invoke-WinUtilCurrentSystem -CheckBox "winget")
$result | Should -HaveCount 2
$result | Should -Contain "WPFInstallGit"
$result | Should -Contain "WPFInstallChatGPT"
$result | Should -Not -Contain "WPFInstallMissing"
Should -Invoke -CommandName winget -Times 1 -Exactly
$script:wingetArguments | Should -Be @("list", "--accept-source-agreements", "--disable-interactivity")
}
It "fails promptly when Winget cannot list applications" {
Mock winget {
$global:LASTEXITCODE = 1
"winget failed"
}
{ Invoke-WinUtilCurrentSystem -CheckBox "winget" } | Should -Throw "winget list failed with exit code 1."
}
It "matches the primary Chocolatey package ID in one list call" {
$result = @(Invoke-WinUtilCurrentSystem -CheckBox "choco")
$result | Should -Be @("WPFInstallGit")
Should -Invoke -CommandName choco -Times 1 -Exactly
$script:chocoArguments | Should -Be @("list")
}
}
Describe "Set-WinUtilRegistry" { Describe "Set-WinUtilRegistry" {
BeforeEach { BeforeEach {
$script:testPathResults = @{} $script:testPathResults = @{}
+110
View File
@@ -61,6 +61,7 @@ namespace System.Windows.Controls
. (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1") . (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1")
. (Join-Path $script:repoRoot "functions\private\Reset-WPFCheckBoxes.ps1") . (Join-Path $script:repoRoot "functions\private\Reset-WPFCheckBoxes.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFGetInstalled.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFSelectedCheckboxesUpdate.ps1") . (Join-Path $script:repoRoot "functions\public\Invoke-WPFSelectedCheckboxesUpdate.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1") . (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFToggleAllCategories.ps1") . (Join-Path $script:repoRoot "functions\public\Invoke-WPFToggleAllCategories.ps1")
@@ -68,6 +69,24 @@ namespace System.Windows.Controls
function Set-WinUtilTweaksProgressIndicator { function Set-WinUtilTweaksProgressIndicator {
param($Visible, $Label, $Percent) param($Visible, $Label, $Percent)
} }
function Invoke-WPFRunspace {
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
}
function Invoke-WPFUIThread {
param([scriptblock]$ScriptBlock)
}
function Invoke-WinUtilCurrentSystem {
param($CheckBox)
}
function Set-WinUtilTaskbaritem {
param($state)
}
function Test-WinUtilPackageManager {
param([switch]$winget)
}
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function script:New-WinUtilFakeCheckBox { function script:New-WinUtilFakeCheckBox {
param([bool]$IsChecked = $false) param([bool]$IsChecked = $false)
@@ -180,6 +199,9 @@ Describe "Invoke-WPFSelectedCheckboxesUpdate" {
@($script:sync.selectedToggles) | Should -Be @("WPFToggleDarkMode") @($script:sync.selectedToggles) | Should -Be @("WPFToggleDarkMode")
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox") @($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
@($script:sync.selectedAppx) | Should -Be @("WPFAppxExample") @($script:sync.selectedAppx) | Should -Be @("WPFAppxExample")
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 1"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 1
$script:sync.selectedAppsstackPanel.Children[0].Key | Should -Be "WPFInstallGit"
} }
It "removes checkbox keys from the matching selected lists" { It "removes checkbox keys from the matching selected lists" {
@@ -200,6 +222,94 @@ Describe "Invoke-WPFSelectedCheckboxesUpdate" {
$script:sync.selectedToggles.Count | Should -Be 0 $script:sync.selectedToggles.Count | Should -Be 0
$script:sync.selectedFeatures.Count | Should -Be 0 $script:sync.selectedFeatures.Count | Should -Be 0
$script:sync.selectedAppx.Count | Should -Be 0 $script:sync.selectedAppx.Count | Should -Be 0
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 0"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 0
}
}
Describe "Invoke-WPFGetInstalled selection state" {
BeforeEach {
New-WinUtilUiStateTestContext
$script:sync.ProcessRunning = $false
$script:sync.ChocoRadioButton = [pscustomobject]@{ IsChecked = $false }
$script:sync.preferences = [pscustomobject]@{ packagemanager = "Winget" }
$script:sync.WPFInstallGit = New-WinUtilFakeCheckBox
$dispatcher = [pscustomobject]@{}
$dispatcher | Add-Member -MemberType ScriptMethod -Name BeginInvoke -Value {
param($Action, [object[]]$Arguments)
$Action.DynamicInvoke($Arguments)
}
$script:sync.Form = [pscustomobject]@{ Dispatcher = $dispatcher }
$script:capturedGetInstalledScriptBlock = $null
$script:capturedGetInstalledParameters = @{}
Mock Test-WinUtilPackageManager { "installed" }
Mock Invoke-WinUtilCurrentSystem { @("WPFInstallGit") }
Mock Set-WinUtilTaskbaritem { }
Mock Write-WinUtilLog { }
Mock Write-Warning { }
Mock Invoke-WPFRunspace {
$script:capturedGetInstalledScriptBlock = $ScriptBlock
foreach ($parameter in $ParameterList) {
$script:capturedGetInstalledParameters[$parameter[0]] = $parameter[1]
}
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
Remove-Variable -Name capturedGetInstalledScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedGetInstalledParameters -Scope Script -ErrorAction SilentlyContinue
}
It "updates the selected app model, checkbox, count, and popup" {
Invoke-WPFGetInstalled -CheckBox "winget"
& $script:capturedGetInstalledScriptBlock `
-checkbox "winget" `
-managerPreference "Winget" `
-operation $script:capturedGetInstalledParameters.operation `
-completeAction $script:capturedGetInstalledParameters.completeAction
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
$script:sync.WPFInstallGit.IsChecked | Should -BeTrue
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 1"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 1
$script:sync.selectedAppsstackPanel.Children[0].Key | Should -Be "WPFInstallGit"
}
It "clears the running state when detection fails" {
Mock Invoke-WinUtilCurrentSystem { throw "detection failed" }
Invoke-WPFGetInstalled -CheckBox "winget"
& $script:capturedGetInstalledScriptBlock `
-checkbox "winget" `
-managerPreference "Winget" `
-operation $script:capturedGetInstalledParameters.operation `
-completeAction $script:capturedGetInstalledParameters.completeAction
$script:sync.ProcessRunning | Should -BeFalse
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "Install" -and
$Message -eq "Get installed state failed: detection failed"
}
Should -Invoke -CommandName Set-WinUtilTaskbaritem -Times 1 -Exactly -ParameterFilter { $state -eq "None" }
}
It "clears the running state when the worker cannot be queued" {
Mock Invoke-WPFRunspace { throw "queue failed" }
Invoke-WPFGetInstalled -CheckBox "winget"
$script:sync.ProcessRunning | Should -BeFalse
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "Install" -and
$Message -eq "Get installed state failed: queue failed"
}
Should -Invoke -CommandName Set-WinUtilTaskbaritem -Times 1 -Exactly -ParameterFilter { $state -eq "None" }
} }
} }
+324 -85
View File
@@ -37,14 +37,12 @@ Describe "Win11 Creator setup media" {
} }
$script:modifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOModify" $script:modifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOModify"
$script:mountAndVerifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOMountAndVerify"
$script:cleanAndResetFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOCleanAndReset" $script:cleanAndResetFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOCleanAndReset"
$script:exportFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOExport" $script:exportFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOExport"
$script:writeUsbFunction = Get-WinUtilFunctionText -Path $script:isoUsbWorkflowPath -FunctionName "Invoke-WinUtilISOWriteUSB" $script:writeUsbFunction = Get-WinUtilFunctionText -Path $script:isoUsbWorkflowPath -FunctionName "Invoke-WinUtilISOWriteUSB"
$script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName" $script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName"
$script:addDriversFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Add-DriversToImage" $script:wimMetadataAssertionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Assert-WinUtilISOWimMetadata"
$script:answerFileChildElementFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Get-WinUtilISOScriptChildElement"
$script:answerFileConversionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "ConvertTo-WinUtilISOAnswerFile"
$script:editionConfigFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Write-WinUtilISOEditionConfig"
} }
It "autounattend template does not force a product key" { It "autounattend template does not force a product key" {
@@ -58,12 +56,35 @@ Describe "Win11 Creator setup media" {
} }
} }
It "ISO script accepts selected edition setup metadata" { It "sets every hardware bypass before Windows Setup checks requirements" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$paths = @($xml.SelectNodes('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:RunSynchronous/u:RunSynchronousCommand/u:Path', $nsMgr) | ForEach-Object InnerText) -join "`n"
foreach ($bypass in 'BypassTPMCheck', 'BypassSecureBootCheck', 'BypassRAMCheck', 'BypassCPUCheck', 'BypassStorageCheck') {
$paths | Should -Match ([regex]::Escape($bypass))
}
}
It "sets OOBE-sensitive registry values before OOBE starts" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$paths = @($xml.SelectNodes('/u:unattend/u:settings[@pass="specialize"]/u:component[@name="Microsoft-Windows-Deployment"]/u:RunSynchronous/u:RunSynchronousCommand/u:Path', $nsMgr) | ForEach-Object InnerText) -join "`n"
foreach ($valueName in 'BypassNRO', 'PreventDeviceEncryption', 'ShippedWithReserves') {
$paths | Should -Match ([regex]::Escape($valueName))
}
}
It "ISO script accepts selected edition and driver-only WIM servicing metadata" {
$isoScriptPath = Join-Path $PSScriptRoot "..\functions\private\Invoke-WinUtilISOScript.ps1" $isoScriptPath = Join-Path $PSScriptRoot "..\functions\private\Invoke-WinUtilISOScript.ps1"
$content = Get-Content -Path $isoScriptPath -Raw $content = Get-Content -Path $isoScriptPath -Raw
foreach ($pattern in @( foreach ($pattern in @(
'\[string\]\$InstallEditionId', '\[string\]\$InstallEditionId',
'\[string\]\$InstallImagePath',
'\[int\]\$InstallImageIndex', '\[int\]\$InstallImageIndex',
'sources\\ei\.cfg', 'sources\\ei\.cfg',
'PID\.txt' 'PID\.txt'
@@ -85,8 +106,57 @@ Describe "Win11 Creator setup media" {
$script:modifyFunction | Should -Not -Match ([regex]::Escape("Reusing existing temp directory")) $script:modifyFunction | Should -Not -Match ([regex]::Escape("Reusing existing temp directory"))
} }
It "keeps WIM servicing limited to one driver-only mount and commit" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
foreach ($expectedText in @(
"'/Mount-Image'",
"'/Add-Driver'",
"'/Commit'",
"`$mountDir = Join-Path (Split-Path -Path `$ContentRoot -Parent) 'wim_mount'",
'install.wim metadata validation passed'
)) {
$isoScriptContent | Should -Match ([regex]::Escape($expectedText))
}
foreach ($forbiddenText in @(
'Mount-WindowsImage',
'Dismount-WindowsImage',
'Export-WindowsImage',
'Set-WindowsImage',
'/ResetBase',
'/Cleanup-Image'
)) {
$isoScriptContent | Should -Not -Match ([regex]::Escape($forbiddenText))
}
}
It "stages only boot-storage drivers in WinPE" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
$isoScriptContent | Should -Match 'SCSIAdapter\|HDC'
$isoScriptContent | Should -Not -Match ([regex]::Escape('sources\$OEM$\$$\Drivers'))
$isoScriptContent | Should -Not -Match ([regex]::Escape('WinUtil-InstallDrivers.ps1'))
$isoScriptContent | Should -Not -Match ([regex]::Escape('SetupComplete.cmd'))
}
It "rejects invalid WIM metadata before and after driver injection" {
. ([scriptblock]::Create($script:wimMetadataAssertionFunction))
$valid = @{ Languages = 'en-US'; Installation = 'Client'; Edition = 'Professional'; ProductSuite = 'Terminal Server'; ProductType = 'WinNT' }
$invalidBefore = $valid.Clone()
$invalidBefore.Edition = '<undefined>'
$invalidAfter = $valid.Clone()
$invalidAfter.ProductType = '<undefined>'
{ Assert-WinUtilISOWimMetadata -Before $invalidBefore } | Should -Throw '*already invalid*'
{ Assert-WinUtilISOWimMetadata -Before $valid -After $invalidAfter } | Should -Throw '*validation failed*'
}
It "tracks every background ISO workflow with the shared busy state" { It "tracks every background ISO workflow with the shared busy state" {
foreach ($functionText in @( foreach ($functionText in @(
$script:mountAndVerifyFunction,
$script:modifyFunction, $script:modifyFunction,
$script:cleanAndResetFunction, $script:cleanAndResetFunction,
$script:exportFunction, $script:exportFunction,
@@ -97,14 +167,28 @@ Describe "Win11 Creator setup media" {
} }
} }
It "mounts the copied image file that was verified from the ISO" { It "runs ISO mount and verification outside the UI thread" {
foreach ($expectedText in @( $script:mountAndVerifyFunction | Should -Match ([regex]::Escape("Invoke-WPFRunspace -ParameterList @(,('isoPath', `$isoPath))"))
'$sourceImageFileName = Split-Path $wimPath -Leaf', $script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Invoke-WPFUIThread {'))
'$localWim = Join-Path $isoContents "sources\$sourceImageFileName"', $script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Write-WinUtilISOLog'))
'Copied ISO image file not found: sources\$sourceImageFileName' $script:mountAndVerifyFunction | Should -Not -Match ([regex]::Escape('Write-Win11ISOLog'))
)) { $script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $false'))
$script:modifyFunction | Should -Match ([regex]::Escape($expectedText)) $script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $true'))
} $script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOMountButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOMountButton"].IsEnabled = $true'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOModifyButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOModifyButton"].IsEnabled = $true'))
}
It "blocks oversized install.esd before USB erase confirmation" {
$script:writeUsbFunction | Should -Match ([regex]::Escape('$installEsd = Join-Path $contentsDir "sources\install.esd"'))
$script:writeUsbFunction | Should -Match ([regex]::Escape('$esdSizeBytes -ge 4GB'))
$script:writeUsbFunction | Should -Match 'install\.esd file'
$guardIndex = $script:writeUsbFunction.IndexOf('$installEsd = Join-Path $contentsDir "sources\install.esd"')
$confirmationIndex = $script:writeUsbFunction.IndexOf('Confirm USB Erase')
$guardIndex | Should -BeGreaterThan -1
$confirmationIndex | Should -BeGreaterThan $guardIndex
} }
It "maps Windows edition names to setup edition IDs" { It "maps Windows edition names to setup edition IDs" {
@@ -135,9 +219,7 @@ Describe "Win11 Creator setup media" {
Get-WinUtilEditionIdFromName -EditionName "Windows 11 Unknown Edition" | Should -Be "" Get-WinUtilEditionIdFromName -EditionName "Windows 11 Unknown Edition" | Should -Be ""
} }
It "writes ei.cfg and removes stale PID.txt for selected editions" { It "writes ei.cfg and removes stale PID.txt for the selected edition" {
. ([scriptblock]::Create($script:editionConfigFunction))
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())" $contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$sourcesDir = Join-Path $contentRoot "sources" $sourcesDir = Join-Path $contentRoot "sources"
$logs = [System.Collections.Generic.List[string]]::new() $logs = [System.Collections.Generic.List[string]]::new()
@@ -146,8 +228,10 @@ Describe "Win11 Creator setup media" {
try { try {
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $sourcesDir "PID.txt") -Value "stale-key" -Encoding UTF8 Set-Content -Path (Join-Path $sourcesDir "PID.txt") -Value "stale-key" -Encoding UTF8
Set-Content -Path (Join-Path $sourcesDir "ei.cfg") -Value "stale-cfg" -Encoding UTF8
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "Professional" -Logger $logger . $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InstallEditionId "Professional" -Log $logger
Test-Path (Join-Path $sourcesDir "PID.txt") | Should -BeFalse Test-Path (Join-Path $sourcesDir "PID.txt") | Should -BeFalse
Test-Path (Join-Path $sourcesDir "ei.cfg") | Should -BeTrue Test-Path (Join-Path $sourcesDir "ei.cfg") | Should -BeTrue
@@ -160,90 +244,245 @@ Describe "Win11 Creator setup media" {
} }
} }
It "skips ei.cfg when selected edition ID is unknown" { It "stages the complete WinUtil customization script and selected image index" {
. ([scriptblock]::Create($script:editionConfigFunction)) $contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoAnswerFile_$([guid]::NewGuid())"
$template = Get-Content -Path $script:autoUnattendPath -Raw
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$logs = [System.Collections.Generic.List[string]]::new()
$logger = { param($message) $logs.Add([string]$message) }
try { try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InstallEditionId "Core" -InstallImageIndex 6
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "" -Logger $logger [xml]$answerFile = Get-Content -Path (Join-Path $contentRoot "autounattend.xml") -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
Test-Path (Join-Path $contentRoot "sources\ei.cfg") | Should -BeFalse $answerFile.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[u:Key="/IMAGE/INDEX"]/u:Value', $nsMgr).InnerText | Should -Be '6'
($logs -join "|") | Should -Match "selected edition ID is unknown"
$postInstallFile = $answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1"]', $nsMgr)
$postInstallFile | Should -Not -BeNullOrEmpty
$postInstallFile.InnerText | Should -Match 'Remove-AppxProvisionedPackage'
$postInstallFile.InnerText | Should -Match 'DisableWindowsConsumerFeatures'
$postInstallFile.InnerText | Should -Match 'Microsoft Compatibility Appraiser'
$postInstallFile.InnerText | Should -Match 'OneDriveSetup.exe'
$postInstallFile.InnerText | Should -Match 'function Set-WinUtilContentDeliveryManagerValues'
$postInstallFile.InnerText | Should -Match ([regex]::Escape('Set-WinUtilContentDeliveryManagerValues $defaultHive'))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilContentDeliveryManagerValues 'HKCU'"))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV1'"))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV2'"))
foreach ($defaultProfilePath in @(
'$defaultHive\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo',
'$defaultHive\Software\Microsoft\Windows\CurrentVersion\Privacy',
'$defaultHive\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy',
'$defaultHive\Software\Microsoft\Input\TIPC',
'$defaultHive\Software\Microsoft\InputPersonalization',
'$defaultHive\Software\Microsoft\InputPersonalization\TrainedDataStore',
'$defaultHive\Software\Microsoft\Personalization\Settings'
)) {
$postInstallFile.InnerText | Should -Match ([regex]::Escape($defaultProfilePath))
}
$firstLogonFile = $answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\FirstLogon.ps1"]', $nsMgr)
$firstLogonFile.InnerText | Should -Match 'WinUtil-PostInstall.ps1'
$setupScriptsRoot = Join-Path $contentRoot 'sources\$OEM$\$$\Setup\Scripts'
Test-Path (Join-Path $setupScriptsRoot 'Specialize.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'DefaultUser.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'FirstLogon.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'WinUtil-PostInstall.ps1') | Should -BeTrue
Get-Content -Path (Join-Path $setupScriptsRoot 'FirstLogon.ps1') -Raw | Should -Match 'WinUtil-PostInstall.ps1'
Get-Content -Path (Join-Path $setupScriptsRoot 'WinUtil-PostInstall.ps1') -Raw | Should -Match 'Remove-AppxProvisionedPackage'
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseInput($postInstallFile.InnerText, [ref]$tokens, [ref]$errors) | Out-Null
$errors.Count | Should -Be 0
} finally { } finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
} }
} }
It "converts autounattend setup metadata without preserving placeholder product keys" { It "stages storage drivers for WinPE and adds all drivers to one install.wim index" {
. ([scriptblock]::Create($script:answerFileChildElementFunction)) $contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDrivers_$([guid]::NewGuid())"
. ([scriptblock]::Create($script:answerFileConversionFunction)) $installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
[xml]$templateXml = Get-Content -Path $script:autoUnattendPath -Raw
$unattendNs = "urn:schemas-microsoft-com:unattend"
$nsMgr = New-Object System.Xml.XmlNamespaceManager($templateXml.NameTable)
$nsMgr.AddNamespace("u", $unattendNs)
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$setupComponent = $templateXml.SelectSingleNode('//u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
$userData = $templateXml.CreateElement("UserData", $unattendNs)
$productKey = $templateXml.CreateElement("ProductKey", $unattendNs)
$key = $templateXml.CreateElement("Key", $unattendNs)
$key.InnerText = "00000-00000-00000-00000-00000"
[void]$productKey.AppendChild($key)
[void]$userData.AppendChild($productKey)
[void]$setupComponent.AppendChild($userData)
$originalSetupFileCount = $templateXml.SelectNodes("//sg:File", $nsMgr).Count
[xml]$converted = ConvertTo-WinUtilISOAnswerFile -XmlContent $templateXml.OuterXml -ImageIndex 3
$convertedNsMgr = New-Object System.Xml.XmlNamespaceManager($converted.NameTable)
$convertedNsMgr.AddNamespace("u", $unattendNs)
$convertedNsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$converted.DocumentElement.NamespaceURI | Should -Be $unattendNs
$converted.DocumentElement.GetAttribute("xmlns:wcm") | Should -Be "http://schemas.microsoft.com/WMIConfig/2002/State"
$converted.SelectNodes('//u:component[@name="Microsoft-Windows-Setup"]/u:UserData/u:ProductKey', $convertedNsMgr).Count | Should -Be 0
$converted.SelectSingleNode('//u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[u:Key="/IMAGE/INDEX"]/u:Value', $convertedNsMgr).InnerText | Should -Be "3"
$converted.SelectNodes("//sg:File", $convertedNsMgr).Count | Should -Be $originalSetupFileCount
}
It "logs DISM output for driver injection through a mocked command" {
. ([scriptblock]::Create($script:addDriversFunction))
$script:dismArguments = @()
function dism {
param([Parameter(ValueFromRemainingArguments)]$Arguments)
$script:dismArguments = @($Arguments)
"driver one"
"driver two"
}
$logs = [System.Collections.Generic.List[string]]::new() $logs = [System.Collections.Generic.List[string]]::new()
Add-DriversToImage -MountPath "C:\Mount" -DriverDir "C:\Drivers" -Label "install" -Logger { $script:dismCalls = [System.Collections.Generic.List[string]]::new()
param($message)
$logs.Add([string]$message) function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
$global:LASTEXITCODE = 0
if ($Arguments -contains '/Get-WimInfo') {
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
'[==========================100.0%==========================]'
}
} }
($script:dismArguments -join "|") | Should -Be "/English|/image:C:\Mount|/Add-Driver|/Driver:C:\Drivers|/Recurse" Mock Start-Process {
($logs -join "|") | Should -Be " dism[install]: driver one| dism[install]: driver two" param($FilePath, $ArgumentList)
if ($FilePath -ne 'dism.exe') {
throw "Unexpected process in driver export mock: $FilePath"
}
$destinationMatch = [regex]::Match([string]$ArgumentList, '/destination:"([^"]+)"')
if (-not $destinationMatch.Success) {
throw "Unable to find the mocked DISM export destination in: $ArgumentList"
}
$exportRoot = $destinationMatch.Groups[1].Value
$fixtures = @(
@{ Path = 'system_pkg'; Name = 'chipset.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'group_a\duplicate'; Name = 'audio.inf'; Class = 'Media' },
@{ Path = 'group_b\duplicate'; Name = 'extension.inf'; Class = 'Extension' }
)
foreach ($fixture in $fixtures) {
$fixturePath = Join-Path $exportRoot $fixture.Path
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $fixturePath $fixture.Name) -Value "[Version]`r`nClass=$($fixture.Class)" -Encoding ASCII
}
return [pscustomobject]@{ ExitCode = 0 }
} -ParameterFilter { $FilePath -eq 'dism.exe' }
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
$winpeDriverRoot = Join-Path $contentRoot '$WinpeDriver$'
@(Get-ChildItem -Path $winpeDriverRoot -Directory).Count | Should -Be 2
Test-Path (Join-Path $winpeDriverRoot 'system_pkg\chipset.inf') | Should -BeFalse
Test-Path (Join-Path $winpeDriverRoot 'storage_pkg\iaStorAC.inf') | Should -BeTrue
Test-Path (Join-Path $winpeDriverRoot 'scsi_pkg\controller.inf') | Should -BeTrue
Test-Path (Join-Path $winpeDriverRoot 'net_pkg\network.inf') | Should -BeFalse
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Get-WimInfo' }).Count | Should -Be 2
($script:dismCalls -join "`n") | Should -Not -Match '/Cleanup-Image|/Export-Image'
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
($logs -join '|') | Should -Match 'staged 2 boot-storage packages for WinPE'
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
($logs -join '|') | Should -Match 'DISM mount completed.'
($logs -join '|') | Should -Not -Match '100.0%'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
} }
It "keeps driver export and boot.wim injection behind the selected branch" { It "discards a partially mounted install.wim after mount failure" {
$content = Get-Content -Path $script:isoScriptPath -Raw $contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailure_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
foreach ($expectedText in @( function dism.exe {
'if ($InjectCurrentSystemDrivers)', param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
'Export-WindowsDriver -Online -Destination $driverExportRoot',
'Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log', $script:dismCalls.Add(($Arguments -join '|'))
'Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log', if ($Arguments -contains '/Get-WimInfo') {
'Warning: boot.wim not found - skipping boot.wim driver injection.', $global:LASTEXITCODE = 0
'Driver injection skipped.' 'Languages : en-US'
)) { 'Installation : Client'
$content | Should -Match ([regex]::Escape($expectedText)) 'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
$global:LASTEXITCODE = 50
'Mount failed'
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} else {
$global:LASTEXITCODE = 0
}
}
Mock Start-Process {
param($FilePath, $ArgumentList)
$destinationMatch = [regex]::Match([string]$ArgumentList, '/destination:"([^"]+)"')
$exportRoot = $destinationMatch.Groups[1].Value
$fixturePath = Join-Path $exportRoot 'storage_pkg'
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $fixturePath 'iaStorAC.inf') -Value "[Version]`r`nClass=System" -Encoding ASCII
return [pscustomobject]@{ ExitCode = 0 }
} -ParameterFilter { $FilePath -eq 'dism.exe' }
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
{ Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' } |
Should -Throw '*DISM mount failed*'
@($script:dismCalls | Where-Object { $_ -match '/Get-MountedImageInfo' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "does not add driver setup artifacts when injection is disabled" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoNoDrivers_$([guid]::NewGuid())"
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InjectCurrentSystemDrivers $false -InstallEditionId 'Core'
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "enables configuration-set fallback when staging OEM setup scripts" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoFallback_$([guid]::NewGuid())"
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InstallEditionId 'Core'
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
$answerFile.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:UseConfigurationSet', $nsMgr).InnerText |
Should -Be 'true'
Test-Path (Join-Path $contentRoot 'sources\$OEM$\$$\Setup\Scripts\FirstLogon.ps1') | Should -BeTrue
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
} }
} }
-1
View File
@@ -435,7 +435,6 @@ Describe "XAML and sync wiring" {
"appPopup", "appPopup",
"appPopupSelectedApp", "appPopupSelectedApp",
"ItemsControl", "ItemsControl",
"InstalledPrograms",
"ImportInProgress", "ImportInProgress",
"ScriptsInstallPrograms", "ScriptsInstallPrograms",
"keys", "keys",
+20
View File
@@ -21,6 +21,14 @@
<Order>3</Order> <Order>3</Order>
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path> <Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand> </RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>4</Order>
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassCPUCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>5</Order>
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassStorageCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
</RunSynchronous> </RunSynchronous>
</component> </component>
</settings> </settings>
@@ -48,6 +56,18 @@
<Order>5</Order> <Order>5</Order>
<Path>reg.exe unload "HKU\DefaultUser"</Path> <Path>reg.exe unload "HKU\DefaultUser"</Path>
</RunSynchronousCommand> </RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>6</Order>
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>7</Order>
<Path>reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\BitLocker" /v PreventDeviceEncryption /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>8</Order>
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v ShippedWithReserves /t REG_DWORD /d 0 /f</Path>
</RunSynchronousCommand>
</RunSynchronous> </RunSynchronous>
</component> </component>
</settings> </settings>
+54 -14
View File
@@ -6,7 +6,7 @@
xmlns:local="clr-namespace:WinUtility" xmlns:local="clr-namespace:WinUtility"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
UseLayoutRounding="True" UseLayoutRounding="True"
WindowStyle="None" WindowStyle="SingleBorderWindow"
Width="Auto" Width="Auto"
Height="Auto" Height="Auto"
MinWidth="800" MinWidth="800"
@@ -964,6 +964,39 @@
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </Style>
<!-- Filter Chip Style — used by the Install tab category filter buttons -->
<Style x:Key="FilterChipStyle" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Margin" Value="2"/>
<Setter Property="Padding" Value="12,0,12,0"/>
<Setter Property="Width" Value="Auto"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Name="ChipBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{DynamicResource ButtonBorderThickness}"
CornerRadius="{DynamicResource ButtonCornerRadius}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="ChipBorder" Property="Background" Value="{DynamicResource ButtonBackgroundPressedColor}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ChipBorder" Property="Background" Value="{DynamicResource ButtonBackgroundMouseoverColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ChipBorder" Property="Background" Value="{DynamicResource ButtonBackgroundSelectedColor}"/>
<Setter Property="Foreground" Value="DimGray"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources> </Window.Resources>
<Grid Background="{DynamicResource MainBackgroundColor}" ShowGridLines="False" Name="WPFMainGrid" Width="Auto" Height="Auto" HorizontalAlignment="Stretch"> <Grid Background="{DynamicResource MainBackgroundColor}" ShowGridLines="False" Name="WPFMainGrid" Width="Auto" Height="Auto" HorizontalAlignment="Stretch">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -1282,16 +1315,23 @@
<!-- Quick Category Search Chips --> <!-- Quick Category Search Chips -->
<WrapPanel Grid.Row="0" Orientation="Horizontal" Margin="5,5,5,5" Name="WPFSearchChips"> <WrapPanel Grid.Row="0" Orientation="Horizontal" Margin="5,5,5,5" Name="WPFSearchChips">
<Button Name="WPFSearchChipAll" Content="All" Width="Auto" Height="Auto" Margin="2"/> <TextBlock Text="Filters"
<Button Name="WPFSearchChipBrowsers" Content="Browsers" Width="Auto" Height="Auto" Margin="2"/> FontSize="{DynamicResource HeaderFontSize}"
<Button Name="WPFSearchChipCommunications" Content="Communications" Width="Auto" Height="Auto" Margin="2"/> FontFamily="{DynamicResource HeaderFontFamily}"
<Button Name="WPFSearchChipDevelopment" Content="Development" Width="Auto" Height="Auto" Margin="2"/> Foreground="{DynamicResource LabelboxForegroundColor}"
<Button Name="WPFSearchChipGames" Content="Games" Width="Auto" Height="Auto" Margin="2"/> Background="Transparent"
<Button Name="WPFSearchChipMicrosoftTools" Content="Microsoft Tools" Width="Auto" Height="Auto" Margin="2"/> VerticalAlignment="Center"
<Button Name="WPFSearchChipMultimediaTools" Content="Multimedia Tools" Width="Auto" Height="Auto" Margin="2"/> Margin="15,0,8,0"/>
<Button Name="WPFSearchChipProTools" Content="Pro Tools" Width="Auto" Height="Auto" Margin="2"/> <Button Name="WPFSearchChipAll" Content="All" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipSelfhostedTools" Content="Selfhosted Tools" Width="Auto" Height="Auto" Margin="2"/> <Button Name="WPFSearchChipBrowsers" Content="Browsers" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipUtilities" Content="Utilities" Width="Auto" Height="Auto" Margin="2"/> <Button Name="WPFSearchChipCommunications" Content="Communications" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipDevelopment" Content="Development" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipGames" Content="Games" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipMicrosoftTools" Content="Microsoft Tools" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipMultimediaTools" Content="Multimedia Tools" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipProTools" Content="Pro Tools" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipSelfhostedTools" Content="Selfhosted Tools" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipUtilities" Content="Utilities" Style="{StaticResource FilterChipStyle}"/>
</WrapPanel> </WrapPanel>
<Grid Grid.Row="1" Margin="{DynamicResource TabContentMargin}"> <Grid Grid.Row="1" Margin="{DynamicResource TabContentMargin}">
@@ -1326,14 +1366,14 @@
<StackPanel Background="{DynamicResource MainBackgroundColor}" Orientation="Vertical" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="5"> <StackPanel Background="{DynamicResource MainBackgroundColor}" Orientation="Vertical" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="5">
<Label Content="Recommended Selections:" FontSize="{DynamicResource FontSize}" VerticalAlignment="Center" Margin="2"/> <Label Content="Recommended Selections:" FontSize="{DynamicResource FontSize}" VerticalAlignment="Center" Margin="2"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,2,0,0"> <WrapPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,2,0,0">
<Button Name="WPFstandard" Content=" Standard " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/> <Button Name="WPFstandard" Content=" Standard " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFminimal" Content=" Minimal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/> <Button Name="WPFminimal" Content=" Minimal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFAdvanced" Content=" Advanced " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/> <Button Name="WPFAdvanced" Content=" Advanced " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFClearTweaksSelection" Content=" Clear " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/> <Button Name="WPFClearTweaksSelection" Content=" Clear " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFGetInstalledTweaks" Content=" Get Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/> <Button Name="WPFGetInstalledTweaks" Content=" Get Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFAppxRemoval" Content=" AppX Removal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/> <Button Name="WPFAppxRemoval" Content=" AppX Removal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
</StackPanel> </WrapPanel>
</StackPanel> </StackPanel>
<Grid Name="tweakspanel" Grid.Row="1"> <Grid Name="tweakspanel" Grid.Row="1">
@@ -1642,7 +1682,7 @@
Foreground="{DynamicResource MainForegroundColor}" Foreground="{DynamicResource MainForegroundColor}"
IsChecked="False" IsChecked="False"
Margin="0,8,0,0" Margin="0,8,0,0"
ToolTip="Exports all drivers from this machine and injects them into install.wim and boot.wim. Recommended for systems with unsupported NVMe or network controllers."/> ToolTip="Stages boot-storage drivers for Setup and adds all exported drivers to the selected install.wim edition in one DISM pass."/>
</StackPanel> </StackPanel>
<!-- Verification results panel --> <!-- Verification results panel -->