Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,35 @@ Windows. Test your ISO in a VM before installing it.**

![Build settings with Turbo mode and optional image cleanup](docs/images/ui-build-settings.png)

## Booting from Ventoy

ISOs built here boot from Ventoy in its default (non-wimboot) mode — no Rufus,
no extracted USB stick, no manual patching. If an older build stops at
**"A media driver your computer needs is missing"**, the wording is misleading:
it is not a missing disk driver, it means Setup never saw its installation
media. Rebuild the ISO; nothing about the Ventoy stick needs to change.

Ventoy does not extract a Windows ISO. It boots the ISO's own `boot.wim` and
injects `vtoyjump.exe` so the ISO can be mounted as a virtual drive before
`setup.exe` runs. Two builder behaviours broke that:

| Change | Why it broke Ventoy |
| --- | --- |
| The WinPE launch chain forced to a fixed `X:\sources\setup.exe` (`Setup\CmdLine` in the scripts, `winpeshl.ini` in the app) | Setup takes the boot RAM disk as its media root, finds no `install.wim` beside it and stops. Core only. |
| `oscdimg -udfver102` | UDF 1.02 cannot describe a file larger than 4 GB, so a trimmed `install.wim` is written with metadata that lean readers mis-parse. |

Both are fixed: the stock WinPE launch chain is left alone, so the boot image's
own `setup.exe` bootstrap locates the media before running that media's
`sources\setup.exe`, and ISOs are built with `-udfver150`. A build also refuses
to emit an ISO whose `sources\` has no usable `install.wim`/`install.esd`,
because that failure looks identical to this one.

If a rebuilt ISO still fails, press `Shift+F10` at the error and run `diskpart`,
then `list volume`. No virtual DVD drive means `vtoyjump` never ran;
`X:\Windows\System32\ventoy\vtoype.log` records why a mount failed. `Ctrl+W` at
the Ventoy menu switches to wimboot mode, a different boot path. Also check that
Ventoy is 1.1.07 or newer and that its data partition is NTFS rather than exFAT.

## More information

[Usage, compatibility and build instructions](docs/usage.md) ·
Expand Down
49 changes: 46 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,14 @@ The six-step wizard provides:
acknowledgement before a build.
- Core and Custom WinSxS/component-payload removals always use the legacy
Windows installer, including on 24H2 and newer media. The private Setup
boot image starts `wpeinit.exe`, then its matching `sources\setup.exe`
(`setup_old.exe` when present), instead of the newer SetupHost workflow.
This is automatic, not a hardware-bypass option; no installer from a
boot image starts `wpeinit.exe`, then the boot image's own `setup.exe`
bootstrap, which locates the installation media and runs that media's
matching `sources\setup.exe` (`setup_old.exe` when present) instead of the
newer SetupHost workflow. Starting a fixed `X:\sources\setup.exe` instead
would make Setup treat the boot RAM disk as the media root and stop with
"A media driver your computer needs is missing" — including under Ventoy,
which mounts the ISO as a virtual drive only after its own WinPE hook has
run. This is automatic, not a hardware-bypass option; no installer from a
different Windows release is downloaded. Media without the classic
executable fails explicitly. A resumed Core job also receives this change
even if its final installation image was already compressed.
Expand Down Expand Up @@ -94,6 +99,44 @@ The six-step wizard provides:
state, verified saved progress for **Resume build**, an output SHA-256, a JSON
report, and separate interrupted-workspace discard/recovery.

## Booting from Ventoy

Output ISOs boot from Ventoy in its default (non-wimboot) mode, for both Tiny11
and Tiny11 Core. Ventoy does not extract a Windows ISO: it boots the ISO's own
`boot.wim` and injects `vtoyjump.exe`, which mounts the ISO as a virtual drive
before `setup.exe` runs. Setup only reads `sources\install.wim` after that
mount, so anything that stops the hook from running, or that hides the media
from Setup, surfaces as the same misleading message:

> A media driver your computer needs is missing. This could be a DVD, USB, or Hard disk driver.

That message is not about a driver. It means Setup never saw its installation
media. Three build-time causes are handled:

| Cause | Handling |
| --- | --- |
| The WinPE launch chain forced to a fixed `X:\sources\setup.exe` (`Setup\CmdLine` in the scripts, `winpeshl.ini` in the app) | The stock launch chain is left alone. The boot image's own `setup.exe` bootstrap locates the media and runs that media's `sources\setup.exe`. |
| `oscdimg -udfver102` | ISOs are built with `-udfver150`. UDF 1.02 cannot describe a file larger than 4 GB, which a trimmed `install.wim` exceeds. |
| A failed image export that still deleted the old image | The build refuses to create an ISO whose `sources\` has no usable `install.wim`/`install.esd`, and removes leftover `install2.wim`/`install2.esd`. |

If a current ISO still stops at that message, press `Shift+F10` and run:

```
diskpart
list volume
type X:\Windows\System32\ventoy\vtoype.log
```

- **No virtual DVD drive in `list volume`** — `vtoyjump` never ran, so the
problem is the boot image patch rather than the ISO contents.
- **`vtoype.log`** records what `vtoyjump` did and why a mount failed.
- **`Ctrl+W`** at the Ventoy menu switches to wimboot mode, a different boot
path worth trying as a workaround.

Also check that Ventoy is **1.1.07 or newer**, that its data partition is
**NTFS** rather than exFAT, and that the ISO sits on the first, large Ventoy
partition rather than the 32 MB `VTOYEFI` one.

## Turbo mode and smaller images

The settings page has independent **Turbo mode**, **Remove redundant image
Expand Down
43 changes: 42 additions & 1 deletion src/Tiny11Builder.Core/BuildEngine.Tail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ await s.Tracking.RunAsync("checkpoint:compressed", "Compress", "Save verified co
private async Task CreateIsoAsync(BuildSession s, CancellationToken token)
{
s.Stage("Create ISO", "Creating a UEFI-bootable ISO using Microsoft's oscdimg.", token);
RequireInstallableMedia(s);
if (File.Exists(s.TemporaryIso))
{
PathSafety.RejectReparsePoints(s.TemporaryIso);
Expand All @@ -313,12 +314,52 @@ private async Task CreateIsoAsync(BuildSession s, CancellationToken token)
var efi = Path.Combine(s.Staging, s.EfiRelative);
var boot = s.Image.Architecture == "x64" && File.Exists(bios) ?
$"-bootdata:2#p0,e,b{bios}#pEF,e,b{efi}" : $"-bootdata:1#pEF,e,b{efi}";
// -u2 is the ISO9660 + Joliet + UDF layout Microsoft's own retail media uses; ISO9660 alone cannot hold a
// file larger than 4 GB, so the UDF layer is what carries install.wim. -udfver150 rather than the default
// UDF 1.02: 1.02 cannot describe a single file larger than 4 GB, and a trimmed install.wim routinely exceeds
// that. oscdimg still writes such a file, but the on-disc metadata ends up in a form lenient readers accept
// and lean ones mis-parse. This is a large-file robustness fix; it is not what stopped Ventoy booting.
var isoResult = await runner.RunAsync(s.Recipe.OscdimgPath,
["-m", "-o", "-u2", "-udfver102", boot, s.Staging, s.TemporaryIso], s.Log, token, finishOnCancellation: false).ConfigureAwait(false);
["-m", "-o", "-u2", "-udfver150", boot, s.Staging, s.TemporaryIso], s.Log, token, finishOnCancellation: false).ConfigureAwait(false);
if (isoResult.ExitCode != 0 || !File.Exists(s.TemporaryIso) || new FileInfo(s.TemporaryIso).Length < 34816)
throw new IOException($"oscdimg did not produce an ISO (exit {isoResult.ExitCode}). {isoResult.Output}");
}

/// <summary>
/// Refuses to publish media that Windows Setup cannot install from. Every failure below looks identical to the
/// user - "A media driver your computer needs is missing" - while the real cause is an ISO with no installation
/// image, so the build has to stop here and name it instead of shipping a disc that cannot install.
/// </summary>
private static void RequireInstallableMedia(BuildSession s)
{
var sources = Path.Combine(s.Staging, "sources");
// An export interrupted between writing and renaming can leave a second image behind. Setup picks the first
// image it finds in sources\, so a stale partial one would silently win over the finished one.
foreach (var stray in new[] { "install2.wim", "install2.esd" })
{
var file = Path.Combine(sources, stray);
if (!File.Exists(file)) continue;
PathSafety.RejectReparsePoints(file);
File.Delete(file);
s.Warn($"Removed a leftover {stray} from the sources folder; Setup would have detected it as the installation image.");
}
var images = new[] { "install.wim", "install.esd" }.Select(name => Path.Combine(sources, name))
.Where(File.Exists).ToArray();
if (images.Length == 0)
throw new IOException("The media has no sources\\install.wim or install.esd, so Windows Setup would stop " +
"with \"A media driver your computer needs is missing\" even though no driver is involved. The image " +
"export step failed; no ISO was created.");
// The export stage already proved each image is complete (DISM lists its single edition and its WIM header and
// footer parse), so only the size is worth reporting here.
foreach (var image in images)
s.Log($"Install image {Path.GetFileName(image)}: {new FileInfo(image).Length / (1024d * 1024d):0} MB");
// Ventoy only applies its Windows boot patch to an ISO it recognises as standard installation media; without
// these it is treated as a generic image and never mounts itself as the drive Setup reads from.
foreach (var required in new[] { @"setup.exe", @"sources\boot.wim", @"boot\bcd", @"boot\boot.sdi" })
if (!File.Exists(Path.Combine(s.Staging, required)))
s.Log($"Ventoy compatibility: the staged media has no {required}, so Ventoy will not recognise this ISO as Windows media.");
}

private static async Task<BuildResult> VerifyAndPublishAsync(BuildSession s, CancellationToken token)
{
s.Stage("Verify output", "Hashing the completed ISO and publishing it without overwriting existing files.", token);
Expand Down
25 changes: 23 additions & 2 deletions src/Tiny11Builder.Core/SetupInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,37 @@ internal static void ApplyLegacy(string mount, Action<string> log)
ImageFileAccess.ValidateFile(program);
if (new FileInfo(program).Length == 0)
throw new InvalidDataException("The legacy installer in the Windows Setup image is empty.");

// Start the boot image's own setup bootstrap, and let it find the media, instead of starting a fixed path
// inside the boot RAM disk.
//
// boot.wim is loaded to X:, but the installation media is not there. From a USB stick it is another drive;
// under Ventoy it is not a drive at all until vtoyjump.exe has mounted the ISO as a virtual one, and Ventoy
// runs that hook from the same WinPE launch chain this file configures. Setup started as
// X:\sources\setup.exe takes the directory it was loaded from as the media root, finds no install.wim beside
// it, and stops with "A media driver your computer needs is missing" - which reads like a missing storage
// driver but only means Setup never saw its media. setup.exe at the root of the boot image is the stock
// bootstrap: it locates the media and then runs that media's matching sources\setup.exe, which is the classic
// installer this method exists to select. Only when the image ships no bootstrap does the fixed path remain.
var launch = "%SYSTEMDRIVE%\\" + executable;
var bootstrap = Path.Combine(mount, "setup.exe");
if (File.Exists(bootstrap))
{
ImageFileAccess.ValidateFile(bootstrap);
if (new FileInfo(bootstrap).Length == 0)
throw new InvalidDataException("The Setup bootstrap in the Windows Setup image is empty.");
launch = @"%SYSTEMDRIVE%\setup.exe";
}
var path = Path.Combine(mount, @"Windows\System32\winpeshl.ini");
PathSafety.RejectReparsePoints(Path.GetDirectoryName(path)!);
if (File.Exists(path))
{
using var privileges = new ImageOperations.PrivilegeScope();
ImageFileAccess.DeleteEntry(path);
}
var content = "[LaunchApps]\r\n%SYSTEMROOT%\\System32\\wpeinit.exe\r\n%SYSTEMDRIVE%\\" + executable + "\r\n";
var content = "[LaunchApps]\r\n%SYSTEMROOT%\\System32\\wpeinit.exe\r\n" + launch + "\r\n";
File.WriteAllText(path, content, Encoding.ASCII);
if (File.ReadAllText(path) != content) throw new IOException("The legacy Windows Setup startup configuration could not be verified.");
log($"Legacy installer enabled: %SYSTEMDRIVE%\\{executable}. Required by Core/component-store removals.");
log($"Legacy installer enabled: {launch}. Required by Core/component-store removals.");
}
}
17 changes: 17 additions & 0 deletions tests/Tiny11Builder.Core.Tests/SetupInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ public void Legacy_startup_initializes_PE_then_runs_the_matching_classic_install
Assert.Throws<InvalidDataException>(() => SetupInstaller.ApplyLegacy(fixture.Root, messages.Add));
}

[Fact]
public void Legacy_startup_uses_the_media_bootstrap_so_Setup_finds_its_media_outside_the_ram_disk()
{
using var fixture = new BuildFixture();
var mount = NativeTests.CreateOwnedMount(fixture.Root);
Directory.CreateDirectory(Path.Combine(mount, "sources"));
File.WriteAllText(Path.Combine(mount, @"sources\setup.exe"), "same-build classic binary");
File.WriteAllText(Path.Combine(mount, "setup.exe"), "stock media bootstrap");
var messages = new List<string>();
SetupInstaller.ApplyLegacy(mount, messages.Add);
var ini = File.ReadAllText(Path.Combine(mount, @"Windows\System32\winpeshl.ini"));
// A fixed X:\sources\setup.exe makes Setup treat the RAM disk as the media root, which is empty under
// Ventoy until vtoyjump mounts the ISO. The bootstrap locates the real media first.
Assert.Equal("[LaunchApps]\r\n%SYSTEMROOT%\\System32\\wpeinit.exe\r\n%SYSTEMDRIVE%\\setup.exe\r\n", ini);
Assert.Single(messages);
}

[Fact]
public void Missing_classic_installer_is_a_loud_failure_not_a_new_installer_fallback()
{
Expand Down
Loading