Esempio n. 1
0
def get_bootstrap_modules():
    """
    Get TOC with the bootstrapping modules and their dependencies.
    :return: TOC with modules
    """
    # Import 'struct' modules to get real paths to module file names.
    mod_struct = __import__('struct')
    # Basic modules necessary for the bootstrap process.
    loader_mods = TOC()
    loaderpath = os.path.join(HOMEPATH, 'PyInstaller', 'loader')
    # On some platforms (Windows, Debian/Ubuntu) '_struct' and zlib modules are
    # built-in modules (linked statically) and thus does not have attribute __file__.
    # 'struct' module is required for reading Python bytecode from executable.
    # 'zlib' is required to decompress this bytecode.
    for mod_name in ['_struct', 'zlib']:
        mod = __import__(mod_name)  # C extension.
        if hasattr(mod, '__file__'):
            loader_mods.append((mod_name, os.path.abspath(mod.__file__), 'EXTENSION'))
    # NOTE:These modules should be kept simple without any complicated dependencies.
    loader_mods +=[
        ('struct', os.path.abspath(mod_struct.__file__), 'PYMODULE'),
        ('pyimod01_os_path', os.path.join(loaderpath, 'pyimod01_os_path.pyc'), 'PYMODULE'),
        ('pyimod02_archive',  os.path.join(loaderpath, 'pyimod02_archive.pyc'), 'PYMODULE'),
        ('pyimod03_importers',  os.path.join(loaderpath, 'pyimod03_importers.pyc'), 'PYMODULE'),
        ('pyiboot01_bootstrap', os.path.join(loaderpath, 'pyiboot01_bootstrap.py'), 'PYSOURCE'),
    ]
    return loader_mods
Esempio n. 2
0
def add_suffix_to_extensions(toc):
    """
    Returns a new TOC with proper library suffix for EXTENSION items.
    """
    # TODO: Fix this recursive import
    from PyInstaller.building.datastruct import TOC
    new_toc = TOC()
    for inm, fnm, typ in toc:
        if typ == 'EXTENSION':
            # Change the dotted name into a relative path. This places C
            # extensions in the Python-standard location.
            inm = inm.replace('.', os.sep)
            # In some rare cases extension might already contain a suffix.
            # Skip it in this case.
            if os.path.splitext(inm)[1] not in EXTENSION_SUFFIXES:
                # Determine the base name of the file.
                base_name = os.path.basename(inm)
                assert '.' not in base_name
                # Use this file's existing extension. For extensions such as
                # ``libzmq.cp36-win_amd64.pyd``, we can't use
                # ``os.path.splitext``, which would give only the ```.pyd`` part
                # of the extension.
                inm = inm + os.path.basename(fnm)[len(base_name):]

        elif typ == 'DEPENDENCY':
            # Use the suffix from the filename.
            # TODO Verify what extensions are by DEPENDENCIES.
            binext = os.path.splitext(fnm)[1]
            if not os.path.splitext(inm)[1] == binext:
                inm = inm + binext
        new_toc.append((inm, fnm, typ))
    return new_toc
Esempio n. 3
0
def test_append_other_case_pymodule():
    # python modules should not use C-I comparisons. Both 'encodings' and
    # 'EnCodIngs' should be added.
    toc = TOC(ELEMS1)
    elem = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'PYMODULE')
    toc.append(elem)
    expected = list(ELEMS1)
    expected.append(elem)
    assert toc == expected
Esempio n. 4
0
def test_append_other_case_mixed():
    # If a binary file is added with the same filename as an existing pymodule,
    # it should not be added.

    toc = TOC(ELEMS1)
    elem = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'BINARY')
    toc.append(elem)
    expected = list(ELEMS1)
    assert toc == expected
Esempio n. 5
0
def test_append_other_case_pymodule():
    # python modules should not use C-I comparisons. Both 'encodings' and
    # 'EnCodIngs' should be added.
    toc = TOC(ELEMS1)
    elem = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'PYMODULE')
    toc.append(elem)
    expected = list(ELEMS1)
    expected.append(elem)
    assert toc == expected
Esempio n. 6
0
def test_append_other_case_mixed():
    # If a binary file is added with the same filename as an existing pymodule,
    # it should not be added.

    toc = TOC(ELEMS1)
    elem = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'BINARY')
    toc.append(elem)
    expected = list(ELEMS1)
    assert toc == expected
Esempio n. 7
0
File: api.py Progetto: cbgp/diyabc
class COLLECT(Target):
    """
    In one-dir mode creates the output folder with all necessary files.
    """
    def __init__(self, *args, **kws):
        """
        args
                One or more arguments that are either TOCs Targets.
        kws
            Possible keywork arguments:

                name
                    The name of the directory to be built.
        """
        from ..config import CONF
        Target.__init__(self)
        self.strip_binaries = kws.get('strip', False)

        if CONF['hasUPX']:
           self.upx_binaries = kws.get('upx', False)
        else:
           self.upx_binaries = False

        self.name = kws.get('name')
        # Old .spec format included in 'name' the path where to collect files
        # for the created app.
        # app. New format includes only directory name.
        #
        # The 'name' directory is created in DISTPATH and necessary files are
        # then collected to this directory.
        self.name = os.path.join(CONF['distpath'], os.path.basename(self.name))

        self.toc = TOC()
        for arg in args:
            if isinstance(arg, TOC):
                self.toc.extend(arg)
            elif isinstance(arg, Target):
                self.toc.append((os.path.basename(arg.name), arg.name, arg.typ))
                if isinstance(arg, EXE):
                    for tocnm, fnm, typ in arg.toc:
                        if tocnm == os.path.basename(arg.name) + ".manifest":
                            self.toc.append((tocnm, fnm, typ))
                    if not arg.append_pkg:
                        self.toc.append((os.path.basename(arg.pkgname), arg.pkgname, 'PKG'))
                self.toc.extend(arg.dependencies)
            else:
                self.toc.extend(arg)
        self.__postinit__()

    _GUTS = (
        # COLLECT always builds, just want the toc to be written out
        ('toc', None),
    )

    def _check_guts(self, data, last_build):
        # COLLECT always needs to be executed, since it will clean the output
        # directory anyway to make sure there is no existing cruft accumulating
        return 1

    def assemble(self):
        if _check_path_overlap(self.name) and os.path.isdir(self.name):
            _rmtree(self.name)
        logger.info("Building COLLECT %s", self.tocbasename)
        os.makedirs(self.name)
        toc = add_suffix_to_extensions(self.toc)
        for inm, fnm, typ in toc:
            if not os.path.exists(fnm) or not os.path.isfile(fnm) and is_path_to_egg(fnm):
                # file is contained within python egg, it is added with the egg
                continue
            if os.pardir in os.path.normpath(inm) or os.path.isabs(inm):
                raise SystemExit('Security-Alert: try to store file outside '
                                 'of dist-directory. Aborting. %r' % inm)
            tofnm = os.path.join(self.name, inm)
            todir = os.path.dirname(tofnm)
            if not os.path.exists(todir):
                os.makedirs(todir)
            if typ in ('EXTENSION', 'BINARY'):
                fnm = checkCache(fnm, strip=self.strip_binaries,
                                 upx=(self.upx_binaries and (is_win or is_cygwin)),
                                 dist_nm=inm)
            if typ != 'DEPENDENCY':
                shutil.copy(fnm, tofnm)
                try:
                    shutil.copystat(fnm, tofnm)
                except OSError:
                    logger.warn("failed to copy flags of %s", fnm)
            if typ in ('EXTENSION', 'BINARY'):
                os.chmod(tofnm, 0o755)
Esempio n. 8
0
File: api.py Progetto: cbgp/diyabc
class EXE(Target):
    """
    Creates the final executable of the frozen app.
    This bundles all necessary files together.
    """
    typ = 'EXECUTABLE'

    def __init__(self, *args, **kwargs):
        """
        args
                One or more arguments that are either TOCs Targets.
        kwargs
            Possible keywork arguments:

            console
                On Windows or OSX governs whether to use the console executable
                or the windowed executable. Always True on Linux/Unix (always
                console executable - it does not matter there).
            debug
                Setting to True gives you progress mesages from the executable
                (for console=False there will be annoying MessageBoxes on Windows).
            name
                The filename for the executable. On Windows suffix '.exe' is
                appended.
            exclude_binaries
                Forwarded to the PKG the EXE builds.
            icon
                Windows or OSX only. icon='myicon.ico' to use an icon file or
                icon='notepad.exe,0' to grab an icon resource.
            version
                Windows only. version='myversion.txt'. Use grab_version.py to get
                a version resource from an executable and then edit the output to
                create your own. (The syntax of version resources is so arcane
                that I wouldn't attempt to write one from scratch).
            uac_admin
                Windows only. Setting to True creates a Manifest with will request
                elevation upon application restart
            uac_uiaccess
                Windows only. Setting to True allows an elevated application to
                work with Remote Desktop
        """
        from ..config import CONF
        Target.__init__(self)

        # Available options for EXE in .spec files.
        self.exclude_binaries = kwargs.get('exclude_binaries', False)
        self.console = kwargs.get('console', True)
        self.debug = kwargs.get('debug', False)
        self.name = kwargs.get('name', None)
        self.icon = kwargs.get('icon', None)
        self.versrsrc = kwargs.get('version', None)
        self.manifest = kwargs.get('manifest', None)
        self.resources = kwargs.get('resources', [])
        self.strip = kwargs.get('strip', False)
        # If ``append_pkg`` is false, the archive will not be appended
        # to the exe, but copied beside it.
        self.append_pkg = kwargs.get('append_pkg', True)

        # On Windows allows the exe to request admin privileges.
        self.uac_admin = kwargs.get('uac_admin', False)
        self.uac_uiaccess = kwargs.get('uac_uiaccess', False)

        if CONF['hasUPX']:
           self.upx = kwargs.get('upx', False)
        else:
           self.upx = False

        # Old .spec format included in 'name' the path where to put created
        # app. New format includes only exename.
        #
        # Ignore fullpath in the 'name' and prepend DISTPATH or WORKPATH.
        # DISTPATH - onefile
        # WORKPATH - onedir
        if self.exclude_binaries:
            # onedir mode - create executable in WORKPATH.
            self.name = os.path.join(CONF['workpath'], os.path.basename(self.name))
        else:
            # onefile mode - create executable in DISTPATH.
            self.name = os.path.join(CONF['distpath'], os.path.basename(self.name))

        # Old .spec format included on Windows in 'name' .exe suffix.
        if is_win or is_cygwin:
            # Append .exe suffix if it is not already there.
            if not self.name.endswith('.exe'):
                self.name += '.exe'
            base_name = os.path.splitext(os.path.basename(self.name))[0]
        else:
            base_name = os.path.basename(self.name)
        self.pkgname = base_name + '.pkg'

        self.toc = TOC()

        for arg in args:
            if isinstance(arg, TOC):
                self.toc.extend(arg)
            elif isinstance(arg, Target):
                self.toc.append((os.path.basename(arg.name), arg.name, arg.typ))
                self.toc.extend(arg.dependencies)
            else:
                self.toc.extend(arg)

        if is_win:
            filename = os.path.join(CONF['workpath'], CONF['specnm'] + ".exe.manifest")
            self.manifest = winmanifest.create_manifest(filename, self.manifest,
                self.console, self.uac_admin, self.uac_uiaccess)

            manifest_filename = os.path.basename(self.name) + ".manifest"

            self.toc.append((manifest_filename, filename, 'BINARY'))
            if not self.exclude_binaries:
                # Onefile mode: manifest file is explicitly loaded.
                # Store name of manifest file as bootloader option. Allows
                # the exe to be renamed.
                self.toc.append(("pyi-windows-manifest-filename " + manifest_filename,
                                 "", "OPTION"))

        self.pkg = PKG(self.toc, cdict=kwargs.get('cdict', None),
                       exclude_binaries=self.exclude_binaries,
                       strip_binaries=self.strip, upx_binaries=self.upx,
                       )
        self.dependencies = self.pkg.dependencies

        # Get the path of the bootloader and store it in a TOC, so it
        # can be checked for being changed.
        exe = self._bootloader_file('run', '.exe' if is_win or is_cygwin else '')
        self.exefiles = TOC([(os.path.basename(exe), exe, 'EXECUTABLE')])

        self.__postinit__()

    _GUTS = (# input parameters
            ('name', _check_guts_eq),
            ('console', _check_guts_eq),
            ('debug', _check_guts_eq),
            ('exclude_binaries', _check_guts_eq),
            ('icon', _check_guts_eq),
            ('versrsrc', _check_guts_eq),
            ('uac_admin', _check_guts_eq),
            ('uac_uiaccess', _check_guts_eq),
            ('manifest', _check_guts_eq),
            ('append_pkg', _check_guts_eq),
            # for the case the directory ius shared between platforms:
            ('pkgname', _check_guts_eq),
            ('toc', _check_guts_eq),
            ('resources', _check_guts_eq),
            ('strip', _check_guts_eq),
            ('upx', _check_guts_eq),
            ('mtm', None,),  # checked below
            # no calculated/analysed values
            ('exefiles', _check_guts_toc),
            )

    def _check_guts(self, data, last_build):
        if not os.path.exists(self.name):
            logger.info("Rebuilding %s because %s missing",
                        self.tocbasename, os.path.basename(self.name))
            return 1
        if not self.append_pkg and not os.path.exists(self.pkgname):
            logger.info("Rebuilding because %s missing",
                        os.path.basename(self.pkgname))
            return 1

        if Target._check_guts(self, data, last_build):
            return True

        if (data['versrsrc'] or data['resources']) and not is_win:
            # todo: really ignore :-)
            logger.warn('ignoring version, manifest and resources, platform not capable')
        if data['icon'] and not (is_win or is_darwin):
            logger.warn('ignoring icon, platform not capable')

        mtm = data['mtm']
        if mtm != misc.mtime(self.name):
            logger.info("Rebuilding %s because mtimes don't match", self.tocbasename)
            return True
        if mtm < misc.mtime(self.pkg.tocfilename):
            logger.info("Rebuilding %s because pkg is more recent", self.tocbasename)
            return True
        return False

    def _bootloader_file(self, exe, extension=None):
        """
        Pick up the right bootloader file - debug, console, windowed.
        """
        # Having console/windowed bootolader makes sense only on Windows and
        # Mac OS X.
        if is_win or is_darwin:
            if not self.console:
                exe = exe + 'w'
        # There are two types of bootloaders:
        # run     - release, no verbose messages in console.
        # run_d   - contains verbose messages in console.
        if self.debug:
            exe = exe + '_d'
        if extension:
            exe = exe + extension
        bootloader_file = os.path.join(HOMEPATH, 'PyInstaller', 'bootloader', PLATFORM, exe)
        logger.info('Bootloader %s' % bootloader_file)
        return bootloader_file

    def assemble(self):
        logger.info("Building EXE from %s", self.tocbasename)
        trash = []
        if not os.path.exists(os.path.dirname(self.name)):
            os.makedirs(os.path.dirname(self.name))
        outf = open(self.name, 'wb')
        exe = self.exefiles[0][1]  # pathname of bootloader
        if not os.path.exists(exe):
            raise SystemExit(_MISSING_BOOTLOADER_ERRORMSG)


        if is_win and (self.icon or self.versrsrc or self.resources):
            tmpnm = tempfile.mktemp()
            shutil.copy2(exe, tmpnm)
            os.chmod(tmpnm, 0o755)
            if self.icon:
                icon.CopyIcons(tmpnm, self.icon)
            if self.versrsrc:
                versioninfo.SetVersion(tmpnm, self.versrsrc)
            for res in self.resources:
                res = res.split(",")
                for i in range(1, len(res)):
                    try:
                        res[i] = int(res[i])
                    except ValueError:
                        pass
                resfile = res[0]
                restype = resname = reslang = None
                if len(res) > 1:
                    restype = res[1]
                if len(res) > 2:
                    resname = res[2]
                if len(res) > 3:
                    reslang = res[3]
                try:
                    winresource.UpdateResourcesFromResFile(tmpnm, resfile,
                                                        [restype or "*"],
                                                        [resname or "*"],
                                                        [reslang or "*"])
                except winresource.pywintypes.error as exc:
                    if exc.args[0] != winresource.ERROR_BAD_EXE_FORMAT:
                        logger.exception(exc)
                        continue
                    if not restype or not resname:
                        logger.error("resource type and/or name not specified")
                        continue
                    if "*" in (restype, resname):
                        logger.error("no wildcards allowed for resource type "
                                     "and name when source file does not "
                                     "contain resources")
                        continue
                    try:
                        winresource.UpdateResourcesFromDataFile(tmpnm,
                                                             resfile,
                                                             restype,
                                                             [resname],
                                                             [reslang or 0])
                    except winresource.pywintypes.error as exc:
                        logger.exception(exc)
            trash.append(tmpnm)
            exe = tmpnm
        exe = checkCache(exe, strip=self.strip, upx=self.upx)
        self.copy(exe, outf)
        if self.append_pkg:
            logger.info("Appending archive to EXE %s", self.name)
            self.copy(self.pkg.name, outf)
        else:
            logger.info("Copying archive to %s", self.pkgname)
            shutil.copy2(self.pkg.name, self.pkgname)
        outf.close()

        if is_darwin:
            # Fix Mach-O header for codesigning on OS X.
            logger.info("Fixing EXE for code signing %s", self.name)
            import PyInstaller.utils.osx as osxutils
            osxutils.fix_exe_for_code_signing(self.name)
            pass

        os.chmod(self.name, 0o755)
        # get mtime for storing into the guts
        self.mtm = misc.mtime(self.name)
        for item in trash:
            os.remove(item)


    def copy(self, fnm, outf):
        inf = open(fnm, 'rb')
        while 1:
            data = inf.read(64 * 1024)
            if not data:
                break
            outf.write(data)
