Skip to content

fix[closes #4820]: report failed dependency installers - #4904

Merged
mirkobrombin merged 3 commits into
bottlesdevs:mainfrom
masonliiu:mason/4820-dependency-install-result
Oct 26, 2026
Merged

mirkobrombin merged 3 commits into
bottlesdevs:mainfrom
masonliiu:mason/4820-dependency-install-result

Conversation

@masonliiu

Copy link
Copy Markdown
Contributor

Description

A dependency whose installer fails was still reported as installed: the toast said "dotnet35" installed, the dependency was added to Installed_Dependencies and the row showed Remove/Reinstall.

I found two causes. DependencyManager.__step_install_exe_msi calls WineExecutor.run() and throws the result away. And even if it had looked at it, dependency installers are downloaded to Paths.temp, outside the bottle, so they are launched through wine start /wait (or MsiExec for .msi), and those launch paths return success as soon as the process has been started. 4e5bdab added exit code handling for program installers, but dependencies never got it.

What I changed:

  • WineExecutor takes an optional success_codes. When it is set, the result of an exe or msi launch follows the exit status of the installer. When it is not set (every other caller), nothing changes.
  • Start.run and MsiExec.install return the launch result, and WineProgram keeps the exit status of its last launch in returncode, so the executor can see it.
  • The dependency install step passes success_codes and fails when the installer exits with a status that is not accepted. The accepted ones are 0, 1638 (another version is already installed), 1641, 3010 and 3011 (done, restart needed), plus an optional success_codes list on the step, same name as in installer.py.
  • A failed step already made install() return a failure, so the dependency is not recorded as installed and the UI uses the existing "failed to install" state. The failure message now also says why (for example One or more steps failed for dotnet35. Command exited with status 67.) and a failed download gets a message too.

Wine passes the exit status to the host as one byte, so Windows codes above 255 arrive truncated (3010 becomes 194, 1603 becomes 67). I compare the low byte on both sides, like the & 0xFF check from 4e5bdab. I did not reuse the cmd.exe batch wrapper from that commit, because installers here are host paths launched through start /unix, and start does not set ERRORLEVEL, so the wrapper would not see the full code either.

Other steps (register_dll, override_dll, set_register_key, register_font and so on) also ignore their result. I left them alone on purpose: Wine's regsvr32 and reg often return non-zero for harmless reasons, so failing on them could break dependencies that work today. The program installer manager has the same unconditional-success launch for install_exe/install_msi steps in installer.py. I kept it out of this PR to keep it focused, and I'm happy to send it as a follow-up.

Related issues

Fixes #4820

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Added unit tests for the executor (exe and msi, accepted and failing codes, truncated codes, behaviour without success_codes), for Start/WineProgram, and for DependencyManager.install (failed installer is not recorded, message, prerequisites, download failure). The new dependency and executor tests fail on main and pass with this change.
  • Ran the real code (DependencyManager.install) against a real Wine 11.20 prefix with a test installer that exits with a chosen code. On main every code is reported as installed. With this change 0, 3010 and 1641 install, 1603 and 1 fail and are not recorded, 1603 installs when listed in success_codes, and a broken .msi fails.
  • Installed msxml3, vcredist2008, 2010, 2013, 2015 and 2019 from the catalog the same way, to check that dependencies that work today still pass: all of them still report success. This used the system Wine, not a Soda runner.
  • Not tested: the Flatpak build and the GTK UI (there is no Flatpak runtime here), and dotnet35 itself.
python -m pytest bottles/tests/backend/manager/test_dependency_install.py bottles/tests/backend/wine/test_executor.py bottles/tests/backend/wine/test_start.py
ruff check <changed files>

Generative AI assistance

AI assistance: none

Installing a dependency ran the installer through WineExecutor and
ignored the outcome. Installers that live outside the bottle are
launched through `wine start /wait`, and that path reported success as
soon as the process had been started, so a dependency was recorded as
installed even when its installer exited with an error.

Let the caller ask WineExecutor to follow the exit status
(`success_codes`). Start and MsiExec now return the launch result and
WineProgram keeps the exit status of its last launch. The dependency
install step fails when the installer exits with a status that is not
0, 1638, 1641, 3010 or 3011, or a code listed in the step's optional
`success_codes`. A failed step makes the whole install fail, so the
dependency is not recorded as installed and the existing failure UI is
used. Wine reports exit statuses as a single byte, so codes are compared
on their low byte, like the installer manager does.

Without `success_codes`, WineExecutor behaves as before.

 bottles/backend/managers/dependency.py        |  45 +++--
 bottles/backend/wine/executor.py              |  38 +++-
 bottles/backend/wine/msiexec.py               |   2 +-
 bottles/backend/wine/start.py                 |   2 +-
 bottles/backend/wine/wineprogram.py           |   7 +-
 .../manager/test_dependency_install.py        | 167 ++++++++++++++++++
 bottles/tests/backend/wine/test_executor.py   | 131 ++++++++++++++
 bottles/tests/backend/wine/test_start.py      |  29 +++
 8 files changed, 403 insertions(+), 18 deletions(-)
 create mode 100644 bottles/tests/backend/manager/test_dependency_install.py

@mirkobrombin mirkobrombin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exit-status handling is useful, but is_success_exit_code currently accepts negative subprocess return codes. For example, -62 is a killed process and (-62 & 0xff) equals the low byte of 3010, so it becomes a successful install. Please reject negative statuses before masking and add that regression case.

@mirkobrombin
mirkobrombin merged commit fb8cf1f into bottlesdevs:main Oct 26, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bottles incorrectly reports successful installation when a dependency fails to install

2 participants