Skip to content

Add support for menuinst.toml#477

Open
lrandersson wants to merge 10 commits into
conda:mainfrom
lrandersson:dev-ra-829
Open

Add support for menuinst.toml#477
lrandersson wants to merge 10 commits into
conda:mainfrom
lrandersson:dev-ra-829

Conversation

@lrandersson
Copy link
Copy Markdown
Contributor

@lrandersson lrandersson commented May 11, 2026

Summary

Adds the possibility to override DISTRIBUTION_NAME and improves tracking of installed shortcuts.

Changes

  • Add MENUINST_DISTRIBUTION_NAME env var support with resolution order:
    1. MENUINST_DISTRIBUTION_NAME env var (highest priority)
    2. $BASE_PREFIX/Menu/menuinst.toml value
    3. base_prefix.name (fallback)
  • Add menuinst.toml for persistent storage of distribution_name and shortcut tracking
  • Add read_menuinst_toml() in utils.py and write_menuinst_toml() in api.py
  • Add record_shortcuts() / remove_shortcut_records() called from install() / remove()
  • Add tomli (py<3.11) and tomli-w dependencies

Planned changes once on main

  1. constructor: Set MENUINST_DISTRIBUTION_NAME=%APPNAME% in run_installation.bat for MSI installers
  2. Integration tests - Add tests that call install()/remove() with real menu JSON to verify end-to-end TOML recording
  3. Use recorded paths for removal - Update remove() to use the recorded shortcut paths from menuinst.toml whenever possible instead of recomputing from menu JSON metadata

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?

@lrandersson lrandersson self-assigned this May 11, 2026
@conda-bot conda-bot added the cla-signed [bot] added once the contributor has signed the CLA label May 11, 2026
@github-project-automation github-project-automation Bot moved this to 🆕 New in 🔎 Review May 11, 2026
@lrandersson lrandersson marked this pull request as ready for review May 11, 2026 17:44
@lrandersson lrandersson requested a review from a team as a code owner May 11, 2026 17:44
@lrandersson lrandersson marked this pull request as draft May 12, 2026 17:19
@lrandersson lrandersson marked this pull request as ready for review May 12, 2026 19:28
Comment thread menuinst/api.py
# Pass path so install/remove records the actual filename in menuinst.toml
if remove:
_api_remove(metadata, target_prefix=prefix, **kwargs)
_api_remove(json_path, target_prefix=prefix, **kwargs)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change was done to properly register the file name the shortcuts originate from in the toml file. There are other ways to achieve the same behavior but this approach required the least intrusive code changes.

Copy link
Copy Markdown
Contributor

@marcoesters marcoesters left a comment

Choose a reason for hiding this comment

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

This is nice - I am a little worried about the special-casing of DISTRIBUTION_NAME and made an alternative suggestion. I haven't thought through all the ramifications of that suggestion yet though.

Comment thread menuinst/cli/cli.py Outdated
Comment on lines +9 to +10
from ..api import _install_adapter, write_menuinst_toml
from ..utils import read_menuinst_toml
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think I understand why you're doing this ("public" API vs "private" utility function), but it looks awkward to have the read and write functions in different modules. I'm not sure if we should add the read function to the API just because of that though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah the intent was that the utils is read-only but the other ones actually cause changes. I originally had them all n api.py but decided last minute to move it. I'm fine with either. Proposals:

  1. Move both to utils.py (keep read/write together)
  2. Move both to api.py (where mutations happen)
  3. Keep as-is and document the rationale

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't have too strong of an opinion either way, but 2. for the "main" TOML functions (top-level read and write) sounds the most sensible to me.

Comment thread menuinst/cli/cli.py Outdated
# The env var allows installers to set distribution_name dynamically at
# install time. We must persist it here because the env var is transient
# and may not be set when packages with shortcuts are installed later.
if env_name := os.environ.get("MENUINST_DISTRIBUTION_NAME"):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's call it distribution_name to be consistent.

I'm wondering though if it makes sense to record all MENUINST_* environment variables here.

I'm just generally wondering how we would distinguish between those "global" parameters that should persist vs. parameters that should be recalculated during install-time. Maybe we need a separate command for that? menuinst --set-global-placeholder or something similar?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think the generalization is interesting but comes with additional complexity, which I would propose we postpone until we have concrete use cases. Refactoring this wouldn't be difficult but I'm afraid we run into fixing something where we don't have any real use case (if that makes sense).
Also renamed the variable: d5324fb

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking about #334 in particular. Or the environment placeholder in the Anaconda Prompt menus or the package version placeholders for spyder. Right now, this needs to be implemented with a sed-like workaround for Windows.

Setting variables like this via menuinst and then using placeholder syntax would be really convenient and remove all special casing. Installers would then just call _conda.exe menuinst --set-(global|local)-placeholder CUSTOM_PLACEHOLDER=something (or similar).