Esempio n. 9
0
def test_append_keep_filename():
    # name in TOC should be the same as the one added
    toc = TOC()
    entry = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'BINARY')
    toc.append(entry)
    assert toc[0][0] == entry[0]
Esempio n. 10
0
def test_append_existing():
    toc = TOC(ELEMS1)
    toc.append(ELEMS1[-1])
    expected = list(ELEMS1)
    assert toc == expected
Esempio n. 11
0
def test_append():
    toc = TOC(ELEMS1)
    toc.append(('li-la-lu', '/home/myself/li-la-su', 'SOMETHING'))
    expected = list(ELEMS1)
    expected.append(('li-la-lu', '/home/myself/li-la-su', 'SOMETHING'))
    assert toc == expected
Esempio n. 12
0
def test_append_other_case_binary():
    # binary files should use C-I comparisons. 'LiBrEADlInE.so.6' should not be added.
    toc = TOC(ELEMS1)
    toc.append(('LiBrEADlInE.so.6', '/lib64/libreadline.so.6', 'BINARY'))
    expected = list(ELEMS1)
    assert toc == expected
Esempio n. 13
0
class EXE(Target):
    """
    Creates the final executable of the frozen app.
    This bundles all necessary files together.
    """
    typ = 'EXECUTABLE'

    def __init__(self, *args, **kwargs):
        """
        args
                One or more arguments that are either TOCs Targets.
        kwargs
            Possible keywork arguments:

            bootloader_ignore_signals
                Non-Windows only. If True, the bootloader process will ignore
                all ignorable signals. If False (default), it will forward
                all signals to the child process. Useful in situations where
                e.g. a supervisor process signals both the bootloader and
                child (e.g. via a process group) to avoid signalling the
                child twice.
            console
                On Windows or OSX governs whether to use the console executable
                or the windowed executable. Always True on Linux/Unix (always
                console executable - it does not matter there).
            debug
                Setting to True gives you progress mesages from the executable
                (for console=False there will be annoying MessageBoxes on Windows).
            name
                The filename for the executable. On Windows suffix '.exe' is
                appended.
            exclude_binaries
                Forwarded to the PKG the EXE builds.
            icon
                Windows or OSX only. icon='myicon.ico' to use an icon file or
                icon='notepad.exe,0' to grab an icon resource.
            version
                Windows only. version='myversion.txt'. Use grab_version.py to get
                a version resource from an executable and then edit the output to
                create your own. (The syntax of version resources is so arcane
                that I wouldn't attempt to write one from scratch).
            uac_admin
                Windows only. Setting to True creates a Manifest with will request
                elevation upon application restart
            uac_uiaccess
                Windows only. Setting to True allows an elevated application to
                work with Remote Desktop
        """
        from ..config import CONF
        Target.__init__(self)

        # Available options for EXE in .spec files.
        self.exclude_binaries = kwargs.get('exclude_binaries', False)
        self.bootloader_ignore_signals = kwargs.get(
            'bootloader_ignore_signals', False)
        self.console = kwargs.get('console', True)
        self.debug = kwargs.get('debug', False)
        self.name = kwargs.get('name', None)
        self.icon = kwargs.get('icon', None)
        self.versrsrc = kwargs.get('version', None)
        self.manifest = kwargs.get('manifest', None)
        self.resources = kwargs.get('resources', [])
        self.strip = kwargs.get('strip', False)
        self.runtime_tmpdir = kwargs.get('runtime_tmpdir', None)
        # If ``append_pkg`` is false, the archive will not be appended
        # to the exe, but copied beside it.
        self.append_pkg = kwargs.get('append_pkg', True)

        # On Windows allows the exe to request admin privileges.
        self.uac_admin = kwargs.get('uac_admin', False)
        self.uac_uiaccess = kwargs.get('uac_uiaccess', False)

        if CONF['hasUPX']:
            self.upx = kwargs.get('upx', False)
        else:
            self.upx = False

        # Old .spec format included in 'name' the path where to put created
        # app. New format includes only exename.
        #
        # Ignore fullpath in the 'name' and prepend DISTPATH or WORKPATH.
        # DISTPATH - onefile
        # WORKPATH - onedir
        if self.exclude_binaries:
            # onedir mode - create executable in WORKPATH.
            self.name = os.path.join(CONF['workpath'],
                                     os.path.basename(self.name))
        else:
            # onefile mode - create executable in DISTPATH.
            self.name = os.path.join(CONF['distpath'],
                                     os.path.basename(self.name))

        # Old .spec format included on Windows in 'name' .exe suffix.
        if is_win or is_cygwin:
            # Append .exe suffix if it is not already there.
            if not self.name.endswith('.exe'):
                self.name += '.exe'
            base_name = os.path.splitext(os.path.basename(self.name))[0]
        else:
            base_name = os.path.basename(self.name)
        self.pkgname = base_name + '.pkg'

        self.toc = TOC()

        for arg in args:
            if isinstance(arg, TOC):
                self.toc.extend(arg)
            elif isinstance(arg, Target):
                self.toc.append(
                    (os.path.basename(arg.name), arg.name, arg.typ))
                self.toc.extend(arg.dependencies)
            else:
                self.toc.extend(arg)

        if self.runtime_tmpdir is not None:
            self.toc.append(
                ("pyi-runtime-tmpdir " + self.runtime_tmpdir, "", "OPTION"))

        if self.bootloader_ignore_signals:
            # no value; presence means "true"
            self.toc.append(("pyi-bootloader-ignore-signals", "", "OPTION"))

        if is_win:
            filename = os.path.join(CONF['workpath'],
                                    CONF['specnm'] + ".exe.manifest")
            self.manifest = winmanifest.create_manifest(
                filename, self.manifest, self.console, self.uac_admin,
                self.uac_uiaccess)

            manifest_filename = os.path.basename(self.name) + ".manifest"

            self.toc.append((manifest_filename, filename, 'BINARY'))
            if not self.exclude_binaries:
                # Onefile mode: manifest file is explicitly loaded.
                # Store name of manifest file as bootloader option. Allows
                # the exe to be renamed.
                self.toc.append(
                    ("pyi-windows-manifest-filename " + manifest_filename, "",
                     "OPTION"))

        self.pkg = PKG(
            self.toc,
            cdict=kwargs.get('cdict', None),
            exclude_binaries=self.exclude_binaries,
            strip_binaries=self.strip,
            upx_binaries=self.upx,
        )
        self.dependencies = self.pkg.dependencies

        # Get the path of the bootloader and store it in a TOC, so it
        # can be checked for being changed.
        exe = self._bootloader_file('run',
                                    '.exe' if is_win or is_cygwin else '')
        self.exefiles = TOC([(os.path.basename(exe), exe, 'EXECUTABLE')])

        self.__postinit__()

    _GUTS = (  # input parameters
        ('name', _check_guts_eq),
        ('console', _check_guts_eq),
        ('debug', _check_guts_eq),
        ('exclude_binaries', _check_guts_eq),
        ('icon', _check_guts_eq),
        ('versrsrc', _check_guts_eq),
        ('uac_admin', _check_guts_eq),
        ('uac_uiaccess', _check_guts_eq),
        ('manifest', _check_guts_eq),
        ('append_pkg', _check_guts_eq),
        # for the case the directory ius shared between platforms:
        ('pkgname', _check_guts_eq),
        ('toc', _check_guts_eq),
        ('resources', _check_guts_eq),
        ('strip', _check_guts_eq),
        ('upx', _check_guts_eq),
        (
            'mtm',
            None,
        ),  # checked below
        # no calculated/analysed values
        ('exefiles', _check_guts_toc),
    )

    def _check_guts(self, data, last_build):
        if not os.path.exists(self.name):
            logger.info("Rebuilding %s because %s missing", self.tocbasename,
                        os.path.basename(self.name))
            return 1
        if not self.append_pkg and not os.path.exists(self.pkgname):
            logger.info("Rebuilding because %s missing",
                        os.path.basename(self.pkgname))
            return 1

        if Target._check_guts(self, data, last_build):
            return True

        if (data['versrsrc'] or data['resources']) and not is_win:
            # todo: really ignore :-)
            logger.warning(
                'ignoring version, manifest and resources, platform not capable'
            )
        if data['icon'] and not (is_win or is_darwin):
            logger.warning('ignoring icon, platform not capable')

        mtm = data['mtm']
        if mtm != misc.mtime(self.name):
            logger.info("Rebuilding %s because mtimes don't match",
                        self.tocbasename)
            return True
        if mtm < misc.mtime(self.pkg.tocfilename):
            logger.info("Rebuilding %s because pkg is more recent",
                        self.tocbasename)
            return True
        return False

    def _bootloader_file(self, exe, extension=None):
        """
        Pick up the right bootloader file - debug, console, windowed.
        """
        # Having console/windowed bootolader makes sense only on Windows and
        # Mac OS X.
        if is_win or is_darwin:
            if not self.console:
                exe = exe + 'w'
        # There are two types of bootloaders:
        # run     - release, no verbose messages in console.
        # run_d   - contains verbose messages in console.
        if self.debug:
            exe = exe + '_d'
        if extension:
            exe = exe + extension
        bootloader_file = os.path.join(HOMEPATH, 'PyInstaller', 'bootloader',
                                       PLATFORM, exe)
        logger.info('Bootloader %s' % bootloader_file)
        return bootloader_file

    def assemble(self):
        logger.info("Building EXE from %s", self.tocbasename)
        trash = []
        if os.path.exists(self.name):
            os.remove(self.name)
        if not os.path.exists(os.path.dirname(self.name)):
            os.makedirs(os.path.dirname(self.name))
        exe = self.exefiles[0][1]  # pathname of bootloader
        if not os.path.exists(exe):
            raise SystemExit(_MISSING_BOOTLOADER_ERRORMSG)

        if is_win and (self.icon or self.versrsrc or self.resources):
            tmpnm = tempfile.mktemp()
            self._copyfile(exe, tmpnm)
            os.chmod(tmpnm, 0o755)
            if self.icon:
                icon.CopyIcons(tmpnm, self.icon)
            if self.versrsrc:
                versioninfo.SetVersion(tmpnm, self.versrsrc)
            for res in self.resources:
                res = res.split(",")
                for i in range(1, len(res)):
                    try:
                        res[i] = int(res[i])
                    except ValueError:
                        pass
                resfile = res[0]
                restype = resname = reslang = None
                if len(res) > 1:
                    restype = res[1]
                if len(res) > 2:
                    resname = res[2]
                if len(res) > 3:
                    reslang = res[3]
                try:
                    winresource.UpdateResourcesFromResFile(
                        tmpnm, resfile, [restype or "*"], [resname or "*"],
                        [reslang or "*"])
                except winresource.pywintypes.error as exc:
                    if exc.args[0] != winresource.ERROR_BAD_EXE_FORMAT:
                        logger.error(
                            "Error while updating resources in %s"
                            " from resource file %s",
                            tmpnm,
                            resfile,
                            exc_info=1)
                        continue

                    # Handle the case where the file contains no resources, and is
                    # intended as a single resource to be added to the exe.
                    if not restype or not resname:
                        logger.error("resource type and/or name not specified")
                        continue
                    if "*" in (restype, resname):
                        logger.error("no wildcards allowed for resource type "
                                     "and name when source file does not "
                                     "contain resources")
                        continue
                    try:
                        winresource.UpdateResourcesFromDataFile(
                            tmpnm, resfile, restype, [resname], [reslang or 0])
                    except winresource.pywintypes.error:
                        logger.error(
                            "Error while updating resource %s %s in %s"
                            " from data file %s",
                            restype,
                            resname,
                            tmpnm,
                            resfile,
                            exc_info=1)
            trash.append(tmpnm)
            exe = tmpnm

        # NOTE: Do not look up for bootloader file in the cache because it might
        #       get corrupted by UPX when UPX is available. See #1863 for details.

        if not self.append_pkg:
            logger.info("Copying bootloader exe to %s", self.name)
            self._copyfile(exe, self.name)
            logger.info("Copying archive to %s", self.pkgname)
            self._copyfile(self.pkg.name, self.pkgname)
        elif is_linux:
            self._copyfile(exe, self.name)
            logger.info("Appending archive to ELF section in EXE %s",
                        self.name)
            retcode, stdout, stderr = exec_command_all(
                'objcopy', '--add-section', 'pydata=%s' % self.pkg.name,
                self.name)
            logger.debug("objcopy returned %i", retcode)
            if stdout:
                logger.debug(stdout)
            if stderr:
                logger.debug(stderr)
            if retcode != 0:
                raise SystemError("objcopy Failure: %s" % stderr)
        else:
            # Fall back to just append on end of file
            logger.info("Appending archive to EXE %s", self.name)
            with open(self.name, 'wb') as outf:
                # write the bootloader data
                with open(exe, 'rb') as infh:
                    shutil.copyfileobj(infh, outf, length=64 * 1024)
                # write the archive data
                with open(self.pkg.name, 'rb') as infh:
                    shutil.copyfileobj(infh, outf, length=64 * 1024)

        if is_darwin:
            # Fix Mach-O header for codesigning on OS X.
            logger.info("Fixing EXE for code signing %s", self.name)
            import PyInstaller.utils.osx as osxutils
            osxutils.fix_exe_for_code_signing(self.name)

        os.chmod(self.name, 0o755)
        # get mtime for storing into the guts
        self.mtm = misc.mtime(self.name)
        for item in trash:
            os.remove(item)
        logger.info("Building EXE from %s completed successfully.",
                    self.tocbasename)

    def _copyfile(self, infile, outfile):
        with open(infile, 'rb') as infh:
            with open(outfile, 'wb') as outfh:
                shutil.copyfileobj(infh, outfh, length=64 * 1024)
