CEP 13 - A new Recipe format (Part 1)
Title | A new recipe format (Part 1) |
Status | Accepted |
Author(s) | Wolf Vollprecht <wolf@prefix.dev> |
Created | May 23, 2023 |
Updated | October 20, 2023 |
Discussion | https://github.com/conda-incubator/ceps/pull/54 |
Implementation | https://github.com/prefix-dev/rattler-build |
Abstract
We propose a new recipe format that is heavily inspired by conda-build. The main change is a pure YAML format without arbitrary Jinja or comments with semantic meaning.
Motivation
The conda-build format has grown over the years to become quite complex. Unfortunately it has never been formally "specified" and it has grown some features over time that make it hard to parse as straightforward YAML.
The CEP attempts to introduce a subset of the conda build format that allows for fast parsing and building of recipes.
History
A discussion was started on what a new recipe spec could or should look like. The fragments of this discussion can be found here: https://github.com/mamba-org/conda-specs/blob/master/proposed_specs/recipe.md The reason for a new spec are:
- Make it easier to parse ("pure yaml"). conda-build uses a mix of comments and jinja to achieve a great deal of flexibility, but it's hard to parse the recipe with a computer
- iron out some inconsistencies around multiple outputs (build vs. build/script and more)
- remove any need for recursive parsing & solving
- cater to needs for automation and dependency tree analysis via a deterministic format
Major differences with conda-build
- no full Jinja2 support: no block support
{% set ...
support, only string interpolation. Variables can be set in the toplevel "context" which is valid YAML (all new features should be native to YAML specs) - Jinja variable syntax is changed to begin with
${{
so that it becomes valid YAML, e.g.- ${{ version }}
- Selectors use a YAML dictionary with
if / then / else
(vs. comments in conda-build) and are only allowed in lists (dependencies, scripts, ...). The syntax looks like:- if: win
then: this
else: that # optional - for inline values, the Jinja ternary operator can be used, e.g.
number: ${{ 0 if linux else 100 }}
Selectors
Selectors in the new spec are only allowed in lists and take an explicit if / then / else
syntax.
For example the following script
section:
script:
- if: unix
then: |
# this is the unix script
- if: win
then: |
@rem a script for batch
The same could have been expressed with an else
:
script:
- if: unix
then: |
# this is the unix script
else: |
@rem a script for batch
This is a valid YAML dictionary. Selector if statements are simple boolean expressions and follow Python syntax. The following selectors are all valid:
win and arm64
(osx or linux) and aarch64
something == "test"
If the value of a selector statement is a list, it extends the "outer" list. For example:
build:
- ${{ compiler('cxx') }}
- if: unix
then:
- make
- cmake
- pkg-config
evaluates for unix == true
to a list with elements [${{ compiler('cxx') }}, make, cmake, pkg-config]
.
Preprocessing selectors
You can add selectors to any item, and the selector is evaluated in a
preprocessing stage. If a selector evaluates to true
, the item is flattened
into the parent element. If a selector evaluates to false
, the item is
removed.
source:
- if: not win
then:
# note that we omit the `-`, both is valid
url: http://path/to/unix/source
sha256: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
else:
- url: http://path/to/windows/source
sha256: 06f961b802bc46ee168555f066d28f4f0e9afdf3f88174c1ee6f9de004fc30a0
Because the selector is a valid Jinja expression, complicated logic is possible:
source:
- if: win
then:
url: http://path/to/windows/source
- if: (unix and cmp(python, "2"))
then:
url: http://path/to/python2/unix/source
- if: unix and cmp(python, "3")
then:
url: http://path/to/python3/unix/source
Lists are automatically "merged" upwards, so it is possible to group multiple items under a single selector:
test:
commands:
- if: unix
then:
- test -d ${PREFIX}/include/xtensor
- test -f ${PREFIX}/include/xtensor/xarray.hpp
- test -f ${PREFIX}/lib/cmake/xtensor/xtensorConfig.cmake
- test -f ${PREFIX}/lib/cmake/xtensor/xtensorConfigVersion.cmake
- if: win
then:
- if not exist %LIBRARY_PREFIX%\include\xtensor\xarray.hpp (exit 1)
- if not exist %LIBRARY_PREFIX%\lib\cmake\xtensor\xtensorConfig.cmake (exit 1)
- if not exist %LIBRARY_PREFIX%\lib\cmake\xtensor\xtensorConfigVersion.cmake (exit 1)
# On unix this is rendered to:
test:
commands:
- test -d ${PREFIX}/include/xtensor
- test -f ${PREFIX}/include/xtensor/xarray.hpp
- test -f ${PREFIX}/lib/cmake/xtensor/xtensorConfig.cmake
- test -f ${PREFIX}/lib/cmake/xtensor/xtensorConfigVersion.cmake
Templating with Jinja
The spec supports simple Jinja templating in the recipe.yaml
file.
You can set up Jinja variables in the context YAML section:
context:
name: "test"
version: "5.1.2"
major_version: ${{ version.split('.')[0] }}
Later in your recipe.yaml
you can use these values in string interpolation
with Jinja. For example:
source:
url: https://github.com/mamba-org/${{ name }}/v${{ version }}.tar.gz
Jinja has built-in support for some common string manipulations.
In the new spec, complex Jinja is completely disallowed as we try to produce
YAML that is valid at all times. So you should not use any {% if ... %}
or
similar Jinja constructs that produce invalid YAML. We also do not use the
standard Jinja delimiters ({{ .. }}
) because that is confused by the YAML
parser as a dictionary. We follow Github Actions and others and use ${{ ... }}
instead:
package:
name: {{ name }} # WRONG: invalid yaml
name: ${{ name }} # correct
Jinja functions work as usual. As an example, the compiler
Jinja function will
look like this:
requirements:
build:
- ${{ compiler('cxx') }}
Shortcomings
Since we deliberately limit the amount of "Jinja" that is allowed in recipes there will be several shortcomings.
For example, using a {% for ... %}
loop is prohibited. After searching through
the conda-forge recipes with the Github search, we found for loops mostly used
in tests.
In our view, for loops are a nice helper, but not necessary for many tasks: the same functionality could be achieved in a testing script, for example. At the same time we also plan to formalize a more powerful testing harness (prototyped in boa).
This could be used instead of a for loop to check the existence of shared libraries or header files cross-platform (instead of relying on Jinja templates as done here or here).
Other uses of for
loops should be relatively easy to refactor, such as
here.
However, since the new recipe format is "pure YAML" it is very easy to create and pre-process these files using a script, or even generating them with Python or any other scripting language. That means, many of the features that are currently done with Jinja could be done with a simple pre-processing step in the future.
Another option would be to allow "full" Jinja inside the test script text blocks (as long as it doesn't change the structure of the YAML).
Examples
xtensor
Original recipe found here.
context:
name: xtensor
version: 0.24.6
sha256: f87259b51aabafdd1183947747edfff4cff75d55375334f2e81cee6dc68ef655
package:
name: ${{ name|lower }}
version: ${{ version }}
source:
fn: ${{ name }}-${{ version }}.tar.gz
url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz
sha256: ${{ sha256 }}
build:
number: 0
# note: in the new recipe format, `skip` is a list of conditional expressions
# but for the "YAML format" discussion we pretend that we still use the
# `skip: bool` syntax
skip: ${{ true if (win and vc14) }}
requirements:
build:
- ${{ compiler('cxx') }}
- cmake
- if: unix
then: make
host:
- xtl >=0.7,<0.8
run:
- xtl >=0.7,<0.8
run_constrained:
- xsimd >=8.0.3,<10
test:
commands:
- if: unix
then:
- test -d ${PREFIX}/include/xtensor
- test -f ${PREFIX}/include/xtensor/xarray.hpp
- test -f ${PREFIX}/share/cmake/xtensor/xtensorConfig.cmake
- test -f ${PREFIX}/share/cmake/xtensor/xtensorConfigVersion.cmake
- if: win
then:
- if not exist %LIBRARY_PREFIX%\include\xtensor\xarray.hpp (exit 1)
- if not exist %LIBRARY_PREFIX%\share\cmake\xtensor\xtensorConfig.cmake (exit 1)
- if not exist %LIBRARY_PREFIX%\share\cmake\xtensor\xtensorConfigVersion.cmake (exit 1)
about:
home: https://github.com/xtensor-stack/xtensor
license: BSD-3-Clause
license_family: BSD
license_file: LICENSE
summary: The C++ tensor algebra library
description: Multi dimensional arrays with broadcasting and lazy computing
doc_url: https://xtensor.readthedocs.io
dev_url: https://github.com/xtensor-stack/xtensor
extra:
recipe-maintainers:
- some-maintainer