Skip to content

MSI: Update uninstall scripts#3

Merged
lrandersson merged 12 commits intodev-ra-798-2from
dev-ra-753
Feb 19, 2026
Merged

MSI: Update uninstall scripts#3
lrandersson merged 12 commits intodev-ra-798-2from
dev-ra-753

Conversation

@lrandersson
Copy link
Owner

@lrandersson lrandersson commented Feb 10, 2026

Description

Note that this PR builds upon #2

Checklist - did you ...

  • Add a file to the news directory (using the template) for the next release's release notes?
  • Add / update necessary tests?
  • Add / update outdated documentation?

original_exception=e,
)

# Sanity check the installation directory
Copy link
Owner Author

Choose a reason for hiding this comment

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

This is not really needed but something I had use of while debugging last week. Let me know if I should remove it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this can be removed to keep the code cleaner.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Forgot to respond to this one, removed f39a787

input_path = _example_path("register_envs") # The specific example we use here is not important
options = ["/InstallationType=JustMe", f"/NoRegistry={no_registry}"]
for installer, install_dir in create_installer(input_path, tmp_path):
if installer.suffix == ".msi":
Copy link
Owner Author

Choose a reason for hiding this comment

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

This test is for the .exe flag /NoRegistry so it's not applicable for the .msi installers.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just change the installer type to exe in the construct.yaml file then? Using pytest.skip will skip the whole test. At least, you should use continue.

Copy link
Owner Author

Choose a reason for hiding this comment

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

We can't use continue because it will fail on the assert statements that follow after.
I wonder if we should move this test into a separate file, some background: The test is running the example register_envs, which was picked arbitrarily when creating the test because we just needed any example in order to test the /NoRegistry flag. Therefore changing the installer type is not feasible because we still need the installer type set to all for test_example_register_envs. We could also create a new example for exe only, but that doesn't seem to be the correct setup since the test is only intended for /NoRegistry.

