Repository: conda/conda-build

Issue:
conda-build no longer generates .exe files for my scripts on Windows
I notice that conda-build, verson 1.21.7, used to generate .exe files to invoke my scripts.  But 2.0.6 doesn't seem to do that.  I've got Python scripts, goodstuff, goodstuff-data, and goodstuff-library, which are marked as scripts in my setup.py file.  1.21.7 turns them into goodstuff-script.py and goodstuff.exe, goodstuff-data-script.py and goodstuff-data.exe, etc.  Which I found very useful.

Is there any way to get that 1.21.functionality back again?



Discussion:
Looks like this was removed by https://github.com/conda/conda-build/pull/1246/commits/d598cdbda1bbe6ce6cd8b3c49a2240f38cf4161b

Where would the sources for cli-32.exe and cli-64.exe be?

Sorry, this does seem like a regression.  If you have a main entry point, you can use the entry points functionality instead.  I'll fix this soon - 1-2 week timeframe.

I don't actually know where the source for those exe's is.  They pre-date me by quite a lot.  It seems they may have been copied from setuptools at some point: https://bugs.python.org/setuptools/issue2

Unfortunately, my script files have hyphens in their name, and there's a name clash between my Python package, "goodstuff", and the Python script called "goodstuff", too.  So the entry_points feature in meta.yaml doesn't seem like it would work without some re-arranging.  If one uses that feature, would there still be a "scripts" entry in setup.py?

Btw, 2.0.7 would be fine.

The source is apparently this: https://github.com/pypa/setuptools/blob/master/launcher.c


Resolving patch:
diff --git a/conda_build/windows.py b/conda_build/windows.py
--- a/conda_build/windows.py
+++ b/conda_build/windows.py
@@ -2,7 +2,7 @@
 
 import os
 import sys
-from os.path import isdir, join
+from os.path import isdir, join, dirname, isfile
 
 # importing setuptools patches distutils so that it knows how to find VC for python 2.7
 import setuptools  # noqa
@@ -14,7 +14,7 @@
 from .conda_interface import bits
 
 from conda_build import environ
-from conda_build.utils import _check_call, root_script_dir, path_prepended
+from conda_build.utils import _check_call, root_script_dir, path_prepended, copy_into
 
 
 assert sys.platform == 'win32'
@@ -30,6 +30,36 @@
 }
 
 
