Repository: conda/conda-build

Issue:
`pin_compatible` has no effect
I created a recipe for the `tifffile` package that uses variants to build multiple versions. Unfortunately, it does not work. `conda build --output -m conda_build_config.yaml tifffile` yields
```
.../conda-bld/linux-64/tifffile-0.13.5-py35_nomklhba1e662_3.tar.bz2
.../conda-bld/linux-64/tifffile-0.13.5-py35hb4372cb_3.tar.bz2
.../conda-bld/linux-64/tifffile-0.13.5-py36_nomklhba1e662_3.tar.bz2
.../conda-bld/linux-64/tifffile-0.13.5-py36hb4372cb_3.tar.bz2
```
There should be 12 outputs (2 Python versions, 2 BLAS implementations, 3 numpy versions). It seems as if `pin_compatible("numpy", min_pin="x.x", max_pin="x.x")` has no effect.

Recipe and build config are attached below.
[meta.yaml](https://github.com/conda/conda-build/files/1668545/meta.yaml.txt)
[conda_build_config.yaml](https://github.com/conda/conda-build/files/1668525/conda_build_config.yaml.txt)



Discussion:
confirmed.  Thanks for reporting.  I think it's a bug related to numpy not being considered a "used" variable.  We'll fix for 3.3.1.

Resolving patch:
diff --git a/conda_build/metadata.py b/conda_build/metadata.py
--- a/conda_build/metadata.py
+++ b/conda_build/metadata.py
@@ -52,6 +52,12 @@
 #    regex, which extract all outputs in order.
 output_re = re.compile(r"^\s+-\s(?:name|type):.+?(?=^\w|\Z|^\s+-\s(?:name|type))",
                        flags=re.M | re.S)
+numpy_xx_re = re.compile(r'(numpy\s*x\.x)|pin_compatible\([\'\"]numpy.*max_pin=[\'\"]x\.x[\'\"]')
+# TODO: there's probably a way to combine these, but I can't figure out how to many the x
+#     capturing group optional.
+numpy_compatible_x_re = re.compile(
+    r'pin_\w+\([\'\"]numpy[\'\"].*((?<=x_pin=[\'\"])[x\.]*(?=[\'\"]))')
+numpy_compatible_re = re.compile(r"pin_\w+\([\'\"]numpy[\'\"]")
 
 # used to avoid recomputing/rescanning recipe contents for used variables
 used_vars_cache = {}
@@ -1101,8 +1107,14 @@ def get_hash_contents(self):
         dependencies = self.get_used_vars()
 
         # filter out ignored versions
-        build_string_excludes = ['python', 'r_base', 'perl', 'lua', 'numpy', 'target_platform']
+        build_string_excludes = ['python', 'r_base', 'perl', 'lua', 'target_platform']
         build_string_excludes.extend(ensure_list(self.config.variant.get('ignore_version', [])))
+        if 'numpy' in dependencies:
+            pin_compatible, not_xx = self.uses_numpy_pin_compatible_without_xx
+            # numpy_xx means it is accounted for in the build string, with npXYY
+            # if not pin_compatible, then we don't care about the usage, and omit it from the hash.
+            if self.numpy_xx or not pin_compatible:
+                build_string_excludes.append('numpy')
         # always exclude older stuff that's always in the build string (py, np, pl, r, lua)
         if build_string_excludes:
             exclude_pattern = re.compile('|'.join('{}[\s$]?.*'.format(exc)
@@ -1520,16 +1532,22 @@ def numpy_xx(self):
         '''This is legacy syntax that we need to support for a while.  numpy x.x means
         "pin run as build" for numpy.  It was special-cased to only numpy.'''
         text = self.extract_requirements_text()
-        uses_xx = bool(re.search(r'numpy\s*x\.x', text))
-        if uses_xx:
-            log = utils.get_logger(__name__)
-            log.warn("Recipe at {path} uses numpy x.x.  This is deprecated as of conda-build 3.0, "
-                     "and will be removed in conda-build 4.0.  Please consider using variants with "
-                     "pin_run_as_build instead.  More info at "
-                     "https://conda.io/docs/building/variants.html#customizing-compatibility"
-                     .format(path=self.path))
+        uses_xx = bool(numpy_xx_re.search(text))
         return uses_xx
 
+    @property
+    def uses_numpy_pin_compatible_without_xx(self):
+        text = self.extract_requirements_text()
+        compatible_search = numpy_compatible_re.search(text)
+        max_pin_search = None
+        if compatible_search:
+            max_pin_search = numpy_compatible_x_re.search(text)
+        # compatible_search matches simply use of pin_compatible('numpy')
+        # max_pin_search quantifies the actual number of x's in the max_pin field.  The max_pin
+        #     field can be absent, which is equivalent to a single 'x'
+        return (bool(compatible_search),
+                max_pin_search.group(1).count('x') != 2 if max_pin_search else True)
+
     @property
     def uses_subpackage(self):
         outputs = self.get_section('outputs')
@@ -1734,7 +1752,8 @@ def get_output_metadata_set(self, permit_undefined_jinja=False,
             output_tuples = [(outputs, self)]
         else:
             all_output_metadata = OrderedDict()
-            for variant in (self.config.variants if hasattr(self.config, 'variants')
+            for variant in (self.config.variants if (hasattr(self.config, 'variants') and
+                                                     self.config.variants)
                             else [self.config.variant]):
                 om = self.copy()
                 om.config.variant = variant
diff --git a/conda_build/source.py b/conda_build/source.py
--- a/conda_build/source.py
+++ b/conda_build/source.py
@@ -561,7 +561,7 @@ def apply_patch(src_dir, path, config, git=None):
             or conda, m2-patch (Windows),
         """ % (os.pathsep.join(external.dir_paths)))
         patch_strip_level = _guess_patch_strip_level(files, src_dir)
-        patch_args = ['-p%d' % patch_strip_level, '-i', path]
+        patch_args = ['-p%d' % patch_strip_level, '--ignore-whitespace', '-i', path]
 
         # line endings are a pain.
         # https://unix.stackexchange.com/a/243748/34459
diff --git a/conda_build/variants.py b/conda_build/variants.py
--- a/conda_build/variants.py
+++ b/conda_build/variants.py
@@ -515,7 +515,7 @@ def get_vars(variants, loop_only=False):
 def find_used_variables_in_text(variant, recipe_text):
     used_variables = set()
     for v in variant:
-        variant_regex = r"(^.*\{\{\s*%s\s*(?:.*?)?\}\})" % v
+        variant_regex = r"(^.*\{\{\s*(?:pin_.*?)?%s[\'\"\s,]*(?:.*?)?\}\})" % v
         selector_regex = r"\#?\s\[(?:.*[^_\w\d])?(%s)[^_\w\d]" % v
         conditional_regex = r"(.*\{%\s*(?:el)?if\s*" + v + r"\s*(?:.*?)?%\})"
         requirement_regex = r"(\-\s+%s(?:\s+[\[#]|$))" % v.replace('_', '[-_]')


Test patch:
diff --git a/tests/test-recipes/variants/numpy_used/conda_build_config.yaml b/tests/test-recipes/variants/numpy_used/conda_build_config.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/variants/numpy_used/conda_build_config.yaml
@@ -0,0 +1,8 @@
+python:
+    - 3.5
+    - 3.6
+numpy:
+    - 1.11
+    - 1.13
+blas_impl:
+    - mkl
diff --git a/tests/test-recipes/variants/numpy_used/meta.yaml b/tests/test-recipes/variants/numpy_used/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/variants/numpy_used/meta.yaml
@@ -0,0 +1,42 @@
+{% set version = "0.13.5" %}
+
+package:
+  name: tifffile
+  version: {{ version }}
+
+source:
+  git_url: https://github.com/blink1073/tifffile
+  git_tag: v{{ version }}
+
+build:
+  number: 3
+  # conda 4.4+
+  skip: True  # [blas_impl != "mkl" and win]
+  requires_features:
+    blas: {{ blas_impl }}
+  # conda 4.3-
+  features:
+    - nomkl  # [x86 and blas_impl != "mkl"]
+
+  script: python setup.py install
+
+requirements:
+  build:
+    - nomkl  # [blas_impl != "mkl"]
+    - python {{ python }}
+    - numpy {{ numpy }}
+    - setuptools
+
+  run:
+    - nomkl  # [blas_impl != "mkl"]
+    - python {{ python }}
+    - {{ pin_compatible("numpy", min_pin="x.x", max_pin="x.x") }}
+
+test:
+  imports:
+    - tifffile
+
+about:
+  home: https://github.com/blink1073/tifffile
+  license:  BSD License
+  summary: 'Read and write image data from and to TIFF files.'
diff --git a/tests/test_variants.py b/tests/test_variants.py
--- a/tests/test_variants.py
+++ b/tests/test_variants.py
@@ -339,3 +339,9 @@ def test_target_platform_looping(testing_config):
     outputs = api.get_output_file_paths(os.path.join(recipe_dir, '25_target_platform_looping'),
                                    platform='win', arch='64')
     assert len(outputs) == 2
+
+
+def test_numpy_used_variable_looping(testing_config):
+    outputs = api.get_output_file_paths(os.path.join(recipe_dir, 'numpy_used'),
+                                   platform='win', arch='64')
+    assert len(outputs) == 4