1# setup.py
2# A distutils setup script to install TortoiseHg in Windows and Posix
3# environments.
4#
5# On Windows, this script is mostly used to build a stand-alone
6# TortoiseHg package.  See installer\build.txt for details. The other
7# use is to report the current version of the TortoiseHg source.
8
9import time
10import sys
11import os
12import shutil
13import tempfile
14import re
15import subprocess
16import tarfile
17from fnmatch import fnmatch
18from distutils import log
19if 'FORCE_SETUPTOOLS' in os.environ:
20    from setuptools import setup
21else:
22    if 'py2app' in sys.argv[1:]:
23        sys.exit("py2app requires FORCE_SETUPTOOLS=1 to be set in os.environ.")
24
25    from distutils.core import setup
26from distutils.core import Command
27from distutils.command.build import build as _build_orig
28from distutils.command.clean import clean as _clean_orig
29from distutils.spawn import spawn, find_executable
30from i18n.msgfmt import Msgfmt
31
32thgcopyright = 'Copyright (C) 2010-2021 Steve Borho and others'
33hgcopyright = 'Copyright (C) 2005-2021 Matt Mackall and others'
34
35if sys.version_info[0] >= 3:
36    unicode = str  # pycompat.unicode
37
38def _walklocales():
39    podir = 'i18n/tortoisehg'
40    for po in os.listdir(podir):
41        if not po.endswith('.po'):
42            continue
43        pofile = os.path.join(podir, po)
44        modir = os.path.join('locale', po[:-3], 'LC_MESSAGES')
45        mofile = os.path.join(modir, 'tortoisehg.mo')
46        yield pofile, modir, mofile
47
48def _msgfmt(pofile, mofile):
49    modata = Msgfmt(pofile).get()
50    with open(mofile, "wb") as fp:
51        fp.write(modata)
52
53class build_mo(Command):
54    description = "build translations (.mo files)"
55    user_options = []
56
57    def initialize_options(self):
58        pass
59
60    def finalize_options(self):
61        pass
62
63    def run(self):
64        for pofile, modir, mofile in _walklocales():
65            self.mkpath(modir)
66            self.make_file(pofile, mofile, _msgfmt, (pofile, mofile))
67
68class import_po(Command):
69    description = "import translations (.po file)"
70    user_options = [
71        ("package=", "p", ("launchpad export package or bzr repo "
72                           "[default: launchpad-export.tar.gz]")),
73        ("lang=", "l", "languages to be imported, separated by ','"),
74        ]
75
76    def initialize_options(self):
77        self.package = None
78        self.lang = None
79
80    def finalize_options(self):
81        if not self.package:
82            self.package = 'launchpad-export.tar.gz'
83
84        if self.lang:
85            self.lang = self.lang.upper().split(',')
86
87    def _untar(self, name, path='.'):
88        with tarfile.open(name, 'r') as tar:
89            path = os.path.abspath(path)
90            for tarinfo in tar.getmembers():
91                # Extract the safe file only
92                p = os.path.abspath(os.path.join(path, tarinfo.name))
93                if p.startswith(path):
94                    tar.extract(tarinfo, path)
95
96    def run(self):
97        if not find_executable('msgcat'):
98            self.warn("could not find msgcat executable")
99            return
100
101        dest_prefix = 'i18n/tortoisehg'
102        src_prefix = 'po/tortoisehg'
103
104        log.info('import from %s' % self.package)
105
106        if os.path.isdir(self.package):
107            self.bzrrepo = True
108            self.package_path = self.package
109        elif tarfile.is_tarfile(self.package):
110            self.bzrrepo = False
111            self.package_path = tempfile.mkdtemp()
112            self._untar(self.package, self.package_path)
113        else:
114            self.warn('%s is not a valid tranlation package' % self.package)
115            return
116
117        if self.bzrrepo:
118            filter = r'^([\S]+)\.po$'
119        else:
120            filter = r'^tortoisehg-([\S]+)\.po$'
121        r = re.compile(filter)
122
123        src_dir = os.path.join(self.package_path, src_prefix)
124        for src_file in os.listdir(src_dir):
125            m = r.match(src_file)
126            if not m:
127                continue
128
129            # filter the language
130            lang = m.group(1)
131            if self.lang and lang.upper() not in self.lang:
132                continue
133
134            dest_file = os.path.join(dest_prefix, lang) + '.po'
135            msg = 'updating %s...' % dest_file
136            cmd = ['msgcat',
137                   '--no-location',
138                   '-o', dest_file,
139                   os.path.join(src_dir, src_file)
140                   ]
141            self.execute(spawn, (cmd,), msg)
142
143        if not self.bzrrepo:
144            shutil.rmtree(self.package_path)
145
146class update_pot(Command):
147    description = "extract translatable strings to tortoisehg.pot"
148    user_options = []
149
150    def initialize_options(self):
151        pass
152
153    def finalize_options(self):
154        pass
155
156    def run(self):
157        if not find_executable('xgettext'):
158            self.warn("could not find xgettext executable, tortoisehg.pot "
159                      "won't be built")
160            return
161
162        dirlist = [
163            '.',
164            'contrib',
165            'contrib/win32',
166            'tortoisehg',
167            'tortoisehg/hgqt',
168            'tortoisehg/util',
169            'tortoisehg/thgutil/iniparse',
170            ]
171
172        filelist = []
173        for pathname in dirlist:
174            if not os.path.exists(pathname):
175                continue
176            for filename in os.listdir(pathname):
177                if filename.endswith('.py'):
178                    filelist.append(os.path.join(pathname, filename))
179        filelist.sort()
180
181        potfile = 'tortoisehg.pot'
182
183        cmd = [
184            'xgettext',
185            '--package-name', 'TortoiseHg',
186            '--msgid-bugs-address', '<thg-devel@googlegroups.com>',
187            '--copyright-holder', thgcopyright,
188            '--from-code', 'ISO-8859-1',
189            '--keyword=_:1,2c,2t',
190            '--add-comments=i18n:',
191            '-d', '.',
192            '-o', potfile,
193            ]
194        cmd += filelist
195        self.make_file(filelist, potfile, spawn, (cmd,))
196
197class build_config(Command):
198    description = 'create config module for unix installation'
199    user_options = [
200        ('build-lib=', 'd', 'directory to "build" (copy) to'),
201        ]
202
203    def initialize_options(self):
204        self.build_lib = None
205
206    def finalize_options(self):
207        self.set_undefined_options('build',
208                                   ('build_lib', 'build_lib'))
209
210    def _generate_config(self, cfgfile):
211        from tortoisehg.hgqt import qtcore
212        # dirty hack to get the install root
213        installcmd = self.get_finalized_command('install')
214        rootlen = len(installcmd.root or '')
215        sharedir = os.path.join(installcmd.install_data[rootlen:], 'share')
216        data = {
217            'bin_path': installcmd.install_scripts[rootlen:],
218            'license_path': os.path.join(sharedir, 'doc', 'tortoisehg',
219                                         'COPYING.txt'),
220            'locale_path': os.path.join(sharedir, 'locale'),
221            'icon_path': os.path.join(sharedir, 'pixmaps', 'tortoisehg'),
222            'nofork': True,
223            'qt_api': qtcore._detectapi(),
224            }
225        # Distributions will need to supply their own
226        with open(cfgfile, "w") as f:
227            for k, v in sorted(data.items()):
228                f.write('%s = %r\n' % (k, v))
229
230    def run(self):
231        cfgdir = os.path.join(self.build_lib, 'tortoisehg', 'util')
232        cfgfile = os.path.join(cfgdir, 'config.py')
233        self.mkpath(cfgdir)
234        self.make_file(__file__, cfgfile, self._generate_config, (cfgfile,))
235
236class build_py2app_config(build_config):
237    description = 'create config module for standalone OS X bundle'
238
239    def _generate_config(self, cfgfile):
240        from tortoisehg.hgqt import qtcore
241        # Since py2app seems to ignore the build dir in favor of the src tree,
242        # ignore the given path and generate it in the source tree.  The file
243        # is conditionalized such that it won't interfere when run from source.
244        cwd = os.path.dirname(__file__)
245        cfgfile = os.path.join(cwd, 'tortoisehg', 'util', 'config.py')
246        data = {
247            'license_path': 'COPYING.txt',
248            'locale_path': 'locale',
249            'icon_path': 'icons',
250            'qt_api': qtcore._detectapi(),
251        }
252        rsrc_dir = 'os.environ["RESOURCEPATH"]'
253
254        with open(cfgfile, "w") as f:
255            f.write("import os, sys\n"
256                    "\n"
257                    "if 'THG_OSX_APP' in os.environ:\n"
258                    "    nofork = True\n")
259            for k, v in sorted(data.items()):
260                f.write("    %s = os.path.join(%s, '%s')\n" % (k, rsrc_dir, v))
261            f.write("    bin_path = os.path.dirname(sys.executable)\n")
262
263class build_ui(Command):
264    description = 'build PyQt user interfaces (.ui)'
265    user_options = [
266        ('force', 'f', 'forcibly compile everything (ignore file timestamps)'),
267        ]
268    boolean_options = ('force',)
269
270    def initialize_options(self):
271        self.force = None
272
273    def finalize_options(self):
274        self.set_undefined_options('build', ('force', 'force'))
275
276    def _compile_ui(self, ui_file, py_file):
277        uic = self._impuic()
278        with open(py_file, 'w') as fp:
279            uic.compileUi(ui_file, fp)
280
281    @staticmethod
282    def _impuic():
283        from tortoisehg.hgqt.qtcore import QT_API
284        mod = __import__(QT_API, globals(), locals(), ['uic'])
285        return mod.uic
286
287    _wrappeduic = False
288    @classmethod
289    def _wrapuic(cls):
290        """wrap uic to use gettext's _() in place of
291        QtGui.QApplication.translate as _translate()"""
292        if cls._wrappeduic:
293            return
294
295        uic = cls._impuic()
296        compiler = uic.Compiler.compiler
297        qtproxies = uic.Compiler.qtproxies
298        indenter = uic.Compiler.indenter
299
300        class _UICompiler(compiler.UICompiler):
301            def createToplevelWidget(self, classname, widgetname):
302                o = indenter.getIndenter()
303                o.level = 0
304                o.write('from tortoisehg.util.i18n import _')
305                return super(_UICompiler, self).createToplevelWidget(classname,
306                                                                     widgetname)
307        compiler.UICompiler = _UICompiler
308
309        class _i18n_string(qtproxies.i18n_string):
310            def __str__(self):
311                # Note: ignoring self.disambig and qtproxies.i18n_context
312                return '_(%s)' % qtproxies.as_string(self.string)
313        qtproxies.i18n_string = _i18n_string
314
315        cls._wrappeduic = True
316
317    def run(self):
318        self._wrapuic()
319        basepath = os.path.join(os.path.dirname(__file__), 'tortoisehg', 'hgqt')
320        for f in os.listdir(basepath):
321            if not f.endswith('.ui'):
322                continue
323            uifile = os.path.join(basepath, f)
324            pyfile = uifile[:-3] + '_ui.py'
325            # setup.py is the source of "from i18n import _" line
326            self.make_file([uifile, __file__], pyfile,
327                           self._compile_ui, (uifile, pyfile))
328
329class build_qrc(Command):
330    description = 'build PyQt resource files (.qrc)'
331    user_options = [
332        ('build-lib=', 'd', 'directory to "build" (copy) to'),
333        ('force', 'f', 'forcibly compile everything (ignore file timestamps)'),
334        ]
335    boolean_options = ('force',)
336
337    def initialize_options(self):
338        self.build_lib = None
339        self.force = None
340
341    def finalize_options(self):
342        self.set_undefined_options('build',
343                                   ('build_lib', 'build_lib'),
344                                   ('force', 'force'))
345
346    def _findrcc(self):
347        from tortoisehg.hgqt.qtcore import QT_API
348        try:
349            rcc = {'PyQt4': 'pyrcc4', 'PyQt5': 'pyrcc5'}[QT_API]
350        except KeyError:
351            raise RuntimeError('unsupported Qt API: %s' % QT_API)
352        if os.name != 'nt' or QT_API == 'PyQt5':
353            return rcc
354        mod = __import__(QT_API, globals(), locals())
355        return os.path.join(os.path.dirname(mod.__file__), rcc)
356
357    def _generate_qrc(self, qrc_file, srcfiles, prefix):
358        from tortoisehg.hgqt import qtlib
359        basedir = os.path.dirname(qrc_file)
360        with open(qrc_file, 'w') as f:
361            f.write('<!DOCTYPE RCC><RCC version="1.0">\n')
362            f.write('  <qresource prefix="%s">\n' % qtlib.htmlescape(prefix))
363            for e in srcfiles:
364                relpath = e[len(basedir) + 1:]
365                f.write('    <file>%s</file>\n'
366                        % qtlib.htmlescape(relpath.replace(os.path.sep, '/'),
367                                           False))
368            f.write('  </qresource>\n')
369            f.write('</RCC>\n')
370
371    def _build_rc(self, srcfiles, py_file, basedir, prefix):
372        """Generate compiled resource including any files under basedir"""
373        # For details, see http://doc.qt.nokia.com/latest/resources.html
374        qrc_file = os.path.join(basedir, '%s.qrc' % os.path.basename(basedir))
375        try:
376            self._generate_qrc(qrc_file, srcfiles, prefix)
377            env = os.environ.copy()
378            if os.name == 'nt' and 'VIRTUAL_ENV' in env:
379                # pyrcc5.bat gets installed to the root of the virtualenv, not
380                # in the Scripts directory with the binaries on PATH.
381                env['PATH'] = '%s%s%s' % (
382                    env.get('PATH'), os.pathsep, env.get('VIRTUAL_ENV')
383                )
384            subprocess.check_call(
385                [self._findrcc(), qrc_file, '-o', py_file],
386                env=env,
387                shell=os.name == 'nt'
388            )
389        finally:
390            os.unlink(qrc_file)
391
392    def _build_icons(self, basepath):
393        icondir = os.path.join(os.path.dirname(__file__), 'icons')
394        iconfiles = []
395        for root, dirs, files in os.walk(icondir):
396            if root == icondir:
397                dirs.remove('svg')  # drop source of .ico files
398            iconfiles.extend(os.path.join(root, f) for f in files
399                             if f.endswith(('.png', '.svg')))
400        pyfile = os.path.join(basepath, 'icons_rc.py')
401        # we cannot detect deleted icons
402        self.make_file(iconfiles, pyfile,
403                       self._build_rc, (iconfiles, pyfile, icondir, '/icons'),
404                       exec_msg='generating %s from %s' % (pyfile, icondir))
405
406    def _build_translations(self, basepath):
407        """Build translations_rc.py which inclues qt_xx.qm"""
408        from tortoisehg.hgqt.qtcore import QT_API
409        if QT_API == 'PyQt4':
410            if os.name == 'nt':
411                import PyQt4
412                trpath = os.path.join(
413                    os.path.dirname(PyQt4.__file__), 'translations')
414            else:
415                from PyQt4.QtCore import QLibraryInfo
416                trpath = unicode(
417                    QLibraryInfo.location(QLibraryInfo.TranslationsPath))
418        else:
419            if os.name == 'nt':
420                import PyQt5
421                trpath = os.path.join(
422                    os.path.dirname(PyQt5.__file__), 'translations')
423            else:
424                from PyQt5.QtCore import QLibraryInfo
425                trpath = unicode(
426                    QLibraryInfo.location(QLibraryInfo.TranslationsPath))
427        builddir = os.path.join(self.get_finalized_command('build').build_base,
428                                'qt-translations')
429        self.mkpath(builddir)
430
431        # we have to copy .qm files to build directory because .qrc file must
432        # specify files by relative paths
433        qmfiles = []
434        for e in os.listdir(trpath):
435            if (not e.startswith(('qt_', 'qscintilla_'))
436                or e.startswith('qt_help_')
437                or not e.endswith('.qm')):
438                continue
439            f = os.path.join(builddir, e)
440            self.copy_file(os.path.join(trpath, e), f)
441            qmfiles.append(f)
442        pyfile = os.path.join(basepath, 'translations_rc.py')
443        self.make_file(qmfiles, pyfile, self._build_rc,
444                       (qmfiles, pyfile, builddir, '/translations'),
445                       exec_msg='generating %s from Qt translation' % pyfile)
446
447    def run(self):
448        basepath = os.path.join(self.build_lib, 'tortoisehg', 'hgqt')
449        self.mkpath(basepath)
450        self._build_icons(basepath)
451        self._build_translations(basepath)
452
453class clean_local(Command):
454    pats = ['*.py[co]', '*_ui.py', '*.mo', '*.orig', '*.rej']
455    excludedirs = ['.hg', 'build', 'dist']
456    description = 'clean up generated files (%s)' % ', '.join(pats)
457    user_options = []
458
459    def initialize_options(self):
460        pass
461
462    def finalize_options(self):
463        pass
464
465    def run(self):
466        for e in self._walkpaths('.'):
467            log.info("removing '%s'" % e)
468            os.remove(e)
469
470    def _walkpaths(self, path):
471        for root, _dirs, files in os.walk(path):
472            if any(root == os.path.join(path, e)
473                   or root.startswith(os.path.join(path, e, ''))
474                   for e in self.excludedirs):
475                continue
476            for e in files:
477                fpath = os.path.join(root, e)
478                if any(fnmatch(fpath, p) for p in self.pats):
479                    yield fpath
480
481class build(_build_orig):
482    sub_commands = [
483        ('build_config',
484         lambda self: (os.name != 'nt' and
485                       'py2app' not in self.distribution.commands)),
486        ('build_py2app_config',
487         lambda self: 'py2app' in self.distribution.commands),
488        ('build_ui', None),
489        ('build_qrc', lambda self: 'py2exe' in self.distribution.commands),
490        ('build_mo', None),
491        ] + _build_orig.sub_commands
492
493class clean(_clean_orig):
494    sub_commands = [
495        ('clean_local', None),
496        ] + _clean_orig.sub_commands
497
498    def run(self):
499        _clean_orig.run(self)
500        for e in self.get_sub_commands():
501            self.run_command(e)
502
503cmdclass = {
504    'build': build,
505    'build_config': build_config,
506    'build_py2app_config': build_py2app_config,
507    'build_ui': build_ui,
508    'build_qrc': build_qrc,
509    'build_mo': build_mo,
510    'clean': clean,
511    'clean_local': clean_local,
512    'update_pot': update_pot,
513    'import_po': import_po,
514    }
515
516def setup_windows(version):
517    # Specific definitios for Windows NT-alike installations
518    _scripts = []
519    _data_files = []
520    _packages = ['tortoisehg.hgqt', 'tortoisehg.util', 'tortoisehg', 'hgext3rd']
521    extra = {}
522    hgextmods = []
523
524    # py2exe needs to be installed to work
525    try:
526        import py2exe
527
528        # Help py2exe to find win32com.shell
529        try:
530            import modulefinder
531            import win32com
532            for p in win32com.__path__[1:]: # Take the path to win32comext
533                modulefinder.AddPackagePath("win32com", p)
534            pn = "win32com.shell"
535            __import__(pn)
536            m = sys.modules[pn]
537            for p in m.__path__[1:]:
538                modulefinder.AddPackagePath(pn, p)
539        except ImportError:
540            pass
541
542    except ImportError:
543        if '--version' not in sys.argv:
544            raise
545
546    # Allow use of environment variables to specify the location of Mercurial
547    import modulefinder
548    path = os.getenv('MERCURIAL_PATH')
549    if path:
550        modulefinder.AddPackagePath('mercurial', path)
551    path = os.getenv('HGEXT_PATH')
552    if path:
553        modulefinder.AddPackagePath('hgext', path)
554    modulefinder.AddPackagePath('hgext3rd', 'hgext3rd')
555
556    if 'py2exe' in sys.argv:
557        import hgext
558        hgextdir = os.path.dirname(hgext.__file__)
559        hgextmods = set(["hgext." + os.path.splitext(f)[0]
560                         for f in os.listdir(hgextdir)])
561        # most icons are packed into Qt resource, but .ico files must reside
562        # in filesystem so that shell extension can read them
563        root = 'icons'
564        _data_files.append((root,
565                            [os.path.join(root, f) for f in os.listdir(root)
566                             if f.endswith('.ico') or f == 'README.txt']))
567
568    # for PyQt, see http://www.py2exe.org/index.cgi/Py2exeAndPyQt
569    includes = ['PyQt5.sip']
570
571    # Qt4 plugins, see http://stackoverflow.com/questions/2206406/
572    def qt4_plugins(subdir, *dlls):
573        import PyQt4
574        pluginsdir = os.path.join(os.path.dirname(PyQt4.__file__), 'plugins')
575        return subdir, [os.path.join(pluginsdir, subdir, e) for e in dlls]
576
577    def qt5_plugins(subdir, *dlls):
578        import PyQt5
579        pluginsdir = os.path.join(os.path.dirname(PyQt5.__file__), 'plugins')
580        return subdir, [os.path.join(pluginsdir, subdir, e) for e in dlls]
581
582    from tortoisehg.hgqt.qtcore import QT_API
583    if QT_API == 'PyQt4':
584        _data_files.append(qt4_plugins('imageformats',
585                                       'qico4.dll', 'qsvg4.dll'))
586    else:
587        _data_files.append(qt5_plugins('platforms', 'qwindows.dll'))
588        _data_files.append(qt5_plugins('imageformats',
589                                       'qico.dll', 'qsvg.dll', 'qjpeg.dll',
590                                       'qgif.dll', 'qicns.dll', 'qtga.dll',
591                                       'qtiff.dll', 'qwbmp.dll', 'qwebp.dll'))
592
593    # Manually include other modules py2exe can't find by itself.
594    if 'hgext.highlight' in hgextmods:
595        includes += ['pygments.*', 'pygments.lexers.*', 'pygments.formatters.*',
596                     'pygments.filters.*', 'pygments.styles.*']
597    if 'hgext.patchbomb' in hgextmods:
598        includes += ['email.*', 'email.mime.*']
599
600    extra['options'] = {}
601    extra['options']['py2exe'] = {
602        "skip_archive": 0,
603        # Don't pull in all this MFC stuff used by the makepy UI.
604        "excludes": ("pywin,pywin.dialogs,pywin.dialogs.list,setuptools"
605                     "setup,distutils"),  # required only for in-place use
606        "includes": includes,
607        "optimize": 1,
608        }
609    extra['console'] = [
610        {'script': 'thg',
611         'icon_resources': [(0, 'icons/thg_logo.ico')],
612         'description': 'TortoiseHg GUI tools for Mercurial SCM',
613         'copyright': thgcopyright,
614         'product_version': version,
615         },
616        {'script': 'contrib/hg',
617         'icon_resources': [(0, 'icons/hg.ico')],
618         'description': 'Mercurial Distributed SCM',
619         'copyright': hgcopyright,
620         'product_version': version,
621         },
622        {'script': 'win32/docdiff.py',
623         'icon_resources': [(0, 'icons/TortoiseMerge.ico')],
624         'copyright': thgcopyright,
625         'product_version': version,
626         },
627        ]
628    extra['windows'] = [
629        {'script': 'thg',
630         'dest_base': 'thgw',
631         'icon_resources': [(0, 'icons/thg_logo.ico')],
632         'description': 'TortoiseHg GUI tools for Mercurial SCM',
633         'copyright': thgcopyright,
634         'product_version': version,
635         },
636        {'script': 'TortoiseHgOverlayServer.py',
637         'icon_resources': [(0, 'icons/thg_logo.ico')],
638         'description': 'TortoiseHg Overlay Icon Server',
639         'copyright': thgcopyright,
640         'product_version': version,
641         },
642        ]
643    # put dlls in sub directory so that they won't pollute PATH
644    extra['zipfile'] = 'lib/library.zip'
645
646    return _scripts, _packages, _data_files, extra
647
648def setup_osx(version):
649    _extra = {}
650
651    # This causes py2app to copy the scripts into build/ and then adjust the
652    # mode, but the build dir is ignored for some reason.
653    _scripts = ['thg']
654
655    _packages = ['tortoisehg.hgqt', 'tortoisehg.util', 'tortoisehg', 'hgext3rd']
656    _data_files = []
657
658    def qt5_plugins(subdir, *dlls):
659        import PyQt5
660        pluginsdir = os.path.join(os.path.dirname(PyQt5.__file__), 'plugins')
661        return subdir, [os.path.join(pluginsdir, subdir, e) for e in dlls]
662
663    from tortoisehg.hgqt.qtcore import QT_API
664    _data_files.append(qt5_plugins('platforms', 'libqcocoa.dylib'))
665    _data_files.append(qt5_plugins('imageformats', 'libqsvg.dylib'))
666
667    _py2app_options = {
668        'arch': 'x86_64',
669        'argv_emulation': False,
670        'no_chdir': True,
671        'excludes': ['Carbon', 'curses', 'distools', 'distutils', 'docutils',
672                     'PyQt4.phonon', 'PyQt4.QtDeclarative', 'PyQt4.QtDesigner',
673                     'PyQt4.QtHelp', 'PyQt4.QtMultimedia', 'PyQt4.QtOpenGL',
674                     'PyQt4.QtScript', 'PyQt4.QtScriptTools', 'PyQt4.QtSql',
675                     'PyQt4.QtTest', 'PyQt4.QtWebKit', 'PyQt4.QtXmlPatterns',
676                     'PyQt4.QtXmlPatterns', 'PyQt5.QtDBus',
677                     'PyQt5.QtDeclarative', 'PyQt5.QtDesigner', 'PyQt5.QtHelp',
678                     'PyQt5.QtMultimedia', 'PyQt5.QtOpenGL', 'PyQt5.QtScript',
679                     'PyQt5.QtScriptTools', 'PyQt5.QtSql', 'PyQt5.QtTest',
680                     'PyQt5.QtWebKit', 'PyQt5.QtXmlPatterns', 'PyQt5.phonon',
681                     'py2app', 'setup', 'setuptools', 'unittest', 'PIL'],
682
683        'extra_scripts': ['contrib/hg'],
684        'iconfile': 'contrib/packaging/macos/TortoiseHg.icns',
685        'includes': ['email.mime.text', 'sip'],
686        'packages': ['certifi', 'hgext', 'iniparse', 'keyring', 'mercurial',
687                     'pygments', 'tortoisehg'],
688
689        'plist': {
690            'CFBundleDisplayName': 'TortoiseHg',
691            'CFBundleExecutable': 'TortoiseHg',
692            'CFBundleIdentifier': 'org.tortoisehg.thg',
693            'CFBundleName': 'TortoiseHg',
694            'CFBundleShortVersionString': version,
695            'CFBundleVersion': version,
696            'LSEnvironment': {
697                # because launched app can't inherit environment variables from
698                # console, the encoding would be set to "ascii" by default
699                'HGENCODING': 'utf-8',
700                'THG_OSX_APP': '1',
701                'QT_API': QT_API,
702            },
703            'NSHumanReadableCopyright': thgcopyright,
704        },
705
706        'resources': ['COPYING.txt', 'icons', 'locale'],
707    }
708
709    _extra['app'] = ['thg']
710    _extra['setup_requires'] = ['py2app']
711    _extra['options'] = {'py2app': _py2app_options}
712
713    return _scripts, _packages, _data_files, _extra
714
715def setup_posix():
716    # Specific definitios for Posix installations
717    _extra = {}
718    _scripts = ['thg']
719    _packages = ['tortoisehg', 'tortoisehg.hgqt', 'tortoisehg.util', 'hgext3rd']
720    _data_files = []
721    # .svg and .png are loaded by thg, .ico by nautilus extension
722    for root, dirs, files in os.walk('icons'):
723        if root == 'icons':
724            dirs.remove('svg')  # drop source of .ico files
725        # do not add directories to the packing list
726        if not files:
727            continue
728        _data_files.append((os.path.join('share/pixmaps/tortoisehg', root[6:]),
729                            [os.path.join(root, f) for f in files]))
730    # install SVG icon for the desktop file
731    _data_files.append(('share/pixmaps', ['icons/svg/thg_logo.svg']))
732    _data_files.append(('share/doc/tortoisehg', ['COPYING.txt']))
733    _data_files.extend((os.path.join('share', modir), [mofile])
734                       for pofile, modir, mofile in _walklocales())
735#     _data_files += [('share/nautilus-python/extensions',
736#                      ['contrib/nautilus-thg.py'])]
737
738    return _scripts, _packages, _data_files, _extra
739
740if __name__ == '__main__':
741    version = ''
742
743    if os.path.isdir('.hg'):
744        from tortoisehg.util import version as _version
745        branch, version = _version.liveversion()
746        if version.endswith('+'):
747            version += time.strftime('%Y%m%d')
748    elif os.path.exists('.hg_archival.txt'):
749        with open('.hg_archival.txt') as fp:
750            kw = dict([t.strip() for t in l.split(':', 1)] for l in fp)
751        if 'tag' in kw:
752            version = kw['tag']
753        elif 'latesttag' in kw:
754            version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
755        else:
756            version = kw.get('node', '')[:12]
757
758    if version:
759        with open("tortoisehg/util/__version__.py", "w") as f:
760            f.write('# this file is autogenerated by setup.py\n')
761            f.write('version = "%s"\n' % version)
762
763    try:
764        import tortoisehg.util.__version__
765        version = tortoisehg.util.__version__.version
766    except ImportError:
767        version = 'unknown'
768
769    if os.name == "nt":
770        (scripts, packages, data_files, extra) = setup_windows(version)
771        # Windows binary file versions for exe/dll files must have the
772        # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
773        from tortoisehg.util.version import package_version
774        setupversion = package_version()
775        productname = 'TortoiseHg'
776    elif sys.platform == "darwin" and 'py2app' in sys.argv[1:]:
777        (scripts, packages, data_files, extra) = setup_osx(version)
778        setupversion = version
779        productname = 'TortoiseHg'
780    else:
781        (scripts, packages, data_files, extra) = setup_posix()
782        setupversion = version
783        productname = 'tortoisehg'
784
785    setup(name=productname,
786          version=setupversion,
787          author='Steve Borho',
788          author_email='steve@borho.org',
789          url='https://tortoisehg.bitbucket.io',
790          description='TortoiseHg dialogs for Mercurial VCS',
791          license='GNU GPL2',
792          scripts=scripts,
793          packages=packages,
794          data_files=data_files,
795          cmdclass=cmdclass,
796          **extra)
797