This could also be solved in a more neat way using proper parametrization (related conda#1146)

Copy link
Collaborator

Choose a reason for hiding this comment

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

The assertions won't fail because they are not inside the for-loop. The test seems to be written with the assumption that only exe installers are created for Windows. Using continue will create an MSI installer, but nothing will be done with it.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes that's correct. I don't see however how using continue and moving the assert statements into the for-loop makes the test-skip more explicit over the current approach. It is unfortunately already clunky as is since the installer needs to be created in order to identify what kind of installer type we are testing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The difference is that currently, your tests are marked as skipped: https://github.com/lrandersson/constructor/actions/runs/22183340055/job/64150179614?pr=3#step:12:82

The loop will first create the exe installer (due to the sorting) and then get to the msi installer. So, the tests aren't even executed for the exe installer since they are outside the for-loop.

I agree that it is clunky. We don't even need to move the test into the for-loop because in the end as long as we ensure that the exe installer is the only installer that has been run. I think this is the only thing we need to add:

if installer.suffix != "exe":
    continue

Copy link
Owner Author

Choose a reason for hiding this comment

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

I see, that's tedious. but it makes sense now when you say that.
I updated it here 60f85bb, I did indent the assert anyway because I tried the continue approach earlier last week and it will fail on the assert statement for the second parametrized test (because it will find the program in the 'Installed Apps Menu').

CONSTRUCTOR_EXAMPLES_KEEP_ARTIFACTS: "${{ runner.temp }}/examples_artifacts"
CONSTRUCTOR_SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x86/signtool.exe"
CONSTRUCTOR_VERBOSE: 1
CONSTRUCTOR_VERBOSE: 0
Copy link
Owner Author

Choose a reason for hiding this comment

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

This I'll change back to 1 later.

{%- if add_debug %}
rem Get the name of the install directory
for %%I in ("%INSTDIR%") do set "APPNAME=%%~nxI"
set "LOG=%TEMP%\%APPNAME%-preuninstall.log"
Copy link
Owner Author

@lrandersson lrandersson Feb 17, 2026

Choose a reason for hiding this comment

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

I'm not really happy with this solution of setting the file name here, since it is also explicitly set in test_examples.py, ideally it would be only defined in one place instead of two as it is right now (same also applies for postinstall.log). But it does suffice for now and helped tremendously while debugging last week, I also don't see any need to change this line in the near future since it just works.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a way to access the log file for the MSI installer somehow?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Do you mean accessing it from the CI pipeline? Otherwise no, only by opening the file, I think it's a bit clunky, MSI has "support" for logging but it seems very generic since as you might have seen that we enable logging for MSI Installers via cmd.extend(["/L*V", str(log_path)]) - this doesn't include the logging for pre/post install/uninstall.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, that's why I'm asking if there is any avenue that we can access log_path from within these scripts.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ah, it could work but it's risky. We can end-up with encoding mismatch, MSI log is usually UTF-16. cmd.exe echo writes in the console codepage (effectively “ANSI”/OEM), so you’ll end up mixing encodings in one file.
We can also end-up with weird/corrupted log files from the MSI engine trying to write at the same time as the scripts.

Copy link
Collaborator

Choose a reason for hiding this comment

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

A problem to solve for a different PR, but I would really like to see those logged in the same file. Maybe we need to extend the logger to detect the file encoding of an existing log file and change it accordingly? The documentation is a little sparse about what happens when encoding is None.

Is the risk for race-condition really there? Would an MSI installer even write to log while the script is running?

Copy link
Owner Author

Choose a reason for hiding this comment

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

To answer this correctly, I think we need to do a lot of testing. I agree something for a different PR.

"%CONDA_EXE%" --version >> "%LOG%" 2>&1
{%- endif %}

{%- set redir = ' >> "%LOG%" 2>&1' if add_debug else '' %}
Copy link
Collaborator

Choose a reason for hiding this comment

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

conda-standalone has logging functionality you could use - it essentially works like the Tee-Object commandlet.

input_path = _example_path("register_envs") # The specific example we use here is not important
options = ["/InstallationType=JustMe", f"/NoRegistry={no_registry}"]
for installer, install_dir in create_installer(input_path, tmp_path):
if installer.suffix == ".msi":
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just change the installer type to exe in the construct.yaml file then? Using pytest.skip will skip the whole test. At least, you should use continue.

{%- set redir = ' >> "%LOG%" 2>&1' if add_debug else '' %}
{%- set dump_and_exit = 'type "%LOG%" & exit /b %errorlevel%' if add_debug else 'exit /b %errorlevel%' %}

rem Sanity checks
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we call these "consistency checks" instead of "sanity checks"?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

I still see some instances

Copy link
Owner Author

Choose a reason for hiding this comment

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

Comment on lines +32 to +34
if not exist "%CONDA_EXE%" (
{% if add_debug %}echo [ERROR] CONDA_EXE not found: "%CONDA_EXE%" >> "%LOG%" & type "%LOG%" & {% endif %}exit /b 10
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think if we error out, we should always add a message. So, I'd skip the entire check if not debugging or always add the message.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why don't we just always add to the log file? That answer may depend on how we deal with the MSI vs post-install logging inconsistency.

Copy link
Owner Author

Choose a reason for hiding this comment

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

We cant add everything that is outputted from the scripts to the log file while also displaying the output "live" to the person who runs the installer, see my answer in #3 (comment) (the first bullet).

Copy link
Owner Author

@lrandersson lrandersson Feb 19, 2026

Choose a reason for hiding this comment

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

Or we might be able to achieve "always write to file" (but not the same file for the MSI engine + pre/post uninstall/install), with the following approach:

  1. <all calls to conda-standalone utilize --log-file> // because it seems like its writing to stdout and well as the file? I've only tried so far with conda run python -c 'raise Exception(...)'
  2. All bat-file "echo ..." needs to be written twice, once without redirection and one with redirection

Copy link
Collaborator

Choose a reason for hiding this comment

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

The workaround I used for NSIS was to write to a step-log file and then type to screen and append to the main log. I think logging will need its own PR though - it's a bigger challenge than anticipated.

{%- if add_debug %}
rem Get the name of the install directory
for %%I in ("%INSTDIR%") do set "APPNAME=%%~nxI"
set "LOG=%TEMP%\%APPNAME%-preuninstall.log"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a way to access the log file for the MSI installer somehow?

{%- endif %}

{%- set redir = ' >> "%LOG%" 2>&1' if add_debug else '' %}
{%- set dump_and_exit = 'type "%LOG%" & exit /b %errorlevel%' if add_debug else 'exit /b %errorlevel%' %}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should always create a log if possible. The log level should just determine what goes into the log. We do have to make sure it gets deleted in the end though.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@marcoesters thanks for your first set of review comments. I thought I'd respond here first because the current design is due to some limitations I ran into last week, and I think we need to discuss this before I make any additional changes.

  1. I agree that we should always log to a file, unfortunately it doesn't seem possible to do this without redirecting all visible output from the user (the person who runs the installer) - when logging to a file, all you see is an empty console that closes after a while. I looked into this last week and the only way to use tee (or similar) is through PowerShell, which makes us "stuck" with above. Therefore I'm a bit reluctant to log all output to a file by default for the average user. Any thoughts on this?
  2. You mentioned; "conda-standalone has logging functionality you could use - it essentially works like the Tee-Object commandlet.". Will this also log exceptions that could occur when conda.exe is running? If yes this sounds like a good idea, the current approach will also make any stack traces from conda.exe visible when the tests are running in our CI environment. This is also how I discovered that we the canary builds use an older version of conda-standalone, because they caused stack traces that were not visible without redirecting.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is how the logger is set up: https://github.com/conda/conda-standalone/blob/00deca6accc6fd0b9dae8346f125f1ad1d107524/src/entry_point.py#L134-L164

So, it should catch all stdout and stderr, but you could try by running conda.exe python -c 'raise RuntimeError("Error message")' --log-file <path to log file> and see what happens.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I just tried it and unfortunately it doesn't add unhandled exceptions to the log file.

Copy link
Collaborator

Choose a reason for hiding this comment

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

As we found, that was an incorrect example, but it does work.

@lrandersson lrandersson merged commit 2ad0d23 into dev-ra-798-2 Feb 19, 2026
15 checks passed
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.

2 participants