Repository: conda/conda-build

Issue:
Conda build repeats entry point when outputs is included
Looks like build is repeating the generation/inclusion of entry points for every output option specified, instead of just once for the top level package.

```
...
cehold_pl
number of files: 4
Fixing permissions
Fixing permissions
Detected hard-coded path in text file bin/app
Detected hard-coded path in text file bin/app-examples
Detected hard-coded path in text file bin/app_jupyter
Detected hard-coded path in text file bin/app_vanilla
/Users/fpliger/miniconda3/conda-bld/osx-64/app-assets-2.0_alpha_1-py35_0.tar.bz2
updating index in: /Users/fpliger/miniconda3/conda-bld
updating index in: /Users/fpliger/miniconda3/conda-bld/noarch
INFO conda_build.build:bundle_conda(864): Packaging app-2.0_alpha_1-py35_0
compiling .pyc files...
number of files: 470
Fixing permissions
Fixing permissions
Detected hard-coded path in text file bin/app
Detected hard-coded path in text file bin/app-examples
Detected hard-coded path in text file bin/app_jupyter
Detected hard-coded path in text file bin/app_vanilla
```


Discussion:
It also appears that import tests for the top-level recipe are repeated for each subpackage.

Resolving patch:
diff --git a/conda_build/build.py b/conda_build/build.py
--- a/conda_build/build.py
+++ b/conda_build/build.py
@@ -863,6 +863,9 @@ def post_process_files(m, initial_prefix_files):
 def bundle_conda(output, metadata, config, env, **kw):
     log = logging.getLogger(__name__)
     log.info('Packaging %s', metadata.dist())
+
+    with utils.path_prepended(metadata.config.build_prefix):
+        env = environ.get_dict(config=metadata.config, m=metadata)
     files = output.get('files', [])
     if not files and output.get('script'):
         interpreter = output.get('script_interpreter')
@@ -1406,7 +1409,8 @@ def test(recipedir_or_package_or_metadata, config, move_broken=True):
     try:
         utils.check_call_env(cmd, env=env, cwd=config.test_dir)
     except subprocess.CalledProcessError:
-        tests_failed(metadata, move_broken=move_broken, broken_dir=config.broken_dir, config=config)
+        tests_failed(recipedir_or_package_or_metadata, move_broken=move_broken,
+                     broken_dir=config.broken_dir, config=config)
 
     if need_cleanup:
         utils.rm_rf(recipe_dir)
@@ -1415,7 +1419,7 @@ def test(recipedir_or_package_or_metadata, config, move_broken=True):
     return True
 
 
