From 67a247f6d9697e64db0e2ab0133cebd3fc330259 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20O=C5=BCarowski?= <piotr@debian.org>
Date: Sun, 10 Jun 2012 14:32:29 +0200
Subject: [PATCH] =?UTF-8?q?*=20dh=5Fpython2:=20=20=20-=20rewrite=20shebang?=
 =?UTF-8?q?s=20by=20default=20(disable=20via=20--no-shebang-rewrite),=20?=
 =?UTF-8?q?=20=20=20=20examples:=20=20=20=20=20=20+=20"/usr/bin/env=20pyth?=
 =?UTF-8?q?on*"=20=E2=86=92=20"/usr/bin/python*"=20=20=20=20=20=20+=20"/us?=
 =?UTF-8?q?r/local/bin/python=20foo"=20=E2=86=92=20"/usr/bin/python=20foo"?=
 =?UTF-8?q?=20=20=20=20=20=20+=20"/usr/bin/python2"=20=E2=86=92=20"/usr/bi?=
 =?UTF-8?q?n/python"=20=20=20-=20new=20--shebang=20option=20to=20replace?=
 =?UTF-8?q?=20all=20shebangs=20in=20bin=20dirs=20=20=20=20=20(example:=20-?=
 =?UTF-8?q?-shebang=20/usr/bin/python2.6)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 debian/changelog              | 12 ++++++++--
 debpython/depends.py          | 10 +++-----
 debpython/tools.py            | 45 ++++++++++++++++++++++++++++++-----
 dh_python2                    | 45 ++++++++++++++++++++++-------------
 dh_python2.rst                |  4 ++++
 tests/Makefile                |  2 +-
 tests/t7/Makefile             | 24 +++++++++++++++++++
 tests/t7/bar.py               |  2 ++
 tests/t7/baz.py               |  2 ++
 tests/t7/debian/changelog     |  5 ++++
 tests/t7/debian/compat        |  1 +
 tests/t7/debian/control       | 13 ++++++++++
 tests/t7/debian/copyright     |  2 ++
 tests/t7/debian/install       |  7 ++++++
 tests/t7/debian/rules         | 11 +++++++++
 tests/t7/debian/source/format |  1 +
 tests/t7/foo.py               |  2 ++
 tests/t7/setup.py             |  0
 tests/t7/spam.py              |  1 +
 19 files changed, 156 insertions(+), 33 deletions(-)
 create mode 100644 tests/t7/Makefile
 create mode 100755 tests/t7/bar.py
 create mode 100755 tests/t7/baz.py
 create mode 100644 tests/t7/debian/changelog
 create mode 100644 tests/t7/debian/compat
 create mode 100644 tests/t7/debian/control
 create mode 100644 tests/t7/debian/copyright
 create mode 100644 tests/t7/debian/install
 create mode 100755 tests/t7/debian/rules
 create mode 100644 tests/t7/debian/source/format
 create mode 100755 tests/t7/foo.py
 create mode 100644 tests/t7/setup.py
 create mode 100644 tests/t7/spam.py

diff --git a/debian/changelog b/debian/changelog
index 17f10e6..540b484 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,7 +1,15 @@
 python-defaults (2.7.3~rc2-2) unstable; urgency=low
 
-  * dh_python2: remove even more \.so.* dangling symlinks, thanks to Stefano
-    Rivera for providing a test case
+  * dh_python2:
+    - remove even more \.so.* dangling symlinks, thanks to Stefano
+      Rivera for providing a test case
+    - rewrite shebangs by default (disable via --no-shebang-rewrite),
+      examples:
+       + "/usr/bin/env python*" → "/usr/bin/python*"
+       + "/usr/local/bin/python foo" → "/usr/bin/python foo"
+       + "/usr/bin/python2" → "/usr/bin/python"
+    - new --shebang option to replace all shebangs in bin dirs
+      (example: --shebang /usr/bin/python2.6)
 
  -- Piotr Ożarowski <piotr@debian.org>  Sun, 10 Jun 2012 13:52:38 +0200
 
