Add a windows11 target that downloads the Windows 11 ISO with headless Chromium

Move the Node.js script and its dependencies into nodejs/ and let
download-images.sh call it for the image architecture.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GuofangandClaude Sonnet 5.5 committed 2026-10-21 16:45:36 +08:00
1 parent 5e58722aca
commit 592c7294c3
6 files changed
+228 -3

No files matched your search

+3
View File
@@ -13,3 +13,6 @@
!base-qcow2/.gitkeep !base-qcow2/.gitkeep
!install-iso/.gitkeep !install-iso/.gitkeep
!tools-iso/.gitkeep !tools-iso/.gitkeep
# Node dependencies of nodejs/get-windows-iso-url.js.
node_modules/
+20
View File
@@ -30,6 +30,26 @@ arm64 hosts) and `.iso` in that order (`*Win*11*x64*.iso` or
version-sorted name is used. Check which file was picked with version-sorted name is used. Check which file was picked with
`./download-images.sh --print-path windows11 --iso`. `./download-images.sh --print-path windows11 --iso`.
To get a fresh temporary URL, `nodejs/get-windows-iso-url.js` drives Microsoft's
download page with headless Chromium (needs Node.js and an installed
Chromium, for example from Playwright):
```bash
cd nodejs
npm ci # once, installs playwright-core
node get-windows-iso-url.js # print the URL for the host architecture
node get-windows-iso-url.js --arch arm64 # choose the architecture explicitly
```
The script only prints the URL. `./download-images.sh windows11` downloads that
URL into `install-iso/` like the other images (aria2c, resumable) for the image
architecture (`VM_IMAGE_ARCH=arm64` for arm64), and skips it when a matching ISO
is already there. The `all` target does not include it.
The URL expires after about 24 hours and may be bound to the IP that requested
it, so download from the machine that ran the script. Run it with `--help` for
the language and path options.
## Query image paths ## Query image paths
The path interface prints one absolute path without downloading anything: The path interface prints one absolute path without downloading anything:
+49 -3
View File
@@ -130,7 +130,7 @@ force_download=false
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") [--force] [all|debian13|almalinux10|fedora44|ubuntu2604|virtio-win|openssh-win] ... Usage: $(basename "$0") [--force] [all|debian13|almalinux10|fedora44|ubuntu2604|virtio-win|openssh-win|windows11] ...
$(basename "$0") --print-path TARGET [--iso] $(basename "$0") --print-path TARGET [--iso]
Download one or more shared VM images. With no arguments, all targets are Download one or more shared VM images. With no arguments, all targets are
@@ -141,6 +141,10 @@ When aria2c is installed and an image has several mirrors, the image is
downloaded from all of them at once, with one connection per mirror. downloaded from all of them at once, with one connection per mirror.
Otherwise curl or wget downloads from the mirrors in order. Otherwise curl or wget downloads from the mirrors in order.
The windows11 target is not part of "all". It downloads the Windows 11 ISO for
the image architecture with nodejs/get-windows-iso-url.js, which needs Node.js,
\`npm ci\` in nodejs/ and an installed Chromium (see README.md).
--force removes existing files and partial downloads for the selected targets, --force removes existing files and partial downloads for the selected targets,
then downloads them again from the beginning. then downloads them again from the beginning.
--print-path prints only the absolute image path without downloading it. --print-path prints only the absolute image path without downloading it.
@@ -206,7 +210,7 @@ fi
for target in "${targets[@]}"; do for target in "${targets[@]}"; do
case "$target" in case "$target" in
all|debian13|almalinux10|fedora44|ubuntu2604|virtio-win|openssh-win) ;; all|debian13|almalinux10|fedora44|ubuntu2604|virtio-win|openssh-win|windows11) ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) printf 'Error: unknown download target: %s\n' "$target" >&2; usage >&2; exit 2 ;; *) printf 'Error: unknown download target: %s\n' "$target" >&2; usage >&2; exit 2 ;;
esac esac
@@ -297,6 +301,9 @@ download_file() {
local destination="$3" local destination="$3"
local checksum_algorithm="${4:-}" local checksum_algorithm="${4:-}"
local expected_checksum="${5:-}" local expected_checksum="${5:-}"
# Signed URLs change on every request; this key identifies the source for
# resuming a partial download instead of the URLs when it is given.
local source_key="${6:-}"
local partial="${destination}.part" local partial="${destination}.part"
# Records how the partial file was started, so that a partial download is # Records how the partial file was started, so that a partial download is
# only resumed with the same downloader and URLs. # only resumed with the same downloader and URLs.
@@ -340,7 +347,7 @@ download_file() {
# aria2c fills a partial file out of order, so it must not be resumed by # aria2c fills a partial file out of order, so it must not be resumed by
# curl or wget, and vice versa. # curl or wget, and vice versa.
download_state="$(printf '%s\n' "$downloader" "${urls[@]}")" download_state="$(printf '%s\n' "$downloader" "${source_key:-${urls[@]}}")"
if [[ -e "$partial" ]]; then if [[ -e "$partial" ]]; then
if [[ ! -f "$partial_url" ]] || [[ "$(<"$partial_url")" != "$download_state" ]]; then if [[ ! -f "$partial_url" ]] || [[ "$(<"$partial_url")" != "$download_state" ]]; then
printf 'Download source changed; restarting partial download.\n' printf 'Download source changed; restarting partial download.\n'
@@ -386,6 +393,41 @@ download_file() {
printf 'Downloaded %s:\n%s\n' "$label" "$destination" printf 'Downloaded %s:\n%s\n' "$label" "$destination"
} }
# The Microsoft download link is temporary, so it is fetched with headless
# Chromium only when no matching ISO exists yet. "all" does not include it.
download_windows_iso() {
local iso
local url
local name
if [[ "$force_download" == true ]]; then
printf 'Forcing a fresh download of the Windows 11 %s ISO.\n' "$windows_arch"
while iso="$(find_windows_image "$install_iso_dir" iso)"; do
rm -f -- "$iso"
done
fi
if iso="$(find_windows_image "$install_iso_dir" iso)"; then
printf 'Windows 11 %s ISO already exists; skipping:\n%s\n' "$windows_arch" "$iso"
return
fi
if ! command -v node >/dev/null 2>&1; then
printf 'Error: node is required to get the Windows 11 ISO URL.\n' >&2
exit 1
fi
url="$(node "${images_dir}/nodejs/get-windows-iso-url.js" --arch "$windows_arch")"
[[ -n "$url" ]] || { printf 'Error: no Windows 11 ISO URL.\n' >&2; exit 1; }
name="${url%%\?*}"
name="${name##*/}"
name="$(printf '%b' "${name//%/\\x}")"
download_file \
"Windows 11 ${windows_arch} ISO" \
"$url" \
"${install_iso_dir}/${name}" \
'' \
'' \
"$name"
}
if wants_target debian13; then if wants_target debian13; then
download_file \ download_file \
'Debian 13 cloud image' \ 'Debian 13 cloud image' \
@@ -435,3 +477,7 @@ if wants_target openssh-win; then
"$openssh_url" \ "$openssh_url" \
"$openssh_image" "$openssh_image"
fi fi
if [[ " ${targets[*]} " == *" windows11 "* ]]; then
download_windows_iso
fi
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env node
// Print a signed, temporary download URL for the Windows 11 ISO by driving
// Microsoft's download page with headless Chromium.
//
// Usage: node get-windows-iso-url.js [--arch x64|arm64] [--lang NAME]
// Environment:
// PLAYWRIGHT_CORE playwright-core directory (default: node_modules here)
// CHROME_PATH Chromium executable (default: newest one in ~/.cache/ms-playwright)
// The URL expires after about 24 hours and may be bound to the IP that
// requested it. Only the URL goes to stdout; logs go to stderr.
const fs = require('fs');
const os = require('os');
const path = require('path');
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node get-windows-iso-url.js [options]
Print a signed, temporary download URL for the Windows 11 ISO (only the URL
goes to stdout; logs go to stderr).
Options:
--arch <x64|arm64> Architecture, default: the host's (x64 on amd64 hosts)
--lang <name> Language, default "English (United States)",
for example "Chinese Simplified"
-h, --help Show this help
Environment:
PLAYWRIGHT_CORE playwright-core directory (default: node_modules here)
CHROME_PATH Chromium executable (default: newest one in
~/.cache/ms-playwright)
Examples:
node get-windows-iso-url.js --arch x64 --lang "Chinese Simplified"
wget -c "$(node get-windows-iso-url.js 2>/dev/null)"
The URL expires after about 24 hours and may be bound to the IP that
requested it.`);
process.exit(0);
}
const opt = (name, def) => {
const i = args.indexOf(name);
return i < 0 ? def : (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true);
};
const hostArch = { x64: 'x64', arm64: 'arm64' }[process.arch];
const arch = opt('--arch', hostArch);
const lang = opt('--lang', 'English (United States)');
const pages = { arm64: 'windows11arm64', x64: 'windows11' };
if (!pages[arch]) {
console.error(arch === undefined ? `unsupported host architecture ${process.arch}: pass --arch x64|arm64` : '--arch must be x64 or arm64');
process.exit(2);
}
const log = (...a) => console.error('[iso]', ...a);
function findPlaywright() {
if (process.env.PLAYWRIGHT_CORE) return process.env.PLAYWRIGHT_CORE;
try { return path.dirname(require.resolve('playwright-core/package.json')); } catch {}
console.error('playwright-core not found: run npm ci or set PLAYWRIGHT_CORE');
process.exit(2);
}
function findChrome() {
if (process.env.CHROME_PATH) return process.env.CHROME_PATH;
const root = path.join(os.homedir(), '.cache/ms-playwright');
const dirs = fs.existsSync(root) ? fs.readdirSync(root).filter(d => /^chromium-\d+$/.test(d)).sort() : [];
if (!dirs.length) { console.error('Chromium not found: set CHROME_PATH or run npx playwright-core install chromium'); process.exit(2); }
return path.join(root, dirs.pop(), 'chrome-linux64/chrome');
}
(async () => {
const { chromium } = require(findPlaywright());
const browser = await chromium.launch({ executablePath: findChrome(), headless: true });
try {
const ctx = await browser.newContext({
locale: 'en-US',
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36',
});
const page = await ctx.newPage();
let api = null;
page.on('response', async r => {
if (/GetProductDownloadLinks/i.test(r.url())) api = await r.json().catch(() => null);
});
log('opening', pages[arch]);
await page.goto(`https://www.microsoft.com/en-us/software-download/${pages[arch]}`, { waitUntil: 'networkidle', timeout: 60000 });
await page.click('button:has-text("Accept")', { timeout: 3000 }).catch(() => {});
// Pick the first real edition (currently the only multi-edition ISO).
const edition = await page.$$eval('#product-edition option', o => o.map(x => x.value).find(v => v && v !== 'null'));
if (!edition) throw new Error('no edition option found; the page layout may have changed');
await page.selectOption('#product-edition', edition);
await page.click('#submit-product-edition');
await page.waitForSelector('#product-languages option:not([value="null"])', { state: 'attached', timeout: 60000 });
const langs = await page.$$eval('#product-languages option', o => o.map(x => ({ value: x.value, text: x.text })));
const hit = langs.find(l => l.text === lang);
if (!hit) throw new Error(`no language "${lang}"; available: ${langs.map(l => l.text).filter(t => t !== 'Choose one').join(', ')}`);
await page.selectOption('#product-languages', hit.value);
await page.click('#submit-sku');
await page.waitForSelector('a[href*="prss.microsoft.com"]', { timeout: 60000 });
const url = await page.$eval('a[href*="prss.microsoft.com"]', a => a.href);
const opts = api && api.ProductDownloadOptions;
log(opts ? opts[0].Name : '(API response not captured)', '| expires:', api && api.DownloadExpirationDatetime);
console.log(url);
} finally {
await browser.close();
}
})().catch(e => { console.error('[iso] failed:', e.message); process.exit(1); });
+28
View File
@@ -0,0 +1,28 @@
{
"name": "win-iso",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "win-iso",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"playwright-core": "^1.64.1"
}
},
"node_modules/playwright-core": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "win-iso",
"version": "1.0.0",
"description": "",
"main": "get-windows-iso-url.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"playwright-core": "^1.64.1"
}
}