rattler-buildにおけるcondaパッケージ作成の注意点

# 概要
ここではcondaビルド済みパッケージを作成する際に、躓いた点について備忘録的にまとめていく。

# 目次
- `rattler-build`とは
- build環境とhost環境
- `run_exports`について
- `stdlib("c")`の設定
- cmakeにおけるbuildスクリプトの書き方基本
- テスト時におけるソースコードの利用
- platform-specific `noarch` package
- `meson-python`利用時のビルド注意点

# [`rattler-buid`](https://prefix-dev.github.io/rattler-build/latest/)とは
[prefix.dev](https://prefix.dev)社が開発している`Rust`を使って開発されている次世代のcondaパッケージ作成ツールである。[`conda-build`](https://docs.conda.io/projects/conda-build/en/stable/)と同様なyaml形式のレシピ(`meta.yaml` ではなく `recipe.yaml`)を作成し、それに基づいてpypiにおけるwheel形式のような、conda用のビルド済み配布物(`.conda`ファイル)の作成を行う。
詳しくはこちらのブログから: https://prefix.dev/blog/the_new_rattler_build

# build環境とhost環境
condaビルドを行う際、build環境とhost環境がある。build環境はソースコードをビルドする環境、host環境はビルド済み配布物をインストールする環境である。そのため、host環境はtarget_platformとも呼ばれ、cross-compiling (例えば、linux-64からlinux-aarch64 (intelからARM)のコンパイルを行うこと)の際にも使用される。
また、それぞれの環境に必要なパッケージが異なり、build、host環境で必要となるパッケージはそれぞれ
- buildtimeに必要なパッケージ (`cmake`, `gcc`, `meson`, `pkg-config`などのコンパイラー・ビルドツール)
- コンパイラ時にリンクするパッケージ (`libboost`, `glib`, `hdf5`, `python`などのsharedライブラリやpythonライブラリ)
である。

Python-C++ライブラリにおける`recipe.yaml`のrequirementセクションの例として以下のように設定する。
```yaml: recipe.yamlの一部
requirements:
  build:
    - ${{ compiler("c") }}
    - ${{ compiler("cxx") }}
    - ${{ stdlib("c") }}
    - git
    - cmake
    - ninja
    - pkg-config
  host:
    - pip
    - python
    - numpy
    - cython
    - setuptools
    - setuptools-scm
    - scikit-build-core
    - scikit-build
    - libboost-devel
    - hdf5
```
このとき、`libboost-devel`パッケージを`build`セクションに移動させると、build時に以下のようなエラーが表示される。
```bash
file Multiple conflicting paths found for libc++.1.dylib:
  $BUILD_PREFIX/lib//libc++.1.dylib
  $PREFIX/lib/libc++.1.dylib
```
これはコンパイル時のリンクで`-Wl,-rpath,$BUILD_PREFIX/lib $BUILD_PREFIX/lib/libboost_filesystem.dylib`と`-ldl $BUILD_PREFIX/lib/libboost_atomic.dylib`が追加されるため、conflictionが起きていると思われる。

参考: https://prefix-dev.github.io/rattler-build/latest/compilers/#cross-compilation

# `run_exports`について
ビルド時にリンクしたライブラリ(shared library等)はruntime環境でも必要になる。そのため、hostやbuildのrequirementで指定したパッケージをruntimeでのrequirementにも自動で追加させるために`run_exports`が必要となる。
```yaml: recipe.yaml
requirements:
    run_exports: my-package
```
例えば、`numpy`をhost環境のrequirementとして指定した場合、runtime環境でのrequirementに明示的に指定する必要はない。レシピ作成者がリンク時に利用したものと同じバージョンの`numpy`を自動的にruntime環境で指定してくれる。

参考: https://prefix-dev.github.io/rattler-build/latest/reference/recipe_file/#run-exports

# `stdlib("c")`の設定
build環境で`${{ compiler("c") }}`を指定する場合、conda-forgeにおいてC言語のstandard libraryを`stdlib("c")`として明示的に指定する必要がある。
参照: https://conda-forge.org/news/2024/03/24/stdlib-migration/

conda-forgeにおいてcompilerやstdlibは、[conda_build_config.yaml](https://github.com/conda-forge/conda-forge-pinning-feedstock/blob/main/recipe/conda_build_config.yaml)にあるデフォル値によって決まるが、ローカルなPC環境でratller-buildを用いる場合、`variants.yaml`を手動で作成する必要がある。LinuxとmacOSにおけるC言語のstdlibのみを設定する場合、以下のようになる。
```yaml:variants.yaml
c_stdlib:
  - if: linux
    then: sysroot
  - if: osx
    then: macosx_deployment_target

c_stdlib_version:
  - if: linux
    then: 2.17
  - if: osx and x86_64
    then: 10.13
  - if: osx and arm64
    then: 11.0
```

また、このファイルにpythonのバージョンを複数設定することで、github actionにおけるmatrixのような、複数ビルドも可能となる。(conda-forgeの[conda_build_config.yaml](https://github.com/conda-forge/conda-forge-pinning-feedstock/blob/main/recipe/conda_build_config.yaml)のpythonセクションを参照)


# cmakeにおけるbuildスクリプトの書き方基本
`cmake`をbuild requirementに指定した場合、`CMAKE_ARGS`など自動的に設定される。
また、cmakeにおける`Python_EXECUTABLE`を、Host環境上のpythonに指定する必要がある。
以下に`cmake`と`ninja`を用いたビルド・コンパイルスクリプト(`build.sh`)の例を示す。

```shell: build.sh
#!/bin/bash

# CMake extra configuration:
extra_cmake_args=(
    -G Ninja
    -DCMAKE_INSTALL_LIBDIR=lib
    -DBUILD_SHARED_LIBS=ON
    # Python bindings
    -DPython_EXECUTABLE="$PYTHON"
    -DPython3_EXECUTABLE="$PYTHON"
)

cmake ${CMAKE_ARGS} "${extra_cmake_args[@]}" \
    -DCMAKE_PREFIX_PATH=$PREFIX \
    -DCMAKE_INSTALL_PREFIX=$PREFIX \
    -B build -S $SRC_DIR

# Build and install
ninja
ninja install
```
ビルド時のデフォルト環境変数については[こちら](https://prefix-dev.github.io/rattler-build/latest/build_script/#default-environment-variables-set-during-the-build-process)を参照

参考: https://conda-forge.org/docs/maintainer/knowledge_base/#using-cmake


# テスト時におけるソースコードの利用
ビルド配布物を作成後、rattler-buildではテストが行われる。その際、ビルド環境は削除され、ソースコードへのパスである`$SRC_DIR`も利用できない。
テスト環境へ、ソースコードにあるテストスクリプトや設定ファイルをコピーするためには`recipe.yaml`のtestセクションを以下のように設定する。
```yaml: recipe.yamlの一部
tests:
  - script:
      - pytest
    files:
      source:
        - python/tests/
        - pyproject.toml
    requirements:
      run:
        - pytest
```
ここでは`pytest`を用いたテストスクリプトを想定している。`script:`における`file: sources:`にテスト時必要となるファイルをソースフォルダからコピーできる。

参考: https://prefix-dev.github.io/rattler-build/latest/reference/recipe_file/#extra-test-files

# Platform-specific `noarch` package
`noarch`パッケージとはpure-PythonパッケージのようなCPU architectureやOSに依存しない(コンパイラされたバイナリーファイルを含まない)パッケージである。しかし、例えば、activationスクリプトを含める場合や、runtime依存にOS依存がある場合、プラットフォーム(OS)依存の`noarch`パッケージが必要となる。
このとき、conda-forgeでのビルドであればこれを達成できる。

### 方法
`recipe.yaml`のrun requirementにてターゲットOSを示すvirtual packages: `__linux`, `__osx`, `__win`, `__unix`を加える。(`__unix`はLinuxとMacOS両方を示す。)
```yaml:recipe.yaml
build:
  number: 0
  noarch: python
requirements:
  # ...
  run:
    - python >=${{ python_min }}
    - numpy
    - if: linux
      then:
        - __linux
        - linux-only-dependency 
    - if: osx
      then:
        - __osx
        - osx-only-dependency
    - if: win
      then:
        - __win
        - windows-only-dependency
```
conda-forgeにおいて、`noarch`だと`linux_64`のCIランナーしかデフォルトで走らないが、複数のビルドplatformを走らせたい場合は`conda-forge.yaml`の`noarch_platforms`に追加する。

```yaml:conda-forge.yaml
noarch_platforms:
  - linux_64
  - osx_64
  - win_64
```

参照: https://conda-forge.org/docs/maintainer/knowledge_base/#noarch-packages-with-os-specific-dependencies


# `meson-python`利用時のビルド注意点
meson-pythonをbuild backendに持つパッケージをインストールする際のコマンドは
```bash
python -m pip install --no-deps --no-build-isolation .
```
であるが、cross build (cross-compile)をする際は注意が必要である。
conda-forgeではosx-arm64用のビルドをする際はosx-64からcross-compileを利用して、行われる。
上記のままだと、meson側がそれを検知できずにnative buildのまま処理されてしまいエラーになってしまう。

conda-forgeではmesonパッケージが読み出された場合、環境変数`$MESON_ARGS`が自動的に定義され、その中に`--cross-file $BUILD_PREFIX/meson_cross_file.txt`が記述されている。
このため、このオプションをうまくビルドオプションとして渡す必要がある。

[matplolib-feedstock](https://github.com/conda-forge/matplotlib-feedstock)を参考にすると、pypaの`build`(`python-build`)を用いたbuildスクリプトを記述している:

```build.sh
#!/bin/bash
set -ex

export AR=$GCC_AR

# binary is called `pkg-config`, but package metadata is under `lib/pkgconfig`
export MESON_ARGS="${MESON_ARGS} --pkg-config-path=${PREFIX}/lib/pkgconfig"

# meson-python already sets up a -Dbuildtype=release argument to meson, so
# we need to strip --buildtype out of MESON_ARGS or fail due to redundancy
MESON_ARGS_REDUCED="$(echo $MESON_ARGS | sed 's/--buildtype release //g')"

# -wnx flags mean: --wheel --no-isolation --skip-dependency-check
$PYTHON -m build -w -n -x \
    -Cbuilddir=builddir \
    -Csetup-args=${MESON_ARGS_REDUCED// / -Csetup-args=} \
    || (cat builddir/meson-logs/meson-log.txt && exit 1)

$PYTHON -m pip install --find-links dist cherab-lhd
```
ここで、`setup-args`に`$MESON_ARGS`を改良した変数`$MESON_ARGS_REDUCED`を渡すことで、cross buildが可能となる。

meson-pythonを利用しているパッケージビルドは常に、`build.sh`を用いたカスタムビルドを行うほうが良いだろう。