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:
1 parent
5e58722aca
commit
592c7294c3
6 files changed
+228
-3
No files matched your search
@@ -13,3 +13,6 @@
|
||||
!base-qcow2/.gitkeep
|
||||
!install-iso/.gitkeep
|
||||
!tools-iso/.gitkeep
|
||||
|
||||
# Node dependencies of nodejs/get-windows-iso-url.js.
|
||||
node_modules/
|
||||
@@ -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
|
||||
`./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
|
||||
|
||||
The path interface prints one absolute path without downloading anything:
|
||||
|
||||
+49
-3
@@ -130,7 +130,7 @@ force_download=false
|
||||
|
||||
usage() {
|
||||
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]
|
||||
|
||||
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.
|
||||
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,
|
||||
then downloads them again from the beginning.
|
||||
--print-path prints only the absolute image path without downloading it.
|
||||
@@ -206,7 +210,7 @@ fi
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
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 ;;
|
||||
*) printf 'Error: unknown download target: %s\n' "$target" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -297,6 +301,9 @@ download_file() {
|
||||
local destination="$3"
|
||||
local checksum_algorithm="${4:-}"
|
||||
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"
|
||||
# Records how the partial file was started, so that a partial download is
|
||||
# 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
|
||||
# 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 [[ ! -f "$partial_url" ]] || [[ "$(<"$partial_url")" != "$download_state" ]]; then
|
||||
printf 'Download source changed; restarting partial download.\n'
|
||||
@@ -386,6 +393,41 @@ download_file() {
|
||||
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
|
||||
download_file \
|
||||
'Debian 13 cloud image' \
|
||||
@@ -435,3 +477,7 @@ if wants_target openssh-win; then
|
||||
"$openssh_url" \
|
||||
"$openssh_image"
|
||||
fi
|
||||
|
||||
if [[ " ${targets[*]} " == *" windows11 "* ]]; then
|
||||
download_windows_iso
|
||||
fi
|
||||
Executable
+112
@@ -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); });
|
||||
Generated
+28
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user