Esempio n. 14
0
class EXE(Target):
    """
    Creates the final executable of the frozen app. This bundles all necessary files together.
    """
    typ = 'EXECUTABLE'

    def __init__(self, *args, **kwargs):
        """
        args
                One or more arguments that are either TOCs Targets.
        kwargs
            Possible keyword arguments:

            bootloader_ignore_signals
                Non-Windows only. If True, the bootloader process will ignore all ignorable signals. If False (default),
                it will forward all signals to the child process. Useful in situations where for example a supervisor
                process signals both the bootloader and the child (e.g., via a process group) to avoid signalling the
                child twice.
            console
                On Windows or Mac OS governs whether to use the console executable or the windowed executable. Always
                True on Linux/Unix (always console executable - it does not matter there).
            disable_windowed_traceback
                Disable traceback dump of unhandled exception in windowed (noconsole) mode (Windows and macOS only),
                and instead display a message that this feature is disabled.
            debug
                Setting to True gives you progress messages from the executable (for console=False there will be
                annoying MessageBoxes on Windows).
            name
                The filename for the executable. On Windows suffix '.exe' is appended.
            exclude_binaries
                Forwarded to the PKG the EXE builds.
            icon
                Windows and Mac OS only. icon='myicon.ico' to use an icon file or icon='notepad.exe,0' to grab an icon
                resource. Defaults to use PyInstaller's console or windowed icon. Use icon=`NONE` to not add any icon.
            version
                Windows only. version='myversion.txt'. Use grab_version.py to get a version resource from an executable
                and then edit the output to create your own. (The syntax of version resources is so arcane that I would
                not attempt to write one from scratch).
            uac_admin
                Windows only. Setting to True creates a Manifest with will request elevation upon application start.
            uac_uiaccess
                Windows only. Setting to True allows an elevated application to work with Remote Desktop.
            embed_manifest
                Windows only. Setting to True (the default) embeds the manifest into the executable. Setting to False
                generates an external .exe.manifest file. Applicable only in onedir mode (exclude_binaries=True); in
                onefile mode (exclude_binaries=False), the manifest is always embedded in the executable, regardless
                of this option.
            target_arch
                macOS only. Used to explicitly specify the target architecture; either single-arch ('x86_64' or 'arm64')
                or 'universal2'. Used in checks that the collected binaries contain the requires arch slice(s) and/or
                to convert fat binaries into thin ones as necessary. If not specified (default), a single-arch build
                corresponding to running architecture is assumed.
            codesign_identity
                macOS only. Use the provided identity to sign collected binaries and the generated executable. If
                signing identity is not provided, ad-hoc signing is performed.
            entitlements_file
                macOS only. Optional path to entitlements file to use with code signing of collected binaries
                (--entitlements option to codesign utility).
        """
        from PyInstaller.config import CONF
        Target.__init__(self)

        # Available options for EXE in .spec files.
        self.exclude_binaries = kwargs.get('exclude_binaries', False)
        self.bootloader_ignore_signals = kwargs.get('bootloader_ignore_signals', False)
        self.console = kwargs.get('console', True)
        self.disable_windowed_traceback = kwargs.get('disable_windowed_traceback', False)
        self.debug = kwargs.get('debug', False)
        self.name = kwargs.get('name', None)
        self.icon = kwargs.get('icon', None)
        self.versrsrc = kwargs.get('version', None)
        self.manifest = kwargs.get('manifest', None)
        self.embed_manifest = kwargs.get('embed_manifest', True)
        self.resources = kwargs.get('resources', [])
        self.strip = kwargs.get('strip', False)
        self.upx_exclude = kwargs.get("upx_exclude", [])
        self.runtime_tmpdir = kwargs.get('runtime_tmpdir', None)
        # If ``append_pkg`` is false, the archive will not be appended to the exe, but copied beside it.
        self.append_pkg = kwargs.get('append_pkg', True)

        # On Windows allows the exe to request admin privileges.
        self.uac_admin = kwargs.get('uac_admin', False)
        self.uac_uiaccess = kwargs.get('uac_uiaccess', False)

        # Target architecture (macOS only)
        self.target_arch = kwargs.get('target_arch', None)
        if is_darwin:
            if self.target_arch is None:
                import platform
                self.target_arch = platform.machine()
            else:
                assert self.target_arch in {'x86_64', 'arm64', 'universal2'}, \
                    f"Unsupported target arch: {self.target_arch}"
            logger.info("EXE target arch: %s", self.target_arch)
        else:
            self.target_arch = None  # explicitly disable

        # Code signing identity (macOS only)
        self.codesign_identity = kwargs.get('codesign_identity', None)
        if is_darwin:
            logger.info("Code signing identity: %s", self.codesign_identity)
        else:
            self.codesign_identity = None  # explicitly disable
        # Code signing entitlements
        self.entitlements_file = kwargs.get('entitlements_file', None)

        if CONF['hasUPX']:
            self.upx = kwargs.get('upx', False)
        else:
            self.upx = False

        # Old .spec format included in 'name' the path where to put created app. New format includes only exename.
        #
        # Ignore fullpath in the 'name' and prepend DISTPATH or WORKPATH.
        # DISTPATH - onefile
        # WORKPATH - onedir
        if self.exclude_binaries:
            # onedir mode - create executable in WORKPATH.
            self.name = os.path.join(CONF['workpath'], os.path.basename(self.name))
        else:
            # onefile mode - create executable in DISTPATH.
            self.name = os.path.join(CONF['distpath'], os.path.basename(self.name))

        # Old .spec format included on Windows in 'name' .exe suffix.
        if is_win or is_cygwin:
            # Append .exe suffix if it is not already there.
            if not self.name.endswith('.exe'):
                self.name += '.exe'
            base_name = os.path.splitext(os.path.basename(self.name))[0]
        else:
            base_name = os.path.basename(self.name)
        # Create the CArchive PKG in WORKPATH. When instancing PKG(), set name so that guts check can test whether the
        # file already exists.
        self.pkgname = os.path.join(CONF['workpath'], base_name + '.pkg')

        self.toc = TOC()

        for arg in args:
            if isinstance(arg, TOC):
                self.toc.extend(arg)
            elif isinstance(arg, Target):
                self.toc.append((os.path.basename(arg.name), arg.name, arg.typ))
                self.toc.extend(arg.dependencies)
            else:
                self.toc.extend(arg)

        if self.runtime_tmpdir is not None:
            self.toc.append(("pyi-runtime-tmpdir " + self.runtime_tmpdir, "", "OPTION"))

        if self.bootloader_ignore_signals:
            # no value; presence means "true"
            self.toc.append(("pyi-bootloader-ignore-signals", "", "OPTION"))

        if self.disable_windowed_traceback:
            # no value; presence means "true"
            self.toc.append(("pyi-disable-windowed-traceback", "", "OPTION"))

        if is_win:
            if not self.exclude_binaries:
                # onefile mode forces embed_manifest=True
                if not self.embed_manifest:
                    logger.warning("Ignoring embed_manifest=False setting in onefile mode!")
                self.embed_manifest = True
            if not self.icon:
                # --icon not specified; use default from bootloader folder
                if self.console:
                    ico = 'icon-console.ico'
                else:
                    ico = 'icon-windowed.ico'
                self.icon = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'bootloader', 'images', ico)
            filename = os.path.join(CONF['workpath'], CONF['specnm'] + ".exe.manifest")
            self.manifest = winmanifest.create_manifest(
                filename, self.manifest, self.console, self.uac_admin, self.uac_uiaccess
            )

            manifest_filename = os.path.basename(self.name) + ".manifest"

            # If external manifest file is requested (supported only in onedir mode), add the file to the TOC in order
            # for it to be collected as an external manifest file. Otherwise, the assembly pipeline will embed the
            # manifest into the executable later on.
            if not self.embed_manifest:
                self.toc.append((manifest_filename, filename, 'BINARY'))

            if self.versrsrc:
                if not isinstance(self.versrsrc, versioninfo.VSVersionInfo) and not os.path.isabs(self.versrsrc):
                    # relative version-info path is relative to spec file
                    self.versrsrc = os.path.join(CONF['specpath'], self.versrsrc)

        self.pkg = PKG(
            self.toc,
            name=self.pkgname,
            cdict=kwargs.get('cdict', None),
            exclude_binaries=self.exclude_binaries,
            strip_binaries=self.strip,
            upx_binaries=self.upx,
            upx_exclude=self.upx_exclude,
            target_arch=self.target_arch,
            codesign_identity=self.codesign_identity,
            entitlements_file=self.entitlements_file
        )
        self.dependencies = self.pkg.dependencies

        # Get the path of the bootloader and store it in a TOC, so it can be checked for being changed.
        exe = self._bootloader_file('run', '.exe' if is_win or is_cygwin else '')
        self.exefiles = TOC([(os.path.basename(exe), exe, 'EXECUTABLE')])

        self.__postinit__()

    _GUTS = (  # input parameters
        ('name', _check_guts_eq),
        ('console', _check_guts_eq),
        ('debug', _check_guts_eq),
        ('exclude_binaries', _check_guts_eq),
        ('icon', _check_guts_eq),
        ('versrsrc', _check_guts_eq),
        ('uac_admin', _check_guts_eq),
        ('uac_uiaccess', _check_guts_eq),
        ('manifest', _check_guts_eq),
        ('embed_manifest', _check_guts_eq),
        ('append_pkg', _check_guts_eq),
        ('target_arch', _check_guts_eq),
        ('codesign_identity', _check_guts_eq),
        ('entitlements_file', _check_guts_eq),
        # for the case the directory ius shared between platforms:
        ('pkgname', _check_guts_eq),
        ('toc', _check_guts_eq),
        ('resources', _check_guts_eq),
        ('strip', _check_guts_eq),
        ('upx', _check_guts_eq),
        ('mtm', None),  # checked below
        # no calculated/analysed values
        ('exefiles', _check_guts_toc),
    )

    def _check_guts(self, data, last_build):
        if not os.path.exists(self.name):
            logger.info("Rebuilding %s because %s missing", self.tocbasename, os.path.basename(self.name))
            return 1
        if not self.append_pkg and not os.path.exists(self.pkgname):
            logger.info("Rebuilding because %s missing", os.path.basename(self.pkgname))
            return 1

        if Target._check_guts(self, data, last_build):
            return True

        if (data['versrsrc'] or data['resources']) and not is_win:
            # todo: really ignore :-)
            logger.warning('ignoring version, manifest and resources, platform not capable')
        if data['icon'] and not (is_win or is_darwin):
            logger.warning('ignoring icon, platform not capable')

        mtm = data['mtm']
        if mtm != misc.mtime(self.name):
            logger.info("Rebuilding %s because mtimes don't match", self.tocbasename)
            return True
        if mtm < misc.mtime(self.pkg.tocfilename):
            logger.info("Rebuilding %s because pkg is more recent", self.tocbasename)
            return True
        return False

    def _bootloader_file(self, exe, extension=None):
        """
        Pick up the right bootloader file - debug, console, windowed.
        """
        # Having console/windowed bootloader makes sense only on Windows and Mac OS.
        if is_win or is_darwin:
            if not self.console:
                exe = exe + 'w'
        # There are two types of bootloaders:
        # run     - release, no verbose messages in console.
        # run_d   - contains verbose messages in console.
        if self.debug:
            exe = exe + '_d'
        if extension:
            exe = exe + extension
        bootloader_file = os.path.join(HOMEPATH, 'PyInstaller', 'bootloader', PLATFORM, exe)
        logger.info('Bootloader %s' % bootloader_file)
        return bootloader_file

    def assemble(self):
        from PyInstaller.config import CONF
        logger.info("Building EXE from %s", self.tocbasename)
        if os.path.exists(self.name):
            os.remove(self.name)
        if not os.path.exists(os.path.dirname(self.name)):
            os.makedirs(os.path.dirname(self.name))
        exe = self.exefiles[0][1]  # pathname of bootloader
        if not os.path.exists(exe):
            raise SystemExit(_MISSING_BOOTLOADER_ERRORMSG)

        # Step 1: copy the bootloader file, and perform any operations that need to be done prior to appending the PKG.
        logger.info("Copying bootloader EXE to %s", self.name)
        self._copyfile(exe, self.name)
        os.chmod(self.name, 0o755)

        if is_win:
            # First, remove all resources from the file. This ensures that no manifest is embedded, even if bootloader
            # was compiled with a toolchain that forcibly embeds a default manifest (e.g., mingw toolchain from msys2).
            winresource.RemoveAllResources(self.name)
            # Embed icon.
            if self.icon != "NONE":
                logger.info("Copying icon to EXE")
                icon.CopyIcons(self.name, self.icon)
            # Embed version info.
            if self.versrsrc:
                logger.info("Copying version information to EXE")
                versioninfo.SetVersion(self.name, self.versrsrc)
            # Embed other resources.
            logger.info("Copying %d resources to EXE", len(self.resources))
            for res in self.resources:
                res = res.split(",")
                for i in range(1, len(res)):
                    try:
                        res[i] = int(res[i])
                    except ValueError:
                        pass
                resfile = res[0]
                if not os.path.isabs(resfile):
                    resfile = os.path.join(CONF['specpath'], resfile)
                restype = resname = reslang = None
                if len(res) > 1:
                    restype = res[1]
                if len(res) > 2:
                    resname = res[2]
                if len(res) > 3:
                    reslang = res[3]
                try:
                    winresource.UpdateResourcesFromResFile(
                        self.name, resfile, [restype or "*"], [resname or "*"], [reslang or "*"]
                    )
                except winresource.pywintypes.error as exc:
                    if exc.args[0] != winresource.ERROR_BAD_EXE_FORMAT:
                        logger.error(
                            "Error while updating resources in %s from resource file %s!",
                            self.name,
                            resfile,
                            exc_info=1
                        )
                        continue

                    # Handle the case where the file contains no resources, and is intended as a single resource to be
                    # added to the exe.
                    if not restype or not resname:
                        logger.error("Resource type and/or name not specified!")
                        continue
                    if "*" in (restype, resname):
                        logger.error(
                            "No wildcards allowed for resource type and name when the source file does not contain "
                            "any resources!"
                        )
                        continue
                    try:
                        winresource.UpdateResourcesFromDataFile(self.name, resfile, restype, [resname], [reslang or 0])
                    except winresource.pywintypes.error:
                        logger.error(
                            "Error while updating resource %s %s in %s from data file %s!",
                            restype,
                            resname,
                            self.name,
                            resfile,
                            exc_info=1
                        )
            # Embed the manifest into the executable.
            if self.embed_manifest:
                logger.info("Emedding manifest in EXE")
                self.manifest.update_resources(self.name, [1])
        elif is_darwin:
            # Convert bootloader to the target arch
            logger.info("Converting EXE to target arch (%s)", self.target_arch)
            osxutils.binary_to_target_arch(self.name, self.target_arch, display_name='Bootloader EXE')

        # Step 2: append the PKG, if necessary
        if not self.append_pkg:
            # In onefile mode, copy the stand-alone pkg next to the executable. In onedir, this will be done by the
            # COLLECT() target.
            if not self.exclude_binaries:
                pkg_dst = os.path.join(os.path.dirname(self.name), os.path.basename(self.pkgname))
                logger.info("Copying stand-alone PKG archive from %s to %s", self.pkg.name, pkg_dst)
                self._copyfile(self.pkg.name, pkg_dst)
            else:
                logger.info("Stand-alone PKG archive will be handled by COLLECT")
        elif is_linux:
            # Linux: append PKG into ELF section using objcopy
            logger.info("Appending PKG archive to ELF section in EXE")
            retcode, stdout, stderr = exec_command_all(
                'objcopy', '--add-section', 'pydata=%s' % self.pkg.name, self.name
            )
            logger.debug("objcopy returned %i", retcode)
            if stdout:
                logger.debug(stdout)
            if stderr:
                logger.debug(stderr)
            if retcode != 0:
                raise SystemError("objcopy Failure: %s" % stderr)
        elif is_darwin:
            # macOS: remove signature, append PKG, and fix-up headers so that PKG appears to be part of the executable.

            # Strip signatures from all arch slices. Strictly speaking, we need to remove signature (if present) from
            # the last slice, because we will be appending data to it. When building universal2 bootloaders natively on
            # macOS, only arm64 slices have a (dummy) signature. However, when cross-compiling with osxcross, we seem to
            # get dummy signatures on both x86_64 and arm64 slices. While the former should not have any impact, it does
            # seem to cause issues with further binary signing using real identity. Therefore, we remove all signatures
            # and re-sign the binary using dummy signature once the data is appended.
            logger.info("Removing signature(s) from EXE")
            osxutils.remove_signature_from_binary(self.name)

            # Append the PKG data
            logger.info("Appending PKG archive to EXE")
            with open(self.name, 'ab') as outf:
                with open(self.pkg.name, 'rb') as inf:
                    shutil.copyfileobj(inf, outf, length=64 * 1024)

            # Fix Mach-O header for code signing
            logger.info("Fixing EXE headers for code signing")
            osxutils.fix_exe_for_code_signing(self.name)
        else:
            # Fall back to just appending PKG at the end of the file
            logger.info("Appending PKG archive to EXE")
            with open(self.name, 'ab') as outf:
                with open(self.pkg.name, 'rb') as inf:
                    shutil.copyfileobj(inf, outf, length=64 * 1024)

        # Step 3: post-processing
        if is_win:
            # Set checksum to appease antiviral software.
            set_exe_checksum(self.name)
        elif is_darwin:
            # If the version of macOS SDK used to build bootloader exceeds that of macOS SDK used to built Python
            # library (and, by extension, bundled Tcl/Tk libraries), force the version declared by the frozen executable
            # to match that of the Python library.
            # Having macOS attempt to enable new features (based on SDK version) for frozen application has no benefit
            # if the Python library does not support them as well.
            # On the other hand, there seem to be UI issues in tkinter due to failed or partial enablement of dark mode
            # (i.e., the bootloader executable being built against SDK 10.14 or later, which causes macOS to enable dark
            # mode, and Tk libraries being built against an earlier SDK version that does not support the dark mode).
            # With python.org Intel macOS installers, this manifests as black Tk windows and UI elements (see issue
            # #5827), while in Anaconda python, it may result in white text on bright background.
            pylib_version = osxutils.get_macos_sdk_version(bindepend.get_python_library_path())
            exe_version = osxutils.get_macos_sdk_version(self.name)
            if pylib_version < exe_version:
                logger.info(
                    "Rewriting the executable's macOS SDK version (%d.%d.%d) to match the SDK version of the Python "
                    "library (%d.%d.%d) in order to avoid inconsistent behavior and potential UI issues in the "
                    "frozen application.", *exe_version, *pylib_version
                )
                osxutils.set_macos_sdk_version(self.name, *pylib_version)

            # Re-sign the binary (either ad-hoc or using real identity, if provided).
            logger.info("Re-signing the EXE")
            osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file)

        # Ensure executable flag is set
        os.chmod(self.name, 0o755)
        # Get mtime for storing into the guts
        self.mtm = misc.mtime(self.name)
        logger.info("Building EXE from %s completed successfully.", self.tocbasename)

    def _copyfile(self, infile, outfile):
        with open(infile, 'rb') as infh:
            with open(outfile, 'wb') as outfh:
                shutil.copyfileobj(infh, outfh, length=64 * 1024)
