diff --git a/conda_build/jinja_context.py b/conda_build/jinja_context.py index 61219be134..9d507e43a6 100644 --- a/conda_build/jinja_context.py +++ b/conda_build/jinja_context.py @@ -494,34 +494,42 @@ def native_compiler(language, config): return compiler -def compiler(language, config, permit_undefined_jinja=False): - """Support configuration of compilers. This is somewhat platform specific. +def _target(language, config, permit_undefined_jinja=False, component="compiler"): + """Support configuration of compilers/stdlib. This is somewhat platform specific. - Native compilers never list their host - it is always implied. Generally, they are + Native compilers/stdlib never list their host - it is always implied. Generally, they are metapackages, pointing at a package that does specify the host. These in turn may be metapackages, pointing at a package where the host is the same as the target (both being the native architecture). """ - compiler = native_compiler(language, config) + if component == "compiler": + package_prefix = native_compiler(language, config) + else: + package_prefix = language + version = None if config.variant: target_platform = config.variant.get("target_platform", config.subdir) - language_compiler_key = f"{language}_compiler" - # fall back to native if language-compiler is not explicitly set in variant - compiler = config.variant.get(language_compiler_key, compiler) - version = config.variant.get(language_compiler_key + "_version") + language_key = f"{language}_{component}" + # fall back to native if language-key is not explicitly set in variant + package_prefix = config.variant.get(language_key, package_prefix) + version = config.variant.get(language_key + "_version") else: target_platform = config.subdir - # support cross compilers. A cross-compiler package will have a name such as + # support cross components. A cross package will have a name such as # gcc_target # gcc_linux-cos6-64 - compiler = "_".join([compiler, target_platform]) + package = f"{package_prefix}_{target_platform}" if version: - compiler = " ".join((compiler, version)) - compiler = ensure_valid_spec(compiler, warn=False) - return compiler + package = f"{package} {version}" + package = ensure_valid_spec(package, warn=False) + return package + + +# ensure we have compiler in namespace +compiler = partial(_target, component="compiler") def ccache(method, config, permit_undefined_jinja=False): @@ -788,7 +796,16 @@ def context_processor( skip_build_id=skip_build_id, ), compiler=partial( - compiler, config=config, permit_undefined_jinja=permit_undefined_jinja + _target, + config=config, + permit_undefined_jinja=permit_undefined_jinja, + component="compiler", + ), + stdlib=partial( + _target, + config=config, + permit_undefined_jinja=permit_undefined_jinja, + component="stdlib", ), cdt=partial(cdt, config=config, permit_undefined_jinja=permit_undefined_jinja), ccache=partial( diff --git a/docs/source/resources/compiler-tools.rst b/docs/source/resources/compiler-tools.rst index d206d1c947..d4832b5a0c 100644 --- a/docs/source/resources/compiler-tools.rst +++ b/docs/source/resources/compiler-tools.rst @@ -394,6 +394,71 @@ not available. You'd need to create a metapackage ``m2w64-gcc_win-64`` to point at the ``m2w64-gcc`` package, which does exist on the msys2 channel on `repo.anaconda.com `_. +Expressing the relation between compiler and its standard library +================================================================= + +For most languages, certainly for "c" and for "cxx", compiling any given +program *may* create a run-time dependence on symbols from the respective +standard library. For example, the standard library for C on linux is generally +``glibc``, and a core component of your operating system. Conda is not able to +change or supersede this library (it would be too risky to try to). A similar +situation exists on MacOS and on Windows. + +Compiler packages usually have two ways to deal with this dependence: + +* assume the package must be there (like ``glibc`` on linux). +* always add a run-time requirement on the respective stdlib (e.g. ``libcxx`` + on MacOS). + +However, even if we assume the package must be there, the information about the +``glibc`` version is still a highly relevant piece of information, which is +also why it is reflected in the ``__glibc`` +`virtual package `_. + +For example, newer packages may decide over time to increase the lowest version +of ``glibc`` that they support. We therefore need a way to express this +dependence in a way that conda will be able to understand, so that (in +conjunction with the ``__glibc`` virtual package) the environment resolver will +not consider those packages on machines whose ``glibc`` version is too old. + +The way to do this is to use the Jinja2 function ``{{ stdlib('c') }}``, which +matches ``{{ compiler('c') }}`` in as many ways as possible. Let's start again +with the ``conda_build_config.yaml``:: + + c_stdlib: + - sysroot # [linux] + - macosx_deployment_target # [osx] + c_stdlib_version: + - 2.17 # [linux] + - 10.13 # [osx] + +In the recipe we would then use:: + + requirements: + build: + - {{ compiler('c') }} + - {{ stdlib('c') }} + +This would then express that the resulting package requires ``sysroot ==2.17`` +(corresponds to ``glibc``) on linux and ``macosx_deployment_target ==10.13`` on +MacOS in the build environment, respectively. How this translates into a +run-time dependence can be defined in the metadata of the respective conda +(meta-)package which represents the standard library (i.e. those defined under +``c_stdlib`` above). + +In this example, ``sysroot 2.17`` would generate a run-export on +``__glibc >=2.17`` and ``macosx_deployment_target 10.13`` would similarly +generate ``__osx >=10.13``. This way, we enable packages to define their own +expectations about the standard library in a unified way, and without +implicitly depending on some global assumption about what the lower version +on a given platform must be. + +In principle, this facility would make it possible to also express the +dependence on separate stdlib implementations (like ``musl`` instead of +``glibc``), or to remove the need to assume that a C++ compiler always needs to +add a run-export on the C++ stdlib -- it could then be left up to packages +themselves whether they need ``{{ stdlib('cxx') }}`` or not. + Anaconda compilers implicitly add RPATH pointing to the conda environment ========================================================================= diff --git a/tests/test-recipes/metadata/_stdlib_jinja2/conda_build_config.yaml b/tests/test-recipes/metadata/_stdlib_jinja2/conda_build_config.yaml new file mode 100644 index 0000000000..a6ac88cd33 --- /dev/null +++ b/tests/test-recipes/metadata/_stdlib_jinja2/conda_build_config.yaml @@ -0,0 +1,8 @@ +c_stdlib: # [unix] + - sysroot # [linux] + - macosx_deployment_target # [osx] +c_stdlib_version: # [unix] + - 2.12 # [linux64] + - 2.17 # [aarch64 or ppc64le] + - 10.13 # [osx and x86_64] + - 11.0 # [osx and arm64] diff --git a/tests/test-recipes/metadata/_stdlib_jinja2/meta.yaml b/tests/test-recipes/metadata/_stdlib_jinja2/meta.yaml new file mode 100644 index 0000000000..c655aac2ca --- /dev/null +++ b/tests/test-recipes/metadata/_stdlib_jinja2/meta.yaml @@ -0,0 +1,9 @@ +package: + name: stdlib-test + version: 1.0 + +requirements: + host: + - {{ stdlib('c') }} + # - {{ stdlib('cxx') }} + # - {{ stdlib('fortran') }} diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 37319f0de4..e122b45b4b 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -223,6 +223,33 @@ def test_compiler_metadata_cross_compiler(): ) +@pytest.mark.parametrize( + "platform,arch,stdlibs", + [ + ("linux", "64", {"sysroot_linux-64 2.12.*"}), + ("linux", "aarch64", {"sysroot_linux-aarch64 2.17.*"}), + ("osx", "64", {"macosx_deployment_target_osx-64 10.13.*"}), + ("osx", "arm64", {"macosx_deployment_target_osx-arm64 11.0.*"}), + ], +) +def test_native_stdlib_metadata( + platform: str, arch: str, stdlibs: set[str], testing_config +): + testing_config.platform = platform + metadata = api.render( + os.path.join(metadata_dir, "_stdlib_jinja2"), + config=testing_config, + variants={"target_platform": f"{platform}-{arch}"}, + platform=platform, + arch=arch, + permit_unsatisfiable_variants=True, + finalize=False, + bypass_env_check=True, + python="3.11", # irrelevant + )[0][0] + assert stdlibs <= set(metadata.meta["requirements"]["host"]) + + def test_hash_build_id(testing_metadata): testing_metadata.config.variant["zlib"] = "1.2" testing_metadata.meta["requirements"]["host"] = ["zlib"]