Comment on lines 93 to 95
"BASE_PREFIX": str(self.base_prefix),
"DISTRIBUTION_NAME": self.base_prefix.name,
"DISTRIBUTION_NAME": self._get_distribution_name(),
"BASE_PYTHON": str(self.base_prefix / "bin" / "python"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we already have MENUINST_BASE_PREFIX and the like, I wonder if we can generalize overwriting those placeholders instead of special-casing DISTRIBUTION_NAME (even though BASE_PREFIX and PREFIX are special cases).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah my answer to this is similar to above. I think the idea is good but something I think we can do better once we have concrete use cases. I would also argue that MENUINST_BASE_PREFIX and MENUINST_PREFIX are runtime path overrides and are slightly different from DISTRIBUTION_NAME in this scenario.

Comment thread menuinst/api.py
Comment on lines +70 to +72
# Write distribution_name only to base prefix, and only if not already set
if prefix.samefile(base_prefix) and distribution_name:
data.setdefault("distribution_name", distribution_name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this illustrates the brittleness of the special casing a little bit. If we come up with other variables that must only be written into the base prefix, we have to keep making sure that they are in the correct place. Using the menuinst --set-global-placeholder would make make this more stable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah I think that's definitely a valid concern. I think in its current state with only one variable, it's doable, but if we add more I think definitely we can iterate on the design to something more robust. I've been trying to keep it simple and avoid over-engineering.

Comment thread menuinst/utils.py Outdated
import tomli as tomllib

logger = getLogger(__name__)
MENUINST_TOML_SCHEMA_VERSION = 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suspect we will need to be more fine-grained than that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You mean something like 1.0.0? Absolutely, I just made it simple (1) right now to illustrate the purpose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would use SchemaVer like we do for the menuinst schema: https://docs.snowplow.io/docs/api-reference/iglu/common-architecture/schemaver/

Comment thread menuinst/utils.py Outdated
Comment on lines +97 to +99
except Exception as e:
logger.warning("Failed to read menuinst.toml from %s: %s", toml_path, e)
return {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should actually error out here. The only way I can reasonably imagine that the file is invalid is if someone manipulated the file manually. In that case, returning {} may result in unexpected behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See d5324fb

Comment thread menuinst/api.py
Comment on lines +85 to +87
TODO: Use the recorded paths as the source of truth for shortcut removal,
instead of recomputing paths from menu JSON metadata. This would handle
cases where shortcuts were moved or the menu JSON changed.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I may be missing some of the complexity here, but I think this is well in scope of this PR. All the pieces are already there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added this TODO to help the reviewers and keep it simple. Merging this wouldn't break any existing behavior. I'm happy to do it in this PR but perhaps even opening a separate PR targeting this branch would be more feasible, let me know what you prefer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A separate PR targeting this branch would work for me. As of this PR, all we do with those recorded paths is remove them from the record on uninstallation. So, the records currently don't find much use.

Comment thread tests/test_cli.py Outdated
def test_cli_env_var_persistence(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, env_var_set: bool, expect_toml: bool
) -> None:
"""Test that MENUINST_DISTRIBUTION_NAME env var is persisted to menuinst.toml."""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"""Test that MENUINST_DISTRIBUTION_NAME env var is persisted to menuinst.toml."""
"""Test that MENUINST_DISTRIBUTION_NAME env var persists in menuinst.toml."""

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@lrandersson lrandersson requested a review from marcoesters May 13, 2026 17:45
@lrandersson
Copy link
Copy Markdown
Contributor Author

@marcoesters for the second round of review, see my responses below (trying to avoid spamming with email notifications):

  1. Use SchemaVer format: df00f53
  2. Move functions such that read/write are now in the same module (both in utils.py): df00f53
  3. Regarding the custom placeholders: Add support for menuinst.toml #477 (comment)
    I see the value and agree that it would be good to add, but to reduce scope creep of this PR which is intended primarily to resolve the MSI installer issue I propose that I create a separate issue as a follow-up to this one, since as far as I understand there are questions that we need to address that I don't think is ideal to resolve via review comments:
  • CLI interface design - --set-global-placeholder KEY=value vs --set-local-placeholder KEY=value
  • Storage format - How to distinguish global vs local placeholders in menuinst.toml? As an example: Currently menuinst.toml has distribution_name as a top-level key. For arbitrary placeholders, we need to decide:
    • Maybe a flat structure? FOO = "value" at root level (risk: collision with future schema keys)
    • or nested under a section? [placeholders] section with FOO = "value"
  • Resolution ordering - When resolving {{ FOO }} in a menu.json, what takes precedence?
    • Environment variable MENUINST_FOO?
    • Value in current prefix's menuinst.toml?
    • Value in base_prefix's menuinst.toml?
  • Scope semantics - global = base_prefix only vs local = per-environment?
    Let me know what you think, just to recall, I think the idea is great but I just think it should be done as part of a separate PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed [bot] added once the contributor has signed the CLA

Projects

Status: 🆕 New

Development

Successfully merging this pull request may close these issues.

3 participants