Skip to content

Do not treat a named environment as the base environment - #538

Draft
hmaarrfk wants to merge 3 commits into
conda:mainfrom
hmaarrfk:detect-named-environments
Draft

Do not treat a named environment as the base environment#538
hmaarrfk wants to merge 3 commits into
conda:mainfrom
hmaarrfk:detect-named-environments

Conversation

@hmaarrfk

@hmaarrfk hmaarrfk commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

I'm not yet convinced about this strategy, but I often install conda in my working environments since managing the base prefix's conda can get tiring.

Opened by an AI agent (Claude) acting on behalf of @hmaarrfk, who has reviewed and takes responsibility for this change.

Draft, because the "should an explicit --root-prefix win?" question below deserves a maintainer opinion.

Description

menuinst decides whether the target prefix is the base environment by comparing it against base_prefix:

https://github.com/conda/menuinst/blob/main/menuinst/platforms/base.py#L44

if self.prefix.samefile(self.base_prefix):
    self.env_name = "base"

and again in MenuItem.__init__ for the base/non-base name object.

base_prefix ultimately comes from menuinst.utils.DEFAULT_BASE_PREFIX, which follows the running interpreter. Installing conda into an environment is legitimate and common, and such a conda reports that environment as its own base:

$ <root>/envs/myenv/bin/conda info --base
<root>/envs/myenv

$ <root>/envs/myenv/bin/python -c "from menuinst.utils import DEFAULT_PREFIX, DEFAULT_BASE_PREFIX; print(DEFAULT_PREFIX); print(DEFAULT_BASE_PREFIX)"
<root>/envs/myenv
<root>/envs/myenv

So anything driving menuinst from that environment — including conda itself, when it is the conda in that environment — hands menuinst the environment as both the target prefix and the base prefix, and menuinst concludes it is base. On main:

$ <root>/envs/myenv/bin/python -c "
from menuinst.platforms.base import Menu, MenuItem
from menuinst.utils import DEFAULT_PREFIX, DEFAULT_BASE_PREFIX
menu = Menu('MyApp', DEFAULT_PREFIX, DEFAULT_BASE_PREFIX)
print('env_name =', menu.env_name)
item = MenuItem(menu, {'name': {'target_environment_is_base': 'MyApp',
                                'target_environment_is_not_base': 'MyApp ({{ ENV_NAME }})'}})
print('name     =', item.metadata['name'])
"
env_name = base
name     = MyApp

The target prefix is <root>/envs/myenv, so both answers are wrong. {{ ENV_NAME }} renders as base, and the name object resolves to its target_environment_is_base value — which loses exactly the distinction that object exists to provide. Install the same package into several environments from such a conda and every environment's entries collide under one name.

The patch

conda lays named environments out as <root>/envs/<name>, and never puts a base prefix there. <root> is itself a conda prefix, so it carries conda-meta; requiring that keeps the rule from firing on an installation that merely happens to sit under a directory called envs. _is_base_prefix takes that layout as authoritative and falls back to comparing against base_prefix otherwise:

def _is_base_prefix(prefix, base_prefix):
    prefix = Path(prefix)
    if prefix.parent.name == "envs" and (prefix.parent.parent / "conda-meta").is_dir():
        return False
    return _same_path(prefix, base_prefix)

_same_path replaces the bare Path.samefile, which requires both paths to exist and raises otherwise. menuinst is handed prefixes that may already be gone — removing shortcuts for a prefix being torn down — and today that is a FileNotFoundError out of Menu.__init__ rather than a fallback. It also makes the comparison correct on case-insensitive filesystems:

def _same_path(path, other):
    try:
        return os.path.samefile(path, other)
    except OSError:
        return os.path.normcase(os.path.abspath(path)) == os.path.normcase(os.path.abspath(other))

Menu caches the answer as is_base_environment so the two decision sites share one definition instead of repeating the comparison.

With the patch, the reproduction above prints env_name = myenv and picks the target_environment_is_not_base value.

Should an explicit --root-prefix still win?

