Repository: conda/conda-build

Issue:
Include virtual packages in hash contents
### Checklist

- [X] I added a descriptive title
- [X] I searched open requests and couldn't find a duplicate

### What is the idea?

Quoting from https://github.com/conda/conda-build/pull/4606:

> Noarch packages that use virtual packages have the virtual packages added to the hash contents of the package. This facilitates the building of noarch packages multiple times for different platforms with platform specific dependencies.
> 
> If we build `noarch` packages with diverging run dependencies for different platforms, the hash in the build string stayed the same prior to this PR, i.e., the built variants got the exact same build strings and thus package file names didn't change. With this PR we can build different variants for, e.g., `__linux`/`__osx`/`__win`, and get distinguished, non-clashing package file names.



### Why is this needed?

To move https://github.com/conda/conda-build/pull/4606 & https://github.com/conda-forge/conda-forge.github.io/pull/1839 & and much work based on that in conda-forge forward.

### What should happen?

Review & merge https://github.com/conda/conda-build/pull/4606

### Additional Context

More noarch packages = less unnecessary builds = less unnecessary downloads = less resources waste 🌍 = savings 💸 


Discussion:

Resolving patch:
diff --git a/conda_build/metadata.py b/conda_build/metadata.py
--- a/conda_build/metadata.py
+++ b/conda_build/metadata.py
@@ -1583,9 +1583,17 @@ def get_hash_contents(self):
 
         # if dependencies are only 'target_platform' then ignore that.
         if dependencies == ["target_platform"]:
-            return {}
+            hash_contents = {}
         else:
-            return {key: self.config.variant[key] for key in dependencies}
+            hash_contents = {key: self.config.variant[key] for key in dependencies}
+
+        # include virtual packages in run
+        run_reqs = self.meta.get("requirements", {}).get("run", [])
+        virtual_pkgs = [req for req in run_reqs if req.startswith("__")]
+
+        # add name -> match spec mapping for virtual packages
+        hash_contents.update({pkg.split(" ")[0]: pkg for pkg in virtual_pkgs})
+        return hash_contents
 
     def hash_dependencies(self):
         """With arbitrary pinning, we can't depend on the build string as done in


Test patch:
diff --git a/tests/test-recipes/metadata/_noarch_with_no_platform_deps/meta.yaml b/tests/test-recipes/metadata/_noarch_with_no_platform_deps/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_noarch_with_no_platform_deps/meta.yaml
@@ -0,0 +1,14 @@
+package:
+  name: test-noarch-with-no-platform-deps
+  version: 0.0.1
+
+build:
+  number: 0
+  noarch: python
+
+requirements:
+  build:
+  host:
+    - python >=3.7
+  run:
+    - python >=3.7
diff --git a/tests/test-recipes/metadata/_noarch_with_platform_deps/meta.yaml b/tests/test-recipes/metadata/_noarch_with_platform_deps/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_noarch_with_platform_deps/meta.yaml
@@ -0,0 +1,20 @@
+package:
+  name: test-noarch-with-platform-deps
+  version: 0.0.1
+
+build:
+  number: 0
+  noarch: python
+
+requirements:
+  build:
+  host:
+    - python >=3.7
+  run:
+    - python >=3.7
+    - colorama   # [win]
+    - __win      # [win]
+    - appnope    # [osx]
+    - __osx      # [osx]
+    - __archspec * ppc64le  # [ppc64le]
+    - __linux    # [linux]
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
@@ -182,6 +182,34 @@ def test_cross_info_index_platform(testing_config):
     assert metadata.config.host_platform == info_index["platform"]
 
 
+def test_noarch_with_platform_deps(testing_workdir, testing_config):
+    recipe_path = os.path.join(metadata_dir, "_noarch_with_platform_deps")
+    build_ids = {}
+    for subdir_ in ["linux-64", "linux-aarch64", "linux-ppc64le", "osx-64", "win-64"]:
+        platform, arch = subdir_.split("-")
+        m = api.render(
+            recipe_path, config=testing_config, platform=platform, arch=arch
+        )[0][0]
+        build_ids[subdir_] = m.build_id()
+
+    # one hash for each platform, plus one for the archspec selector
+    assert len(set(build_ids.values())) == 4
+    assert build_ids["linux-64"] == build_ids["linux-aarch64"]
+    assert (
+        build_ids["linux-64"] != build_ids["linux-ppc64le"]
+    )  # not the same due to archspec
+
+
+def test_noarch_with_no_platform_deps(testing_workdir, testing_config):
+    recipe_path = os.path.join(metadata_dir, "_noarch_with_no_platform_deps")
+    build_ids = set()
+    for platform in ["osx", "linux", "win"]:
+        m = api.render(recipe_path, config=testing_config, platform=platform)[0][0]
+        build_ids.add(m.build_id())
+
+    assert len(build_ids) == 1
+
+
 def test_setting_condarc_vars_with_env_var_expansion(testing_workdir):
     os.makedirs("config")
     # python won't be used - the stuff in the recipe folder will override it