Repository: conda/conda-build

Issue:
Add an option to export build time dependencies as run_constrained keys and pin strictly
Following the discussion here: https://github.com/conda/conda-build/pull/2001#issuecomment-332327309, I am proposing to add the following feature to conda build.
Currently, there is an option in conda build recipes which allows you to pin **all** the packages that were used during build time as runtime dependencies. See original pull request here: https://github.com/conda/conda-build/pull/741

For example a recipe like:
```
package:
  name: bob-devel

build:
  pin_depends: strict

requirements:
  run:
    - curl 7.52.1
```
will have the following `index.json`
```json
{
  "depends": [
    "curl 7.52.1 0",
    "openssl 1.0.2l 0",
    "zlib 1.2.8 3",
  ],
  "name": "bob-devel",
}
```
where `openssl` and `zlib` are dependencies of `curl`. Note that everything is pinned strictly and normally dependencies of `zlib` and `openssl` get pinned strictly too but I did not put it in the example above.

I propose a very similar functionality to be added for `run_constrained` keys as well.
For example for the following recipe:
```
package:
  name: bob-devel

build:
  pin_constrains: strict

run_constrained:
  - curl 7.52.1
```
we get this:
```json
{
  "constrains": [
    "curl 7.52.1 0",
    "openssl 1.0.2l 0",
    "zlib 1.2.8 3",
  ],
  "name": "bob-devel",
}
```



Discussion:
I was thinking maybe instead of this, we can have a jinja function to help me do this. Something like:
```yaml
package:
  name: bob-devel
  version: 2018.02.21

requirements:
  host:
    - curl 7.55.1
  run_constrained:
  {% for package in packages('host', all=True) %}
    - {{ pin_compatible(package, exact=True) }}
  {% endfor %}
```
Would render to:
```yaml
package:
    name: bob-devel
    version: 2018.02.21
requirements:
    host:
        - ca-certificates 2017.08.26 h1d4fec5_0
        - libgcc-ng 7.2.0 h7cc24e2_2
        - openssl 1.0.2n hb7f436b_0
        - curl 7.55.1 h78862de_4
        - libssh2 1.8.0 h9cfc8f7_4
        - zlib 1.2.11 ha838bed_2
    run_constrained:
        - ca-certificates 2017.08.26 h1d4fec5_0
        - libgcc-ng 7.2.0 h7cc24e2_2
        - openssl 1.0.2n hb7f436b_0
        - curl 7.55.1 h78862de_4
        - libssh2 1.8.0 h9cfc8f7_4
        - zlib 1.2.11 ha838bed_2
```
For that, you'd need to add a new jinja2 function to the context.  See these two bits to see how pin_compatible works:

https://github.com/conda/conda-build/blob/master/conda_build/jinja_context.py#L192
https://github.com/conda/conda-build/blob/master/conda_build/jinja_context.py#L454

If you put up a PR, I can help on it.  Otherwise, this can go in the backlog, and I'm not sure when I'll get to it.

Resolving patch:
diff --git a/conda_build/jinja_context.py b/conda_build/jinja_context.py
--- a/conda_build/jinja_context.py
+++ b/conda_build/jinja_context.py
@@ -429,6 +429,52 @@ def cdt(package_name, config, permit_undefined_jinja=False):
     return result
 
 
