# -*- coding: utf-8 -*-
#
# Copyright © 2009-2011 CEA
# Pierre Raybaut
# Licensed under the terms of the CECILL License
# (see guidata/__init__.py for details)
# pylint: disable=W0613
"""
disthelpers
-----------
The ``guidata.disthelpers`` module provides helper functions for Python
package distribution on Microsoft Windows platforms with ``py2exe`` or on
all platforms thanks to ``cx_Freeze``.
"""
from __future__ import print_function
import sys
import os
import os.path as osp
import shutil
import traceback
import atexit
import imp
from subprocess import Popen, PIPE
import warnings
from distutils.version import LooseVersion, StrictVersion
# ==============================================================================
# Module, scripts, programs
# ==============================================================================
def get_module_path(modname):
"""Return module *modname* base path"""
module = sys.modules.get(modname, __import__(modname))
return osp.abspath(osp.dirname(module.__file__))
# ==============================================================================
# Dependency management
# ==============================================================================
def get_changeset(path, rev=None):
"""Return Mercurial repository *path* revision number"""
args = ['hg', 'parent']
if rev is not None:
args += ['--rev', str(rev)]
process = Popen(
args, stdout=PIPE, stderr=PIPE, cwd=path, shell=True
)
try:
return (
process.stdout.read().splitlines()[0].split()[1]
)
except IndexError:
raise RuntimeError(process.stderr.read())
def prepend_module_to_path(module_path):
"""
Prepend to sys.path module located in *module_path*
Return string with module infos: name, revision, changeset
Use this function:
1) In your application to import local frozen copies of internal libraries
2) In your py2exe distributed package to add a text file containing the returned string
"""
if not osp.isdir(module_path):
# Assuming py2exe distribution
return
sys.path.insert(0, osp.abspath(module_path))
changeset = get_changeset(module_path)
name = osp.basename(module_path)
prefix = "Prepending module to sys.path"
message = prefix + (
"%s [revision %s]" % (name, changeset)
).rjust(80 - len(prefix), ".")
print(message, file=sys.stderr)
if name in sys.modules:
sys.modules.pop(name)
nbsp = 0
for modname in sys.modules.keys():
if modname.startswith(name + '.'):
sys.modules.pop(modname)
nbsp += 1
warning = '(removed %s from sys.modules' % name
if nbsp:
warning += ' and %d subpackages' % nbsp
warning += ')'
print(warning.rjust(80), file=sys.stderr)
return message
def prepend_modules_to_path(module_base_path):
"""Prepend to sys.path all modules located in *module_base_path*"""
if not osp.isdir(module_base_path):
# Assuming py2exe distribution
return
fnames = [
osp.join(module_base_path, name)
for name in os.listdir(module_base_path)
]
messages = [
prepend_module_to_path(dirname)
for dirname in fnames
if osp.isdir(dirname)
]
return os.linesep.join(messages)
# ==============================================================================
# Distribution helpers
# ==============================================================================
def _remove_later(fname):
"""Try to remove file later (at exit)"""
def try_to_remove(fname):
if osp.exists(fname):
os.remove(fname)
atexit.register(try_to_remove, osp.abspath(fname))
def get_msvc_version(python_version):
"""Return Microsoft Visual C++ version used to build this Python version"""
if python_version is None:
python_version = '2.7'
warnings.warn("assuming Python 2.7 target")
if python_version in (
'2.6',
'2.7',
'3.0',
'3.1',
'3.2',
):
# Python 2.6-2.7, 3.0-3.2 were built with Visual Studio 9.0.21022.8
# (i.e. Visual C++ 2008, not Visual C++ 2008 SP1!)
return "9.0.21022.8"
elif python_version in ('3.3', '3.4'):
# Python 3.3+ were built with Visual Studio 10.0.30319.1
# (i.e. Visual C++ 2010)
return '10.0'
elif python_version in ('3.5', '3.6'):
return '15.0'
elif python_version in ('3.7', '3.8'):
return '15.0'
elif StrictVersion(python_version) >= StrictVersion('3.9'):
return '15.0'
else:
raise RuntimeError(
"Unsupported Python version %s" % python_version
)
def get_msvc_dlls(msvc_version, architecture=None):
"""Get the list of Microsoft Visual C++ DLLs associated to
architecture and Python version, create the manifest file.
architecture: integer (32 or 64) -- if None, take the Python build arch
python_version: X.Y"""
current_architecture = (
64 if sys.maxsize > 2 ** 32 else 32
)
if architecture is None:
architecture = current_architecture
filelist = []
# simple vs2015 situation: nothing (system dll)
if msvc_version == '14.0':
return filelist
msvc_major = msvc_version.split('.')[0]
msvc_minor = msvc_version.split('.')[1]
if msvc_major == '9':
key = "1fc8b3b9a1e18e3b"
atype = "" if architecture == 64 else "win32"
arch = "amd64" if architecture == 64 else "x86"
groups = {
'CRT': (
'msvcr90.dll',
'msvcp90.dll',
'msvcm90.dll',
),
# 'OPENMP': ('vcomp90.dll',)
}
for group, dll_list in groups.items():
dlls = ''
for dll in dll_list:
dlls += ' %s' % (
dll,
os.linesep,
)
manifest = """
%(dlls)s
""" % dict(
version=msvc_version,
key=key,
atype=atype,
arch=arch,
group=group,
dlls=dlls,
)
vc90man = "Microsoft.VC90.%s.manifest" % group
open(vc90man, 'w').write(manifest)
_remove_later(vc90man)
filelist += [vc90man]
winsxs = osp.join(
os.environ['windir'], 'WinSxS'
)
vcstr = '%s_Microsoft.VC90.%s_%s_%s' % (
arch,
group,
key,
msvc_version,
)
for fname in os.listdir(winsxs):
path = osp.join(winsxs, fname)
if osp.isdir(
path
) and fname.lower().startswith(
vcstr.lower()
):
for dllname in os.listdir(path):
filelist.append(
osp.join(path, dllname)
)
break
else:
raise RuntimeError(
"Microsoft Visual C++ %s DLLs version %s "
"were not found" % (group, msvc_version)
)
elif (
msvc_major == '10' or msvc_major == '15'
): # 15 for vs 2015
namelist = [
name % (msvc_major + msvc_minor)
for name in (
'msvcp%s.dll',
'msvcr%s.dll',
'vcomp%s.dll',
)
]
if msvc_major == '15' and architecture == 64:
namelist = [
name % ('14' + msvc_minor)
for name in (
'vcruntime%s.dll',
'vcruntime%s_1.dll',
'msvcp%s.dll',
'vccorlib%s.dll',
'concrt%s.dll',
'vcomp%s.dll',
)
]
if msvc_major == '15' and architecture != 64:
namelist = [
name % ('14' + msvc_minor)
for name in (
'vcruntime%s.dll',
'msvcp%s.dll',
'vccorlib%s.dll',
'concrt%s.dll',
'vcomp%s.dll',
)
]
windir = os.environ['windir']
is_64bit_windows = osp.isdir(
osp.join(windir, "SysWOW64")
)
# Reminder: WoW64 (*W*indows 32-bit *o*n *W*indows *64*-bit) is a
# subsystem of the Windows operating system capable of running 32-bit
# applications and is included on all 64-bit versions of Windows
# (source: http://en.wikipedia.org/wiki/WoW64)
#
# In other words, "SysWOW64" contains 64-bit DLL and applications,
# whereas "System32" contains 64-bit DLL and applications on a 64-bit
# system.
sysdir = "System32"
if not is_64bit_windows and architecture == 64:
raise RuntimeError(
"Can't find 64-bit MSVC DLLs on a 32-bit OS"
)
if is_64bit_windows and architecture == 32:
sysdir = "SysWOW64"
for dllname in namelist:
fname = osp.join(windir, sysdir, dllname)
print('searching', fname)
if osp.exists(fname):
filelist.append(fname)
else:
raise RuntimeError(
"Microsoft Visual C++ DLLs version %s "
"were not found" % msvc_version
)
else:
raise RuntimeError(
"Unsupported MSVC version %s" % msvc_version
)
return filelist
def create_msvc_data_files(
architecture=None, python_version=None, verbose=False
):
"""Including Microsoft Visual C++ DLLs"""
msvc_version = get_msvc_version(python_version)
filelist = get_msvc_dlls(
msvc_version, architecture=architecture
)
print(create_msvc_data_files.__doc__)
if verbose:
for name in filelist:
print(" ", name)
msvc_major = msvc_version.split('.')[0]
if msvc_major == '9':
return [("Microsoft.VC90.CRT", filelist)]
else:
return [("", filelist)]
def to_include_files(data_files):
"""Convert data_files list to include_files list
data_files:
* this is the ``py2exe`` data files format
* list of tuples (dest_dirname, (src_fname1, src_fname2, ...))
include_files:
* this is the ``cx_Freeze`` data files format
* list of tuples ((src_fname1, dst_fname1),
(src_fname2, dst_fname2), ...))
"""
include_files = []
for dest_dir, fnames in data_files:
for source_fname in fnames:
dest_fname = osp.join(
dest_dir, osp.basename(source_fname)
)
include_files.append((source_fname, dest_fname))
return include_files
def strip_version(version):
"""Return version number with digits only
(Windows does not support strings in version numbers)"""
return (
version.split('beta')[0]
.split('alpha')[0]
.split('rc')[0]
.split('dev')[0]
)
def remove_dir(dirname):
"""Remove directory *dirname* and all its contents
Print details about the operation (progress, success/failure)"""
print("Removing directory '%s'..." % dirname, end=' ')
try:
shutil.rmtree(dirname, ignore_errors=True)
print("OK")
except Exception:
print("Failed!")
traceback.print_exc()
class Distribution(object):
"""Distribution object
Help creating an executable using ``py2exe`` or ``cx_Freeze``
"""
DEFAULT_EXCLUDES = [
'Tkconstants',
'Tkinter',
'tcl',
'tk',
'wx',
'_imagingtk',
'curses',
'PIL._imagingtk',
'ImageTk',
'PIL.ImageTk',
'FixTk',
'bsddb',
'email',
'pywin.debugger',
'pywin.debugger.dbgcon',
'matplotlib',
]
DEFAULT_INCLUDES = []
DEFAULT_BIN_EXCLUDES = [
'MSVCP100.dll',
'MSVCP90.dll',
'w9xpopen.exe',
'MSVCP80.dll',
'MSVCR80.dll',
]
DEFAULT_BIN_INCLUDES = []
DEFAULT_BIN_PATH_INCLUDES = []
DEFAULT_BIN_PATH_EXCLUDES = []
def __init__(self):
self.name = None
self.version = None
self.description = None
self.target_name = None
self._target_dir = None
self.icon = None
self.data_files = []
self.includes = self.DEFAULT_INCLUDES
self.excludes = self.DEFAULT_EXCLUDES
self.bin_includes = self.DEFAULT_BIN_INCLUDES
self.bin_excludes = self.DEFAULT_BIN_EXCLUDES
self.bin_path_includes = (
self.DEFAULT_BIN_PATH_INCLUDES
)
self.bin_path_excludes = (
self.DEFAULT_BIN_PATH_EXCLUDES
)
self.msvc = os.name == 'nt'
self._py2exe_is_loaded = False
self._pyqt4_added = False
self._pyside_added = False
# Attributes relative to cx_Freeze:
self.executables = []
@property
def target_dir(self):
"""Return target directory (default: 'dist')"""
dirname = self._target_dir
if dirname is None:
return 'dist'
else:
return dirname
@target_dir.setter # analysis:ignore
def target_dir(self, value):
self._target_dir = value
def setup(
self,
name,
version,
description,
script,
target_name=None,
target_dir=None,
icon=None,
data_files=None,
includes=None,
excludes=None,
bin_includes=None,
bin_excludes=None,
bin_path_includes=None,
bin_path_excludes=None,
msvc=None,
):
"""Setup distribution object
Notes:
* bin_path_excludes is specific to cx_Freeze (ignored if it's None)
* if msvc is None, it's set to True by default on Windows
platforms, False on non-Windows platforms
"""
self.name = name
self.version = (
strip_version(version)
if os.name == 'nt'
else version
)
self.description = description
assert osp.isfile(script)
self.script = script
self.target_name = target_name
self.target_dir = target_dir
self.icon = icon
if data_files is not None:
self.data_files += data_files
if includes is not None:
self.includes += includes
if excludes is not None:
self.excludes += excludes
if bin_includes is not None:
self.bin_includes += bin_includes
if bin_excludes is not None:
self.bin_excludes += bin_excludes
if bin_path_includes is not None:
self.bin_path_includes += bin_path_includes
if bin_path_excludes is not None:
self.bin_path_excludes += bin_path_excludes
if msvc is not None:
self.msvc = msvc
if self.msvc:
try:
self.data_files += create_msvc_data_files()
except IOError:
print(
"Setting the msvc option to False "
"will avoid this error",
file=sys.stderr,
)
raise
# cx_Freeze:
self.add_executable(
self.script, self.target_name, icon=self.icon
)
def add_text_data_file(self, filename, contents):
"""Create temporary data file *filename* with *contents*
and add it to *data_files*"""
open(filename, 'wb').write(contents)
self.data_files += [("", (filename,))]
_remove_later(filename)
def add_data_file(self, filename, destdir=''):
self.data_files += [(destdir, (filename,))]
# ------ Adding packages
def add_pyqt4(self):
"""Include module PyQt4 to the distribution"""
if self._pyqt4_added:
return
self._pyqt4_added = True
self.includes += [
'sip',
'PyQt4.Qt',
'PyQt4.QtSvg',
'PyQt4.QtNetwork',
]
import PyQt4
pyqt_path = osp.dirname(PyQt4.__file__)
# Configuring PyQt4
conf = os.linesep.join(
["[Paths]", "Prefix = .", "Binaries = ."]
)
self.add_text_data_file('qt.conf', conf)
# Including plugins (.svg icons support, QtDesigner support, ...)
if self.msvc:
vc90man = "Microsoft.VC90.CRT.manifest"
pyqt_tmp = 'pyqt_tmp'
if osp.isdir(pyqt_tmp):
shutil.rmtree(pyqt_tmp)
os.mkdir(pyqt_tmp)
vc90man_pyqt = osp.join(pyqt_tmp, vc90man)
man = (
open(vc90man, "r")
.read()
.replace(
'