Skip to content

Commit f78c16b

Browse files
committed
add an R tutorial and update python tutorial with abi3
1 parent 2fbed46 commit f78c16b

File tree

7 files changed

+224
-4
lines changed

7 files changed

+224
-4
lines changed

docs/build_script.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Build scripts
1+
# Scripts for building and testing packages
22

33
The `build.sh` file is the build script for Linux and macOS and `build.bat` is
44
the build script for Windows. These scripts contain the logic that carries out
@@ -77,6 +77,21 @@ So far, the following interpreters are supported:
7777
- `cmd.exe` (default on Windows)
7878
- `nushell`
7979
- `python`
80+
- `perl`
81+
- `rscript` (for R scripts)
82+
83+
`rattler-build` automatically detects the interpreter based on the file extension
84+
(`.sh`, `.bat`, `.nu`, `.py`, `.pl`, `.r`) or you can specify it in the
85+
`interpreter` key in the `script` section of your recipe.
86+
87+
```yaml title="recipe.yaml"
88+
build:
89+
script: myscript.py # automatically selects the Python interpreter
90+
91+
requirements:
92+
build:
93+
- python # required to execute the `myscript.py` script
94+
```
8095

8196
!!! note
8297
Using alternative interpreters is less battle-tested than using `bash` or

docs/tutorials/python.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
Writing a Python package is fairly straightforward, especially for "Python-only" packages.
44
In the second example we will build a package for `numpy` which contains compiled code.
55

6+
## Generating a starter recipe
7+
8+
Rattler-build provides a command to generate a recipe for a package from PyPI.
9+
The generated recipe can be used as a starting point for your recipe.
10+
The recipe generator will fetch the metadata from PyPI and generate a recipe that will build the package from the `sdist` source distriution.
11+
12+
```bash
13+
rattler-build generate-recipe pypi ipywidgets
14+
# select an older version of the package
15+
rattler-build generate-recipe pypi ipywidgets --version 8.0.0
16+
```
17+
618
## A Python-only package
719