+def resolved_packages(m, env, permit_undefined_jinja=False,
+                      bypass_env_check=False):
+    """Returns the final list of packages that are listed in host or build.
+    This include all packages (including the indirect dependencies) that will
+    be installed in the host or build environment. An example usage of this
+    jinja function can be::
+
+        requirements:
+          host:
+            - curl 7.55.1
+          run_constrained:
+          {% for package in resolved_packages('host') %}
+            - {{ package }}
+          {% endfor %}
+
+    which will render to::
+
+        requirements:
+            host:
+                - ca-certificates 2017.08.26 h1d4fec5_0
+                - curl 7.55.1 h78862de_4
+                - libgcc-ng 7.2.0 h7cc24e2_2
+                - libssh2 1.8.0 h9cfc8f7_4
+                - openssl 1.0.2n hb7f436b_0
+                - zlib 1.2.11 ha838bed_2
+            run_constrained:
+                - ca-certificates 2017.08.26 h1d4fec5_0
+                - curl 7.55.1 h78862de_4
+                - libgcc-ng 7.2.0 h7cc24e2_2
+                - libssh2 1.8.0 h9cfc8f7_4
+                - openssl 1.0.2n hb7f436b_0
+                - zlib 1.2.11 ha838bed_2
+    """
+    if env not in ('host', 'build'):
+        raise ValueError('Only host and build dependencies are supported.')
+
+    package_names = []
+
+    # optimization: this is slow (requires solver), so better to bypass it
+    # until the finalization stage as done similarly in pin_compatible.
+    if not bypass_env_check and not permit_undefined_jinja:
+        package_names, _, _ = get_env_dependencies(m, env, m.config.variant)
+
+    return package_names
+
+
 def context_processor(initial_metadata, recipe_dir, config, permit_undefined_jinja,
                       allow_no_other_outputs=False, bypass_env_check=False, skip_build_id=False):
     """
@@ -459,6 +505,9 @@ def context_processor(initial_metadata, recipe_dir, config, permit_undefined_jin
                                allow_no_other_outputs=allow_no_other_outputs),
         compiler=partial(compiler, config=config, permit_undefined_jinja=permit_undefined_jinja),
         cdt=partial(cdt, config=config, permit_undefined_jinja=permit_undefined_jinja),
+        resolved_packages=partial(resolved_packages, initial_metadata,
+                             permit_undefined_jinja=permit_undefined_jinja,
+                             bypass_env_check=bypass_env_check),
         time=time,
         datetime=datetime,
 


Test patch:
diff --git a/tests/test-recipes/metadata/_resolved_packages_host_build/meta.yaml b/tests/test-recipes/metadata/_resolved_packages_host_build/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_resolved_packages_host_build/meta.yaml
@@ -0,0 +1,16 @@
+package:
+  name: test_resolved_packages_host_build
+  version: 1.0
+
+requirements:
+  build:
+    - numpy
+  host:
+    - curl
+  run:
+  {% for package in resolved_packages('build') %}
+    - {{ package }}
+  {% endfor %}
+  {% for package in resolved_packages('host') %}
+    - {{ package }}
+  {% endfor %}
diff --git a/tests/test_api_render.py b/tests/test_api_render.py
--- a/tests/test_api_render.py
+++ b/tests/test_api_render.py
@@ -106,6 +106,20 @@ def test_pin_compatible_semver(testing_config):
     assert 'zlib >=1.2.8,<2.0a0' in metadata.get_value('requirements/run')
 
 
+def test_resolved_packages_recipe(testing_config):
+    recipe_dir = os.path.join(metadata_dir, '_resolved_packages_host_build')
+    metadata = api.render(recipe_dir, config=testing_config)[0][0]
+    assert all(len(pkg.split()) == 3 for pkg in metadata.get_value('requirements/run'))
+    run_requirements = set(x.split()[0] for x in metadata.get_value('requirements/run'))
+    for package in [
+        'curl',  # direct dependency
+        'numpy',  # direct dependency
+        'zlib',  # indirect dependency of curl
+        'python',  # indirect dependency of numpy
+    ]:
+        assert package in run_requirements
+
+
 def test_host_entries_finalized(testing_config):
     recipe = os.path.join(metadata_dir, '_host_entries_finalized')
     metadata = api.render(recipe, config=testing_config)
diff --git a/tests/test_jinja_context.py b/tests/test_jinja_context.py
--- a/tests/test_jinja_context.py
+++ b/tests/test_jinja_context.py
@@ -105,6 +105,14 @@ def test_pin_subpackage_expression(testing_metadata):
     assert len(pin.split()) == 2
 
 
+def test_resolved_packages(testing_metadata):
+    testing_metadata.meta['requirements']['build'] = ['numpy']
+    packages = jinja_context.resolved_packages(testing_metadata, 'build')
+    assert all(len(pkg.split()) == 3 for pkg in packages)
+    assert any('numpy' == pkg.split()[0] for pkg in packages)
+    assert any('python' == pkg.split()[0] for pkg in packages)
+
+
 try:
     from setuptools.config import read_configuration
     del read_configuration