Esempio n. 15
0
class Analysis(Target):
    """
    Class does analysis of the user's main Python scripts.

    An Analysis has five outputs, all TOCs (Table of Contents) accessed as
    attributes of the analysis.

    scripts
            The scripts you gave Analysis as input, with any runtime hook scripts
            prepended.
    pure
            The pure Python modules.
    binaries
            The extensionmodules and their dependencies. The secondary dependecies
            are filtered. On Windows files from C:\\Windows are excluded by default.
            On Linux/Unix only system libraries from /lib or /usr/lib are excluded.
    datas
            Data-file dependencies. These are data-file that are found to be needed
            by modules. They can be anything: plugins, font files, images, translations,
            etc.
    zipfiles
            The zipfiles dependencies (usually .egg files).
    """
    _old_scripts = {
        absnormpath(os.path.join(HOMEPATH, "support", "_mountzlib.py")),
        absnormpath(os.path.join(HOMEPATH, "support", "useUnicode.py")),
        absnormpath(os.path.join(HOMEPATH, "support", "useTK.py")),
        absnormpath(os.path.join(HOMEPATH, "support", "unpackTK.py")),
        absnormpath(os.path.join(HOMEPATH, "support", "removeTK.py"))
    }

    def __init__(self,
                 scripts,
                 pathex=None,
                 binaries=None,
                 datas=None,
                 hiddenimports=None,
                 hookspath=None,
                 hooksconfig=None,
                 excludes=None,
                 runtime_hooks=None,
                 cipher=None,
                 win_no_prefer_redirects=False,
                 win_private_assemblies=False,
                 noarchive=False):
        """
        scripts
                A list of scripts specified as file names.
        pathex
                An optional list of paths to be searched before sys.path.
        binaries
                An optional list of additional binaries (dlls, etc.) to include.
        datas
                An optional list of additional data files to include.
        hiddenimport
                An optional list of additional (hidden) modules to include.
        hookspath
                An optional list of additional paths to search for hooks.
                (hook-modules).
        hooksconfig
                An optional dict of config settings for hooks.
                (hook-modules).
        excludes
                An optional list of module or package names (their Python names,
                not path names) that will be ignored (as though they were not found).
        runtime_hooks
                An optional list of scripts to use as users' runtime hooks. Specified
                as file names.
        cipher
                Add optional instance of the pyz_crypto.PyiBlockCipher class
                (with a provided key).
        win_no_prefer_redirects
                If True, prefers not to follow version redirects when searching for
                Windows SxS Assemblies.
        win_private_assemblies
                If True, changes all bundled Windows SxS Assemblies into Private
                Assemblies to enforce assembly versions.
        noarchive
                If True, don't place source files in a archive, but keep them as
                individual files.
        """
        super(Analysis, self).__init__()
        from PyInstaller.config import CONF

        self.inputs = []
        spec_dir = os.path.dirname(CONF['spec'])
        for script in scripts:
            # If path is relative, it is relative to the location of .spec file.
            if not os.path.isabs(script):
                script = os.path.join(spec_dir, script)
            if absnormpath(script) in self._old_scripts:
                logger.warning('Ignoring obsolete auto-added script %s',
                               script)
                continue
            # Normalize script path.
            script = os.path.normpath(script)
            if not os.path.exists(script):
                raise SystemExit("script '%s' not found" % script)
            self.inputs.append(script)

        # Django hook requires this variable to find the script manage.py.
        CONF['main_script'] = self.inputs[0]

        self.pathex = self._extend_pathex(pathex, self.inputs)
        # Set global config variable 'pathex' to make it available for
        # PyInstaller.utils.hooks and import hooks. Path extensions for module
        # search.
        CONF['pathex'] = self.pathex
        # Extend sys.path so PyInstaller could find all necessary modules.
        logger.info('Extending PYTHONPATH with paths\n' +
                    pprint.pformat(self.pathex))
        sys.path.extend(self.pathex)

        # Set global variable to hold assembly binding redirects
        CONF['binding_redirects'] = []

        self.hiddenimports = hiddenimports or []
        # Include modules detected when parsing options, like 'codecs' and encodings.
        self.hiddenimports.extend(CONF['hiddenimports'])

        self.hookspath = []
        # Append directories in `hookspath` (`--additional-hooks-dir`) to
        # take precedence over those from the entry points.
        if hookspath:
            self.hookspath.extend(hookspath)

        # Add hook directories from PyInstaller entry points.
        self.hookspath += discover_hook_directories()

        self.hooksconfig = {}
        if hooksconfig:
            self.hooksconfig.update(hooksconfig)

        # Custom runtime hook files that should be included and started before
        # any existing PyInstaller runtime hooks.
        self.custom_runtime_hooks = runtime_hooks or []

        if cipher:
            logger.info('Will encrypt Python bytecode with key: %s',
                        cipher.key)
            # Create a Python module which contains the decryption key which will
            # be used at runtime by pyi_crypto.PyiBlockCipher.
            pyi_crypto_key_path = os.path.join(CONF['workpath'],
                                               'pyimod00_crypto_key.py')
            with open(pyi_crypto_key_path, 'w', encoding='utf-8') as f:
                f.write('# -*- coding: utf-8 -*-\n' 'key = %r\n' % cipher.key)
            self.hiddenimports.append('tinyaes')

        self.excludes = excludes or []
        self.scripts = TOC()
        self.pure = TOC()
        self.binaries = TOC()
        self.zipfiles = TOC()
        self.zipped_data = TOC()
        self.datas = TOC()
        self.dependencies = TOC()
        self.binding_redirects = CONF['binding_redirects'] = []
        self.win_no_prefer_redirects = win_no_prefer_redirects
        self.win_private_assemblies = win_private_assemblies
        self._python_version = sys.version
        self.noarchive = noarchive

        self.__postinit__()

        # TODO create function to convert datas/binaries from 'hook format' to TOC.
        # Initialise 'binaries' and 'datas' with lists specified in .spec file.
        if binaries:
            logger.info("Appending 'binaries' from .spec")
            for name, pth in format_binaries_and_datas(binaries,
                                                       workingdir=spec_dir):
                self.binaries.append((name, pth, 'BINARY'))
        if datas:
            logger.info("Appending 'datas' from .spec")
            for name, pth in format_binaries_and_datas(datas,
                                                       workingdir=spec_dir):
                self.datas.append((name, pth, 'DATA'))

    _GUTS = (  # input parameters
        ('inputs', _check_guts_eq),  # parameter `scripts`
        ('pathex', _check_guts_eq),
        ('hiddenimports', _check_guts_eq),
        ('hookspath', _check_guts_eq),
        ('hooksconfig', _check_guts_eq),
        ('excludes', _check_guts_eq),
        ('custom_runtime_hooks', _check_guts_eq),
        ('win_no_prefer_redirects', _check_guts_eq),
        ('win_private_assemblies', _check_guts_eq),
        ('noarchive', _check_guts_eq),

        #'cipher': no need to check as it is implied by an
        # additional hidden import

        #calculated/analysed values
        ('_python_version', _check_guts_eq),
        ('scripts', _check_guts_toc_mtime),
        ('pure', lambda *args: _check_guts_toc_mtime(*args, **{'pyc': 1})),
        ('binaries', _check_guts_toc_mtime),
        ('zipfiles', _check_guts_toc_mtime),
        ('zipped_data', None),  # TODO check this, too
        ('datas', _check_guts_toc_mtime),
        # TODO: Need to add "dependencies"?

        # cached binding redirects - loaded into CONF for PYZ/COLLECT to find.
        ('binding_redirects', None),
    )

    def _extend_pathex(self, spec_pathex, scripts):
        """
        Normalize additional paths where PyInstaller will look for modules and
        add paths with scripts to the list of paths.

        :param spec_pathex: Additional paths defined defined in .spec file.
        :param scripts: Scripts to create executable from.
        :return: list of updated paths
        """
        # Based on main supplied script - add top-level modules directory to PYTHONPATH.
        # Sometimes the main app script is not top-level module but submodule like 'mymodule.mainscript.py'.
        # In that case PyInstaller will not be able find modules in the directory containing 'mymodule'.
        # Add this directory to PYTHONPATH so PyInstaller could find it.
        pathex = []
        # Add scripts paths first.
        for script in scripts:
            logger.debug('script: %s' % script)
            script_toplevel_dir = get_path_to_toplevel_modules(script)
            if script_toplevel_dir:
                pathex.append(script_toplevel_dir)
        # Append paths from .spec.
        if spec_pathex is not None:
            pathex.extend(spec_pathex)
        # Normalize paths in pathex and make them absolute.
        return [absnormpath(p) for p in pathex]

    def _check_guts(self, data, last_build):
        if Target._check_guts(self, data, last_build):
            return True
        for fnm in self.inputs:
            if mtime(fnm) > last_build:
                logger.info("Building because %s changed", fnm)
                return True
        # Now we know that none of the input parameters and none of
        # the input files has changed. So take the values calculated
        # resp. analysed in the last run and store them in `self`.
        self.scripts = TOC(data['scripts'])
        self.pure = TOC(data['pure'])
        self.binaries = TOC(data['binaries'])
        self.zipfiles = TOC(data['zipfiles'])
        self.zipped_data = TOC(data['zipped_data'])
        self.datas = TOC(data['datas'])

        # Store previously found binding redirects in CONF for later use by PKG/COLLECT
        from PyInstaller.config import CONF
        self.binding_redirects = CONF['binding_redirects'] = data[
            'binding_redirects']

        return False

    def assemble(self):
        """
        This method is the MAIN method for finding all necessary files to be bundled.
        """
        from PyInstaller.config import CONF

        for m in self.excludes:
            logger.debug("Excluding module '%s'" % m)
        self.graph = initialize_modgraph(excludes=self.excludes,
                                         user_hook_dirs=self.hookspath)

        # TODO Find a better place where to put 'base_library.zip' and when to created it.
        # For Python 3 it is necessary to create file 'base_library.zip'
        # containing core Python modules. In Python 3 some built-in modules
        # are written in pure Python. base_library.zip is a way how to have
        # those modules as "built-in".
        libzip_filename = os.path.join(CONF['workpath'], 'base_library.zip')
        create_py3_base_library(libzip_filename, graph=self.graph)
        # Bundle base_library.zip as data file.
        # Data format of TOC item:   ('relative_path_in_dist_dir', 'absolute_path_on_disk', 'DATA')
        self.datas.append(
            (os.path.basename(libzip_filename), libzip_filename, 'DATA'))

        # Expand sys.path of module graph.
        # The attribute is the set of paths to use for imports: sys.path,
        # plus our loader, plus other paths from e.g. --path option).
        self.graph.path = self.pathex + self.graph.path
        self.graph.set_setuptools_nspackages()

        logger.info("running Analysis %s", self.tocbasename)
        # Get paths to Python and, in Windows, the manifest.
        python = compat.python_executable
        if not is_win:
            # Linux/MacOS: get a real, non-link path to the running Python executable.
            while os.path.islink(python):
                python = os.path.join(os.path.dirname(python),
                                      os.readlink(python))
            depmanifest = None
        else:
            # Windows: Create a manifest to embed into built .exe, containing the same
            # dependencies as python.exe.
            depmanifest = winmanifest.Manifest(
                type_="win32",
                name=CONF['specnm'],
                processorArchitecture=winmanifest.processor_architecture(),
                version=(1, 0, 0, 0))
            depmanifest.filename = os.path.join(
                CONF['workpath'], CONF['specnm'] + ".exe.manifest")

        # We record "binaries" separately from the modulegraph, as there
        # is no way to record those dependencies in the graph. These include
        # the python executable and any binaries added by hooks later.
        # "binaries" are not the same as "extensions" which are .so or .dylib
        # that are found and recorded as extension nodes in the graph.
        # Reset seen variable before running bindepend. We use bindepend only for
        # the python executable.
        bindepend.seen.clear()

        # Add binary and assembly dependencies of Python.exe.
        # This also ensures that its assembly depencies under Windows get added to the
        # built .exe's manifest. Python 2.7 extension modules have no assembly
        # dependencies, and rely on the app-global dependencies set by the .exe.
        self.binaries.extend(
            bindepend.Dependencies([('', python, '')],
                                   manifest=depmanifest,
                                   redirects=self.binding_redirects)[1:])
        if is_win:
            depmanifest.writeprettyxml()

        ### Module graph.
        #
        # Construct the module graph of import relationships between modules
        # required by this user's application. For each entry point (top-level
        # user-defined Python script), all imports originating from this entry
        # point are recursively parsed into a subgraph of the module graph. This
        # subgraph is then connected to this graph's root node, ensuring
        # imported module nodes will be reachable from the root node -- which is
        # is (arbitrarily) chosen to be the first entry point's node.

        # List to hold graph nodes of scripts and runtime hooks in use order.
        priority_scripts = []

        # Assume that if the script does not exist, Modulegraph will raise error.
        # Save the graph nodes of each in sequence.
        for script in self.inputs:
            logger.info("Analyzing %s", script)
            priority_scripts.append(self.graph.add_script(script))

        # Analyze the script's hidden imports (named on the command line)
        self.graph.add_hiddenimports(self.hiddenimports)

        ### Post-graph hooks.
        self.graph.process_post_graph_hooks(self)

        # Update 'binaries' TOC and 'datas' TOC.
        deps_proc = DependencyProcessor(self.graph,
                                        self.graph._additional_files_cache)
        self.binaries.extend(deps_proc.make_binaries_toc())
        self.datas.extend(deps_proc.make_datas_toc())
        self.zipped_data.extend(deps_proc.make_zipped_data_toc())
        # Note: zipped eggs are collected below

        ### Look for dlls that are imported by Python 'ctypes' module.
        # First get code objects of all modules that import 'ctypes'.
        logger.info('Looking for ctypes DLLs')
        # dict like:  {'module1': code_obj, 'module2': code_obj}
        ctypes_code_objs = self.graph.get_code_using("ctypes")

        for name, co in ctypes_code_objs.items():
            # Get dlls that might be needed by ctypes.
            logger.debug('Scanning %s for shared libraries or dlls', name)
            try:
                ctypes_binaries = scan_code_for_ctypes(co)
                self.binaries.extend(set(ctypes_binaries))
            except Exception as ex:
                raise RuntimeError(f"Failed to scan the module '{name}'. "
                                   f"This is a bug. Please report it.") from ex

        self.datas.extend((dest, source, "DATA")
                          for (dest, source) in format_binaries_and_datas(
                              self.graph.metadata_required()))

        # Analyze run-time hooks.
        # Run-time hooks has to be executed before user scripts. Add them
        # to the beginning of 'priority_scripts'.
        priority_scripts = self.graph.analyze_runtime_hooks(
            self.custom_runtime_hooks) + priority_scripts

        # 'priority_scripts' is now a list of the graph nodes of custom runtime
        # hooks, then regular runtime hooks, then the PyI loader scripts.
        # Further on, we will make sure they end up at the front of self.scripts

        ### Extract the nodes of the graph as TOCs for further processing.

        # Initialize the scripts list with priority scripts in the proper order.
        self.scripts = self.graph.nodes_to_toc(priority_scripts)

        # Extend the binaries list with all the Extensions modulegraph has found.
        self.binaries = self.graph.make_binaries_toc(self.binaries)
        # Fill the "pure" list with pure Python modules.
        assert len(self.pure) == 0
        self.pure = self.graph.make_pure_toc()
        # And get references to module code objects constructed by ModuleGraph
        # to avoid writing .pyc/pyo files to hdd.
        self.pure._code_cache = self.graph.get_code_objects()

        # Add remaining binary dependencies - analyze Python C-extensions and what
        # DLLs they depend on.
        logger.info('Looking for dynamic libraries')
        self.binaries.extend(
            bindepend.Dependencies(self.binaries,
                                   redirects=self.binding_redirects))

        ### Include zipped Python eggs.
        logger.info('Looking for eggs')
        self.zipfiles.extend(deps_proc.make_zipfiles_toc())

        # Verify that Python dynamic library can be found.
        # Without dynamic Python library PyInstaller cannot continue.
        self._check_python_library(self.binaries)

        if is_win:
            # Remove duplicate redirects
            self.binding_redirects[:] = list(set(self.binding_redirects))
            logger.info("Found binding redirects: \n%s",
                        self.binding_redirects)

        # Filter binaries to adjust path of extensions that come from
        # python's lib-dynload directory. Prefix them with lib-dynload
        # so that we'll collect them into subdirectory instead of
        # directly into _MEIPASS
        for idx, tpl in enumerate(self.binaries):
            name, path, typecode = tpl
            if typecode == 'EXTENSION' \
               and not os.path.dirname(os.path.normpath(name)) \
               and os.path.basename(os.path.dirname(path)) == 'lib-dynload':
                name = os.path.join('lib-dynload', name)
                self.binaries[idx] = (name, path, typecode)

        # Place Python source in data files for the noarchive case.
        if self.noarchive:
            # Create a new TOC of ``(dest path for .pyc, source for .py, type)``.
            new_toc = TOC()
            for name, path, typecode in self.pure:
                assert typecode == 'PYMODULE'
                # Transform a python module name into a file name.
                name = name.replace('.', os.sep)
                # Special case: modules have an implied filename to add.
                if os.path.splitext(os.path.basename(path))[0] == '__init__':
                    name += os.sep + '__init__'
                # Append the extension for the compiled result.
                # In python 3.5 (PEP-488) .pyo files were replaced by
                # .opt-1.pyc and .opt-2.pyc. However, it seems that for
                # bytecode-only module distribution, we always need to
                # use the .pyc extension.
                name += '.pyc'
                new_toc.append((name, path, typecode))
            # Put the result of byte-compiling this TOC in datas. Mark all entries as data.
            for name, path, typecode in compile_py_files(
                    new_toc, CONF['workpath']):
                self.datas.append((name, path, 'DATA'))
            # Store no source in the archive.
            self.pure = TOC()

        # Write warnings about missing modules.
        self._write_warnings()
        # Write debug information about hte graph
        self._write_graph_debug()

    def _write_warnings(self):
        """
        Write warnings about missing modules. Get them from the graph
        and use the graph to figure out who tried to import them.
        """
        def dependency_description(name, depInfo):
            if not depInfo or depInfo == 'direct':
                imptype = 0
            else:
                imptype = (depInfo.conditional + 2 * depInfo.function +
                           4 * depInfo.tryexcept)
            return '%s (%s)' % (name, IMPORT_TYPES[imptype])

        from PyInstaller.config import CONF
        miss_toc = self.graph.make_missing_toc()
        with open(CONF['warnfile'], 'w', encoding='utf-8') as wf:
            wf.write(WARNFILE_HEADER)
            for (n, p, status) in miss_toc:
                importers = self.graph.get_importers(n)
                print(status,
                      'module named',
                      n,
                      '- imported by',
                      ', '.join(
                          dependency_description(name, data)
                          for name, data in importers),
                      file=wf)
        logger.info("Warnings written to %s", CONF['warnfile'])

    def _write_graph_debug(self):
        """Write a xref (in html) and with `--log-level DEBUG` a dot-drawing
        of the graph.
        """
        from PyInstaller.config import CONF
        with open(CONF['xref-file'], 'w', encoding='utf-8') as fh:
            self.graph.create_xref(fh)
            logger.info("Graph cross-reference written to %s",
                        CONF['xref-file'])
        if logger.getEffectiveLevel() > logging.DEBUG:
            return
        # The `DOT language's <https://www.graphviz.org/doc/info/lang.html>`_
        # default character encoding (see the end of the linked page) is UTF-8.
        with open(CONF['dot-file'], 'w', encoding='utf-8') as fh:
            self.graph.graphreport(fh)
            logger.info("Graph drawing written to %s", CONF['dot-file'])

    def _check_python_library(self, binaries):
        """
        Verify presence of the Python dynamic library in the binary dependencies.
        Python library is an essential piece that has to be always included.
        """
        # First check that libpython is in resolved binary dependencies.
        for (nm, filename, typ) in binaries:
            if typ == 'BINARY' and nm in PYDYLIB_NAMES:
                # Just print its filename and return.
                logger.info('Using Python library %s', filename)
                # Checking was successful - end of function.
                return

        # Python lib not in dependencies - try to find it.
        logger.info(
            'Python library not in binary dependencies. Doing additional searching...'
        )
        python_lib = bindepend.get_python_library_path()
        logger.debug('Adding Python library to binary dependencies')
        binaries.append((os.path.basename(python_lib), python_lib, 'BINARY'))
        logger.info('Using Python library %s', python_lib)

    def exclude_system_libraries(self, list_of_exceptions=[]):
        """
        This method may be optionally called from the spec file to exclude
        any system libraries from the list of binaries other than those
        containing the shell-style wildcards in list_of_exceptions.
        Those that match '*python*' or are stored under 'lib-dynload' are
        always treated as exceptions and not excluded.
        """

        self.binaries = \
            [i for i in self.binaries
                if _should_include_system_binary(i, list_of_exceptions)]
