Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop injecting installed channels that do not exist #274

Merged
merged 22 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions conda_libmamba_solver/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from conda.base.constants import REPODATA_FN, UNKNOWN_CHANNEL, ChannelPriority, on_win
from conda.base.context import context
from conda.common.constants import NULL
from conda.common.io import Spinner
from conda.common.io import Spinner, timeout
from conda.common.path import paths_equal
from conda.common.url import join_url, percent_decode
from conda.core.prefix_data import PrefixData
Expand All @@ -44,6 +44,7 @@
from .index import LibMambaIndexHelper, _CachedLibMambaIndexHelper
from .mamba_utils import init_api_context, mamba_version
from .state import SolverInputState, SolverOutputState
from .utils import is_channel_available

log = logging.getLogger(f"conda.{__name__}")

Expand Down Expand Up @@ -171,21 +172,23 @@ def solve_final_state(
else:
IndexHelper = LibMambaIndexHelper

if os.getenv("CONDA_LIBMAMBA_SOLVER_NO_CHANNELS_FROM_INSTALLED") or (
getattr(context, "_argparse_args", None) or {}
).get("override_channels"):
# see https://github.com/conda/conda-libmamba-solver/issues/108
channels_from_installed = ()
else:
channels_from_installed = in_state.channels_from_installed()

all_channels = (
all_channels = [
*conda_bld_channels,
*self.channels,
*in_state.channels_from_specs(),
*channels_from_installed,
*in_state.maybe_free_channel(),
)
]
override = (getattr(context, "_argparse_args", None) or {}).get("override_channels")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wonder if conda should have a context flag for override_channels. This is a bit clunky and, unless I am missing a good reason, unnecessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

agreed

I suspect the reasoning was because it would mean users could set it in their condarc and override channel settings set by sys admins

if not os.getenv("CONDA_LIBMAMBA_SOLVER_NO_CHANNELS_FROM_INSTALLED") and not override:
# see https://github.com/conda/conda-libmamba-solver/issues/108
all_urls = [url for c in all_channels for url in Channel(c).urls(False)]
installed_channels = in_state.channels_from_installed(seen=all_urls)
for channel in installed_channels:
# Only add to list if resource is available; check has timeout=1s
if timeout(1, is_channel_available, channel.base_url, default_return=False):
jezdez marked this conversation as resolved.
Show resolved Hide resolved
all_channels.append(channel)
all_channels.extend(in_state.maybe_free_channel())

all_channels = tuple(all_channels)
with Spinner(
self._spinner_msg_metadata(all_channels, conda_bld_channels=conda_bld_channels),
enabled=not context.verbosity and not context.quiet,
Expand Down Expand Up @@ -858,8 +861,19 @@ def _package_record_from_json_payload(
if pkg_filename.startswith("__") and "/@/" in channel:
return PackageRecord(**json.loads(json_payload))

channel_info = index.get_info(channel)
kwargs = json.loads(json_payload)
try:
channel_info = index.get_info(channel)
except KeyError:
# this channel was never used to build the index, which
# means we obtained an already installed PackageRecord
# whose metadata contains a channel that doesn't exist
pd = PrefixData(self.prefix)
record = pd.get(kwargs["name"])
if record and record.fn == pkg_filename:
return record

# Otherwise, these are records from the index
kwargs["fn"] = pkg_filename
kwargs["channel"] = channel_info.channel
kwargs["url"] = join_url(channel_info.full_url, pkg_filename)
Expand Down
14 changes: 8 additions & 6 deletions conda_libmamba_solver/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,8 @@ def channels_from_specs(self) -> Iterable[Channel]:
channel = Channel(spec.original_spec_str.split("::")[0])
yield channel

def channels_from_installed(self) -> Iterable[Channel]:
seen_urls = set()
def channels_from_installed(self, seen=None) -> Iterable[Channel]:
seen_urls = set(seen or [])
# See https://github.com/conda/conda/issues/11790
for record in self.installed.values():
if record.channel.auth or record.channel.token:
Expand All @@ -422,10 +422,12 @@ def channels_from_installed(self) -> Iterable[Channel]:
# These "channels" are not really channels, more like
# metadata placeholders
continue
subdir_url = record.channel.subdir_url
if subdir_url not in seen_urls:
seen_urls.add(subdir_url)
yield record.channel
if record.channel.base_url is None:
continue
if record.channel.subdir_url in seen_urls:
continue
seen_urls.add(record.channel.subdir_url)
yield record.channel

def maybe_free_channel(self) -> Iterable[Channel]:
if context.restore_free_channel:
Expand Down
24 changes: 24 additions & 0 deletions conda_libmamba_solver/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Copyright (C) 2022 Anaconda, Inc
# Copyright (C) 2023 conda
# SPDX-License-Identifier: BSD-3-Clause
from functools import lru_cache
from logging import getLogger
from pathlib import Path
from urllib.parse import quote

from conda.base.context import context
from conda.common.compat import on_win
from conda.common.path import url_to_path
from conda.common.url import urlparse
from conda.gateways.connection import session as gateway_session

log = getLogger(f"conda.{__name__}")

Expand All @@ -31,3 +36,22 @@ def escape_channel_url(channel):
parts = parts.replace(path=path)
return str(parts)
return channel


@lru_cache(maxsize=None)
def is_channel_available(channel_url) -> bool:
if context.offline:
# We don't know where the channel might be (even file:// might be a network share)
# so we play it safe and assume it's not available
return False
try:
if channel_url.startswith("file://"):
return Path(url_to_path(channel_url)).is_dir()
if hasattr(gateway_session, "get_session"):
session = gateway_session.get_session(channel_url)
else:
session = gateway_session.CondaSession()
return session.head(f"{channel_url}/noarch/repodata.json").ok
except Exception as exc:
log.debug("Failed to check if channel %s is available", channel_url, exc_info=exc)
return False
6 changes: 5 additions & 1 deletion dev/linux/bashrc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@ source /opt/conda-src/dev/linux/bashrc.sh

cd /opt/conda-libmamba-solver-src

set +e
set -x
conda list -p /opt/conda
conda info
conda config --show-sources
set +ex
19 changes: 19 additions & 0 deletions news/274-ignore-unavailable-channels
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* <news item>

### Bug fixes

* Do not inject those channels from installed packages that do not exist or are unavailable. (#262 via #274)

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
22 changes: 22 additions & 0 deletions tests/test_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import pytest
from conda.common.compat import on_linux
from conda.common.io import env_vars
from conda.core.prefix_data import PrefixData
from conda.models.channel import Channel
from conda.testing.integration import _get_temp_prefix, make_temp_env
from conda.testing.integration import run_command as conda_inprocess

Expand Down Expand Up @@ -62,6 +64,26 @@ def test_channels_prefixdata():
)


def test_channels_installed_unavailable():
"Ensure we don't fail if a channel coming ONLY from an installed pkg is unavailable"
with make_temp_env("xz", "--solver=libmamba", use_restricted_unicode=True) as prefix:
pd = PrefixData(prefix)
pd.load()
record = pd.get("xz")
assert record
record.channel = Channel.from_url("file:///nonexistent")

_, _, retcode = conda_inprocess(
"install",
prefix,
"zlib",
"--solver=libmamba",
"--dry-run",
use_exception_handler=True,
)
assert retcode == 0


def _setup_channels_alias(prefix):
write_env_config(
prefix,
Expand Down
Loading