diff --git a/debpython/depends.py b/debpython/depends.py
index ec286b7..cf146d1 100644
--- a/debpython/depends.py
+++ b/debpython/depends.py
@@ -112,15 +112,11 @@ class Dependencies(object):
         if stats['compile']:
             self.depend(MINPYCDEP)
 
-        if not options.ignore_shebangs:
-            for interpreter, version in stats['shebangs']:
-                self.depend(interpreter)
+        for interpreter, version in stats['shebangs']:
+            self.depend(interpreter)
 
         for private_dir, details in stats['private_dirs'].iteritems():
-            if options.ignore_shebangs:
-                versions = []
-            else:
-                versions = list(v for i, v in details.get('shebangs', []) if v)
+            versions = list(v for i, v in details.get('shebangs', []) if v)
 
             for v in versions:
                 if v in SUPPORTED:
diff --git a/debpython/tools.py b/debpython/tools.py
index 78979f6..7b90f95 100644
--- a/debpython/tools.py
+++ b/debpython/tools.py
@@ -1,5 +1,5 @@
 # -*- coding: UTF-8 -*-
-# Copyright © 2010 Piotr Ożarowski <piotr@debian.org>
+# Copyright © 2010-2012 Piotr Ożarowski <piotr@debian.org>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -33,7 +33,7 @@ from debpython.version import RANGE_PATTERN, getver, get_requested_versions
 
 log = logging.getLogger(__name__)
 EGGnPTH_RE = re.compile(r'(.*?)(-py\d\.\d(?:-[^.]*)?)?(\.egg-info|\.pth)$')