Esempio n. 16
0
def test_append_other_case_binary():
    # binary files should use C-I comparisons. 'LiBrEADlInE.so.6' should not be added.
    toc = TOC(ELEMS1)
    toc.append(('LiBrEADlInE.so.6', '/lib64/libreadline.so.6', 'BINARY'))
    expected = list(ELEMS1)
    assert toc == expected
Esempio n. 17
0
class Splash(Target):
    """
    Bundles the required resources for the splash screen into a file, which will be included in the CArchive.

    A Splash has two outputs, one is itself and one is sored in splash.binaries. Both need to be passed to other
    build targets in order to enable the splash screen.
    """
    typ = 'SPLASH'

    def __init__(self, image_file, binaries, datas, **kwargs):
        """
        :param str image_file:
            A path-like object to the image to be used. Only the PNG file format is supported.

            .. note:: If a different file format is supplied and PIL (Pillow) is installed, the file will be converted
                automatically.

            .. note:: *Windows*: Due to the implementation, the color Magenta/ RGB(255, 0, 255) must not be used in the
                image or text.

            .. note:: If PIL (Pillow) is installed and the image is bigger than max_img_size, the image will be resized
                to fit into the specified area.
        :param TOC binaries:
            The TOC of binaries the Analysis build target found. This TOC includes all extensionmodules and their
            dependencies. This is required to figure out, if the users program uses tkinter.
        :param TOC datas:
            The TOC of data the Analysis build target found. This TOC includes all data-file dependencies of the
            modules. This is required to check if all splash screen requirements can be bundled.

        :keyword text_pos:
            An optional 2x integer tuple that represents the origin of the text on the splash screen image. The
            origin of the text is its lower left corner. A unit in the respective coordinate system is a pixel of the
            image, its origin lies in the top left corner of the image. This parameter also acts like a switch for
            the text feature. If omitted, no text will be displayed on the splash screen. This text will be used to
            show textual progress in onefile mode.
        :type text_pos: Tuple[int, int]
        :keyword text_size:
            The desired size of the font. If the size argument is a positive number, it is interpreted as a size in
            points. If size is a negative number, its absolute value is interpreted as a size in pixels. Default: ``12``
        :type text_size: int
        :keyword text_font:
            An optional name of a font for the text. This font must be installed on the user system, otherwise the
            system default font is used. If this parameter is omitted, the default font is also used.
        :keyword text_color:
            An optional color for the text. Either RGB HTML notation or color names are supported. Default: black
            (Windows: Due to a implementation issue the color magenta/ rgb(255, 0, 255) is forbidden)
        :type text_color: str
        :keyword text_default:
            The default text which will be displayed before the extraction starts. Default: "Initializing"
        :type text_default: str
        :keyword full_tk:
            By default Splash bundles only the necessary files for the splash screen (some tk components). This
            options enables adding full tk and making it a requirement, meaning all tk files will be unpacked before
            the splash screen can be started. This is useful during development of the splash screen script.
            Default: ``False``
        :type full_tk: bool
        :keyword minify_script:
            The splash screen is created by executing an Tcl/Tk script. This option enables minimizing the script,
            meaning removing all non essential parts from the script. Default: True
        :keyword rundir:
            The folder name in which tcl/tk will be extracted at runtime. There should be no matching folder in your
            application to avoid conflicts. Default:  ``__splash``
        :type rundir: str
        :keyword name:
            An optional alternative filename for the .res file. If not specified, a name is generated.
        :type name: str
        :keyword script_name:
            An optional alternative filename for the Tcl script, that will be generated. If not specified, a name is
            generated.
        :type script_name: str
        :keyword max_img_size:
            Maximum size of the splash screen image as a tuple. If the supplied image exceeds this limit, it will be
            resized to fit the maximum width (to keep the original aspect ratio). This option can be disabled by
            setting it to None. Default: (760, 480)
        :type max_img_size: Tuple[int, int]
        """
        from ..config import CONF
        Target.__init__(self)

        # Splash screen is not supported on macOS. It operates in a secondary thread and macOS disallows UI operations
        # in any thread other than main.
        if is_darwin:
            raise SystemExit("Splash screen is not supported on macOS.")

        # Make image path relative to .spec file
        if not os.path.isabs(image_file):
            image_file = os.path.join(CONF['specpath'], image_file)
        image_file = os.path.normpath(image_file)
        if not os.path.exists(image_file):
            raise ValueError("Image file '%s' not found" % image_file)

        # Copy all arguments
        self.image_file = image_file
        self.full_tk = kwargs.get("full_tk", False)
        self.name = kwargs.get("name", None)
        self.script_name = kwargs.get("script_name", None)
        self.minify_script = kwargs.get("minify_script", True)
        self.rundir = kwargs.get("rundir", None)
        self.max_img_size = kwargs.get("max_img_size", (760, 480))

        # text options
        self.text_pos = kwargs.get("text_pos", None)
        self.text_size = kwargs.get("text_size", 12)
        self.text_font = kwargs.get("text_font", "TkDefaultFont")
        self.text_color = kwargs.get("text_color", "black")
        self.text_default = kwargs.get("text_default", "Initializing")

        # Save the generated file separately so that it is not necessary to generate the data again and again
        root = os.path.splitext(self.tocfilename)[0]
        if self.name is None:
            self.name = root + '.res'
        if self.script_name is None:
            self.script_name = root + '_script.tcl'

        if self.rundir is None:
            self.rundir = self._find_rundir(binaries + datas)

        # Internal variables
        try:
            # Do not import _tkinter at the toplevel, because on some systems _tkinter will fail to load, since it is
            # not installed. This would cause a runtime error in PyInstaller, since this module is imported from
            # build_main.py, instead we just want to inform the user that the splash screen feature is not supported on
            # his platform
            import _tkinter
            self._tkinter_module = _tkinter
            self._tkinter_file = self._tkinter_module.__file__
        except ModuleNotFoundError:
            raise SystemExit(
                "You platform does not support the splash screen feature, since tkinter is not installed. Please "
                "install tkinter and try again."
            )

        # Calculated / analysed values
        self.uses_tkinter = self._uses_tkinter(binaries)
        self.script = self.generate_script()
        self.tcl_lib, self.tk_lib = find_tcl_tk_shared_libs(self._tkinter_file)
        if is_darwin:
            # Outdated Tcl/Tk 8.5 system framework is not supported. Depending on macOS version, the library path will
            # come up empty (hidden system libraries on Big Sur), or will be
            # [/System]/Library/Frameworks/Tcl.framework/Tcl
            if self.tcl_lib[1] is None or 'Library/Frameworks/Tcl.framework' in self.tcl_lib[1]:
                raise SystemExit("The splash screen feature does not support macOS system framework version of Tcl/Tk.")
        # Check if tcl/tk was found
        assert all(self.tcl_lib)
        assert all(self.tk_lib)
        logger.debug("Use Tcl Library from %s and Tk From %s" % (self.tcl_lib, self.tk_lib))
        self.splash_requirements = set([self.tcl_lib[0], self.tk_lib[0]] + splash_requirements)

        logger.info("Collect tcl/tk binaries for the splash screen")
        tcltk_tree = collect_tcl_tk_files(self._tkinter_file)
        if self.full_tk:
            # The user wants a full copy of tk, so make all tk files a requirement.
            self.splash_requirements.update(toc[0] for toc in tcltk_tree)

        self.binaries = TOC()
        if not self.uses_tkinter:
            # The user's script does not use tkinter, so we need to provide a TOC of all necessary files add the shared
            # libraries to the binaries.
            self.binaries.append((self.tcl_lib[0], self.tcl_lib[1], 'BINARY'))
            self.binaries.append((self.tk_lib[0], self.tk_lib[1], 'BINARY'))

            # Only add the intersection of the required and the collected resources, or add all entries if full_tk is
            # true.
            self.binaries.extend(toc for toc in tcltk_tree if toc[0] in self.splash_requirements)

        # Check if all requirements were found.
        fnames = [toc[0] for toc in (binaries + datas + self.binaries)]

        def _filter(_item):
            if _item not in fnames:
                # Item is not bundled, so warn the user about it. This actually may happen on some tkinter installations
                # that are missing the license.terms file.
                logger.warning(
                    "The local Tcl/Tk installation is missing the file %s. The behavior of the splash screen is "
                    "therefore undefined and may be unsupported." % _item
                )
                return False
            return True

        # Remove all files which were not found.
        self.splash_requirements = set(filter(_filter, self.splash_requirements))

        # Test if the tcl/tk version is supported by the bootloader.
        self.test_tk_version()

        logger.debug("Splash Requirements: %s" % self.splash_requirements)

        self.__postinit__()

    _GUTS = (
        # input parameters
        ('image_file', _check_guts_eq),
        ('name', _check_guts_eq),
        ('script_name', _check_guts_eq),
        ('text_pos', _check_guts_eq),
        ('text_size', _check_guts_eq),
        ('text_font', _check_guts_eq),
        ('text_color', _check_guts_eq),
        ('text_default', _check_guts_eq),
        ('full_tk', _check_guts_eq),
        ('minify_script', _check_guts_eq),
        ('rundir', _check_guts_eq),
        ('max_img_size', _check_guts_eq),
        # calculated/analysed values
        ('uses_tkinter', _check_guts_eq),
        ('script', _check_guts_eq),
        ('tcl_lib', _check_guts_eq),
        ('tk_lib', _check_guts_eq),
        ('splash_requirements', _check_guts_eq),
        ('binaries', _check_guts_toc),
        # internal value
        # Check if the tkinter installation changed. This is theoretically possible if someone uses two different python
        # installations of the same version.
        ('_tkinter_file', _check_guts_eq),
    )

    def _check_guts(self, data, last_build):
        if Target._check_guts(self, data, last_build):
            return True

        # Check if the image has been modified.
        if misc.mtime(self.image_file) > last_build:
            logger.info("Building %s because file %s changed", self.tocbasename, self.image_file)
            return True

        return False

    def assemble(self):
        logger.info("Building Splash %s" % self.name)

        # Function to resize a given image to fit into the area defined by max_img_size.
        def _resize_image(_image, _orig_size):
            if PILImage:
                _w, _h = _orig_size
                _ratio_w = self.max_img_size[0] / _w
                if _ratio_w < 1:
                    # Image width exceeds limit
                    _h = int(_h * _ratio_w)
                    _w = self.max_img_size[0]

                _ratio_h = self.max_img_size[1] / _h
                if _ratio_h < 1:
                    # Image height exceeds limit
                    _w = int(_w * _ratio_h)
                    _h = self.max_img_size[1]

                # If a file is given it will be open
                if isinstance(_image, PILImage.Image):
                    _img = _image
                else:
                    _img = PILImage.open(_image)
                _img_resized = _img.resize((_w, _h))

                # Save image into a stream
                _image_stream = io.BytesIO()
                _img_resized.save(_image_stream, format='PNG')
                _img.close()
                _img_resized.close()
                _image_data = _image_stream.getvalue()
                logger.info(
                    "Resized image %s from dimensions %s to (%d, %d)" % (self.image_file, str(_orig_size), _w, _h)
                )
                return _image_data
            else:
                raise ValueError(
                    "The splash image dimensions (w: %d, h: %d) exceed max_img_size (w: %d, h:%d), but the image "
                    "cannot be resized due to missing PIL.Image! Either install the Pillow package, adjust the "
                    "max_img_size, or use an image of compatible dimensions." %
                    (_orig_size[0], _orig_size[1], self.max_img_size[0], self.max_img_size[1])
                )

        # Open image file
        image_file = open(self.image_file, 'rb')

        # Check header of the file to identify it
        if image_file.read(8) == b'\x89PNG\r\n\x1a\n':
            # self.image_file is a PNG file
            image_file.seek(16)
            img_size = (struct.unpack("!I", image_file.read(4))[0], struct.unpack("!I", image_file.read(4))[0])

            if img_size > self.max_img_size:
                # The image exceeds the maximum image size, so resize it
                image = _resize_image(self.image_file, img_size)
            else:
                image = os.path.abspath(self.image_file)
        elif PILImage:
            # Pillow is installed, meaning the image can be converted automatically
            img = PILImage.open(self.image_file, mode='r')

            if img.size > self.max_img_size:
                image = _resize_image(img, img.size)
            else:
                image_data = io.BytesIO()
                img.save(image_data, format='PNG')
                img.close()
                image = image_data.getvalue()
            logger.info("Converted image %s to PNG format" % self.image_file)
        else:
            raise ValueError(
                "The image %s needs to be converted to a PNG file, but PIL.Image is not available! Either install the "
                "Pillow package, or use a PNG image for you splash screen." % self.image_file
            )

        image_file.close()

        SplashWriter(
            self.name,
            self.splash_requirements,
            self.tcl_lib[0],  # tcl86t.dll
            self.tk_lib[0],  # tk86t.dll
            TK_ROOTNAME,
            self.rundir,
            image,
            self.script
        )

    def test_tk_version(self):
        tcl_version = float(self._tkinter_module.TCL_VERSION)
        tk_version = float(self._tkinter_module.TK_VERSION)

        # Test if tcl/tk version is supported
        if tcl_version < 8.6 or tk_version < 8.6:
            logger.warning(
                "The installed Tcl/Tk (%s/%s) version might not work with the splash screen feature of the bootloader. "
                "The bootloader is tested against Tcl/Tk 8.6" %
                (self._tkinter_module.TCL_VERSION, self._tkinter_module.TK_VERSION)
            )

        # This should be impossible, since tcl/tk is released together with the same version number, but just in case
        if tcl_version != tk_version:
            logger.warning(
                "The installed version of Tcl (%s) and Tk (%s) do not match. PyInstaller is tested against matching "
                "versions" % (self._tkinter_module.TCL_VERSION, self._tkinter_module.TK_VERSION)
            )

        # Test if tcl is threaded.
        # If the variable tcl_platform(threaded) exist, the tcl interpreter was compiled with thread support.
        threaded = bool(exec_statement(
            """
            from tkinter import Tcl, TclError
            try:
                print(Tcl().getvar('tcl_platform(threaded)'))
            except TclError:
                pass
            """
        ))  # yapf: disable

        if not threaded:
            # This is a feature breaking problem, so exit.
            raise SystemExit(
                "The installed tcl version is not threaded. PyInstaller only supports the splash screen "
                "using threaded tcl."
            )

    def generate_script(self):
        """
        Generate the script for the splash screen.

        If minify_script is True, all unnecessary parts will be removed.
        """
        d = {}
        if self.text_pos is not None:
            logger.debug("Add text support to splash screen")
            d.update({
                'pad_x': self.text_pos[0],
                'pad_y': self.text_pos[1],
                'color': self.text_color,
                'font': self.text_font,
                'font_size': self.text_size,
                'default_text': self.text_default,
            })
        script = splash_templates.build_script(text_options=d)

        if self.minify_script:
            # Remove any documentation, empty lines and unnecessary spaces
            script = '\n'.join(
                line for line in map(lambda l: l.strip(), script.splitlines())
                if not line.startswith('#')  # documentation
                and line  # empty lines
            )
            # Remove unnecessary spaces
            script = re.sub(' +', ' ', script)

        # Write script to disk, so that it is transparent to the user what script is executed.
        with open(self.script_name, "w") as script_file:
            script_file.write(script)
        return script

    @staticmethod
    def _uses_tkinter(binaries):
        # Test for _tkinter instead of tkinter, because a user might use a different wrapping library for tk.
        return '_tkinter' in binaries.filenames

    @staticmethod
    def _find_rundir(structure):
        # First try a name the user could understand, if one would find the directory.
        rundir = '__splash%s'
        candidate = rundir % ""
        counter = 0

        # Run this loop as long as a folder exist named like rundir. In most cases __splash will be sufficient and this
        # loop wont enter.
        while any(e[0].startswith(candidate + os.sep) for e in structure):
            # just append to rundir a counter
            candidate = rundir % str(counter)
            counter += 1

            # The SPLASH_DATA_HEADER structure limits the name to be 16 bytes at maximum. So if we exceed the limit
            # raise an error. This will never happen, since there are 10^8 different possibilities, but just in case.
            assert len(candidate) <= 16

        return candidate
