Skip to content

Commit 351d00f

Browse files
committed
Add support for multi-output recipes
1 parent 25d657f commit 351d00f

File tree

5 files changed

+126
-37
lines changed

5 files changed

+126
-37
lines changed

conda_build/_rattler_build/compat.py

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from os.path import join
55
from pathlib import Path
66

7+
from conda import CondaError
78
from conda.base.context import context
9+
from rattler_build import RattlerBuildError
810
from rattler_build.progress import RichProgressCallback
911
from rattler_build.render import RenderConfig
10-
from rattler_build.stage0 import Stage0Recipe
12+
from rattler_build.stage0 import MultiOutputRecipe, Stage0Recipe
1113
from rattler_build.tool_config import PlatformConfig, ToolConfiguration
1214
from rattler_build.variant_config import VariantConfig
1315

@@ -91,8 +93,6 @@ def check_arguments_rattler(
9193
def process_recipes(
9294
recipes: list[str],
9395
variant_config: VariantConfig,
94-
render_config: RenderConfig,
95-
tool_config: ToolConfiguration,
9696
command: str,
9797
output_dir: str,
9898
channels: list[str],
@@ -101,6 +101,15 @@ def process_recipes(
101101
package_format: str,
102102
no_include_recipe: bool,
103103
debug: bool,
104+
target_platform: str,
105+
build_platform: str,
106+
host_platform: str,
107+
experimental: bool,
108+
extra_context: dict[str],
109+
test_strategy: str,
110+
skip_existing: bool,
111+
noarch_build_platform: str,
112+
channel_priority: str,
104113
) -> int:
105114
import yaml
106115

@@ -113,11 +122,37 @@ def process_recipes(
113122
# load the recipe file
114123
try:
115124
recipe = Stage0Recipe.from_file(Path(recipe_path))
116-
except Exception as e:
125+
except RattlerBuildError as e:
117126
raise CondaBuildUserError(
118127
f"Failed to process recipe {recipe_path}: {str(e)}"
119128
)
120129

130+
if isinstance(recipe, MultiOutputRecipe):
131+
for idx, output in enumerate(recipe.outputs, 1):
132+
output_dict = output.to_dict()
133+
if "staging" in output_dict:
134+
experimental = True
135+
136+
# common tool / platform / render configuration
137+
tool_config = ToolConfiguration(
138+
test_strategy=test_strategy,
139+
skip_existing=skip_existing,
140+
noarch_build_platform=noarch_build_platform,
141+
channel_priority=channel_priority,
142+
)
143+
144+
platform_config = PlatformConfig(
145+
target_platform=target_platform,
146+
build_platform=build_platform,
147+
host_platform=host_platform,
148+
experimental=experimental,
149+
)
150+
151+
render_config = RenderConfig(
152+
platform=platform_config,
153+
extra_context=extra_context,
154+
)
155+
121156
# render the recipe
122157
try:
123158
rendered = recipe.render(variant_config, render_config)
@@ -127,8 +162,9 @@ def process_recipes(
127162
)
128163

129164
if command == "render":
130-
data = rendered[0].recipe.to_dict()
131-
print(yaml.safe_dump(data, indent=2, sort_keys=False))
165+
for item in rendered:
166+
data = item.recipe.to_dict()
167+
print(yaml.safe_dump(data, indent=2, sort_keys=False))
132168
succeeded.append(recipe_path_str)
133169
continue
134170

@@ -151,9 +187,8 @@ def process_recipes(
151187
no_include_recipe=no_include_recipe,
152188
debug=debug,
153189
)
154-
except Exception as e:
155-
print(f"Build failed for recipe {recipe_path}: {str(e)}")
156-
raise
190+
except RattlerBuildError as e:
191+
raise CondaError(f"Build failed for recipe {recipe_path}: {str(e)}")
157192

158193
# if all variants built without raising, mark recipe as succeeded
159194
succeeded.append(recipe_path_str)
@@ -195,6 +230,7 @@ def run_rattler(command: str, parsed_args: argparse.Namespace, config: Config) -
195230
output_dir: str = config.croot
196231
no_include_recipe: bool = False
197232
no_build_id: bool = False
233+
experimental: bool = False
198234
package_format: str | None = None
199235
debug: bool = False
200236
channels: list[str] = []
@@ -206,6 +242,7 @@ def run_rattler(command: str, parsed_args: argparse.Namespace, config: Config) -
206242
noarch_build_platform: str = config.variant.get(
207243
"noarch_build_platform", config.subdir
208244
)
245+
variant_config: VariantConfig = VariantConfig()
209246

210247
# TODO: investigate why is config.channel_urls
211248
# does not pick up condarc settings, need to use context.channels
@@ -293,36 +330,14 @@ def run_rattler(command: str, parsed_args: argparse.Namespace, config: Config) -
293330
join(recipe_dir, "recipe.yaml") for recipe_dir in parsed_args.recipe
294331
]
295332

296-
from ..variants import find_config_files
297-
298333
# configure variant
299-
for variant in config_files:
300-
variant_config = VariantConfig.from_file(variant)
301-
302-
# common tool / platform / render configuration
303-
tool_config = ToolConfiguration(
304-
test_strategy=test_strategy,
305-
skip_existing=skip_existing,
306-
noarch_build_platform=noarch_build_platform,
307-
channel_priority=channel_priority,
308-
)
309-
310-
platform_config = PlatformConfig(
311-
target_platform=target_platform,
312-
build_platform=build_platform,
313-
host_platform=host_platform,
314-
)
315-
316-
render_config = RenderConfig(
317-
platform=platform_config,
318-
extra_context=extra_context,
319-
)
334+
if config_files:
335+
for variant in config_files:
336+
variant_config = VariantConfig.from_file(variant)
320337

321338
return process_recipes(
322339
recipes=recipes,
323340
variant_config=variant_config,
324-
render_config=render_config,
325-
tool_config=tool_config,
326341
command=command,
327342
output_dir=output_dir,
328343
channels=channels,
@@ -331,4 +346,13 @@ def run_rattler(command: str, parsed_args: argparse.Namespace, config: Config) -
331346
package_format=package_format,
332347
no_include_recipe=no_include_recipe,
333348
debug=debug,
349+
target_platform=target_platform,
350+
build_platform=build_platform,
351+
host_platform=host_platform,
352+
experimental=experimental,
353+
extra_context=extra_context,
354+
test_strategy=test_strategy,
355+
skip_existing=skip_existing,
356+
noarch_build_platform=noarch_build_platform,
357+
channel_priority=channel_priority,
334358
)

recipe/meta.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ requirements:
5050
- pkginfo
5151
- psutil
5252
- py-lief
53-
- py-rattler-build >=0.58.0
53+
- py-rattler-build >=0.58.2
5454
- python
5555
- python-libarchive-c
5656
- pytz

tests/cli/test_main_build.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,9 +550,23 @@ def test_build_with_empty_channel_fails(empty_channel: Path) -> None:
550550
)
551551

552552

553-
def test_build_with_v1_recipe() -> None:
553+
def test_build_v1_recipe() -> None:
554554
"""Test building a v1 recipe"""
555555
recipe = os.path.join(metadata_dir, "..", "variants", "32_v1_recipe")
556556

557557
args = [recipe]
558558
assert main_build.execute(args) == 0
559+
560+
561+
def test_build_v1_recipe_multi_output(testing_workdir: str) -> None:
562+
"""Test building a multi-output v1 recipe"""
563+
recipe = os.path.join(metadata_dir, "..", "variants", "33_v1_recipe_multi_output")
564+
565+
out = Path(testing_workdir, "out")
566+
out.mkdir(parents=True)
567+
568+
args = [recipe, "--output-folder", str(out)]
569+
assert main_build.execute(args) == 0
570+
571+
conda_packages = list(out.rglob("*.conda"))
572+
assert len(conda_packages) == 2

tests/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
beautifulsoup4
22
chardet
33
conda >=25.11.0
4-
conda-forge::py-rattler-build >=0.58.0 # v1 recipe support
4+
conda-forge::py-rattler-build >=0.58.2 # v1 recipe support
55
conda-index >=0.4.0
66
conda-libmamba-solver >=25.11.0 # includes fix for CondaSolver deprecation warnings
77
conda-package-handling >=2.2.0
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
schema_version: 1
2+
3+
context:
4+
name: myproject
5+
version: "2.0.0"
6+
7+
recipe:
8+
version: ${{ version }}
9+
10+
outputs:
11+
# First output: The library
12+
- package:
13+
name: ${{ name }}-lib
14+
build:
15+
script:
16+
interpreter: python
17+
content: |
18+
import os
19+
from pathlib import Path
20+
21+
prefix = Path(os.environ["PREFIX"])
22+
lib_dir = prefix / "lib" / "python"
23+
lib_dir.mkdir(parents=True, exist_ok=True)
24+
25+
(lib_dir / "myproject_lib.py").write_text('VERSION = "2.0.0"')
26+
print(f"Created library at {lib_dir}")
27+
requirements:
28+
build:
29+
- python
30+
31+
# Second output: Uses the library as a host dependency
32+
- package:
33+
name: ${{ name }}-tools
34+
build:
35+
script:
36+
interpreter: python
37+
content: |
38+
import os
39+
from pathlib import Path
40+
41+
prefix = Path(os.environ["PREFIX"])
42+
43+
# Read and print the lib file (installed as host dependency)
44+
lib_file = prefix / "lib" / "python" / "myproject_lib.py"
45+
print(f"Reading library from: {lib_file}")
46+
print(lib_file.read_text())
47+
requirements:
48+
build:
49+
- python
50+
host:
51+
- ${{ name }}-lib

0 commit comments

Comments
 (0)