Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
143 changes: 118 additions & 25 deletions conda_build/_rattler_build/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations

import os
import sys
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING
Expand All @@ -13,16 +16,17 @@
RattlerBuildError,
RecipeParseError,
)
from rattler_build.debug import DebugSession
from rattler_build.progress import SimpleProgressCallback
from rattler_build.render import RenderConfig
from rattler_build.stage0 import Stage0Recipe
from rattler_build.stage0 import MultiOutputRecipe, Stage0Recipe
from rattler_build.tool_config import PlatformConfig, ToolConfiguration
from rattler_build.variant_config import VariantConfig

from ..build import handle_anaconda_upload
from ..config import CondaPkgFormat
from ..exceptions import CondaBuildUserError
from ..utils import get_logger
from ..utils import get_logger, on_win

if TYPE_CHECKING:
import argparse
Expand All @@ -48,6 +52,7 @@ class RecipeResult:
recipe_path: str
outputs: list[OutputResult] = field(default_factory=list)
error: str | None = None
activation_string: str | None = None

@property
def failed(self) -> bool:
Expand Down Expand Up @@ -125,6 +130,15 @@ def check_arguments_rattler(
"channel",
"override_channels",
},
"debug": {
"recipe",
"output_id",
"variant_config_files",
"exclusive_config_files",
"channel",
"override_channels",
"activate_string_only",
},
}