I chose no — the envs layout wins even when base_prefix was passed explicitly. My reasoning, and I am happy to be argued out of it:

  • The heuristic and base_prefix can only disagree in one situation: prefix == base_prefix and that path sits under a directory named envs and the directory above it is a conda prefix. Every other combination — including the ordinary --root-prefix <root> --prefix <root>/envs/myenv — already answers "not base" and is unchanged.
  • In that one situation, a caller is overwhelmingly likelier to be a conda-in-an-environment propagating its own broken notion of base than someone deliberately asserting that <root>/envs/myenv is a base installation.
  • conda's installers never place a base prefix under envs, so the rule costs nothing for installations laid out the normal way.

The conda-meta check narrows this further. A base installation that genuinely lives at a path like /opt/envs/mydist is still reported as base, because /opt is not a conda prefix. What remains is the case where someone deliberately treats <root>/envs/<name> of a real conda installation as a base prefix in its own right, which is the case this PR is arguing is a misreport rather than an intent. If you would rather have base_prefix win whenever it was explicitly supplied, that needs a way to distinguish "explicitly passed" from "defaulted", which today's signature does not have — I would rather add that than guess.

Scope: what I deliberately did not change

  • record_shortcuts in api.py also compares prefix.samefile(base_prefix), to decide where distribution_name gets written. That one is not asking "is this the base environment?" — it is pairing with _get_distribution_name, which reads $BASE_PREFIX/Menu/menuinst.toml. The write and the read have to agree on the same path, so changing only the write would strand the value. Left alone.
  • _get_distribution_name's base_prefix.name fallback has the same root cause: under a conda-in-an-environment it yields the environment's name as the distribution name. Fixing that needs the real root, which menuinst does not have here. Out of scope for this PR; worth its own issue if you want it tracked.
  • Custom envs_dirs. conda can be configured to create named environments somewhere that is not called envs, and this heuristic will not fire there. It is a safety net for the default layout, not a complete answer.

Testing

New tests/test_menu_prefixes.py, which drives Menu/MenuItem directly and runs on every platform:

  • base prefix → is_base_environment, env_name == "base", name resolves to the base value
  • prefix=<root>/envs/myenv, base_prefix=<root> → not base (regression guard; this already worked)
  • prefix == base_prefix == <root>/envs/myenv → not base — the bug
  • prefix == base_prefix outside any envs directory → still base, so the heuristic does not over-fire
  • prefix == base_prefix under an envs directory with no conda prefix above it → still base, so the conda-meta corroboration does its job
  • prefixes that do not exist → no FileNotFoundError

The new tests all reference is_base_environment, which does not exist on main, so "run them on main" only produces AttributeError. To show the behaviour genuinely changes I ran the assertions that do not need the new attribute against unpatched code:

$ python behaviour_check.py   # on main
AssertionError: env_name is 'base', expected 'myenv'

$ python behaviour_check.py   # with this patch
env_name='myenv'  name='MyApp ({{ ENV_NAME }})'
OK

Full suite on macOS (Python 3.12): 77 passed, 27 skipped, 1 failed. The failure is tests/test_elevation.py::test_elevation, which fails identically on unmodified main in this environment and is unrelated.

pre-commit run --all-files passes (all 19 hooks).

What I could not test. No Linux or Windows desktop session here, so the PLATFORM-gated and CI-gated integration tests did not run. I also could not exercise the Windows side of this, where the same base/non-base branch feeds shortcut naming. The reproduction above was run against a real conda installed into a real environment, but on macOS only.

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? Not yet — happy to document the rule next to target_environment_is_base if you are happy with it.

menuinst decides whether the target prefix is the base environment by
comparing it against `base_prefix`, which follows the running interpreter
via `DEFAULT_BASE_PREFIX`. A conda installed *into* an environment reports
that environment as its own base:

    $ <root>/envs/myenv/bin/conda info --base
    <root>/envs/myenv

so anything driving menuinst from such an environment passes it as both the
target prefix and the base prefix. `{{ ENV_NAME }}` then renders as `base`,
and a `name` given as a base/non-base object resolves to its
`target_environment_is_base` value, collapsing every environment's entries
onto one name.

