feat: Refactor Windows answer file handling (#1983)

This commit is contained in:
Kroese
2026-07-26 05:34:24 +02:00
committed by GitHub
parent aa6240ff99
commit 8929f7cdcd
3 changed files with 723 additions and 460 deletions
+9
View File
@@ -487,6 +487,15 @@ jobs:
exit 1
fi
if grep -Fqi \
'CDBOOT: Cannot boot from CD - Code: 5' \
<<< "$container_log"; then
echo
echo "------------------------------------------------------------"
echo "Detected an unbootable installation medium."
exit 1
fi
bios_starts="$(
grep -Fci 'SeaBIOS (version ' <<< "$container_log" || true
)"
+395 -186
View File
@@ -141,30 +141,35 @@ validateUsername() {
local value="$1"
local type="$2"
local maximum
local maximum length_suffix invalid_message
case "$type" in
"local" )
maximum=20
[ -z "$value" ] && return 0
;;
"domain" )
maximum=256
maximum=20
length_suffix=""
invalid_message="The USERNAME variable contains characters that are not supported by Windows local accounts!"
;;
"domain" )
if [ -z "$value" ]; then
error "The USERNAME variable does not contain a valid domain account name!"
return 1
fi ;;
fi
maximum=256
length_suffix=" for a domain account"
invalid_message="The domain account name contains characters that are not supported by Windows unattended setup!"
;;
* )
return 1 ;;
return 1
;;
esac
if [ "${#value}" -gt "$maximum" ]; then
if [[ "$type" == "domain" ]]; then
error "The USERNAME variable cannot contain more than $maximum characters for a domain account!"
else
error "The USERNAME variable cannot contain more than $maximum characters!"
fi
error "The USERNAME variable cannot contain more than $maximum characters$length_suffix!"
return 1
fi
@@ -175,12 +180,9 @@ validateUsername() {
case "$value" in
*'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* | *'%'* | *'@'* )
if [[ "$type" == "domain" ]]; then
error "The domain account name contains characters that are not supported by Windows unattended setup!"
else
error "The USERNAME variable contains characters that are not supported by Windows local accounts!"
fi
return 1 ;;
error "$invalid_message"
return 1
;;
esac
if [[ "$value" == *"." ]]; then
@@ -196,12 +198,15 @@ validateUsername() {
case "${value^^}" in
"NONE" )
error "The USERNAME value \"NONE\" is reserved by Windows!"
return 1 ;;
return 1
;;
"ADMINISTRATOR" | "GUEST" | "DEFAULTACCOUNT" | "WDAGUTILITYACCOUNT" | "WSIACCOUNT" )
if [[ "$type" == "local" ]]; then
[[ "$type" == "domain" ]] && return 0
error "The USERNAME value \"$value\" is reserved for a built-in Windows account!"
return 1
fi ;;
;;
esac
return 0
@@ -234,14 +239,28 @@ validateDomainName() {
return 0
}
getXMLArchitecture() {
local asset="$1"
local arch
arch=$(sed -n -E \
'0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \
"$asset") || return 1
[ -n "$arch" ] || return 1
printf '%s' "$arch"
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
arch=$(getXMLArchitecture "$asset") || return 1
grep -q 'Microsoft-Windows-UnattendedJoin' "$asset" && return 1
@@ -287,12 +306,7 @@ updateDomain() {
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
arch=$(getXMLArchitecture "$asset") || return 1
local cred_domain="$domain"
@@ -397,17 +411,44 @@ updateDomain() {
return 0
}
removeLocalAccountXML() {
local asset="$1"
if ! sed -i -E \
-e '/^[[:space:]]*<LocalAccounts([[:space:]>])/,/^[[:space:]]*<\/LocalAccounts>[[:space:]]*$/d' \
-e '/^[[:space:]]*<AdministratorPassword([[:space:]>])/,/^[[:space:]]*<\/AdministratorPassword>[[:space:]]*$/d' \
"$asset"; then
error "Failed to remove local account configuration from answer file!"
return 1
fi
if ! sed -i -E '
/<SynchronousCommand([[:space:]>])/ {
:command
N
/<\/SynchronousCommand>/!b command
/<Description>Password Never Expires<\/Description>/d
}
' "$asset"; then
error "Failed to remove local account commands from answer file!"
return 1
fi
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!"
enabled "${LOG:-}" || return 0
[ -f "$file" ] || return 1
if ! grep -Fq "$old" "$file"; then
enabled "$DEBUG" && warn "$msg"
return 0
@@ -549,14 +590,15 @@ generateEvalXML() {
local id="$1"
local detected_index="${2:-}"
[[ "${id,,}" == *"-eval" ]] || return 1
local normal="${id::-5}"
local source="/run/assets/$normal.xml"
local target="/run/assets/$id.xml"
local index="$detected_index"
local remove_selector="N"
[[ "${id,,}" == *"-eval" ]] || return 1
removeGeneratedXML "$source" || return 1
if [ ! -s "$source" ]; then
@@ -605,7 +647,7 @@ generateFallbackXML() {
setXML() {
local file
local file="$1"
local index="${2:-}"
local target="/run/assets/$DETECTED.xml"
@@ -634,17 +676,21 @@ setXML() {
file="$1"
if [[ "${DETECTED,,}" == *"-eval" ]]; then
if [ ! -f "$file" ] || [ ! -s "$file" ]; then
generateEvalXML "$DETECTED" "$index" || return 1
fi
fi
if [[ "${DETECTED,,}" == *"-eval" ]] &&
{ [ ! -f "$file" ] || [ ! -s "$file" ]; }; then
if [ ! -f "$file" ] || [ ! -s "$file" ]; then
generateEvalXML "$DETECTED" "$index" || return 1
file="$target"
elif [ ! -f "$file" ] || [ ! -s "$file" ]; then
file="$target"
elif [[ "$file" != "$target" ]]; then
generateFallbackXML "$DETECTED" "$index" || return 1
file="$target"
fi
[ -f "$file" ] && [ -s "$file" ] || return 1
@@ -653,14 +699,7 @@ setXML() {
return 0
}
updateXML() {
local asset="$1"
local language="$2"
local value user
[ -z "${WIDTH:-}" ] && WIDTH="1280"
[ -z "${HEIGHT:-}" ] && HEIGHT="720"
validateXMLSettings() {
validateResolution "WIDTH" "$WIDTH" 320 || return 1
validateResolution "HEIGHT" "$HEIGHT" 200 || return 1
@@ -669,20 +708,34 @@ updateXML() {
validateProductKey "${KEY:-}" || return 1
validatePassword "${PASSWORD:-}" || return 1
local app
return 0
}
updateDisplayXML() {
local asset="$1"
local app host
app=$(escapeXMLSed "$APP for $ENGINE") || return 1
sed -i "s|>Windows for Docker<|>$app<|g" "$asset" || return 1
sed -i -E "s|<VerticalResolution>[^<]*</VerticalResolution>|<VerticalResolution>$HEIGHT</VerticalResolution>|g" "$asset" || return 1
sed -i -E "s|<HorizontalResolution>[^<]*</HorizontalResolution>|<HorizontalResolution>$WIDTH</HorizontalResolution>|g" "$asset" || return 1
if [ -n "${HOST:-}" ]; then
local host
[ -n "${HOST:-}" ] || return 0
host=$(escapeXMLSed "$HOST") || return 1
sed -i -E "s|<ComputerName>[^<]*</ComputerName>|<ComputerName>$host</ComputerName>|g" "$asset" || return 1
fi
local culture
return 0
}
updateLocaleXML() {
local asset="$1"
local language="$2"
local culture region keyboard value
culture=$(getLanguage "$language" "culture") || return 1
if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then
@@ -690,8 +743,7 @@ updateXML() {
sed -i "s|<UILanguage>en-US</UILanguage>|<UILanguage>$value</UILanguage>|g" "$asset" || return 1
fi
local region="${REGION:-}"
[ -z "$region" ] && region="$culture"
region="${REGION:-$culture}"
if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then
value=$(escapeXMLSed "$region") || return 1
@@ -699,8 +751,7 @@ updateXML() {
sed -i "s|<SystemLocale>en-US</SystemLocale>|<SystemLocale>$value</SystemLocale>|g" "$asset" || return 1
fi
local keyboard="${KEYBOARD:-}"
[ -z "$keyboard" ] && keyboard="$culture"
keyboard="${KEYBOARD:-$culture}"
if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then
value=$(escapeXMLSed "$keyboard") || return 1
@@ -708,12 +759,20 @@ updateXML() {
sed -i "s|<InputLocale>0409:00000409</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1
fi
local domain="${DOMAIN:-}"
local workgroup="${WORKGROUP:-}"
return 0
}
if [ -n "$domain" ]; then
prepareDomainAccount() {
if [ -z "${USERNAME:-}" ]; then
local domain="$1"
local -n account_ref="$2"
local -n auth_ref="$3"
local qualifier=""
auth_ref="${USERNAME:-}"
account_ref=""
if [ -z "$auth_ref" ]; then
error "The USERNAME variable must be specified when joining a domain!"
return 1
fi
@@ -725,20 +784,20 @@ updateXML() {
validateDomainName "$domain" || return 1
local auth_user="$USERNAME"
local qualifier=""
if [[ "$auth_user" == *\\* ]]; then
if [[ "$auth_ref" == *\\* ]]; then
error "The USERNAME variable must use either \"user\" or \"user@domain\" format!"
return 1
fi
case "$auth_user" in
case "$auth_ref" in
*@* )
user="${auth_user%%@*}"
qualifier="${auth_user#*@}"
account_ref="${auth_ref%%@*}"
qualifier="${auth_ref#*@}"
if [ -z "$account_ref" ] ||
[ -z "$qualifier" ] ||
[[ "$qualifier" == *@* ]]; then
if [ -z "$user" ] || [ -z "$qualifier" ] || [[ "$qualifier" == *@* ]]; then
error "The USERNAME variable does not contain a valid domain account name!"
return 1
fi
@@ -750,14 +809,15 @@ updateXML() {
return 1
fi
;;
* )
user="$auth_user"
account_ref="$auth_ref"
;;
esac
validateUsername "$user" "domain" || return 1
validateUsername "$account_ref" "domain" || return 1
if [[ "${user,,}" == "docker" ]]; then
if [[ "${account_ref,,}" == "docker" ]]; then
error "The USERNAME variable must be changed from its default value when joining a domain!"
return 1
fi
@@ -767,13 +827,19 @@ updateXML() {
return 1
fi
else
return 0
}
updateLocalAccountXML() {
local asset="$1"
local user="${USERNAME:-}"
local pass="${PASSWORD:-admin}"
local user_xml pw admin
user="${USERNAME:-}"
validateUsername "$user" "local" || return 1
if [ -n "$user" ]; then
local user_xml
user_xml=$(escapeXMLSed "$user") || return 1
sed -i "s|-name \"Docker\"|-name \"\$env:USERNAME\"|g" "$asset" || return 1
@@ -783,85 +849,125 @@ updateXML() {
sed -i "s|<Username>Docker</Username>|<Username>$user_xml</Username>|g" "$asset" || return 1
fi
local pass="${PASSWORD:-admin}"
local pw admin
pw=$(printf '%s' "${pass}Password" |
iconv -f utf-8 -t utf-16le |
base64 -w 0) || return 1
pw=$(printf '%s' "${pass}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1
admin=$(printf '%s' "${pass}AdministratorPassword" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1
admin=$(printf '%s' "${pass}AdministratorPassword" |
iconv -f utf-8 -t utf-16le |
base64 -w 0) || return 1
sed -i -z -E "s#(<Password>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$pw</Value>#g" "$asset" || return 1
sed -i -z -E "s#(<AdministratorPassword>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$admin</Value>#g" "$asset" || return 1
sed -i -z -E \
"s#(<Password>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$pw</Value>#g" \
"$asset" || return 1
fi
sed -i -z -E \
"s#(<AdministratorPassword>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$admin</Value>#g" \
"$asset" || return 1
sed -i -E "s|<PlainText>[^<]*</PlainText>|<PlainText>false</PlainText>|g" "$asset" || return 1
return 0
}
updateMembershipXML() {
local asset="$1"
local domain="$2"
local workgroup="$3"
local account="$4"
local auth="$5"
if [ -n "$domain" ]; then
if updateDomain "$asset" "$domain" "$user" \
"$auth_user" "$PASSWORD" "$DOMAIN_OU"; then
if ! updateDomain \
"$asset" \
"$domain" \
"$account" \
"$auth" \
"$PASSWORD" \
"${DOMAIN_OU:-}"; then
if ! sed -i -E \
-e '/^[[:space:]]*<LocalAccounts([[:space:]>])/,/^[[:space:]]*<\/LocalAccounts>[[:space:]]*$/d' \
-e '/^[[:space:]]*<AdministratorPassword([[:space:]>])/,/^[[:space:]]*<\/AdministratorPassword>[[:space:]]*$/d' \
"$asset"; then
error "Failed to remove local account configuration from answer file!"
return 1
fi
if ! sed -i -E '
/<SynchronousCommand([[:space:]>])/ {
:command
N
/<\/SynchronousCommand>/!b command
/<Description>Password Never Expires<\/Description>/d
}
' "$asset"; then
error "Failed to remove local account commands from answer file!"
return 1
fi
else
warn "failed to add domain configuration to answer file!"
return 0
fi
elif [ -n "$workgroup" ]; then
removeLocalAccountXML "$asset" || return 1
return 0
fi
[ -n "$workgroup" ] || return 0
if ! updateWorkgroup "$asset" "$workgroup"; then
warn "failed to add workgroup configuration to answer file!"
fi
fi
return 0
}
if disabled "${AUTOLOGIN:-}"; then
sed -i -E '/^[[:space:]]*<AutoLogon([[:space:]>])/,/^[[:space:]]*<\/AutoLogon>[[:space:]]*$/d' "$asset" || return 1
fi
updateAutologinXML() {
if enabled "${LOG:-}"; then
enableLog "$asset" || return 1
fi
local asset="$1"
if [ -n "${EDITION:-}" ]; then
disabled "${AUTOLOGIN:-}" || return 0
sed -i -E \
'/^[[:space:]]*<AutoLogon([[:space:]>])/,/^[[:space:]]*<\/AutoLogon>[[:space:]]*$/d' \
"$asset" || return 1
return 0
}
updateEditionXML() {
local asset="$1"
local edition
[ -n "${EDITION:-}" ] || return 0
edition=$(normalizeServerEdition "$EDITION") || return 1
edition="${edition//-/}"
edition="${edition^^}"
edition=$(escapeXMLSed "$edition") || return 1
sed -i "s|SERVERSTANDARD</Value>|SERVER$edition</Value>|g" "$asset" || return 1
fi
sed -i \
"s|SERVERSTANDARD</Value>|SERVER$edition</Value>|g" \
"$asset" || return 1
if [ -n "${KEY:-}" ]; then
return 0
}
updateProductKeyXML() {
local asset="$1"
local key
[ -n "${KEY:-}" ] || return 0
key=$(escapeXMLSed "$KEY") || return 1
sed -i -E '/^[[:space:]]*<ProductKey>[[:space:]]*$/,/^[[:space:]]*<\/ProductKey>[[:space:]]*$/d' "$asset" || return 1
sed -i -E "s|<ProductKey>[^<]*</ProductKey>|<ProductKey>$key</ProductKey>|g" "$asset" || return 1
sed -i "s|</UserData>| <ProductKey>\n <Key>$key</Key>\n <WillShowUI>OnError</WillShowUI>\n </ProductKey>\n </UserData>|g" "$asset" || return 1
sed -i -E \
'/^[[:space:]]*<ProductKey>[[:space:]]*$/,/^[[:space:]]*<\/ProductKey>[[:space:]]*$/d' \
"$asset" || return 1
sed -i -E \
"s|<ProductKey>[^<]*</ProductKey>|<ProductKey>$key</ProductKey>|g" \
"$asset" || return 1
sed -i \
"s|</UserData>| <ProductKey>\n <Key>$key</Key>\n <WillShowUI>OnError</WillShowUI>\n </ProductKey>\n </UserData>|g" \
"$asset" || return 1
return 0
}
removeSharedFolderXML() {
local asset="$1"
if ! disabled "${SHORTCUT:-}" &&
! disabled "${SAMBA:-}"; then
return 0
fi
if disabled "${SHORTCUT:-}" || disabled "${SAMBA:-}"; then
if ! sed -i -E '
/<SynchronousCommand([[:space:]>])/ {
:command
@@ -871,10 +977,17 @@ updateXML() {
/<Description>Map shared folder<\/Description>/d
}
' "$asset"; then
error "Failed to remove shared folder shortcuts from answer file!"
return 1
fi
fi
return 0
}
validateGeneratedXML() {
local asset="$1"
if ! xmllint --nonet --noout "$asset"; then
error "The generated answer file is not valid XML!"
@@ -884,6 +997,49 @@ updateXML() {
return 0
}
updateXML() {
local asset="$1"
local language="$2"
local domain="${DOMAIN:-}"
local workgroup="${WORKGROUP:-}"
local account=""
local auth=""
[ -z "${WIDTH:-}" ] && WIDTH="1280"
[ -z "${HEIGHT:-}" ] && HEIGHT="720"
validateXMLSettings || return 1
updateDisplayXML "$asset" || return 1
updateLocaleXML "$asset" "$language" || return 1
if [ -n "$domain" ]; then
prepareDomainAccount "$domain" account auth || return 1
else
updateLocalAccountXML "$asset" || return 1
fi
sed -i -E \
"s|<PlainText>[^<]*</PlainText>|<PlainText>false</PlainText>|g" \
"$asset" || return 1
updateMembershipXML \
"$asset" \
"$domain" \
"$workgroup" \
"$account" \
"$auth" || return 1
updateAutologinXML "$asset" || return 1
enableLog "$asset" || return 1
updateEditionXML "$asset" || return 1
updateProductKeyXML "$asset" || return 1
removeSharedFolderXML "$asset" || return 1
validateGeneratedXML "$asset" || return 1
return 0
}
escapeSIFValue() {
local s="$1"
@@ -974,50 +1130,70 @@ validateLegacyUsername() {
return 0
}
addLegacyDrivers() {
extractLegacyDrivers() {
local drivers="$1"
rm -rf "$drivers" || return 1
mkdir -p "$drivers" || return 1
if ! bsdtar -xf /var/drivers.txz -C "$drivers"; then
error "Failed to extract drivers!"
return 1
fi
return 0
}
copyLegacyStorageDriver() {
local dir="$1"
local target="$2"
local driver="$3"
local arch="$4"
local drivers="$5"
local file
local msg="Adding drivers to image..."
info "$msg" && html "$msg"
rm -rf "$drivers" || return 1
mkdir -p "$drivers" || return 1
if ! bsdtar -xf /var/drivers.txz -C "$drivers"; then
error "Failed to extract drivers!" && return 1
fi
local destination="$dir/\$OEM\$/\$1/Drivers/viostor"
if [ ! -f "$drivers/viostor/$driver/$arch/viostor.sys" ]; then
error "Failed to locate required storage drivers!" && return 1
error "Failed to locate required storage drivers!"
return 1
fi
cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$target" || return 1
mkdir -p "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1
cp -L "$drivers/viostor/$driver/$arch/viostor.cat" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1
cp -L "$drivers/viostor/$driver/$arch/viostor.inf" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1
cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1
mkdir -p "$destination" || return 1
cp -L "$drivers/viostor/$driver/$arch/viostor.cat" "$destination" || return 1
cp -L "$drivers/viostor/$driver/$arch/viostor.inf" "$destination" || return 1
cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$destination" || return 1
return 0
}
addLegacyNetworkDriver() {
local dir="$1"
local driver="$2"
local arch="$3"
local drivers="$4"
local destination="$dir/\$OEM\$/\$1/Drivers/NetKVM"
if [ ! -f "$drivers/NetKVM/$driver/$arch/netkvm.sys" ]; then
error "Failed to locate required network drivers!" && return 1
error "Failed to locate required network drivers!"
return 1
fi
mkdir -p "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1
cp -L "$drivers/NetKVM/$driver/$arch/netkvm.cat" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1
cp -L "$drivers/NetKVM/$driver/$arch/netkvm.inf" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1
cp -L "$drivers/NetKVM/$driver/$arch/netkvm.sys" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1
mkdir -p "$destination" || return 1
cp -L "$drivers/NetKVM/$driver/$arch/netkvm.cat" "$destination" || return 1
cp -L "$drivers/NetKVM/$driver/$arch/netkvm.inf" "$destination" || return 1
cp -L "$drivers/NetKVM/$driver/$arch/netkvm.sys" "$destination" || return 1
file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1
return 0
}
if [ -z "$file" ]; then
error "The file TXTSETUP.SIF could not be found!" && return 1
fi
patchLegacyStorageDriver() {
local file="$1"
local arch="$2"
sed -i '/^\[SCSI.Load\]/s/$/\nviostor=viostor.sys,4/' "$file" || return 1
sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\nviostor.sys=1,,,,,,4_,4,1,,,1,4/' "$file" || return 1
@@ -1026,12 +1202,25 @@ addLegacyDrivers() {
sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00020000=\"viostor\"/' "$file" || return 1
sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00021AF4=\"viostor\"/' "$file" || return 1
return 0
}
addLegacySataDriver() {
local dir="$1"
local target="$2"
local arch="$3"
local drivers="$4"
local file="$5"
local destination="$dir/\$OEM\$/\$1/Drivers/sata"
if [ ! -d "$drivers/sata/xp/$arch" ]; then
error "Failed to locate required SATA drivers!" && return 1
error "Failed to locate required SATA drivers!"
return 1
fi
mkdir -p "$dir/\$OEM\$/\$1/Drivers/sata" || return 1
cp -Lr "$drivers/sata/xp/$arch/." "$dir/\$OEM\$/\$1/Drivers/sata" || return 1
mkdir -p "$destination" || return 1
cp -Lr "$drivers/sata/xp/$arch/." "$destination" || return 1
cp -Lr "$drivers/sata/xp/$arch/." "$target" || return 1
sed -i '/^\[SCSI.Load\]/s/$/\niaStor=iaStor.sys,4/' "$file" || return 1
@@ -1045,6 +1234,35 @@ addLegacyDrivers() {
sed -i '/^\[SCSI\]/s/$/\niaStor=\"Intel\(R\) SATA RAID\/AHCI Controller\"/' "$file" || return 1
sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_8086\&DEV_2922\&CC_0106=\"iaStor\"/' "$file" || return 1
return 0
}
addLegacyDrivers() {
local dir="$1"
local target="$2"
local driver="$3"
local arch="$4"
local drivers="$5"
local file
local msg="Adding drivers to image..."
info "$msg" && html "$msg"
extractLegacyDrivers "$drivers" || return 1
copyLegacyStorageDriver "$dir" "$target" "$driver" "$arch" "$drivers" || return 1
addLegacyNetworkDriver "$dir" "$driver" "$arch" "$drivers" || return 1
file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1
if [ -z "$file" ]; then
error "The file TXTSETUP.SIF could not be found!"
return 1
fi
patchLegacyStorageDriver "$file" "$arch" || return 1
addLegacySataDriver "$dir" "$target" "$arch" "$drivers" "$file" || return 1
rm -rf "$drivers" || return 1
return 0
@@ -1060,34 +1278,31 @@ setLegacyKey() {
setup=$(find "$target" -maxdepth 1 -type f -iname setupp.ini -print -quit) || return 1
if [ -n "$setup" ] && [ -z "$KEY" ]; then
[[ -n "$setup" ]] || return 0
[[ -z "$KEY" ]] || return 0
pid=$(<"$setup") || return 1
pid="${pid%$'\r'}"
if [[ "$driver" == "2k" ]]; then
echo "${pid:0:$((${#pid})) - 3}270" > "$setup" || return 1
else
echo "${pid::-3}270" > "$setup" || return 1
return 0
fi
if [[ "$pid" == *"270" ]]; then
warn "this version of $desc requires a volume license key (VLK), it will ask for one during installation."
else
return 0
fi
file=$(find "$target" -maxdepth 1 -type f -iname PID.INF -print -quit) || return 1
if [ -n "$file" ]; then
if [[ -n "$file" ]]; then
if [[ "$driver" == "2k3" ]]; then
key=$(grep -i -A 2 "StagingKey" "$file" | tail -n 2 | head -n 1) || key=""
else
key="${pid:$((${#pid})) - 8:5}"
key="${pid: -8:5}"
if [[ "${pid^^}" == *"OEM" ]]; then
key=$(grep -i -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key=""
@@ -1104,7 +1319,7 @@ setLegacyKey() {
fi
if [ -z "$KEY" ]; then
[[ -n "$KEY" ]] && return 0
# These are NOT pirated keys, they come from official MS documentation.
@@ -1133,15 +1348,7 @@ setLegacyKey() {
esac
echo "${pid:0:$((${#pid})) - 3}000" > "$setup" || return 1
fi
fi
fi
fi
echo "${pid::-3}000" > "$setup" || return 1
return 0
}
@@ -1150,13 +1357,12 @@ writeCommand() {
local install="$1"
[ -z "$install" ] && return 0
[ ! -f "$install" ] && return 0
[ -f "$install" ] || return 0
if ! enabled "${LOG:-}"; then
printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat\\\"\""
else
if enabled "${LOG:-}"; then
printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat > C:\\\\OEM\\\\install.log 2>&1\\\"\""
else
printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat\\\"\""
fi
return 0
@@ -1458,7 +1664,8 @@ legacyInstall() {
ETFS="[BOOT]/Boot-NoEmul.img"
if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then
error "Failed to locate file \"$ETFS\" in $desc ISO image!" && return 1
error "Failed to locate file \"$ETFS\" in $desc ISO image!"
return 1
fi
local arch="amd64"
@@ -1468,7 +1675,8 @@ legacyInstall() {
[[ "${arch,,}" == "x86" ]] && target="$dir/I386"
if [ ! -d "$target" ]; then
error "Failed to locate directory \"$target\" in $desc ISO image!" && return 1
error "Failed to locate directory \"$target\" in $desc ISO image!"
return 1
fi
if [[ "${driver,,}" == "xp" || "${driver,,}" == "2k3" ]]; then
@@ -1484,7 +1692,8 @@ legacyInstall() {
mkdir -p "$dir/\$OEM\$" || return 1
if ! addFolder "$dir"; then
error "Failed to add OEM folder to image!" && return 1
error "Failed to add OEM folder to image!"
return 1
fi
local oem=""
+182 -137
View File
@@ -184,7 +184,7 @@ getVersions() {
groups_ref=()
indexes_ref=()
platform=$(getPlatform "$xml")
platform=$(getPlatform "$xml") || return 1
count=$(xmllint --nonet --xpath 'count(/WIM/IMAGE)' - 2>/dev/null <<< "$xml") || return 0
for ((i=1; i<=count; i++)); do
@@ -245,10 +245,10 @@ getVersions() {
case "${candidate_base,,}" in
"winvista"* | "win7"* | "win8"* | "win10"* | "win11"* )
structured=$(normalizeEditionID "${edition_id:-${flags:-}}" "$candidate_base")
structured=$(normalizeEditionID "${edition_id:-${flags:-}}" "$candidate_base") || return 1
;;
"win20"* )
structured=$(normalizeServerEditionID "${flags:-$edition_id}")
structured=$(normalizeServerEditionID "${flags:-$edition_id}") || return 1
# Some media use the same EDITIONID for Core and Desktop images.
# INSTALLATIONTYPE provides the structural distinction without
@@ -342,7 +342,7 @@ selectEdition() {
if [ -n "$EDITION" ]; then
for base in "${edition_bases[@]}"; do
edition=$("$normalize_name" "$EDITION" "$base")
edition=$("$normalize_name" "$EDITION" "$base") || return 1
preferred+=("$base${edition:+-$edition}")
done
@@ -444,7 +444,7 @@ detectVersion() {
versions \
bases \
groups \
image_indexes
image_indexes || return 1
[ "${#versions[@]}" -eq 0 ] && return 0
@@ -479,7 +479,7 @@ detectLanguage() {
local xml="$1"
local index="${2:-}"
local xpath lang
local xpath lang culture
if [[ "$index" =~ ^[0-9]+$ ]]; then
xpath="string((/WIM/IMAGE[@INDEX='$index']/WINDOWS/LANGUAGES/DEFAULT | /WIM/IMAGE[@INDEX='$index']/WINDOWS/LANGUAGES/FALLBACK/DEFAULT)[1])"
@@ -494,9 +494,12 @@ detectLanguage() {
return 0
fi
local culture
culture=$(getLanguage "$lang" "culture")
[ -n "$culture" ] && LANGUAGE="$lang" && return 0
culture=$(getLanguage "$lang" "culture") || return 1
if [ -n "$culture" ]; then
LANGUAGE="$lang"
return 0
fi
warn "Invalid language detected: \"$lang\""
return 0
@@ -517,60 +520,105 @@ skipVersion() {
detectLegacy() {
local dir="$1"
local find
local marker
[[ "${PLATFORM,,}" != "x64" ]] && return 1
[[ "${PLATFORM,,}" == "x64" ]] || return 1
find=$(find "$dir" -maxdepth 1 -type d -iname WIN95 -print -quit)
[ -n "$find" ] && DETECTED="win95" && return 0
marker=$(find "$dir" -maxdepth 1 -type d -iname WIN95 -print -quit) || return 1
find=$(find "$dir" -maxdepth 1 -type d -iname WIN98 -print -quit)
[ -n "$find" ] && DETECTED="win98" && return 0
if [ -n "$marker" ]; then
DETECTED="win95"
return 0
fi
find=$(find "$dir" -maxdepth 1 -type d -iname WIN9X -print -quit)
[ -n "$find" ] && DETECTED="win9x" && return 0
marker=$(find "$dir" -maxdepth 1 -type d -iname WIN98 -print -quit) || return 1
find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_W.40 -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_S.40 -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_TS.40 -print -quit)
[ -n "$find" ] && DETECTED="winnt4" && return 0
if [ -n "$marker" ]; then
DETECTED="win98"
return 0
fi
find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_NT.5 -print -quit)
marker=$(find "$dir" -maxdepth 1 -type d -iname WIN9X -print -quit) || return 1
if [ -n "$find" ]; then
if [ -n "$marker" ]; then
DETECTED="win9x"
return 0
fi
find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IA.5 -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_ID.5 -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IP.5 -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IS.5 -print -quit)
[ -n "$find" ] && DETECTED="win2k" && return 0
marker=$(find "$dir" -maxdepth 1 -type f \
\( \
-iname CDROM_W.40 -o \
-iname CDROM_S.40 -o \
-iname CDROM_TS.40 \
\) \
-print -quit) || return 1
if [ -n "$marker" ]; then
DETECTED="winnt4"
return 0
fi
marker=$(find "$dir" -maxdepth 1 -type f -iname CDROM_NT.5 -print -quit) || return 1
if [ -n "$marker" ]; then
marker=$(find "$dir" -maxdepth 1 -type f \
\( \
-iname CDROM_IA.5 -o \
-iname CDROM_ID.5 -o \
-iname CDROM_IP.5 -o \
-iname CDROM_IS.5 \
\) \
-print -quit) || return 1
if [ -n "$marker" ]; then
DETECTED="win2k"
return 0
fi
fi
find=$(find "$dir" -maxdepth 1 -iname WIN51 -print -quit)
marker=$(find "$dir" -maxdepth 1 -iname WIN51 -print -quit) || return 1
[ -n "$marker" ] || return 1
if [ -n "$find" ]; then
marker=$(find "$dir" -maxdepth 1 -type f -iname WIN51AP -print -quit) || return 1
find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AP -print -quit)
[ -n "$find" ] && DETECTED="winxpx64" && return 0
if [ -n "$marker" ]; then
DETECTED="winxpx64"
return 0
fi
find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IC -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IP -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname setupxp.htm -print -quit)
[ -n "$find" ] && DETECTED="winxpx86" && return 0
marker=$(find "$dir" -maxdepth 1 -type f \
\( \
-iname WIN51IC -o \
-iname WIN51IP -o \
-iname setupxp.htm \
\) \
-print -quit) || return 1
find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IS -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IA -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IB -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51ID -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IL -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AA -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AD -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AS -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51MA -print -quit)
[ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51MD -print -quit)
[ -n "$find" ] && DETECTED="win2003r2" && return 0
if [ -n "$marker" ]; then
DETECTED="winxpx86"
return 0
fi
marker=$(find "$dir" -maxdepth 1 -type f \
\( \
-iname WIN51IS -o \
-iname WIN51IA -o \
-iname WIN51IB -o \
-iname WIN51ID -o \
-iname WIN51IL -o \
-iname WIN51AA -o \
-iname WIN51AD -o \
-iname WIN51AS -o \
-iname WIN51MA -o \
-iname WIN51MD \
\) \
-print -quit) || return 1
if [ -n "$marker" ]; then
DETECTED="win2003r2"
return 0
fi
return 1
@@ -607,15 +655,15 @@ resolveImage() {
setImage() {
skipVersion "${DETECTED,,}" && return 0
setXML "" && return 0
enabled "$MANUAL" && return 0
if ! setXML "" && ! enabled "$MANUAL"; then
MANUAL="Y"
local desc
desc=$(printEdition "$DETECTED" "this version")
warn "the answer file for $desc was not found ($DETECTED.xml), $FB."
fi
desc=$(printEdition "$DETECTED" "this version") || return 1
warn "the answer file for $desc was not found ($DETECTED.xml), $FB."
return 0
}
@@ -677,20 +725,19 @@ getSuggestion() {
validateEdition() {
[ -n "$EDITION" ] || return 0
[[ "${DETECTED,,}" == "win20"* ]] || return 0
case "${DETECTED,,}" in
"win20"* )
local edition
edition=$(normalizeServerEditionID "$EDITION")
edition=$(normalizeServerEditionID "$EDITION") || return 1
if [ -n "$edition" ] &&
[[ "${DETECTED,,}" != *"-${edition,,}" &&
"${DETECTED,,}" != *"-${edition,,}-eval" ]]; then
EDITION=""
[ -n "$edition" ] || return 0
if [[ "${DETECTED,,}" == *"-${edition,,}" ||
"${DETECTED,,}" == *"-${edition,,}-eval" ]]; then
return 0
fi
;;
esac
EDITION=""
return 0
}
@@ -715,13 +762,12 @@ describeImage() {
local result_name="$3"
local result
result=$(printEdition "$DETECTED" "$DETECTED" "Y")
detectLanguage "$info_xml" "$index"
result=$(printEdition "$DETECTED" "$DETECTED" "Y") || return 1
detectLanguage "$info_xml" "$index" || return 1
if [[ "${LANGUAGE,,}" != "en" && "${LANGUAGE,,}" != "en-"* ]]; then
local language
language=$(getLanguage "$LANGUAGE" "desc")
language=$(getLanguage "$LANGUAGE" "desc") || return 1
result+=" ($language)"
fi
@@ -745,12 +791,17 @@ configureImage() {
local msg="the answer file for $desc was not found ($DETECTED.xml)"
local fallback="/run/assets/${DETECTED%%-*}.xml"
if setXML "$fallback" "$index" || enabled "$MANUAL"; then
! enabled "$MANUAL" && warn "${msg}."
else
if setXML "$fallback" "$index"; then
if ! enabled "$MANUAL"; then
warn "${msg}."
fi
return 0
fi
enabled "$MANUAL" && return 0
MANUAL="Y"
warn "${msg}, $FB."
fi
return 0
}
@@ -763,9 +814,7 @@ detectImage() {
XML=""
resolveImage "$version" || :
if [ -n "$DETECTED" ]; then
if resolveImage "$version"; then
setImage || return 1
return 0
fi
@@ -773,7 +822,7 @@ detectImage() {
info "Detecting version from ISO image..."
if detectLegacy "$dir"; then
desc=$(printEdition "$DETECTED" "$DETECTED" "Y")
desc=$(printEdition "$DETECTED" "$DETECTED" "Y") || return 1
info "Detected: $desc"
return 0
fi
@@ -811,7 +860,6 @@ normalizeBatch() {
local file="$1"
local bom tmp encoding
[ ! -f "$file" ] && return 0
[ ! -s "$file" ] && return 0
bom=$(od -An -N2 -tx1 "$file" | tr -d ' \n') || return 1
@@ -842,6 +890,30 @@ normalizeBatch() {
return 0
}
reportBatchMatches() {
local file="$1"
local source="$2"
local pattern="$3"
local message="$4"
local suggestion="$5"
local matches line
matches=$(grep -Pin "$pattern" "$file" || true)
[ -n "$matches" ] || return 0
warn "$message in $source:"
while IFS= read -r line; do
printf ' %s\n' "$line" >&2
done <<< "$matches"
printf ' %s\n\n' "$suggestion" >&2
return 0
}
checkBatch() {
local file="$1"
@@ -936,75 +1008,35 @@ EOC
fi
rm -rf "$tmp"
rm -rf "$tmp" || true
matches=$(
grep -Pin \
reportBatchMatches \
"$file" \
"$source" \
'(?<!\\)\\host[.]lan[\\]' \
"$file" || true
)
"invalid single-backslash UNC path detected" \
'Use "\\host.lan\Data\..." instead of "\host.lan\Data\...".'
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 \
reportBatchMatches \
"$file" \
"$source" \
'(?<![\\[:alnum:]._-])host[.]lan[\\]' \
"$file" || true
)
"UNC path without leading backslashes detected" \
'Use "\\host.lan\Data\..." instead of "host.lan\Data\...".'
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 \
reportBatchMatches \
"$file" \
"$source" \
'//host[.]lan/' \
"$file" || true
)
"invalid forward-slash UNC path detected" \
'Use "\\host.lan\Data\..." instead of "//host.lan/Data/...".'
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 \
reportBatchMatches \
"$file" \
"$source" \
'\\\\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
"invalid Samba share name detected" \
'The "/shared" folder is exposed to Windows as "\\host.lan\Data".'
return 0
}
@@ -1134,7 +1166,10 @@ buildImage() {
local hide="Warning: creating filesystem that does not conform to ISO-9660."
[ -s "$log" ] && err="$(<"$log")"
[[ "$err" != "$hide" ]] && echo "$err"
if [ -n "$err" ] && [[ "$err" != "$hide" ]]; then
echo "$err"
fi
mv -f "$out" "$BOOT" || return 1
@@ -1217,12 +1252,22 @@ extractBootImage() {
rm -rf "$tmp" || true
if ! len=$(isoinfo -d -i "$iso" | grep "Nsect " | grep -o "[^ ]*$"); then
local boot_info
if ! boot_info=$(isoinfo -d -i "$iso"); then
error "Failed to read boot image information from $desc ISO!"
return 1
fi
len=$(awk '/Nsect / { print $NF; exit }' <<< "$boot_info")
offset=$(awk '/Bootoff / { print $NF; exit }' <<< "$boot_info")
if [ -z "$len" ]; then
error "Failed to determine boot image size from $desc ISO!"
return 1
fi
if ! offset=$(isoinfo -d -i "$iso" | grep "Bootoff " | grep -o "[^ ]*$"); then
if [ -z "$offset" ]; then
error "Failed to determine boot image offset from $desc ISO!"
return 1
fi