# check for unsupported CLI arguments
Expand Down Expand Up @@ -198,6 +212,62 @@ def process_recipe(
print(yaml.safe_dump(data, indent=2, sort_keys=False))
return result

if command == "debug":
if isinstance(recipe, MultiOutputRecipe):
if not list(rendered):
sys.exit(
f"No rendered outputs were produced for {Path(recipe_path).resolve()}"
)
if parsed_args.output_id is None:
output_names = sorted(
{
variant.recipe.to_dict().get("package", {}).get("name")
for variant in rendered
}
)
raise CondaBuildUserError(
f"\nFound {len(output_names)} outputs in recipe. Please specify one "
f"using --output-id. Available outputs: {', '.join(output_names)}"
)
else:
selected_output = None
for variant in rendered:
name = variant.recipe.to_dict().get("package", {}).get("name")
if name == parsed_args.output_id:
selected_output = variant
break

if selected_output is None:
raise CondaBuildUserError(
f"Output '{parsed_args.output_id}' not found. "
)

else:
selected_output = rendered[0]
try:
session = DebugSession.create(
variant=selected_output,
tool_config=tool_config,
output_dir=os.path.join(output_dir, f"debug_{int(time.time() * 1000)}"),
channels=channels,
progress_callback=CondaProgressCallback(show_logs=True),
)
except RattlerBuildError as e:
result.error = (
f"Failed to setup debug scripts for output {selected_output}: {e}"
)
return result

result.activation_string = (
"cd {work_dir} && {source} {activation_file}\n".format(
work_dir=session.paths.work_dir,
source="call" if on_win else "source",
activation_file=session.paths.build_env_script,
)
)

return result

for i, variant in enumerate(rendered, 1):
print(
f"\nBuilding variant {i}/{len(rendered)} for recipe {Path(recipe_path).resolve()}"
Expand Down Expand Up @@ -291,9 +361,11 @@ def process_recipe(
return result


def run_rattler(command: str, parsed_args: argparse.Namespace, config: Config) -> int:
def run_rattler(
command: str, parsed_args: argparse.Namespace, config: Config
) -> str | int:
"""Run rattler-build for v1 recipes"""
if command not in ("build", "render"):
if command not in ("build", "debug", "render"):
raise ValueError(f"Unrecognized subcommand: {command}")

# Initialize configuration defaults
Expand Down Expand Up @@ -416,7 +488,7 @@ def get_config_value(name):
else:
package_format = ".tar.bz2"

if command == "render":
if command in ("debug", "render"):
recipes = [str(Path(parsed_args.recipe) / "recipe.yaml")]
else:
recipes = [
Expand Down Expand Up @@ -444,27 +516,48 @@ def get_config_value(name):
)
)

if command == "render":
failed = [r for r in recipe_results if r.failed]
if failed:
msg = "\n".join(
[
"Recipe render failures:",
*[
f" - {Path(r.recipe_path).resolve()}: {r.error or 'Unknown error'}"
for r in failed
],
]
)
raise CondaBuildUserError(msg)
return 0
if command == "render":
# we are expecting a single recipe result
result = recipe_results[0]
if result.failed:
recipe_path = Path(result.recipe_path).resolve()

raise CondaBuildUserError(
"\n".join(
[
"Error: × Failed to render recipe",
f" ╰─▶ {recipe_path}",
"",
f" {result.error}",
]
)
)
return 0

if command == "debug":
# we are expecting a single recipe result
result = recipe_results[0]
if result.failed:
recipe_path = Path(result.recipe_path).resolve()

raise CondaBuildUserError(
"\n".join(
[
"Error: Failed to debug recipe",
f" ╰─▶ {recipe_path}",
"",
f" {result.error}",
]
)
)
return result.activation_string

recipe_count = len(recipe_results)
total_outputs = sum(len(r.outputs) for r in recipe_results)
succeeded_outputs = sum(
1 for r in recipe_results for output in r.outputs if output.success
)
failed_outputs = total_outputs - succeeded_outputs
recipe_count = len(recipe_results)
total_outputs = sum(len(r.outputs) for r in recipe_results)
succeeded_outputs = sum(
1 for r in recipe_results for output in r.outputs if output.success
)
failed_outputs = total_outputs - succeeded_outputs

print("\n=== Build summary ===")
print(
Expand Down
31 changes: 31 additions & 0 deletions conda_build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@
StatsDict = dict[str, Any]


def _error_if_package_contains_recipe_yaml(pkg_path: str) -> None:
from contextlib import redirect_stdout
from io import StringIO

import conda_package_handling.api

buffer = StringIO()
with redirect_stdout(buffer):
conda_package_handling.api.list_contents(
pkg_path,
components=["info"],
)

pkg_contents = [
line.strip() for line in buffer.getvalue().splitlines() if line.strip()
]

for recipe_yaml_file in (
"info/recipe/recipe.yaml",
"info/recipe/rendered_recipe.yaml",
):
if recipe_yaml_file in pkg_contents:
raise ValueError(
f"Package '{pkg_path}' contains v1 '{recipe_yaml_file.rsplit('/', 1)[-1]}' file, "
"which is currently not supported by conda debug."
)


def render(
recipe_path: str | os.PathLike | Path,
config: Config | None = None,
Expand Down Expand Up @@ -659,6 +687,9 @@ def debug(
)
else:
test_input = recipe_or_package_path_or_metadata_tuples

_error_if_package_contains_recipe_yaml(test_input)

# use the package to create an env and extract the test files. Stop short of running the tests.
# tell people what steps to take next
with log_context:
Expand Down
38 changes: 31 additions & 7 deletions conda_build/cli/main_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

import logging
import sys
from pathlib import Path
from typing import TYPE_CHECKING

from conda.base.context import context

from .. import api
from ..utils import on_win
from .._rattler_build.compat import check_arguments_rattler, run_rattler
from ..config import get_or_merge_config
from ..utils import is_v1_recipe, on_win
from . import validators as valid
from .main_render import get_render_parser

Expand Down Expand Up @@ -98,12 +101,29 @@ def execute(args: Sequence[str] | None = None) -> int:
parsed = parser.parse_args(args)
context.__init__(argparse_args=parsed)

try:
activation_string = api.debug(
parsed.recipe_or_package_file_path,
verbose=(not parsed.activate_string_only),
**parsed.__dict__,
# mixed recipe formats found, error out
if (Path(parsed.recipe_or_package_file_path) / "recipe.yaml").is_file() and (
Path(parsed.recipe_or_package_file_path) / "meta.yaml"
).is_file():
print(
"Cannot process several recipe versions at the same time!", file=sys.stderr
)
return 1

try:
if is_v1_recipe(parsed.recipe_or_package_file_path):
config = get_or_merge_config(None, **parsed.__dict__)
parsed_only_recipe = parser.parse_args([parsed.recipe_or_package_file_path])
check_arguments_rattler(parser.prog.split()[-1], parsed, parsed_only_recipe)
parsed.recipe = parsed.recipe_or_package_file_path
command = parser.prog.split()[-1]
activation_string = run_rattler(command, parsed, config)
else:
activation_string = api.debug(
parsed.recipe_or_package_file_path,
verbose=(not parsed.activate_string_only),
**parsed.__dict__,
)

if not parsed.activate_string_only:
print("#" * 80)
Expand All @@ -113,7 +133,11 @@ def execute(args: Sequence[str] | None = None) -> int:

print(activation_string)
if not parsed.activate_string_only:
test_file = "conda_test_runner.bat" if on_win else "conda_test_runner.sh"
test_file = (
f"conda_{'build' if is_v1_recipe(parsed.recipe_or_package_file_path) else 'test_runner'}."
f"{'bat' if on_win else 'sh'}"
)

print(
f"To run your tests, you might want to start with running the {test_file} file."
)
Expand Down
4 changes: 3 additions & 1 deletion conda_build/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@

from . import environ, exceptions, source, utils
from .config import CondaPkgFormat
from .exceptions import CondaBuildUserError, DependencyNeedsBuildingError
from .exceptions import CondaBuildUserError, DependencyNeedsBuildingError, RecipeError
from .index import get_build_index
from .metadata import MetaData, MetaDataTuple, combine_top_level_metadata_with_output
from .utils import (
Expand Down Expand Up @@ -1082,6 +1082,8 @@ def render_recipe(
m = MetaData(str(recipe), config=config)
except exceptions.YamlParsingError as e:
sys.exit(e.error_msg())
except OSError as e:
raise RecipeError(str(e)) from e

# important: set build id *before* downloading source. Otherwise source goes into a different
# build folder.
Expand Down
16 changes: 13 additions & 3 deletions docs/source/user-guide/v1-recipes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ To get started with building v1 recipes, simply invoke ``conda-build`` and pass
the recipe's directory. ``conda-build`` will recognize the recipe format and
handle the build through ``py-rattler-build``.

``conda-build`` currently supports v1 recipes in the ``conda build`` and
``conda render`` commands.
``conda-build`` currently supports v1 recipes in the ``conda build``,
``conda render`` and ``conda-debug`` commands.

Build configuration is done in the same way as for v0 recipes. Many configuration settings are translated into rattler-build equivalents and
passed to ``py-rattler-build``. The following ``conda-build`` command-line arguments (and their corresponding settings in ``~/.condarc`` file) are
Expand Down Expand Up @@ -48,6 +48,17 @@ supported:
- ``--recipe``
- ``--variant-config-files``

``conda debug``
================

- ``--activate-string-only``
- ``--channel``
- ``--exclusive-config-file``
- ``--output-id``
- ``--override-channels``
- ``--recipe``
- ``--variant-config-files``

Package upload
==============

Expand Down Expand Up @@ -88,7 +99,6 @@ For example, to authenticate with a private channel hosted on ``prefix.dev``:
After this setup, ``conda-build`` will be able to access packages hosted on the
private channel.


Limitations
===========

Expand Down
19 changes: 19 additions & 0 deletions news/5950-conda-debug-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Add support for v1 recipes to `conda-debug` (#5950).

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* Add `conda-debug` command to v1 recipe docs. (#5950)

### Other

* <news item>
Loading
Loading