-def tests_failed(m, move_broken, broken_dir, config):
+def tests_failed(package_or_metadata, move_broken, broken_dir, config):
     '''
     Causes conda to exit if any of the given package's tests failed.
 
@@ -1425,9 +1429,15 @@ def tests_failed(m, move_broken, broken_dir, config):
     if not isdir(broken_dir):
         os.makedirs(broken_dir)
 
+    if hasattr(package_or_metadata, 'config'):
+        pkg = bldpkg_path(package_or_metadata)
+    else:
+        pkg = package_or_metadata
+    dest = join(broken_dir, os.path.basename(pkg))
+
     if move_broken:
-        shutil.move(bldpkg_path(m), join(broken_dir, "%s.tar.bz2" % m.dist()))
-    sys.exit("TESTS FAILED: " + m.dist())
+        shutil.move(pkg, dest)
+    sys.exit("TESTS FAILED: " + os.path.basename(pkg))
 
 
 def check_external():
diff --git a/conda_build/config.py b/conda_build/config.py
--- a/conda_build/config.py
+++ b/conda_build/config.py
@@ -214,11 +214,12 @@ def _get_python(self, prefix):
 
     def _get_perl(self, prefix):
         if sys.platform == 'win32':
-            res = join(prefix, 'perl.exe')
+            res = join(prefix, 'Library', 'bin', 'perl.exe')
         else:
             res = join(prefix, 'bin/perl')
         return res
 
+    # TODO: This is probably broken on Windows, but no one has a lua package on windows to test.
     def _get_lua(self, prefix):
         binary_name = "luajit" if "2" == self.CONDA_LUA[0] else "lua"
         if sys.platform == 'win32':
diff --git a/conda_build/metadata.py b/conda_build/metadata.py
--- a/conda_build/metadata.py
+++ b/conda_build/metadata.py
@@ -969,6 +969,18 @@ def get_output_metadata(self, output):
             output_metadata.meta['extra'] = extra
         output_metadata.noarch = output.get('noarch')
         output_metadata.noarch_python = output.get('noarch_python')
+        build = output_metadata.meta.get('build', {})
+        if 'entry_points' in output:
+            build['entry_points'] = output['entry_points']
+        elif 'entry_points' in output:
+            del output['entry_points']
+        output_metadata.meta['build'] = build
+        test = output_metadata.meta.get('test', {})
+        if not output.get('name') == self.name():
+            if 'commands' in test:
+                del test['commands']
+            if 'imports' in test:
+                del test['imports']
         return output_metadata
 
     def get_output_metadata_set(self, files, permit_undefined_jinja=False):
@@ -991,7 +1003,6 @@ def get_output_metadata_set(self, files, permit_undefined_jinja=False):
                 # but only if a matching output name is not explicitly provided
                 if uses_subpackage and not any(self.name() == out.get('name', '')
                                                for out in outputs):
-
                     outputs.append({'name': self.name(), 'requirements': requirements,
                                     'pin_downstream':
                                         self.meta.get('build', {}).get('pin_downstream'),


Test patch:
diff --git a/tests/test-recipes/split-packages/_entry_points/install-pkg1.sh b/tests/test-recipes/split-packages/_entry_points/install-pkg1.sh
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/split-packages/_entry_points/install-pkg1.sh
@@ -0,0 +1,5 @@
+env
+cat <<EOF > $SP_DIR/pkg1.py
+def main():
+    print("Hello pkg1")
+EOF
diff --git a/tests/test-recipes/split-packages/_entry_points/install-pkg2.sh b/tests/test-recipes/split-packages/_entry_points/install-pkg2.sh
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/split-packages/_entry_points/install-pkg2.sh
@@ -0,0 +1,4 @@
+cat <<EOF > $SP_DIR/pkg2.py
+def main():
+    print("Hello pkg2")
+EOF
diff --git a/tests/test-recipes/split-packages/_entry_points/meta.yaml b/tests/test-recipes/split-packages/_entry_points/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/split-packages/_entry_points/meta.yaml
@@ -0,0 +1,42 @@
+package:
+  name: test_split_package_entry_points
+  version: 1.0
+
+build:
+  entry_points:
+    - top1 = pkg1:main
+    - top2 = pkg2:main
+
+requirements:
+  build:
+    - python
+  run:
+    - split_package_entry_points1
+    - split_package_entry_points2
+
+# we want to make sure that top-level entry-points do not propagate down to subpackages
+#   and subpackage entry points should not propagate up to the top-level package
+outputs:
+  - name: split_package_entry_points1
+    entry_points:
+      - pkg1 = pkg1:main
+    script: install-pkg1.sh
+    requirements:
+      - python
+    test:
+      script: test-pkg1.py
+  - name: split_package_entry_points2
+    entry_points:
+      - pkg2 = pkg2:main
+    script: install-pkg2.sh
+    requirements:
+      - python
+    test:
+      script: test-pkg2.py
+
+test:
+  commands:
+    - top1
+    - top2
+  # imports:
+  #   -
diff --git a/tests/test-recipes/split-packages/_entry_points/test-pkg1.py b/tests/test-recipes/split-packages/_entry_points/test-pkg1.py
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/split-packages/_entry_points/test-pkg1.py
@@ -0,0 +1,7 @@
+import os
+import subprocess
+import sys
+import pkg1
+pkg1.main()
+bindir = 'Scripts' if sys.platform == 'win32' else 'bin'
+subprocess.check_call([os.path.join(os.getenv('PREFIX'), bindir, 'pkg1')])
diff --git a/tests/test-recipes/split-packages/_entry_points/test-pkg2.py b/tests/test-recipes/split-packages/_entry_points/test-pkg2.py
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/split-packages/_entry_points/test-pkg2.py
@@ -0,0 +1,7 @@
+import os
+import subprocess
+import sys
+import pkg2
+pkg2.main()
+bindir = 'Scripts' if sys.platform == 'win32' else 'bin'
+subprocess.check_call([os.path.join(os.getenv('PREFIX'), bindir, 'pkg2')])
diff --git a/tests/test_api_build.py b/tests/test_api_build.py
--- a/tests/test_api_build.py
+++ b/tests/test_api_build.py
@@ -402,24 +402,6 @@ def test_render_setup_py_old_funcname(testing_workdir, test_config, caplog):
     assert "Deprecation notice: the load_setuptools function has been renamed to " in caplog.text
 
 
-def test_debug_build_option(test_metadata, caplog, capfd):
-    logging.basicConfig(level=logging.INFO)
-    info_message = "INFO"
-    debug_message = "DEBUG"
-    api.build(test_metadata)
-    # this comes from an info message
-    assert info_message in caplog.text
-    # this comes from a debug message
-    assert debug_message not in caplog.text
-
-    test_metadata.config.debug = True
-    api.build(test_metadata)
-    # this comes from an info message
-    assert info_message in caplog.text
-    # this comes from a debug message
-    assert debug_message in caplog.text
-
-
 @pytest.mark.skipif(not on_win, reason="only Windows is insane enough to have backslashes in paths")
 def test_backslash_in_always_include_files_path(test_config):
     api.build(os.path.join(metadata_dir, '_backslash_in_include_files'))
@@ -571,7 +553,6 @@ def test_relative_git_url_submodule_clone(testing_workdir, monkeypatch):
         api.build(testing_workdir)
 
 
-
 def test_noarch(testing_workdir):
     filename = os.path.join(testing_workdir, 'meta.yaml')
     for noarch in (False, True):
@@ -908,7 +889,8 @@ def test_only_r_env_vars_defined(test_config):
 
 def test_only_perl_env_vars_defined(test_config):
     recipe = os.path.join(metadata_dir, '_perl_env_defined')
-    test_config.channel_urls = ('conda-forge', )
+    if not on_win:
+        test_config.channel_urls = ('conda-forge', )
     api.build(recipe, config=test_config)
 
 
diff --git a/tests/test_subpackages.py b/tests/test_subpackages.py
--- a/tests/test_subpackages.py
+++ b/tests/test_subpackages.py
@@ -1,7 +1,7 @@
 import os
 import pytest
 
-from conda_build import api
+from conda_build import api, utils
 
 from .utils import is_valid_dir, subpackage_dir
 
@@ -32,3 +32,34 @@ def test_rm_rf_does_not_remove_relative_source_package_files(test_config, monkey
     assert os.path.isfile(bin_file_that_disappears)
     api.build('conda', config=test_config)
     assert os.path.isfile(bin_file_that_disappears)
+
+
+def test_toplevel_entry_points_do_not_apply_to_subpackages(test_config):
+    recipe_dir = os.path.join(subpackage_dir, '_entry_points')
+    outputs = api.build(recipe_dir, config=test_config)
+    if utils.on_win:
+        script_dir = 'Scripts'
+        ext = '.exe'
+    else:
+        script_dir = 'bin'
+        ext = ''
+    for out in outputs:
+        fn = os.path.basename(out)
+        if fn.startswith('split_package_entry_points1'):
+            assert utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'pkg1', ext))
+            assert not utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'pkg2', ext))
+            assert not utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'top1', ext))
+            assert not utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'top2', ext))
+        elif fn.startswith('split_package_entry_points2'):
+            assert utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'pkg2', ext))
+            assert not utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'pkg1', ext))
+            assert not utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'top1', ext))
+            assert not utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'top2', ext))
+        elif fn.startswith('test_split_package_entry_points'):
+            # python commands will make sure that these are available.
+            # assert utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'top1', ext))
+            # assert utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'top2', ext))
+            assert not utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'pkg1', ext))
+            assert not utils.package_has_file(out, '{}/{}{}'.format(script_dir, 'pkg2', ext))
+        else:
+            raise ValueError("Didn't see any of the 3 expected filenames.  Filename was {}".format(fn))
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -70,6 +70,7 @@ def test_disallow_merge_conflicts(namespace_setup, test_config):
                                                  'package'))
 
 
+@pytest.mark.skipif(utils.on_win, reason="only unix has full os.chmod capabilities")
 def test_unzip(testing_workdir):
     with open('file_with_execute_permission', 'w') as f:
         f.write("test")