Patchwork [1,of,2] setup.py: attempt to build and install hg.exe on Windows

login
register
mail settings
Submitter Gregory Szorc
Date Dec. 6, 2015, 2:15 a.m.
Message ID <6550ffef53c01743d5d0.1449368103@ubuntu-main>
Download mbox | patch
Permalink /patch/11846/
State Superseded
Delegated to: Yuya Nishihara
Headers show

Comments

Gregory Szorc - Dec. 6, 2015, 2:15 a.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@gmail.com>
# Date 1449217488 28800
#      Fri Dec 04 00:24:48 2015 -0800
# Node ID 6550ffef53c01743d5d0efbd32a13bb9a344579f
# Parent  30a20167ae29e1874163b59a489de0cb1d859565
setup.py: attempt to build and install hg.exe on Windows

Currently, packaging Mercurial on Windows will produce a
Scripts\hg Python script and a Scripts\hg.bat batch script. The
py2exe distribution contains a hg.exe which loads a Python
interpretter and invokes the "hg" Python script. Running a
exe directly has benefits over batch scripts because batch
scripts do things like muck around with command arguments.

This patch implements a custom "build_scripts" command which
attempts to build hg.exe on Windows. If hg.exe is built, it is
marked as a "script" file and installed into the Scripts\
directory on Windows. Since hg.exe is redundant and better than
hg.bat, if hg.exe is built, hg.bat is not installed.

Since some environments don't support compiling C programs,
we treat hg.exe as optional and catch failures building it. This
is not ideal. However, I reckon most Windows users will not be
installing Mercurial from source: they will get it from the MSI
installer or via `pip install Mercurial`, which will download a
wheel that has hg.exe in it. So, I don't think this is a big deal.
Yuya Nishihara - Dec. 6, 2015, 7:50 a.m.
On Sat, 05 Dec 2015 18:15:03 -0800, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@gmail.com>
> # Date 1449217488 28800
> #      Fri Dec 04 00:24:48 2015 -0800
> # Node ID 6550ffef53c01743d5d0efbd32a13bb9a344579f
> # Parent  30a20167ae29e1874163b59a489de0cb1d859565
> setup.py: attempt to build and install hg.exe on Windows

> +class hgbuildscripts(build_scripts):
> +    def run(self):
> +        if os.name != 'nt':
> +            return build_scripts.run(self)
> +
> +        exebuilt = False
> +        try:
> +            self.run_command('build_hgexe')
> +            exebuilt = True
> +        except (DistutilsError, CCompilerError):
> +            log.warn('failed to build optional hg.exe')
> +
> +        if exebuilt:
> +            # Copying hg.exe to the scripts build directory ensures it is
> +            # installed by the install_scripts command.
> +            hgexecommand = self.get_finalized_command('build_hgexe')
> +            dest = os.path.join(self.build_dir, 'hg.exe')
> +            self.mkpath(self.build_dir)
> +            shutil.copyfile(hgexecommand.hgexepath, dest)

Nit: self.copy_file() can handle dry_run and verbose flags appropriately.

Patch

diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -1,15 +1,15 @@ 
 #
 # This is the mercurial setup script.
 #
 # 'python setup.py install', or
 # 'python setup.py --help' for more options
 
-import sys, platform
+import sys, platform, shutil
 if getattr(sys, 'version_info', (0, 0, 0)) < (2, 6, 0, 'final'):
     raise SystemExit("Mercurial requires Python 2.6 or later.")
 
 if sys.version_info[0] >= 3:
     def b(s):
         '''A helper function to emulate 2.6+ bytes literals using string
         literals.'''
         return s.encode('latin1')
@@ -74,21 +74,26 @@  if 'FORCE_SETUPTOOLS' in os.environ:
     from setuptools import setup
 else:
     from distutils.core import setup
 from distutils.core import Command, Extension
 from distutils.dist import Distribution
 from distutils.command.build import build
 from distutils.command.build_ext import build_ext
 from distutils.command.build_py import build_py
+from distutils.command.build_scripts import build_scripts
 from distutils.command.install_lib import install_lib
 from distutils.command.install_scripts import install_scripts
 from distutils.spawn import spawn, find_executable
 from distutils import file_util