conda lays named environments out as `<root>/envs/<name>`, which never names
a base prefix, so `_is_base_prefix` treats that layout as authoritative and
falls back to comparing against `base_prefix` otherwise. `Menu` caches the
answer as `is_base_environment` so both decision sites share it.
@github-project-automation github-project-automation Bot moved this to 🆕 New in 🔎 Review Aug 23, 2026
@conda-bot conda-bot added the cla-signed [bot] added once the contributor has signed the CLA label Aug 23, 2026
<details><summary>Claude's draft</summary>

Two hardening changes to _is_base_prefix.

Require conda-meta above the envs directory. `<root>/envs/<name>` is only
meaningful when `<root>` is itself a conda prefix, and every conda prefix
carries `conda-meta`. Checking for it removes the false positive the PR
description called out as its cost: a genuine base installation at a path
like /opt/envs/mydist is now still reported as base, because /opt is not
a conda prefix. The rule still fires for the case it is meant to catch,
where `<root>` is a real installation.

Do not let the comparison raise. `Path.samefile` requires both paths to
exist. menuinst is handed prefixes that may already be gone -- removing
shortcuts for a prefix being torn down -- and a missing path there is a
FileNotFoundError out of `Menu.__init__`, not a fallback. `_same_path`
tries `os.path.samefile` and falls back to comparing normalized absolute
paths, which also makes the comparison correct on case-insensitive
filesystems.

The test fixture now builds a realistic root (`conda-meta` in the root and
in the environment), plus two cases: an `envs` directory with no conda
prefix above it, and prefixes that do not exist.

Resume this Claude session:
```
cd /home/mark/git/feedstock/menuinst-src
claude --resume b888d0ac-7cec-4e1e-ac68-9c15e6554011
```
</details>

Claude-Session: https://claude.ai/code/session_0135Eijr6BTzjVHcRcn3JP8w
hmaarrfk added a commit to hmaarrfk/menuinst-feedstock that referenced this pull request Aug 23, 2026
<details><summary>Claude's draft</summary>

Two changes to 538.patch, matching what was pushed to
conda/menuinst#538:

Require conda-meta above the envs directory. `<root>/envs/<name>` only
means "named environment" when `<root>` is itself a conda prefix, and
every conda prefix carries conda-meta. Checking for it removes the false
positive the original heuristic accepted: a genuine base installation at
a path like /opt/envs/mydist is still reported as base, because /opt is
not a conda prefix.

Do not let the comparison raise. Path.samefile requires both paths to
exist, so a prefix that has already been removed -- deleting shortcuts
for a prefix being torn down -- raised FileNotFoundError out of
Menu.__init__ instead of falling back. _same_path tries os.path.samefile
and falls back to comparing normalized absolute paths, which also makes
the comparison correct on case-insensitive filesystems.

334.patch's three `self.menu.prefix.samefile(self.menu.base_prefix)` call
sites in linux.py now use `menu.is_base_environment`, so the
target_environment_is_base dispatch agrees with the one in base.py.
Without this they kept the old comparison and took the is_base branch in
every environment, which is the bug 538 is fixing.

Windows builds are restricted to Python 3.12 and 3.14. Rendered matrix is
linux_64_ (noarch), win_64 3.12, win_64 3.14 and win_arm64 -- four jobs
instead of seven.

Build 205 rather than reusing 204, since 204 was already built in CI from
the previous 538.patch and the two are not the same package.

Verified on a pristine 2.5.2 tarball: all six patches apply in order
under both `patch -p1` and `git apply -p1`, upstream suite is 79 passed /
35 skipped, and installing a menu item from a conda-in-an-environment
produces `Name=MyApp (myenv)` with no run_in_bash key in the .desktop
file.

Resume this Claude session:
```
cd /home/mark/git/feedstock/menuinst-feedstock
claude --resume b888d0ac-7cec-4e1e-ac68-9c15e6554011
```
</details>

Claude-Session: https://claude.ai/code/session_0135Eijr6BTzjVHcRcn3JP8w
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.

2 participants