fix(installer): bundled MySQL install failed on a malformed msiexec command line

Reported from a Windows Server 2019 test: a "Windows Installer" dialog listing
every msiexec /Option appeared, then the wizard reported that the bundled MySQL
database could not be installed. That dialog is msiexec's usage help - it prints
it when the command line does not parse - so the install never started.

Cause: $MysqlRoot defaulted to 'C:\Program Files\MySQL\MySQL Server 8.4', which
contains spaces. Invoke-Native wraps any argument containing whitespace in
quotes, producing "INSTALLDIR=C:\Program Files\...". msiexec takes public
properties as PROPERTY=value and expects the VALUE quoted -
INSTALLDIR="C:\Program Files\..." - so it rejected the line, printed usage, and
exited non-zero.

This file already carried the rule, next to the Python target: "Never put a
space in a path this installer controls." I broke it setting the 8.4 path.

Two fixes, because one of them alone leaves the trap in place:

- $MysqlRoot is now C:\MySQL84, space-free like C:\Python314. The MySQL client
  search paths in the installer, the preflight and the operator console all look
  there first, keeping backups working against the bundled server.
- Invoke-Native now quotes PROPERTY=value correctly, so passing a spaced path
  explicitly no longer produces an unparseable command line.

tests/test_installer_defaults.py fails if an installer-controlled path default
ever contains a space again.
This commit is contained in:
cproudlock
2026-08-04 12:06:36 -04:00
parent 5321649e02
commit 8d0afc40d3
4 changed files with 41 additions and 4 deletions

View File

@@ -195,6 +195,7 @@ function Get-DbParts {
function Find-MysqlClient {
$candidates = @(
'C:\MySQL84\bin\mysql.exe',
'C:\Program Files\MySQL\MySQL Server 8.4\bin\mysql.exe',
'C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe',
'C:\mysql56\bin\mysql.exe'

View File

@@ -57,7 +57,13 @@ param(
[ValidateSet('0','1','2','3','4','5','all','uninstall')] [string] $Stage = 'all',
# Bundled-MySQL (stage 0) settings. The root and app passwords are GENERATED,
# never supplied: see New-Secret. Root is shown once and not persisted.
[string] $MysqlRoot = 'C:\Program Files\MySQL\MySQL Server 8.4',
# SPACE-FREE, deliberately - same reason as the Python target below. msiexec
# takes public properties as PROPERTY=value on the command line, and a value
# containing spaces has to be quoted as INSTALLDIR="C:\..." - quoting the
# whole PROPERTY=value token instead makes msiexec reject the command line
# and pop its usage dialog, which is what 'the bundled MySQL database could
# not be installed' looked like from the outside.
[string] $MysqlRoot = 'C:\MySQL84',
[string] $MysqlDataDir = 'C:\ProgramData\MySQL\data',
[string] $MysqlIni = 'C:\ProgramData\MySQL\my.ini',
[string] $MysqlService = 'MySQL84',
@@ -196,7 +202,12 @@ function Invoke-Native {
# "IIS AppPool\name:(OI)(CI)RX" became the parameter "IIS" (icacls exit 87),
# and how TargetDir=C:\Program Files\... installed Python into C:\Program\.
$quoted = $Arguments | ForEach-Object {
if ($_ -match '\s' -and $_ -notmatch '^".*"$') { '"' + $_ + '"' } else { $_ }
if ($_ -notmatch '\s' -or $_ -match '^".*"$') { $_ }
# PROPERTY=value (msiexec, msbuild): the VALUE is quoted, never the whole
# token. "INSTALLDIR=C:\Program Files\..." is rejected outright, whereas
# INSTALLDIR="C:\Program Files\..." is what the tool expects.
elseif ($_ -match '^([A-Za-z_][A-Za-z0-9_]*)=(.*)$') { '{0}="{1}"' -f $Matches[1], $Matches[2] }
else { '"' + $_ + '"' }
}
$so = [System.IO.Path]::GetTempFileName()
$se = [System.IO.Path]::GetTempFileName()
@@ -409,7 +420,10 @@ function Find-MysqlTool {
)
foreach ($b in $bundled) { if (Test-Path $b) { return $b } }
$roots = @('C:\Program Files\MySQL', 'C:\mysql56\bin', 'C:\Program Files (x86)\MySQL')
# C:\MySQL84 first: that is where the bundled server installs (space-free,
# see $MysqlRoot). The Program Files paths cover a server MySQL put there
# by hand or by an older build.
$roots = @('C:\MySQL84', 'C:\Program Files\MySQL', 'C:\mysql56\bin', 'C:\Program Files (x86)\MySQL')
foreach ($r in $roots) {
if (Test-Path $r) {
$hit = Get-ChildItem $r -Filter $Name -Recurse -ErrorAction SilentlyContinue |

View File

@@ -387,7 +387,7 @@ Invoke-Check 'MySQL' 'Backup client' {
$found = ''
foreach ($root in @((Join-Path $PSScriptRoot 'mysqlclient'),
(Join-Path $AppRoot 'mysqlclient'),
'C:\Program Files\MySQL', 'C:\mysql56\bin',
'C:\MySQL84', 'C:\Program Files\MySQL', 'C:\mysql56\bin',
'C:\Program Files (x86)\MySQL')) {
if (-not (Test-Path $root)) { continue }
$hit = Get-ChildItem $root -Filter $names[0] -Recurse -ErrorAction SilentlyContinue |