From da7dd544b39294ef963ec56c3e41b471c6f20902 Mon Sep 17 00:00:00 2001 From: Kroese Date: Sat, 25 Jul 2026 20:00:11 +0200 Subject: [PATCH] feat: Enable optional logging and syntax checking for install.bat (#1964) --- docs/environment.md | 1 + src/answer.sh | 367 ++++++++++++++++++++++++-------------------- src/image.sh | 161 +++++++++++++++++++ src/install.sh | 5 +- src/mido.sh | 15 +- 5 files changed, 382 insertions(+), 167 deletions(-) diff --git a/docs/environment.md b/docs/environment.md index 35b91a8d..22e279ff 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -176,6 +176,7 @@ Also see [Dynamic memory allocation](https://github.com/qemus/qemu/blob/master/d |---|---|---| | `DEBUG` | `N` | Enables verbose debug output. | | `TRACE` | `N` | Enables shell command tracing. | +| `LOG` | `N` | Saves all output from `install.bat` to `C:\OEM\install.log` for troubleshooting. | | `DETECTED` | | Overrides the automatically detected Windows image identifier. | | `SERIAL` | `mon:stdio` | QEMU serial device configuration. | | `MONITOR` | `unix:$QEMU_DIR/monitor.sock,server,wait=off,nodelay` | QEMU monitor configuration. | diff --git a/src/answer.sh b/src/answer.sh index 5ed5cc04..49d1e317 100644 --- a/src/answer.sh +++ b/src/answer.sh @@ -234,6 +234,195 @@ validateDomainName() { return 0 } +updateWorkgroup() { + + local asset="$1" + local workgroup arch tmp + + workgroup=$(escapeXML "$2") || return 1 + arch=$(sed -n -E '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' "$asset") || return 1 + [ -z "$arch" ] && return 1 + + grep -q 'Microsoft-Windows-UnattendedJoin' "$asset" && return 1 + + tmp=$(mktemp -d) || return 1 + local result="$tmp/answer.xml" + + if ! WORKGROUP_XML="$workgroup" ARCH_XML="$arch" awk ' + /]*pass="specialize"[^>]*>/ { section = "specialize" } + + section == "specialize" && !workgroup_added && + /^[[:space:]]*<\/settings>[[:space:]]*$/ { + print " \n" \ + " \n" \ + " " ENVIRON["WORKGROUP_XML"] "\n" \ + " \n" \ + " " + workgroup_added = 1 + } + + { print } + + /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } + END { exit !workgroup_added } + ' "$asset" > "$result" || + ! mv -f "$result" "$asset"; then + + rm -rf "$tmp" || true + return 1 + fi + + rm -rf "$tmp" || return 1 + return 0 +} + +updateDomain() { + + local asset="$1" + local domain account auth pass + local ou arch tmp + + domain=$(escapeXML "$2") || return 1 + account=$(escapeXML "$3") || return 1 + auth=$(escapeXML "$4") || return 1 + pass=$(escapeXML "$5") || return 1 + ou=$(escapeXML "$6") || return 1 + + arch=$(sed -n -E \ + '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \ + "$asset") || return 1 + + [ -z "$arch" ] && return 1 + + local cred_domain="$domain" + + case "$4" in + *@* ) cred_domain="" ;; + esac + + grep -Eq 'Microsoft-Windows-UnattendedJoin|])' "$asset" && return 1 + + tmp=$(mktemp -d) || return 1 + local result="$tmp/answer.xml" + + if ! DOMAIN_XML="$domain" ACCOUNT_XML="$account" \ + AUTH_XML="$auth" PASS_XML="$pass" \ + CRED_DOMAIN="$cred_domain" OU_XML="$ou" \ + ARCH_XML="$arch" \ + awk ' + /]*pass="specialize"[^>]*>/ { section = "specialize" } + /]*pass="oobeSystem"[^>]*>/ { section = "oobeSystem" } + section == "oobeSystem" && /])/ { in_accounts = 1 } + section == "oobeSystem" && /])/ { in_autologon = 1 } + + section == "oobeSystem" && in_accounts && !accounts_added && + /])/ { + print " \n" \ + " \n" \ + " \n" \ + " " ENVIRON["ACCOUNT_XML"] "\n" \ + " Administrators\n" \ + " \n" \ + " " ENVIRON["DOMAIN_XML"] "\n" \ + " \n" \ + " " + accounts_added = 1 + } + + section == "oobeSystem" && in_autologon && + /^[[:space:]]*.*<\/Username>[[:space:]]*$/ { + print " " ENVIRON["ACCOUNT_XML"] "\n" \ + " " ENVIRON["DOMAIN_XML"] "" + autologon_added = 1 + next + } + + section == "oobeSystem" && in_autologon && + /^[[:space:]]*])/ { next } + + section == "oobeSystem" && in_autologon && + /^[[:space:]]*.*<\/Value>[[:space:]]*$/ { + print " " ENVIRON["PASS_XML"] "" + password_added = 1 + next + } + + section == "oobeSystem" && in_autologon && + /^[[:space:]]*])/ { + print " true</PlainText>" + plaintext_added = 1 + next + } + + section == "specialize" && !join_added && + /^[[:space:]]*<\/settings>[[:space:]]*$/ { + print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \ + " <Identification>\n" \ + " <Credentials>" + + if (ENVIRON["CRED_DOMAIN"] != "") { + print " <Domain>" ENVIRON["CRED_DOMAIN"] "</Domain>" + } + + print " <Username>" ENVIRON["AUTH_XML"] "</Username>\n" \ + " <Password>" ENVIRON["PASS_XML"] "</Password>\n" \ + " </Credentials>\n" \ + " <JoinDomain>" ENVIRON["DOMAIN_XML"] "</JoinDomain>" + + if (ENVIRON["OU_XML"] != "") { + print " <MachineObjectOU>" ENVIRON["OU_XML"] "</MachineObjectOU>" + } + + print " </Identification>\n" \ + " </component>" + + join_added = 1 + } + + { print } + + section == "oobeSystem" && /<\/AutoLogon>/ { in_autologon = 0 } + section == "oobeSystem" && /<\/UserAccounts>/ { in_accounts = 0 } + /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } + + END { exit !(join_added && accounts_added && autologon_added && password_added && plaintext_added) } + ' "$asset" > "$result" || + ! mv -f "$result" "$asset"; then + + rm -rf "$tmp" || true + return 1 + fi + + rm -rf "$tmp" || return 1 + return 0 +} + +enableLog() { + + local file="$1" + + enabled "$LOG" || return 0 + [ -f "$file" ] || return 1 + + local old='C:\OEM\install.bat"</CommandLine>' + local new='C:\OEM\install.bat &gt; C:\OEM\install.log 2&gt;&amp;1"</CommandLine>' + local msg="failed to enable install logging in the answer file!" + + if ! grep -Fq "$old" "$file"; then + enabled "$DEBUG" && warn "$msg" + return 0 + fi + + if ! sed -i \ + 's|C:\\OEM\\install\.bat"</CommandLine>|C:\\OEM\\install.bat \&gt; C:\\OEM\\install.log 2\&gt;\&amp;1"</CommandLine>|' \ + "$file"; then + + warn "$msg" + fi + + return 0 +} + markGeneratedXML() { local file="$1" @@ -464,169 +653,6 @@ setXML() { return 0 } -updateDomain() { - - local asset="$1" - local domain account auth pass - local ou arch tmp - - domain=$(escapeXML "$2") || return 1 - account=$(escapeXML "$3") || return 1 - auth=$(escapeXML "$4") || return 1 - pass=$(escapeXML "$5") || return 1 - ou=$(escapeXML "$6") || return 1 - - arch=$(sed -n -E \ - '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \ - "$asset") || return 1 - - [ -z "$arch" ] && return 1 - - local cred_domain="$domain" - - case "$4" in - *@* ) cred_domain="" ;; - esac - - grep -Eq 'Microsoft-Windows-UnattendedJoin|<DomainAccounts([[:space:]/>])' "$asset" && return 1 - - tmp=$(mktemp -d) || return 1 - local result="$tmp/answer.xml" - - if ! DOMAIN_XML="$domain" ACCOUNT_XML="$account" \ - AUTH_XML="$auth" PASS_XML="$pass" \ - CRED_DOMAIN="$cred_domain" OU_XML="$ou" \ - ARCH_XML="$arch" \ - awk ' - /<settings[^>]*pass="specialize"[^>]*>/ { section = "specialize" } - /<settings[^>]*pass="oobeSystem"[^>]*>/ { section = "oobeSystem" } - section == "oobeSystem" && /<UserAccounts([[:space:]>])/ { in_accounts = 1 } - section == "oobeSystem" && /<AutoLogon([[:space:]>])/ { in_autologon = 1 } - - section == "oobeSystem" && in_accounts && !accounts_added && - /<AdministratorPassword([[:space:]>])/ { - print " <DomainAccounts>\n" \ - " <DomainAccountList wcm:action=\"add\">\n" \ - " <DomainAccount wcm:action=\"add\">\n" \ - " <Name>" ENVIRON["ACCOUNT_XML"] "</Name>\n" \ - " <Group>Administrators</Group>\n" \ - " </DomainAccount>\n" \ - " <Domain>" ENVIRON["DOMAIN_XML"] "</Domain>\n" \ - " </DomainAccountList>\n" \ - " </DomainAccounts>" - accounts_added = 1 - } - - section == "oobeSystem" && in_autologon && - /^[[:space:]]*<Username>.*<\/Username>[[:space:]]*$/ { - print " <Username>" ENVIRON["ACCOUNT_XML"] "</Username>\n" \ - " <Domain>" ENVIRON["DOMAIN_XML"] "</Domain>" - autologon_added = 1 - next - } - - section == "oobeSystem" && in_autologon && - /^[[:space:]]*<Domain([[:space:]/>])/ { next } - - section == "oobeSystem" && in_autologon && - /^[[:space:]]*<Value>.*<\/Value>[[:space:]]*$/ { - print " <Value>" ENVIRON["PASS_XML"] "</Value>" - password_added = 1 - next - } - - section == "oobeSystem" && in_autologon && - /^[[:space:]]*<PlainText([[:space:]/>])/ { - print " <PlainText>true</PlainText>" - plaintext_added = 1 - next - } - - section == "specialize" && !join_added && - /^[[:space:]]*<\/settings>[[:space:]]*$/ { - print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \ - " <Identification>\n" \ - " <Credentials>" - - if (ENVIRON["CRED_DOMAIN"] != "") { - print " <Domain>" ENVIRON["CRED_DOMAIN"] "</Domain>" - } - - print " <Username>" ENVIRON["AUTH_XML"] "</Username>\n" \ - " <Password>" ENVIRON["PASS_XML"] "</Password>\n" \ - " </Credentials>\n" \ - " <JoinDomain>" ENVIRON["DOMAIN_XML"] "</JoinDomain>" - - if (ENVIRON["OU_XML"] != "") { - print " <MachineObjectOU>" ENVIRON["OU_XML"] "</MachineObjectOU>" - } - - print " </Identification>\n" \ - " </component>" - - join_added = 1 - } - - { print } - - section == "oobeSystem" && /<\/AutoLogon>/ { in_autologon = 0 } - section == "oobeSystem" && /<\/UserAccounts>/ { in_accounts = 0 } - /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } - - END { exit !(join_added && accounts_added && autologon_added && password_added && plaintext_added) } - ' "$asset" > "$result" || - ! mv -f "$result" "$asset"; then - - rm -rf "$tmp" || true - return 1 - fi - - rm -rf "$tmp" || return 1 - return 0 -} - -updateWorkgroup() { - - local asset="$1" - local workgroup arch tmp - - workgroup=$(escapeXML "$2") || return 1 - arch=$(sed -n -E '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' "$asset") || return 1 - [ -z "$arch" ] && return 1 - - grep -q 'Microsoft-Windows-UnattendedJoin' "$asset" && return 1 - - tmp=$(mktemp -d) || return 1 - local result="$tmp/answer.xml" - - if ! WORKGROUP_XML="$workgroup" ARCH_XML="$arch" awk ' - /<settings[^>]*pass="specialize"[^>]*>/ { section = "specialize" } - - section == "specialize" && !workgroup_added && - /^[[:space:]]*<\/settings>[[:space:]]*$/ { - print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \ - " <Identification>\n" \ - " <JoinWorkgroup>" ENVIRON["WORKGROUP_XML"] "</JoinWorkgroup>\n" \ - " </Identification>\n" \ - " </component>" - workgroup_added = 1 - } - - { print } - - /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } - END { exit !workgroup_added } - ' "$asset" > "$result" || - ! mv -f "$result" "$asset"; then - - rm -rf "$tmp" || true - return 1 - fi - - rm -rf "$tmp" || return 1 - return 0 -} - updateXML() { local asset="$1" @@ -811,6 +837,10 @@ updateXML() { sed -i -E '/^[[:space:]]*<AutoLogon([[:space:]>])/,/^[[:space:]]*<\/AutoLogon>[[:space:]]*$/d' "$asset" || return 1 fi + if enabled "${LOG:-}"; then + enableLog "$asset" || return 1 + fi + if [ -n "${EDITION:-}" ]; then local edition @@ -1140,7 +1170,16 @@ legacyInstall() { local oem="" local install="$dir/\$OEM\$/\$1/OEM/install.bat" - [ -f "$install" ] && oem="\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat\\\"\"" + + if [ -f "$install" ]; then + + if ! enabled "${LOG:-}"; then + oem="\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat\\\"\"" + else + oem="\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat > C:\\\\OEM\\\\install.log 2>&1\\\"\"" + fi + + fi [ -z "$WIDTH" ] && WIDTH="1280" [ -z "$HEIGHT" ] && HEIGHT="720" diff --git a/src/image.sh b/src/image.sh index aa7f6a46..e7f1b495 100644 --- a/src/image.sh +++ b/src/image.sh @@ -790,3 +790,164 @@ detectImage() { return 0 } + +checkBatch() { + + local file="$1" + local report="N" + local tmp output + local matches line + + [ ! -f "$file" ] && return 0 + + if ! tmp=$(mktemp -d /tmp/blinter.XXXXXX); then + warn "failed to create temporary Blinter directory." + return 0 + fi + + local source="your install.bat file" + [ -n "${COMMAND:-}" ] && source="your COMMAND variable" + + if enabled "$DEBUG"; then + report="Y" + else + + # First pass: silently check only for Error-level findings. + cat > "$tmp/blinter.ini" <<'EOF' +[general] +min_severity = error +EOF + + if ! ( + cd "$tmp" + python3 -m blinter "$file" >/dev/null 2>&1 + ); then + report="Y" + fi + + fi + + if enabled "$report"; then + + # Show useful diagnostic context, while excluding findings that are + # irrelevant to unattended OEM scripts. + cat > "$tmp/blinter.ini" <<'EOF' +[general] +min_severity = warning +show_summary = false + +[rules] +disabled_rules = W001,W028,W041,SEC002,SEC005 +EOF + + output=$( + cd "$tmp" + python3 -m blinter "$file" 2>&1 || true + ) + + # Remove header + output=$( + awk ' + /^DETAILED ISSUES:/ { + found = 1 + next + } + + found && !started { + if (/^-+$/) { + started = 1 + } + next + } + + started { + print + } + ' <<< "$output" + ) + + output="${output#"${output%%[!$'\r\n ']*}"}" + output="${output%"${output##*[!$'\r\n ']}"}" + + if grep -Eq \ + '^(ERROR|WARNING|SECURITY) LEVEL ISSUES:$' \ + <<< "$output"; then + + warn "possible issues were detected in $source:" + printf '\n%s\n\n' "$output" >&2 + fi + + fi + + rm -rf "$tmp" + + matches=$( + grep -Pin \ + '(?<!\\)\\host[.]lan[\\]' \ + "$file" || true + ) + + if [ -n "$matches" ]; then + warn "invalid single-backslash UNC path detected in $source:" + + while IFS= read -r line; do + printf ' %s\n' "$line" >&2 + done <<< "$matches" + + printf '%s\n\n' \ + ' Use "\\host.lan\Data\..." instead of "\host.lan\Data\...".' >&2 + fi + + matches=$( + grep -Pin \ + '(?<![\\[:alnum:]._-])host[.]lan[\\]' \ + "$file" || true + ) + + if [ -n "$matches" ]; then + warn "UNC path without leading backslashes detected in $source:" + + while IFS= read -r line; do + printf ' %s\n' "$line" >&2 + done <<< "$matches" + + printf '%s\n\n' \ + ' Use "\\host.lan\Data\..." instead of "host.lan\Data\...".' >&2 + fi + + matches=$( + grep -Pin \ + '//host[.]lan/' \ + "$file" || true + ) + + if [ -n "$matches" ]; then + warn "invalid forward-slash UNC path detected in $source:" + + while IFS= read -r line; do + printf ' %s\n' "$line" >&2 + done <<< "$matches" + + printf '%s\n\n' \ + ' Use "\\host.lan\Data\..." instead of "//host.lan/Data/...".' >&2 + fi + + matches=$( + grep -Pin \ + '\\\\host[.]lan\\shared(?:[\\/]|$)' \ + "$file" || true + ) + + if [ -n "$matches" ]; then + warn "invalid Samba share name detected in $source:" + + while IFS= read -r line; do + printf ' %s\n' "$line" >&2 + done <<< "$matches" + + printf '%s\n\n' \ + ' The "/shared" folder is exposed to Windows as "\\host.lan\Data".' >&2 + fi + + return 0 +} diff --git a/src/install.sh b/src/install.sh index 108af80a..11290344 100644 --- a/src/install.sh +++ b/src/install.sh @@ -776,7 +776,7 @@ prepareImage() { [ -f "$dir/$ETFS" ] && [ -s "$dir/$ETFS" ] && [ -f "$dir/$EFISYS" ] && [ -s "$dir/$EFISYS" ] && return 0 - + missing=$(basename "$dir/$EFISYS") if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then missing=$(basename "$dir/$ETFS") @@ -829,10 +829,13 @@ addFolder() { fi if [ -f "$file" ]; then + if ! unix2dos -q "$file"; then error "Failed to convert $file to DOS format!" return 1 fi + + checkBatch "$file" fi return 0 diff --git a/src/mido.sh b/src/mido.sh index 2487f347..5aa2fed7 100644 --- a/src/mido.sh +++ b/src/mido.sh @@ -401,10 +401,21 @@ downloadWindowsEval() { esac if enabled "$DEBUG" && enabled "$VERIFY" && [[ "${lang,,}" == "en"* ]]; then + compare=$(getMido "$id" "$lang" "") - if [ -n "$compare" ] && [[ "${link,,}" != "${compare,,}" ]]; then - echo "Retrieved link does not match the fixed link: $compare" + + if [ -n "$compare" ]; then + link_name="${link%%[?#]*}" + link_name="${link_name##*/}" + + compare_name="${compare%%[?#]*}" + compare_name="${compare_name##*/}" + + if [[ "${link_name,,}" != "${compare_name,,}" ]]; then + echo "Retrieved ISO file $link_name does not match the pre-defined filename: $compare_name" + fi fi + fi MIDO_URL="$link"