Repository: conda/conda-build

Issue:
Infinite loop due to subtle difference between version spec "==0.1" and "0.1"
I accidentally found a way to make `conda build` enter an infinite loop.  Specify an "exact" build dependency version in meta.yaml (e.g. `mydependency 0.1`), but then provide a corresponding recipe that _almost_ matches the version requirement (e.g. `version: "0.1.0"`).

Enter these commands into your own terminal to trigger the infinite loop:

```
$ mkdir aaa bbb

$ cat > aaa/meta.yaml
package:
    name: aaa
    version: "0.1.0"

$ cat > bbb/meta.yaml
package:
    name: bbb
    version: "0.1"
requirements:
    build:
      - aaa 0.1

$ conda build bbb # INFINITE LOOP
```

The loop can be avoided by using a relational operator (`aaa ==0.1`) to specify the version of the dependency:

```
requirements:
    build:
      - aaa ==0.1
```

I guess a possible fix to conda-build might be to assume that "exact" version specifications are treated as if there were an implicit `==` present.  I'm not exactly sure what the repercussions of that would be.



Discussion:
Yeah, I've known about this for some time. We should check in the main_build recursive dependency loop that we don't try to rebuild a recipe more than once. 

Here's another way (slightly different) to trigger the loop, involving a custom build string. (This might make a good test case when this bug is eventually fixed.)

```
$ mkdir aaa bbb

$ cat > aaa/meta.yaml
package:
    name: aaa
    version: "0.1.0"
build:
    string: custom_string

$ cat > bbb/meta.yaml
package:
    name: bbb
    version: "0.1"
requirements:
    build:
      - aaa ==0.1.0 custom_string

$ conda build bbb # INFINITE LOOP
```

Unlike the case above, the loop here can be avoided by _removing_ the relational operator:

```
requirements:
    build:
      - aaa 0.1.0 custom_string
```

Incidentally, I think the naive dependency resolution can be drastically improved, as I have done in conda-build-missing. See https://github.com/pelson/conda-build-missing/blob/master/conda_build_missing/__init__.py#L100-L125. I'd be happy to integrate this properly into conda-build.

Improvements to it are welcome. 


Resolving patch:
diff --git a/conda_build/main_build.py b/conda_build/main_build.py
--- a/conda_build/main_build.py
+++ b/conda_build/main_build.py
@@ -314,6 +314,7 @@ def execute(args, parser):
             override_channels=args.override_channels)
 
     already_built = []
+    to_build = args.recipe[:]
     with Locked(config.croot):
         recipes = deque(args.recipe)
         while recipes:
@@ -396,10 +397,13 @@ def execute(args, parser):
                             recipes.appendleft(arg)
                             try_again = True
                             for recipe_dir in recipe_glob:
+                                if dep_pkg in to_build:
+                                    sys.exit(str(e))
                                 print(("Missing dependency {0}, but found" +
                                        " recipe directory, so building " +
                                        "{0} first").format(dep_pkg))
                                 recipes.appendleft(recipe_dir)
+                                to_build.append(dep_pkg)
                         else:
                             raise
                     else:


Test patch:
diff --git a/tests/test-recipes/build_recipes.sh b/tests/test-recipes/build_recipes.sh
--- a/tests/test-recipes/build_recipes.sh
+++ b/tests/test-recipes/build_recipes.sh
@@ -16,13 +16,18 @@ for recipe in metadata/*/; do
     fi
 done
 
+cd fail
+
 # We use 2>&1 as the error is printed to stderr. We could do >/dev/null to
 # ensure it is printed to stderr, but then we would hide the output of the
 # command from the test output.  The ! ensures that the command fails.
-! OUTPUT=$(conda build --no-anaconda-upload fail/symlinks/ 2>&1)
+! OUTPUT=$(conda build --no-anaconda-upload symlinks/ 2>&1)
 echo "$OUTPUT" | grep "Error" | wc -l | grep 6
 
-! OUTPUT=$(conda build --no-anaconda-upload fail/conda-meta/ 2>&1)
+! OUTPUT=$(conda build --no-anaconda-upload conda-meta/ 2>&1)
 echo "$OUTPUT" | grep 'Error: Untracked file(s) ('\''conda-meta/nope'\'',)'
 
+! OUTPUT=$(conda build --no-anaconda-upload recursive-build/ 2>&1)
+echo "$OUTPUT" | grep 'No packages found in current .* channels matching: recursive-build2 2\.0'
+
 echo "TESTS PASSED"
diff --git a/tests/test-recipes/fail/recursive-build/meta.yaml b/tests/test-recipes/fail/recursive-build/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/fail/recursive-build/meta.yaml
@@ -0,0 +1,9 @@
+package:
+  name: conda-build-test-recursive-build-fail
+  version: 1.0
+
+# This is a version of the package that doesn't exist. The test is that this
+# doesn't run infinitely.
+requirements:
+  build:
+    - recursive-build2 2.0
diff --git a/tests/test-recipes/fail/recursive-build2/meta.yaml b/tests/test-recipes/fail/recursive-build2/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/fail/recursive-build2/meta.yaml
@@ -0,0 +1,3 @@
+package:
+  name: recursive-build2
+  version: 1.0