820
The following recipe uses the `noarch: python` setting to build a `noarch` package that can be installed on any platform without modification.
@@ -210,3 +222,118 @@ numpy-1.26.4-py312h440f24a_0
210222
│ target_platform ┆ osx-arm64 │
211223
╰─────────────────┴───────────╯
212224
```
225+
226+
## An ABI3-compatible package
227+
228+
Certain packages contain compiled code that is compatible with multiple Python versions.
229+
This is the case e.g. for a lot of Rust / PyO3 based Python extensions.
230+
231+
In this case, you can use the special `abi3` settings to build a package that is specific to a certain operating system and architecture, but compatible with multiple Python versions.
232+
233+
Note: this feature relies on the `python-abi3` package which exists in the `conda-forge` channel.
234+
The full recipe can be found on [`conda-forge/py-rattler-feedstock`](https://github.com/conda-forge/py-rattler-feedstock)
235+
236+
```yaml title="recipe.yaml"
237+
context:
238+
name: py-rattler
239+
python_name: py_rattler
240+
version: "0.11.0"
241+
python_min: "3.8"
242+
243+
package:
244+
name: py-rattler
245+
version: ${{ version }}
246+
247+
source:
248+
url: https://pypi.org/packages/source/${{ name[0] }}/${{ name }}/${{ python_name }}-${{ version }}.tar.gz
249+
sha256: b00f91e19863741ce137a504eff3082c0b0effd84777444919bd83357530867f
250+
251+
build:
252+
number: 0
253+
script: build.sh
254+
python:
255+
version_independent: true
256+
257+
requirements:
258+
build:
259+
- ${{ compiler('c') }}
260+
- ${{ compiler('rust') }}
261+
- cargo-bundle-licenses
262+
host:
263+
- python ${{ python_min }}.*
264+
- python-abi3 ${{ python_min }}.* # (1)!
265+
- maturin >=1.2.2,<2
266+
- pip
267+
- if: unix
268+
then:
269+
- openssl
270+
run:
271+
- python >=${{ python_min }}
272+
273+
tests:
274+
- python:
275+
imports:
276+
- rattler
277+
python_version: ["${{ python_min ~ '.*' }}"] # (2)!
278+
# You could run `abi3audit` here, but it is not necessary
279+
# - script:
280+
# - abi3audit ${{ SP_DIR }}/spam.abi3.so -s -v --assume-minimum-abi3 ${{ python_min }}
281+
# requirements:
282+
# run:
283+
# - abi3audit
284+
285+
about:
286+
homepage: https://github.com/conda/rattler
287+
license: BSD-3-Clause
288+
license_file:
289+
- LICENSE
290+
- py-rattler/THIRDPARTY.yml
291+
summary: A blazing fast library to work with the conda ecosystem
292+
description: |
293+
Rattler is a library that provides common functionality used within the conda
294+
ecosystem. The goal of the library is to enable programs and other libraries to
295+
easily interact with the conda ecosystem without being dependent on Python. Its
296+
primary use case is as a library that you can use to provide conda related
297+
workflows in your own tools.
298+
repository: https://github.com/conda/rattler
299+
```
300+
301+
1. The `python-abi3` package is a special package that ensures that the run dependencies
302+
are compatible with the ABI3 standard.
303+
2. The `python_version` setting is used to test against the oldest compatible Python version.
304+
305+
## Testing Python packages
306+
307+
Testing Python packages is done using the `tests` section of the recipe.
308+
We can either use a special "python" test or a regular script test to test the package.
309+
310+
All tests will have the current package and all it's run dependencies installed in an isolated environment.
311+
312+
```yaml title="recipe.yaml"
313+
# contents of the recipe.yaml file
314+
tests:
315+
- python:
316+
# The Python test type will simply import packages as a sanity check.
317+
imports:
318+
- rattler
319+
- rattler.version.Version
320+
# You can select different Python versions to test against.
321+
python_version: ["${{ python_min ~ '.*' }}", "3.12.*"] # (1)!
322+
323+
# You can run a script test to run arbitrary code.
324+
- script:
325+
- pytest ./tests
326+
requirements: # (2)!
327+
run:
328+
- pytest
329+
files: # (3)!
330+
source:
331+
- tests/
332+
# You can also directly execute a Python script and run some tests from it.
333+
# The script is searched in the `recipe` directory.
334+
- script: mytest.py
335+
```
336+
337+
1. The `python_version` setting is used to test against different Python versions. It is useful to test against the minimum version of Python that the package supports.
338+
2. We can add additional requirements for the test run. such as pytest, pytest-cov, ... – you can also specify a `python` version here by adding e.g. `python 3.12.*` to the run requirements.
339+
3. This will copy over the tests from the source directory into the package. Note that this makes the package larger, so you might want to use a different approach for larger packages.

docs/tutorials/r.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Packaging a R (CRAN) package
2+
3+
Packaging a R package is similar to packaging a Python package!
4+
5+
## Generating a starting point
6+
7+
You can use rattler-build to generate a starting point for your recipe from the metadata on CRAN.
8+
9+
```bash
10+
rattler-build generate-recipe cran r-knitr
11+
```
12+
13+
## Building a R Package
14+
15+
```yaml title="recipe.yaml"
16+
context:
17+
version: "1.47"
18+
19+
package:
20+
name: r-knitr
21+
version: ${{ version }}
22+
noarch: generic # (4)!
23+
24+
source:
25+
- url: https://cran.r-project.org/src/contrib/Archive/knitr/knitr_${{ version }}.tar.gz
26+
sha256: fadd849bf94a4e02520088a6626577c3c636227fe11c5cd7e8fcc5d51a7aa6cf
27+
28+
build:
29+
script: R CMD INSTALL --build . # (1)!
30+
31+
requirements:
32+
host:
33+
- r-base # (2)!
34+
- r-evaluate >=0.15
35+
- r-highr >=0.11
36+
- r-xfun >=0.44
37+
- r-yaml >=2.1.19
38+
run:
39+
- r-base
40+
- r-evaluate >=0.15
41+
- r-highr >=0.11
42+
- r-xfun >=0.44
43+
- r-yaml >=2.1.19
44+
45+
tests:
46+
# This is a shorthand test for R packages to ensure that the library loads correctly.
47+
- r:
48+
libraries:
49+
- knitr
50+
# You can also run arbitrary R code in the test section.
51+
- script: test_package.R # (3)!
52+
53+
about:
54+
homepage: https://yihui.org/knitr/
55+
summary: A General-Purpose Package for Dynamic Report Generation in R
56+
description: |-
57+
Provides a general-purpose tool for dynamic report
58+
generation in R using Literate Programming techniques.
59+
license: GPL-2.0
60+
repository: https://github.com/cran/knitr
61+
```
62+
63+
1. The `script` section is where you specify the build commands to run. In this case, we are using `R CMD INSTALL --build .` to build the package.
64+
2. The `r-base` package is required to run R and is specified in the `host` requirements.
65+
3. The `script` key automatically detects the language based on the file extension. In the case of `.R`, it will execute the R script with `rscript`.
66+
4. The `noarch: generic` directive indicates that the package is architecture-independent. This is useful for R packages that do not contain compiled code and can run on any architecture. It allows the package to be installed on any platform without needing to rebuild it for each architecture.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ nav:
104104
- "Rust": tutorials/rust.md
105105
- "Go": tutorials/go.md
106106
- "Perl": tutorials/perl.md
107+
- "R": tutorials/r.md
107108
- "Converting from conda-build": converting_from_conda_build.md
108109

109110
- Build options:

src/recipe_generator/cran.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,18 @@ pub async fn generate_r_recipe(opts: &CranOpts) -> miette::Result<()> {
229229
))
230230
.expect("Failed to parse URL");
231231

232+
// It looks like CRAN moves the package to the archive for old versions
233+
// so let's add that as a fallback mirror
234+
let url_archive = Url::parse(&format!(
235+
"https://cran.r-project.org/src/contrib/Archive/{}",
236+
package_info._file
237+
))
238+
.expect("Failed to parse URL");
239+
232240
let sha256 = fetch_package_sha256sum(&url).await?;
233241

234242
let source = SourceElement {
235-
url: url.to_string(),
243+
url: vec![url.to_string(), url_archive.to_string()],
236244
md5: None,
237245
sha256: Some(format!("{:x}", sha256)),
238246
};

src/recipe_generator/pypi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ pub async fn create_recipe(
358358
};
359359

360360
recipe.source.push(serialize::SourceElement {
361-
url: release_url.replace(metadata.info.version.as_str(), "${{ version }}"),
361+
url: vec![release_url.replace(metadata.info.version.as_str(), "${{ version }}")],
362362
sha256: metadata.release.digests.get("sha256").cloned(),
363363
md5: None,
364364
});

src/recipe_generator/serialize.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ use std::{fmt, path::PathBuf};
22

33
use indexmap::IndexMap;
44
use serde::Serialize;
5+
use serde_with::{serde_as, OneOrMany, formats::PreferOne};
56

7+
#[serde_as]
68
#[derive(Default, Debug, Serialize)]
79
pub struct SourceElement {
8-
pub url: String,
10+
#[serde_as(as = "OneOrMany<_, PreferOne>")]
11+
pub url: Vec<String>,
912
#[serde(skip_serializing_if = "Option::is_none")]
1013
pub sha256: Option<String>,
1114
#[serde(skip_serializing_if = "Option::is_none")]

0 commit comments

Comments
 (0)