Esempio n. 18
0
def test_append_keep_filename():
    # name in TOC should be the same as the one added
    toc = TOC()
    entry = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'BINARY')
    toc.append(entry)
    assert toc[0][0] == entry[0]
Esempio n. 19
0
def test_append_existing():
    toc = TOC(ELEMS1)
    toc.append(ELEMS1[-1])
    expected = list(ELEMS1)
    assert toc == expected
Esempio n. 20
0
def test_append():
    toc = TOC(ELEMS1)
    toc.append(('li-la-lu', '/home/myself/li-la-su', 'SOMETHING'))
    expected = list(ELEMS1)
    expected.append(('li-la-lu', '/home/myself/li-la-su', 'SOMETHING'))
    assert toc == expected
Esempio n. 21
0
    def assemble(self):
        """
        This method is the MAIN method for finding all necessary files to be bundled.
        """
        from PyInstaller.config import CONF

        for m in self.excludes:
            logger.debug("Excluding module '%s'" % m)
        self.graph = initialize_modgraph(excludes=self.excludes,
                                         user_hook_dirs=self.hookspath)

        # TODO Find a better place where to put 'base_library.zip' and when to created it.
        # For Python 3 it is necessary to create file 'base_library.zip'
        # containing core Python modules. In Python 3 some built-in modules
        # are written in pure Python. base_library.zip is a way how to have
        # those modules as "built-in".
        libzip_filename = os.path.join(CONF['workpath'], 'base_library.zip')
        create_py3_base_library(libzip_filename, graph=self.graph)
        # Bundle base_library.zip as data file.
        # Data format of TOC item:   ('relative_path_in_dist_dir', 'absolute_path_on_disk', 'DATA')
        self.datas.append(
            (os.path.basename(libzip_filename), libzip_filename, 'DATA'))

        # Expand sys.path of module graph.
        # The attribute is the set of paths to use for imports: sys.path,
        # plus our loader, plus other paths from e.g. --path option).
        self.graph.path = self.pathex + self.graph.path
        self.graph.set_setuptools_nspackages()

        logger.info("running Analysis %s", self.tocbasename)
        # Get paths to Python and, in Windows, the manifest.
        python = compat.python_executable
        if not is_win:
            # Linux/MacOS: get a real, non-link path to the running Python executable.
            while os.path.islink(python):
                python = os.path.join(os.path.dirname(python),
                                      os.readlink(python))
            depmanifest = None
        else:
            # Windows: Create a manifest to embed into built .exe, containing the same
            # dependencies as python.exe.
            depmanifest = winmanifest.Manifest(
                type_="win32",
                name=CONF['specnm'],
                processorArchitecture=winmanifest.processor_architecture(),
                version=(1, 0, 0, 0))
            depmanifest.filename = os.path.join(
                CONF['workpath'], CONF['specnm'] + ".exe.manifest")

        # We record "binaries" separately from the modulegraph, as there
        # is no way to record those dependencies in the graph. These include
        # the python executable and any binaries added by hooks later.
        # "binaries" are not the same as "extensions" which are .so or .dylib
        # that are found and recorded as extension nodes in the graph.
        # Reset seen variable before running bindepend. We use bindepend only for
        # the python executable.
        bindepend.seen.clear()

        # Add binary and assembly dependencies of Python.exe.
        # This also ensures that its assembly depencies under Windows get added to the
        # built .exe's manifest. Python 2.7 extension modules have no assembly
        # dependencies, and rely on the app-global dependencies set by the .exe.
        self.binaries.extend(
            bindepend.Dependencies([('', python, '')],
                                   manifest=depmanifest,
                                   redirects=self.binding_redirects)[1:])
        if is_win:
            depmanifest.writeprettyxml()

        ### Module graph.
        #
        # Construct the module graph of import relationships between modules
        # required by this user's application. For each entry point (top-level
        # user-defined Python script), all imports originating from this entry
        # point are recursively parsed into a subgraph of the module graph. This
        # subgraph is then connected to this graph's root node, ensuring
        # imported module nodes will be reachable from the root node -- which is
        # is (arbitrarily) chosen to be the first entry point's node.

        # List to hold graph nodes of scripts and runtime hooks in use order.
        priority_scripts = []

        # Assume that if the script does not exist, Modulegraph will raise error.
        # Save the graph nodes of each in sequence.
        for script in self.inputs:
            logger.info("Analyzing %s", script)
            priority_scripts.append(self.graph.add_script(script))

        # Analyze the script's hidden imports (named on the command line)
        self.graph.add_hiddenimports(self.hiddenimports)

        ### Post-graph hooks.
        self.graph.process_post_graph_hooks(self)

        # Update 'binaries' TOC and 'datas' TOC.
        deps_proc = DependencyProcessor(self.graph,
                                        self.graph._additional_files_cache)
        self.binaries.extend(deps_proc.make_binaries_toc())
        self.datas.extend(deps_proc.make_datas_toc())
        self.zipped_data.extend(deps_proc.make_zipped_data_toc())
        # Note: zipped eggs are collected below

        ### Look for dlls that are imported by Python 'ctypes' module.
        # First get code objects of all modules that import 'ctypes'.
        logger.info('Looking for ctypes DLLs')
        # dict like:  {'module1': code_obj, 'module2': code_obj}
        ctypes_code_objs = self.graph.get_code_using("ctypes")

        for name, co in ctypes_code_objs.items():
            # Get dlls that might be needed by ctypes.
            logger.debug('Scanning %s for shared libraries or dlls', name)
            try:
                ctypes_binaries = scan_code_for_ctypes(co)
                self.binaries.extend(set(ctypes_binaries))
            except Exception as ex:
                raise RuntimeError(f"Failed to scan the module '{name}'. "
                                   f"This is a bug. Please report it.") from ex

        self.datas.extend((dest, source, "DATA")
                          for (dest, source) in format_binaries_and_datas(
                              self.graph.metadata_required()))

        # Analyze run-time hooks.
        # Run-time hooks has to be executed before user scripts. Add them
        # to the beginning of 'priority_scripts'.
        priority_scripts = self.graph.analyze_runtime_hooks(
            self.custom_runtime_hooks) + priority_scripts

        # 'priority_scripts' is now a list of the graph nodes of custom runtime
        # hooks, then regular runtime hooks, then the PyI loader scripts.
        # Further on, we will make sure they end up at the front of self.scripts

        ### Extract the nodes of the graph as TOCs for further processing.

        # Initialize the scripts list with priority scripts in the proper order.
        self.scripts = self.graph.nodes_to_toc(priority_scripts)

        # Extend the binaries list with all the Extensions modulegraph has found.
        self.binaries = self.graph.make_binaries_toc(self.binaries)
        # Fill the "pure" list with pure Python modules.
        assert len(self.pure) == 0
        self.pure = self.graph.make_pure_toc()
        # And get references to module code objects constructed by ModuleGraph
        # to avoid writing .pyc/pyo files to hdd.
        self.pure._code_cache = self.graph.get_code_objects()

        # Add remaining binary dependencies - analyze Python C-extensions and what
        # DLLs they depend on.
        logger.info('Looking for dynamic libraries')
        self.binaries.extend(
            bindepend.Dependencies(self.binaries,
                                   redirects=self.binding_redirects))

        ### Include zipped Python eggs.
        logger.info('Looking for eggs')
        self.zipfiles.extend(deps_proc.make_zipfiles_toc())

        # Verify that Python dynamic library can be found.
        # Without dynamic Python library PyInstaller cannot continue.
        self._check_python_library(self.binaries)

        if is_win:
            # Remove duplicate redirects
            self.binding_redirects[:] = list(set(self.binding_redirects))
            logger.info("Found binding redirects: \n%s",
                        self.binding_redirects)

        # Filter binaries to adjust path of extensions that come from
        # python's lib-dynload directory. Prefix them with lib-dynload
        # so that we'll collect them into subdirectory instead of
        # directly into _MEIPASS
        for idx, tpl in enumerate(self.binaries):
            name, path, typecode = tpl
            if typecode == 'EXTENSION' \
               and not os.path.dirname(os.path.normpath(name)) \
               and os.path.basename(os.path.dirname(path)) == 'lib-dynload':
                name = os.path.join('lib-dynload', name)
                self.binaries[idx] = (name, path, typecode)

        # Place Python source in data files for the noarchive case.
        if self.noarchive:
            # Create a new TOC of ``(dest path for .pyc, source for .py, type)``.
            new_toc = TOC()
            for name, path, typecode in self.pure:
                assert typecode == 'PYMODULE'
                # Transform a python module name into a file name.
                name = name.replace('.', os.sep)
                # Special case: modules have an implied filename to add.
                if os.path.splitext(os.path.basename(path))[0] == '__init__':
                    name += os.sep + '__init__'
                # Append the extension for the compiled result.
                # In python 3.5 (PEP-488) .pyo files were replaced by
                # .opt-1.pyc and .opt-2.pyc. However, it seems that for
                # bytecode-only module distribution, we always need to
                # use the .pyc extension.
                name += '.pyc'
                new_toc.append((name, path, typecode))
            # Put the result of byte-compiling this TOC in datas. Mark all entries as data.
            for name, path, typecode in compile_py_files(
                    new_toc, CONF['workpath']):
                self.datas.append((name, path, 'DATA'))
            # Store no source in the archive.
            self.pure = TOC()

        # Write warnings about missing modules.
        self._write_warnings()
        # Write debug information about hte graph
        self._write_graph_debug()