-SHEBANG_RE = re.compile(r'^#!\s*/usr/bin/(?:env\s+)?(python(\d+\.\d+)?(?:-dbg)?).*')
+SHEBANG_RE = re.compile(r'^#!\s*(.*?/bin/.*?)(python(\d+\.\d+)?(?:-dbg)?)(?:\s(.*))?')
 SHAREDLIB_RE = re.compile(r'NEEDED.*libpython(\d\.\d)')
 INSTALL_RE = re.compile(r"""
     (?P<pattern>.+?)  # file pattern
@@ -92,6 +92,39 @@ def relative_symlink(target, link):
     return os.symlink(relpath(target, link), link)
 
 
+def fix_shebang(fpath, replacement=None):
+    """Normalize file's shebang.
+
+    :param replacement: new shebang command (path to interpreter and options)
+    """
+    try:
+        with open(fpath) as fp:
+            fcontent = fp.readlines()
+    except IOError:
+        log.error('cannot open %s', fpath)
+        return False
+
+    match = SHEBANG_RE.match(fcontent[0])
+    if not match:
+        return None
+    if not replacement:
+        path, interpreter, version, argv = match.groups()
+        if path != '/usr/bin':  # f.e. /usr/local/* or */bin/env
+            replacement = "/usr/bin/%s" % interpreter
+        if interpreter == 'python2':
+            replacement = '/usr/bin/python'
+        if replacement and argv:
+            replacement += " %s" % argv
+    if replacement:
+        log.info('replacing shebang in %s (%s)', fpath, fcontent[0])
+        # do not catch IOError here, the file is zeroed at this stage so it's
+        # better to fail dh_python2
+        with open(fpath, 'w') as fp:
+            fp.write("#! %s\n" % replacement)
+            fp.writelines(fcontent[1:])
+    return True
+
+
 def shebang2pyver(fpath):
     """Check file's shebang.
 
@@ -105,10 +138,10 @@ def shebang2pyver(fpath):
             if not match:
                 return None
             res = match.groups()
-            if res != (None, None):
-                if res[1]:
-                    res = res[0], getver(res[1])
-                return res
+            if res[1:3] != (None, None):
+                if res[2]:
+                    return res[1], getver(res[2])
+                return res[1], None
     except IOError:
         log.error('cannot open %s', fpath)
 
diff --git a/dh_python2 b/dh_python2
index 95e3467..c625ceb 100755
--- a/dh_python2
+++ b/dh_python2
@@ -42,8 +42,8 @@ import debpython.namespace as ns
 from debpython.pydist import validate as validate_pydist, \
                              PUBLIC_DIR_RE
 from debpython.tools import sitedir, relative_symlink, \
-                            shebang2pyver, so2pyver, \
-                            clean_egg_name, \
+                            fix_shebang, shebang2pyver, \
+                            so2pyver, clean_egg_name, \
                             pyinstall, pyremove
 from debpython.option import Option
 
@@ -299,7 +299,7 @@ def share_2x(dir1, dir2, dc=None):
 
 
 ### PACKAGE DETAILS ############################################
-def scan(package, dname=None, clean_dbg_pkg=True):
+def scan(package, dname=None, options=None):
     """Gather statistics about Python files in given package."""
     r = {'requires.txt': set(),
          'nsp.txt': set(),
@@ -358,7 +358,7 @@ def scan(package, dname=None, clean_dbg_pkg=True):
         # handle some EGG related data (.egg-info dirs)
         for name in dirs:
             if name.endswith('.egg-info'):
-                if dbg_package and clean_dbg_pkg:
+                if dbg_package and options.clean_dbg_pkg:
                     rmtree(join(root, name))
                     dirs.remove(name)
                     continue
@@ -404,18 +404,22 @@ def scan(package, dname=None, clean_dbg_pkg=True):
                             os.remove(lpath)
                         log.info('renaming %s to %s', dstfpath, fn)
                         os.rename(dstfpath, fpath)
-                if dbg_package and clean_dbg_pkg and fext not in ('so', 'h'):
+                if dbg_package and options.clean_dbg_pkg and \
+                   fext not in ('so', 'h'):
                     os.remove(join(root, fn))
                     continue
 
             elif private_dir:
-                if exists(join(root, fn)):
-                    mode = os.stat(join(root, fn))[ST_MODE]
+                if exists(fpath):
+                    mode = os.stat(fpath)[ST_MODE]
                     if mode & S_IXUSR or mode & S_IXGRP or mode & S_IXOTH:
-                        res = shebang2pyver(join(root, fn))
-                        if res:
-                            r['private_dirs'].setdefault(private_dir, {})\
-                                .setdefault('shebangs', set()).add(res)
+                        if (options.no_shebang_rewrite or \
+                            fix_shebang(fpath, options.shebang)) and \
+                           not options.ignore_shebangs:
+                            res = shebang2pyver(fpath)
+                            if res:
+                                r['private_dirs'].setdefault(private_dir, {})\
+                                    .setdefault('shebangs', set()).add(res)
 
             if public_dir or private_dir:
                 if fext == 'so':
@@ -452,11 +456,14 @@ def scan(package, dname=None, clean_dbg_pkg=True):
                 continue
             # search for scripts in bin dirs
             if bin_dir:
-                res = shebang2pyver(fpath)
-                if res:
-                    r['shebangs'].add(res)
-
-    if dbg_package and clean_dbg_pkg:
+                if (options.no_shebang_rewrite or \
+                    fix_shebang(fpath, options.shebang)) and \
+                   not options.ignore_shebangs:
+                     res = shebang2pyver(fpath)
+                     if res:
+                         r['shebangs'].add(res)
+
+    if dbg_package and options.clean_dbg_pkg:
         # remove empty directories in -dbg packages
         proot = proot + '/usr/lib'
         for root, dirs, file_names in os.walk(proot, topdown=False):
@@ -519,6 +526,8 @@ def main():
         help='recreate __init__.py files for given namespaces at install time')
     parser.add_option('--clean-pycentral', action='store_true', default=False,
         help='generate maintainer script that will remove pycentral files')
+    parser.add_option('--shebang',
+        help='use given command as shebang in scripts')
     parser.add_option('--ignore-shebangs', action='store_true', default=False,
         help='do not translate shebangs into Debian dependencies')
     parser.add_option('--ignore-namespace', action='store_true', default=False,
@@ -526,6 +535,8 @@ def main():
     parser.add_option('--no-dbg-cleaning', action='store_false',
             dest='clean_dbg_pkg', default=True,
         help='do not remove files from debug packages')
+    parser.add_option('--no-shebang-rewrite', action='store_true',
+            default=False, help='do not rewrite shebangs')
     # ignore some debhelper options:
     parser.add_option('-O', help=SUPPRESS_HELP)
 
@@ -593,7 +604,7 @@ def main():
             if not pyremove(package, options.vrange):
                 exit(5)
             fix_locations(package)
-        stats = scan(package, private_dir, options.clean_dbg_pkg)
+        stats = scan(package, private_dir, options)
         if not private_dir:
             share(package, stats, options)
             pyshared_dir = "debian/%s/usr/share/pyshared/" % package
diff --git a/dh_python2.rst b/dh_python2.rst
index 234f621..0b44a7d 100644
--- a/dh_python2.rst
+++ b/dh_python2.rst
@@ -112,6 +112,8 @@ OPTIONS
 
 --no-dbg-cleaning	do not remove any files from debug packages
 
+--no-shebang-rewrite	do not rewrite shebangs
+
 --skip-private	don't check private directories
 
 -v, --verbose	turn verbose mode on
@@ -156,6 +158,8 @@ OPTIONS
 --clean-pycentral	generate maintainer script that will remove byte code
   generated by python-central helper
 
+--shebang=COMMAND	use given command as shebang in scripts
+
 --ignore-shebangs	do not translate shebangs into Debian dependencies
 
 SEE ALSO
diff --git a/tests/Makefile b/tests/Makefile
index 8cade20..224dca2 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -1,7 +1,7 @@
 #!/usr/bin/make -f
 
 # enable or disable tests here:
-TESTS := test1 test2 test3 test4 test5 test6
+TESTS := test1 test2 test3 test4 test5 test6 test7
 
 all: $(TESTS)
 
diff --git a/tests/t7/Makefile b/tests/t7/Makefile
new file mode 100644
index 0000000..a2b7fe8
--- /dev/null
+++ b/tests/t7/Makefile
@@ -0,0 +1,24 @@
+#!/usr/bin/make -f
+
+all: run check
+
+
+run: clean
+	dpkg-buildpackage -b -us -uc
+
+check:
+	# python2.4 hardcoded via `dh_python2 -shebang ...python2.4`
+	grep -q '\-V 2.4 /usr/share/baz24' debian/foo/usr/share/python/runtime.d/foo.rtupdate
+	grep -q '/usr/share/baz24 \-V 2.4' debian/foo/DEBIAN/postinst
+	grep -q '#! /usr/bin/python2.4 -OO' debian/foo/usr/share/baz24/baz.py
+	# python2.6 hardcoded via shebang
+	grep -q '\-V 2.6 /usr/share/foo' debian/foo/usr/share/python/runtime.d/foo.rtupdate
+	grep -q '/usr/share/foo \-V 2.6' debian/foo/DEBIAN/postinst
+	# /env removed from shebang 
+	grep -q '#! /usr/bin/python' debian/foo/usr/share/bar/bar.py
+	# /local removed from shebang 
+	grep -q '#! /usr/bin/python' debian/foo/usr/share/foo/baz.py
+	grep -q '#! /usr/bin/python2.6' debian/foo/usr/share/foo/foo.py
+
+clean:
+	./debian/rules clean
diff --git a/tests/t7/bar.py b/tests/t7/bar.py
new file mode 100755
index 0000000..42a88cc
--- /dev/null
+++ b/tests/t7/bar.py
@@ -0,0 +1,2 @@
+#!/usr/bin/env python
+"env in shebang"
diff --git a/tests/t7/baz.py b/tests/t7/baz.py
new file mode 100755
index 0000000..4dc3658
--- /dev/null
+++ b/tests/t7/baz.py
@@ -0,0 +1,2 @@
+#!/usr/local/bin/python
+"/usr/local in shebang"
diff --git a/tests/t7/debian/changelog b/tests/t7/debian/changelog
new file mode 100644
index 0000000..c1ed13c
--- /dev/null
+++ b/tests/t7/debian/changelog
@@ -0,0 +1,5 @@
+foo (1.0) unstable; urgency=low
+
+  * Initial release
+
+ -- Piotr Ożarowski <piotr@debian.org>  Sun, 10 Jun 2012 14:09:45 +0200
diff --git a/tests/t7/debian/compat b/tests/t7/debian/compat
new file mode 100644
index 0000000..45a4fb7
--- /dev/null
+++ b/tests/t7/debian/compat
@@ -0,0 +1 @@
+8
diff --git a/tests/t7/debian/control b/tests/t7/debian/control
new file mode 100644
index 0000000..d14acc7
--- /dev/null
+++ b/tests/t7/debian/control
@@ -0,0 +1,13 @@
+Source: foo
+Section: misc
+Priority: optional
+Maintainer: Piotr Ożarowski <piotr@debian.org>
+Build-Depends: debhelper (>= 7.0.50~)
+Build-Depends-Indep: python
+Standards-Version: 3.9.3
+
+Package: foo
+Architecture: all
+Depends: ${python:Depends}, ${misc:Depends}
+Description: example 7 - shebangs
+ exemple package #7 - shebang related tests
diff --git a/tests/t7/debian/copyright b/tests/t7/debian/copyright
new file mode 100644
index 0000000..bf78fd0
--- /dev/null
+++ b/tests/t7/debian/copyright
@@ -0,0 +1,2 @@
+The Debian packaging is © 2012, Piotr Ożarowski <piotr@debian.org> and
+is licensed under the MIT License.
diff --git a/tests/t7/debian/install b/tests/t7/debian/install
new file mode 100644
index 0000000..ff65342
--- /dev/null
+++ b/tests/t7/debian/install
@@ -0,0 +1,7 @@
+foo.py /usr/share/foo/
+baz.py /usr/share/foo/
+spam.py /usr/share/foo/
+bar.py /usr/share/bar/
+spam.py /usr/share/bar/
+baz.py /usr/share/baz24/
+spam.py /usr/share/baz24/
diff --git a/tests/t7/debian/rules b/tests/t7/debian/rules
new file mode 100755
index 0000000..551e9c9
--- /dev/null
+++ b/tests/t7/debian/rules
@@ -0,0 +1,11 @@
+#!/usr/bin/make -f
+%:
+	dh $@ --buildsystem=python_distutils
+
+override_dh_pysupport:
+	DH_VERBOSE=1 ../../dh_python2
+	DH_VERBOSE=1 ../../dh_python2 /usr/share/bar
+	DH_VERBOSE=1 ../../dh_python2 /usr/share/baz24 --shebang '/usr/bin/python2.4 -OO'
+
+clean:
+	dh_clean
diff --git a/tests/t7/debian/source/format b/tests/t7/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/tests/t7/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/tests/t7/foo.py b/tests/t7/foo.py
new file mode 100755
index 0000000..6ea2fec
--- /dev/null
+++ b/tests/t7/foo.py
@@ -0,0 +1,2 @@
+#!/usr/local/bin/python2.6
+"/usr/local/bin/python2.6 hardcoded in shebang"
diff --git a/tests/t7/setup.py b/tests/t7/setup.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/t7/spam.py b/tests/t7/spam.py
new file mode 100644
index 0000000..c8b0fd9
--- /dev/null
+++ b/tests/t7/spam.py
@@ -0,0 +1 @@
+print('spam')
-- 
GitLab