[PATCH RFC] setup.py: always build and install hg.exe on Windows

Gregory Szorc gregory.szorc at gmail.com
Fri Dec 4 08:27:53 UTC 2015


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1449217488 28800
#      Fri Dec 04 00:24:48 2015 -0800
# Node ID 6f7ae69f2a3a5686d56436d386974ec87769ddb2
# Parent  71aa5a26162d6e4a165b68f07b331e3e2eedc117
setup.py: always build and install hg.exe on Windows

One of the last blockers for distributing Mercurial as a wheel
on Windows is we need to ship hg.exe.

This patch hacks up the Python build and packaging commands to build
hg.exe.

A custom "build_scripts" command is installed which builds hg.exe.
Later in "install_scripts" we copy over hg.exe into the scripts
directory.

I confirmed that hg.exe is included in the produced wheel and that
`pip install <wheel>` results in Scripts\hg.exe being installed.
There is also a hg.bat file next to it. We may want to further
modify the packaging to not produce hg.bat when hg.exe is present
so we avoid any confusion.

We likely also want to not require hg.exe, as some build environments
don't support compiling C.

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,16 +74,17 @@ 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.sysconfig import get_python_inc, get_config_var
 from distutils.version import StrictVersion
 
@@ -301,16 +302,24 @@ 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':
+            self.run_command('build_hgexe')
+
+        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 +393,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.
 
@@ -464,20 +479,28 @@ class hginstallscripts(install_scripts):
             if b('\0') in data:
                 continue
 
             data = data.replace(b('@LIBDIR@'), libdir.encode(libdir_escape))
             fp = open(outfile, 'wb')
             fp.write(data)
             fp.close()
 
+        if os.name == 'nt':
+            hgexecommand = self.get_finalized_command('build_hgexe')
+            dest = os.path.join(self.install_dir, 'hg.exe')
+            shutil.copyfile(hgexecommand.hgexepath, dest)
+            mode = ((os.stat(dest)[stat.ST_MODE]) | 0555) & 07777
+            os.chmod(dest, mode)
+
 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',
             'hgext', 'hgext.convert', 'hgext.highlight', 'hgext.zeroconf',
@@ -642,8 +665,9 @@ setup(name='mercurial',
       options={'py2exe': {'packages': ['hgext', 'email']},
                'bdist_mpkg': {'zipdist': False,
                               'license': 'COPYING',
                               'readme': 'contrib/macosx/Readme.html',
                               'welcome': 'contrib/macosx/Welcome.html',
                               },
                },
       **extra)
+


More information about the Mercurial-devel mailing list