+def fix_staged_scripts(scripts_dir):
+    """
+    Fixes scripts which have been installed unix-style to have a .bat
+    helper
+    """
+    if not isdir(scripts_dir):
+        return
+    for fn in os.listdir(scripts_dir):
+        # process all the extensionless files
+        if not isfile(join(scripts_dir, fn)) or '.' in fn:
+            continue
+
+        with open(join(scripts_dir, fn)) as f:
+            line = f.readline().lower()
+            # If it's a #!python script
+            if not (line.startswith('#!') and 'python' in line.lower()):
+                continue
+            print('Adjusting unix-style #! script %s, '
+                  'and adding a .bat file for it' % fn)
+            # copy it with a .py extension (skipping that first #! line)
+            with open(join(scripts_dir, fn + '-script.py'), 'w') as fo:
+                fo.write(f.read())
+            # now create the .exe file
+            copy_into(join(dirname(__file__), 'cli-%d.exe' % bits),
+                            join(scripts_dir, fn + '.exe'))
+
+        # remove the original script
+        os.remove(join(scripts_dir, fn))
+
+
 def build_vcvarsall_vs_path(version):
     """
     Given the Visual Studio version, returns the default path to the
@@ -198,3 +228,5 @@ def build(m, bld_bat, config):
 
         cmd = ['cmd.exe', '/c', 'bld.bat']
         _check_call(cmd, cwd=src_dir)
+
+    fix_staged_scripts(join(config.build_prefix, 'Scripts'))


Test patch:
diff --git a/tests/test-recipes/metadata/_script_win_creates_exe/meta.yaml b/tests/test-recipes/metadata/_script_win_creates_exe/meta.yaml
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_script_win_creates_exe/meta.yaml
@@ -0,0 +1,13 @@
+package:
+  name: script_win_creates_exe
+  version: 1.0
+
+source:
+  path: .
+
+build:
+  script: python setup.py install
+
+requirements:
+  build:
+    - python
diff --git a/tests/test-recipes/metadata/_script_win_creates_exe/setup.py b/tests/test-recipes/metadata/_script_win_creates_exe/setup.py
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_script_win_creates_exe/setup.py
@@ -0,0 +1,5 @@
+from distutils.core import setup
+setup(name='foobar',
+      version='1.0',
+      scripts=['test-script']
+      )
diff --git a/tests/test-recipes/metadata/_script_win_creates_exe/test-script b/tests/test-recipes/metadata/_script_win_creates_exe/test-script
new file mode 100644
--- /dev/null
+++ b/tests/test-recipes/metadata/_script_win_creates_exe/test-script
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+import sys
+print(sys.version)
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
@@ -610,10 +610,10 @@ def test_noarch_none_value(testing_workdir, test_config):
         api.build(recipe, config=test_config)
 
 
-def test_noarch_foo_value():
+def test_noarch_foo_value(test_config):
     recipe = os.path.join(metadata_dir, "noarch_foo")
-    fn = api.get_output_file_path(recipe)
-    api.build(recipe)
+    fn = api.get_output_file_path(recipe, test_config)
+    api.build(recipe, config=test_config)
     metadata = json.loads(package_has_file(fn, 'info/index.json').decode())
     assert 'noarch' in metadata
     assert metadata['noarch'] == "foo"
@@ -639,25 +639,25 @@ def test_about_json_content(test_metadata):
 
 
 @pytest.mark.xfail(reason="Conda can not yet install `noarch: python` packages")
-def test_noarch_python_with_tests():
+def test_noarch_python_with_tests(test_config):
     recipe = os.path.join(metadata_dir, "_noarch_python_with_tests")
-    api.build(recipe)
+    api.build(recipe, config=test_config)
 
 
-def test_noarch_python():
+def test_noarch_python(test_config):
     recipe = os.path.join(metadata_dir, "_noarch_python")
-    fn = api.get_output_file_path(recipe)
-    api.build(recipe)
+    fn = api.get_output_file_path(recipe, config=test_config)
+    api.build(recipe, config=test_config)
     assert package_has_file(fn, 'info/files') is not ''
     noarch = json.loads(package_has_file(fn, 'info/noarch.json').decode())
     assert 'entry_points' in noarch
     assert 'type' in noarch
 
 
-def test_skip_compile_pyc():
+def test_skip_compile_pyc(test_config):
     recipe = os.path.join(metadata_dir, "skip_compile_pyc")
-    fn = api.get_output_file_path(recipe)
-    api.build(recipe)
+    fn = api.get_output_file_path(recipe, config=test_config)
+    api.build(recipe, config=test_config)
     tf = tarfile.open(fn)
     pyc_count = 0
     for f in tf.getmembers():
@@ -672,10 +672,19 @@ def test_skip_compile_pyc():
     assert pyc_count == 2, "there should be 2 .pyc files, instead there were {}".format(pyc_count)
 
 
-def test_fix_permissions():
+def test_fix_permissions(test_config):
     recipe = os.path.join(metadata_dir, "fix_permissions")
-    fn = api.get_output_file_path(recipe)
-    api.build(recipe)
+    fn = api.get_output_file_path(recipe, config=test_config)
+    api.build(recipe, config=test_config)
     tf = tarfile.open(fn)
     for f in tf.getmembers():
         assert f.mode & 0o444 == 0o444, "tar member '{}' has invalid (read) mode".format(f.name)
+
+
+@pytest.mark.skipif(not on_win, reason="windows-only functionality")
+def test_script_win_creates_exe(test_config):
+    recipe = os.path.join(metadata_dir, "_script_win_creates_exe")
+    fn = api.get_output_file_path(recipe, config=test_config)
+    api.build(recipe, config=test_config)
+    assert package_has_file(fn, 'Scripts/test-script.exe')
+    assert package_has_file(fn, 'Scripts/test-script-script.py')