-from distutils.errors import CCompilerError, DistutilsExecError
+from distutils.errors import (
+    CCompilerError,
+    DistutilsError,
+    DistutilsExecError,
+)
 from distutils.sysconfig import get_python_inc, get_config_var
 from distutils.version import StrictVersion
 
 convert2to3 = '--c2to3' in sys.argv
 if convert2to3:
     try:
         from distutils.command.build_py import build_py_2to3 as build_py
         from lib2to3.refactor import get_fixers_from_package as getfixers
@@ -97,16 +102,17 @@  if convert2to3:
             raise SystemExit("--c2to3 is only compatible with python3.")
         raise
     sys.path.append('contrib')
 elif sys.version_info[0] >= 3:
     raise SystemExit("setup.py with python3 needs --c2to3 (experimental)")
 
 scripts = ['hg']
 if os.name == 'nt':
+    # We remove hg.bat if we are able to build hg.exe.
     scripts.append('contrib/win32/hg.bat')
 
 # simplified version of distutils.ccompiler.CCompiler.has_function
 # that actually removes its temporary files.
 def hasfunction(cc, funcname):
     tmpdir = tempfile.mkdtemp(prefix='hg-install-')
     devnull = oldstderr = None
     try:
@@ -299,16 +305,42 @@  class hgbuildext(build_ext):
         try:
             build_ext.build_extension(self, ext)
         except CCompilerError:
             if not getattr(ext, 'optional', False):
                 raise
             log.warn("Failed to build optional extension '%s' (skipping)",
                      ext.name)
 
+class hgbuildscripts(build_scripts):
+    def run(self):
+        if os.name != 'nt':
+            return build_scripts.run(self)
+
+        exebuilt = False
+        try:
+            self.run_command('build_hgexe')
+            exebuilt = True
+        except (DistutilsError, CCompilerError):
+            log.warn('failed to build optional hg.exe')
+
+        if exebuilt:
+            # Copying hg.exe to the scripts build directory ensures it is
+            # installed by the install_scripts command.
+            hgexecommand = self.get_finalized_command('build_hgexe')
+            dest = os.path.join(self.build_dir, 'hg.exe')
+            self.mkpath(self.build_dir)
+            shutil.copyfile(hgexecommand.hgexepath, dest)
+
+            # Remove hg.bat because it is redundant with hg.exe.
+            self.scripts.remove('contrib/win32/hg.bat')
+
+        return build_scripts.run(self)
+
+
 class hgbuildpy(build_py):
     if convert2to3:
         fixer_names = sorted(set(getfixers("lib2to3.fixes") +
                                  getfixers("hgfixes")))
 
     def finalize_options(self):
         build_py.finalize_options(self)
 
@@ -384,16 +416,22 @@  class buildhgexe(build_ext):
         objects = self.compiler.compile(['mercurial/exewrapper.c'],
                                          output_dir=self.build_temp)
         dir = os.path.dirname(self.get_ext_fullpath('dummy'))
         target = os.path.join(dir, 'hg')
         self.compiler.link_executable(objects, target,
                                       libraries=[],
                                       output_dir=self.build_temp)
 
+    @property
+    def hgexepath(self):
+        dir = os.path.dirname(self.get_ext_fullpath('dummy'))
+        return os.path.join(self.build_temp, dir, 'hg.exe')
+
+
 class hginstalllib(install_lib):
     '''
     This is a specialization of install_lib that replaces the copy_file used
     there so that it supports setting the mode of files after copying them,
     instead of just preserving the mode that the files originally had.  If your
     system has a umask of something like 027, preserving the permissions when
     copying will lead to a broken install.
 
@@ -468,16 +506,17 @@  class hginstallscripts(install_scripts):
             fp = open(outfile, 'wb')
             fp.write(data)
             fp.close()
 
 cmdclass = {'build': hgbuild,
             'build_mo': hgbuildmo,
             'build_ext': hgbuildext,
             'build_py': hgbuildpy,
+            'build_scripts': hgbuildscripts,
             'build_hgextindex': buildhgextindex,
             'install_lib': hginstalllib,
             'install_scripts': hginstallscripts,
             'build_hgexe': buildhgexe,
             }
 
 packages = ['mercurial', 'mercurial.hgweb', 'mercurial.httpclient',
             'mercurial.pure',