Esempio n. 22
0
class BUNDLE(Target):
    def __init__(self, *args, **kws):
        from PyInstaller.config import CONF

        # BUNDLE only has a sense under Mac OS X, it's a noop on other platforms
        if not is_darwin:
            return

        # get a path to a .icns icon for the app bundle.
        self.icon = kws.get('icon')
        if not self.icon:
            # --icon not specified; use the default in the pyinstaller folder
            self.icon = os.path.join(
                os.path.dirname(os.path.dirname(__file__)), 'bootloader',
                'images', 'icon-windowed.icns')
        else:
            # user gave an --icon=path. If it is relative, make it
            # relative to the spec file location.
            if not os.path.isabs(self.icon):
                self.icon = os.path.join(CONF['specpath'], self.icon)
        # ensure icon path is absolute
        self.icon = os.path.abspath(self.icon)

        Target.__init__(self)

        # .app bundle is created in DISTPATH.
        self.name = kws.get('name', None)
        base_name = os.path.basename(self.name)
        self.name = os.path.join(CONF['distpath'], base_name)

        self.appname = os.path.splitext(base_name)[0]
        self.version = kws.get("version", "0.0.0")
        self.toc = TOC()
        self.strip = False
        self.upx = False
        self.console = True
        self.target_arch = None
        self.codesign_identity = None
        self.entitlements_file = None

        # .app bundle identifier for Code Signing
        self.bundle_identifier = kws.get('bundle_identifier')
        if not self.bundle_identifier:
            # Fallback to appname.
            self.bundle_identifier = self.appname

        self.info_plist = kws.get('info_plist', None)

        for arg in args:
            if isinstance(arg, EXE):
                self.toc.append(
                    (os.path.basename(arg.name), arg.name, arg.typ))
                self.toc.extend(arg.dependencies)
                self.strip = arg.strip
                self.upx = arg.upx
                self.upx_exclude = arg.upx_exclude
                self.console = arg.console
                self.target_arch = arg.target_arch
                self.codesign_identity = arg.codesign_identity
                self.entitlements_file = arg.entitlements_file
            elif isinstance(arg, TOC):
                self.toc.extend(arg)
                # TOC doesn't have a strip or upx attribute, so there is no way for us to
                # tell which cache we should draw from.
            elif isinstance(arg, COLLECT):
                self.toc.extend(arg.toc)
                self.strip = arg.strip_binaries
                self.upx = arg.upx_binaries
                self.upx_exclude = arg.upx_exclude
                self.console = arg.console
                self.target_arch = arg.target_arch
                self.codesign_identity = arg.codesign_identity
                self.entitlements_file = arg.entitlements_file
            else:
                logger.info("unsupported entry %s", arg.__class__.__name__)
        # Now, find values for app filepath (name), app name (appname), and name
        # of the actual executable (exename) from the first EXECUTABLE item in
        # toc, which might have come from a COLLECT too (not from an EXE).
        for inm, name, typ in self.toc:
            if typ == "EXECUTABLE":
                self.exename = name
                break
        self.__postinit__()

    _GUTS = (
        # BUNDLE always builds, just want the toc to be written out
        ('toc', None), )

    def _check_guts(self, data, last_build):
        # BUNDLE always needs to be executed, since it will clean the output
        # directory anyway to make sure there is no existing cruft accumulating
        return 1

    def assemble(self):
        if _check_path_overlap(self.name) and os.path.isdir(self.name):
            _rmtree(self.name)
        logger.info("Building BUNDLE %s", self.tocbasename)

        # Create a minimal Mac bundle structure
        os.makedirs(os.path.join(self.name, "Contents", "MacOS"))
        os.makedirs(os.path.join(self.name, "Contents", "Resources"))
        os.makedirs(os.path.join(self.name, "Contents", "Frameworks"))

        # Copy icns icon to Resources directory.
        if os.path.exists(self.icon):
            shutil.copy(self.icon,
                        os.path.join(self.name, 'Contents', 'Resources'))
        else:
            logger.warning("icon not found %s", self.icon)

        # Key/values for a minimal Info.plist file
        info_plist_dict = {
            "CFBundleDisplayName": self.appname,
            "CFBundleName": self.appname,

            # Required by 'codesign' utility.
            # The value for CFBundleIdentifier is used as the default unique
            # name of your program for Code Signing purposes.
            # It even identifies the APP for access to restricted OS X areas
            # like Keychain.
            #
            # The identifier used for signing must be globally unique. The usal
            # form for this identifier is a hierarchical name in reverse DNS
            # notation, starting with the toplevel domain, followed by the
            # company name, followed by the department within the company, and
            # ending with the product name. Usually in the form:
            #   com.mycompany.department.appname
            # Cli option --osx-bundle-identifier sets this value.
            "CFBundleIdentifier": self.bundle_identifier,
            "CFBundleExecutable": os.path.basename(self.exename),
            "CFBundleIconFile": os.path.basename(self.icon),
            "CFBundleInfoDictionaryVersion": "6.0",
            "CFBundlePackageType": "APPL",
            "CFBundleShortVersionString": self.version,
        }

        # Set some default values.
        # But they still can be overwritten by the user.
        if self.console:
            # Setting EXE console=True implies LSBackgroundOnly=True.
            info_plist_dict['LSBackgroundOnly'] = True
        else:
            # Let's use high resolution by default.
            info_plist_dict['NSHighResolutionCapable'] = True

        # Merge info_plist settings from spec file
        if isinstance(self.info_plist, dict) and self.info_plist:
            info_plist_dict.update(self.info_plist)

        plist_filename = os.path.join(self.name, "Contents", "Info.plist")
        with open(plist_filename, "wb") as plist_fh:
            plistlib.dump(info_plist_dict, plist_fh)

        links = []
        _QT_BASE_PATH = {'PySide2', 'PySide6', 'PyQt5', 'PySide6'}
        for inm, fnm, typ in self.toc:
            # Adjust name for extensions, if applicable
            inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
            # Copy files from cache. This ensures that are used files with relative
            # paths to dynamic library dependencies (@executable_path)
            base_path = inm.split('/', 1)[0]
            if typ in ('EXTENSION', 'BINARY'):
                fnm = checkCache(fnm,
                                 strip=self.strip,
                                 upx=self.upx,
                                 upx_exclude=self.upx_exclude,
                                 dist_nm=inm,
                                 target_arch=self.target_arch,
                                 codesign_identity=self.codesign_identity,
                                 entitlements_file=self.entitlements_file)
            # Add most data files to a list for symlinking later.
            if typ == 'DATA' and base_path not in _QT_BASE_PATH:
                links.append((inm, fnm))
            else:
                tofnm = os.path.join(self.name, "Contents", "MacOS", inm)
                todir = os.path.dirname(tofnm)
                if not os.path.exists(todir):
                    os.makedirs(todir)
                if os.path.isdir(fnm):
                    # beacuse shutil.copy2() is the default copy function
                    # for shutil.copytree, this will also copy file metadata
                    shutil.copytree(fnm, tofnm)
                else:
                    shutil.copy(fnm, tofnm)

        logger.info('Moving BUNDLE data files to Resource directory')

        # Mac OS X Code Signing does not work when .app bundle contains
        # data files in dir ./Contents/MacOS.
        #
        # Put all data files in ./Resources and create symlinks in ./MacOS.
        bin_dir = os.path.join(self.name, 'Contents', 'MacOS')
        res_dir = os.path.join(self.name, 'Contents', 'Resources')
        for inm, fnm in links:
            tofnm = os.path.join(res_dir, inm)
            todir = os.path.dirname(tofnm)
            if not os.path.exists(todir):
                os.makedirs(todir)
            if os.path.isdir(fnm):
                # beacuse shutil.copy2() is the default copy function
                # for shutil.copytree, this will also copy file metadata
                shutil.copytree(fnm, tofnm)
            else:
                shutil.copy(fnm, tofnm)
            base_path = os.path.split(inm)[0]
            if base_path:
                if not os.path.exists(os.path.join(bin_dir, inm)):
                    path = ''
                    for part in iter(base_path.split(os.path.sep)):
                        # Build path from previous path and the next part of the base path
                        path = os.path.join(path, part)
                        try:
                            relative_source_path = os.path.relpath(
                                os.path.join(res_dir, path),
                                os.path.split(os.path.join(bin_dir, path))[0])
                            dest_path = os.path.join(bin_dir, path)
                            os.symlink(relative_source_path, dest_path)
                            break
                        except FileExistsError:
                            pass
                    if not os.path.exists(os.path.join(bin_dir, inm)):
                        relative_source_path = os.path.relpath(
                            os.path.join(res_dir, inm),
                            os.path.split(os.path.join(bin_dir, inm))[0])
                        dest_path = os.path.join(bin_dir, inm)
                        os.symlink(relative_source_path, dest_path)
            else:  # If path is empty, e.g., a top level file, try to just symlink the file
                os.symlink(
                    os.path.relpath(
                        os.path.join(res_dir, inm),
                        os.path.split(os.path.join(bin_dir, inm))[0]),
                    os.path.join(bin_dir, inm))

        # Sign the bundle
        logger.info('Signing the BUNDLE...')
        try:
            osxutils.sign_binary(self.name,
                                 self.codesign_identity,
                                 self.entitlements_file,
                                 deep=True)
        except Exception as e:
            logger.warning("Error while signing the bundle: %s", e)
            logger.warning("You will need to sign the bundle manually!")

        logger.info("Building BUNDLE %s completed successfully.",
                    self.tocbasename)
Esempio n. 23
0
class COLLECT(Target):
    """
    In one-dir mode creates the output folder with all necessary files.
    """
    def __init__(self, *args, **kws):
        """
        args
                One or more arguments that are either TOCs Targets.
        kws
            Possible keywork arguments:

                name
                    The name of the directory to be built.
        """
        from ..config import CONF
        Target.__init__(self)
        self.strip_binaries = kws.get('strip', False)
        self.console = True

        if CONF['hasUPX']:
            self.upx_binaries = kws.get('upx', False)
        else:
            self.upx_binaries = False

        self.name = kws.get('name')
        # Old .spec format included in 'name' the path where to collect files
        # for the created app.
        # app. New format includes only directory name.
        #
        # The 'name' directory is created in DISTPATH and necessary files are
        # then collected to this directory.
        self.name = os.path.join(CONF['distpath'], os.path.basename(self.name))

        self.toc = TOC()
        for arg in args:
            if isinstance(arg, TOC):
                self.toc.extend(arg)
            elif isinstance(arg, Target):
                self.toc.append(
                    (os.path.basename(arg.name), arg.name, arg.typ))
                if isinstance(arg, EXE):
                    self.console = arg.console
                    for tocnm, fnm, typ in arg.toc:
                        if tocnm == os.path.basename(arg.name) + ".manifest":
                            self.toc.append((tocnm, fnm, typ))
                    if not arg.append_pkg:
                        self.toc.append((os.path.basename(arg.pkgname),
                                         arg.pkgname, 'PKG'))
                self.toc.extend(arg.dependencies)
            else:
                self.toc.extend(arg)
        self.__postinit__()

    _GUTS = (
        # COLLECT always builds, just want the toc to be written out
        ('toc', None), )

    def _check_guts(self, data, last_build):
        # COLLECT always needs to be executed, since it will clean the output
        # directory anyway to make sure there is no existing cruft accumulating
        return 1

    def assemble(self):
        _make_clean_directory(self.name)
        logger.info("Building COLLECT %s", self.tocbasename)
        toc = add_suffix_to_extensions(self.toc)
        for inm, fnm, typ in toc:
            if not os.path.exists(
                    fnm) or not os.path.isfile(fnm) and is_path_to_egg(fnm):
                # file is contained within python egg, it is added with the egg
                continue
            if os.pardir in os.path.normpath(inm).split(os.sep) \
               or os.path.isabs(inm):
                raise SystemExit('Security-Alert: try to store file outside '
                                 'of dist-directory. Aborting. %r' % inm)
            tofnm = os.path.join(self.name, inm)
            todir = os.path.dirname(tofnm)
            if not os.path.exists(todir):
                os.makedirs(todir)
            if typ in ('EXTENSION', 'BINARY'):
                fnm = checkCache(fnm,
                                 strip=self.strip_binaries,
                                 upx=(self.upx_binaries
                                      and (is_win or is_cygwin)),
                                 dist_nm=inm)
            if typ != 'DEPENDENCY':
                if os.path.isdir(fnm):
                    # beacuse shutil.copy2() is the default copy function
                    # for shutil.copytree, this will also copy file metadata
                    shutil.copytree(fnm, tofnm)
                else:
                    shutil.copy(fnm, tofnm)
                try:
                    shutil.copystat(fnm, tofnm)
                except OSError:
                    logger.warning("failed to copy flags of %s", fnm)
            if typ in ('EXTENSION', 'BINARY'):
                os.chmod(tofnm, 0o755)
        logger.info("Building COLLECT %s completed successfully.",
                    self.tocbasename)
Esempio n. 24
0
def test_append_other_case():
    # should not be added if the filenames are the same on a case-insensitive system.
    toc = TOC(ELEMS1)
    toc.append(('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'BINARY'))
    expected = list(ELEMS1)
    assert toc == expected