Beispiel #1
0
    def _check_entry_point(entry_point):
        '''
        Validate the passed entry point, describing the current script wrapper
        to be frozen.
        '''

        # Defer heavyweight imports.
        from betse.util.io import stderrs
        from betse.util.py.module import pymodname

        # If this entry module is unimportable, raise an exception.
        if not pymodname.is_module(entry_point.module_name):
            raise ImportError('Entry module "{}" unimportable. {}'.format(
                entry_point.module_name, freeze.EXCEPTION_ADVICE))

        # If this entry module's basename is *NOT* "__main__", print a
        # non-fatal warning. For unknown reasons, attempting to freeze
        # customary modules usually causes the frozen executable to reduce
        # to a noop (i.e., silently do nothing).
        if entry_point.module_name.split('.') != '__main__':
            stderrs.output_warning(
                'Entry module "{}" basename not "__main__".'.format(
                    entry_point.module_name))

        # If this entry module has no entry function, print a non-fatal
        # warning. For unknown reasons, attempting to freeze without an
        # entry function usually causes the frozen executable to reduce to a
        # noop (i.e., silently do nothing).
        if not len(entry_point.attrs):
            stderrs.output_warning(
                'Entry module "{}" entry function undefined.'.format(
                    entry_point.module_name))
Beispiel #2
0
    def run(self):
        '''
        Run the current command and all subcommands thereof.
        '''

        # Defer heavyweight imports.
        from betse.util.app.meta import appmetaone
        from betse.util.io import stderrs
        from betse.util.os.brand import posix

        # If the current operating system is POSIX-incompatible, this system
        # does *NOT* support conventional symbolic links. See details above.
        if not posix.is_posix():
            # Avoid circular import dependencies.
            from betse_setup import build

            # Print a non-fatal warning.
            stderrs.output_warning(
                'Symbolic links require POSIX compatibility. '
                'Since the current platform is\n'
                'POSIX-incompatible (e.g., Windows), '
                'symbolic links will be faked with black magic.')

            # Absolute dirname of this application's project directory.
            project_dirname = appmetaone.get_app_meta().project_dirname
            # print('parent: ' + parent_dirname)

            # Prepend the template for subsequently installed entry points by a
            # Python statement "faking" symlink-based installation.
            build.SCRIPT_TEMPLATE = """
# The current operating system is POSIX-incompatible and hence does *NOT*
# support symlinks. To "fake" symlink-based installation, the standard list of
# search dirnames is prepended by the absolute path of the parent directory of
# the top-level "betse" package. For compatibility with third-party modules,
# the first entry of such list (i.e., the parent directory of this script) is
# preserved by inserting at index 1 rather than 0.
import sys
sys.path.insert(1, {})
            """.format(repr(project_dirname)) + build.SCRIPT_TEMPLATE

        # Run all subcommands.
        for subcommand_name in self.get_sub_commands():
            self.run_command(subcommand_name)
Beispiel #3
0
    def _run_pyinstaller_command(
        self,
        script_basename: str,
        script_type: str,
        entry_point,
    ) -> None:
        '''
        Run the currently configured PyInstaller command for the passed entry
        point's script wrapper.

        Attributes
        ----------
        script_basename : str
            Basename of the executable wrapper script running this entry point.
        script_type : str
            Type of the executable wrapper script running this entry point,
            guaranteed to be either:

            * If this script is console-specific, ``console`` .
            * Else, ``gui``.
        entry_point : EntryPoint
            Entry point, whose attributes specify the module to be imported and
            function to be run by this script.
        '''

        # Defer heavyweight imports.
        from betse.util.io import stderrs
        from betse.util.os.shell import shellstr
        from betse.util.path import files, pathnames

        # If this spec exists, instruct PyInstaller to reuse rather than
        # recreate this file, thus preserving edits to this file.
        if files.is_file(self._pyinstaller_spec_filename):
            print('Reusing spec file "{}".'.format(
                self._pyinstaller_spec_filename))

            # Append the relative path of this spec file.
            self._pyinstaller_args.append(
                shellstr.shell_quote(self._pyinstaller_spec_filename))

            # Freeze this script with this spec file.
            self._run_pyinstaller_imported()
        # Else, instruct PyInstaller to (re)create this spec file.
        else:
            # Absolute path of the directory containing this files.
            pyinstaller_spec_dirname = pathnames.get_dirname(
                self._pyinstaller_spec_filename)

            # Absolute path of the current script wrapper.
            script_filename = pathnames.join(self.install_dir, script_basename)
            files.die_unless_file(
                script_filename,
                'File "{}" not found. {}'.format(script_filename,
                                                 freeze.EXCEPTION_ADVICE))

            # Inform the user of this action *AFTER* the above validation.
            # Since specification files should typically be reused rather
            # than regenerated, do so as a non-fatal warning.
            stderrs.output_warning('Generating spec file "{}".'.format(
                self._pyinstaller_spec_filename))

            # Append all options specific to spec file generation.
            self._pyinstaller_args.extend([
                # If this is a console script, configure standard input and
                # output for console handling; else, do *NOT* and, if the
                # current operating system is OS X, generate an ".app"-suffixed
                # application bundle rather than a customary executable.
                '--console' if script_type == 'console' else '--windowed',

                # Non-default PyInstaller directories.
                '--additional-hooks-dir=' +
                shellstr.shell_quote(self._pyinstaller_hooks_dirname),
                '--specpath=' + shellstr.shell_quote(pyinstaller_spec_dirname),
            ])

            # Append all subclass-specific options.
            self._pyinstaller_args.extend(self._get_pyinstaller_options())

            # Append the absolute path of this script.
            self._pyinstaller_args.append(
                shellstr.shell_quote(script_filename))

            # Freeze this script and generate a spec file.
            self._run_pyinstaller_imported()

            # Absolute path of this file.
            script_spec_filename = pathnames.join(pyinstaller_spec_dirname,
                                                  script_basename + '.spec')

            # Rename this file to have the basename expected by the prior
            # conditional on the next invocation of this setuptools command.
            #
            # Note that "pyinstaller" accepts an option "--name" permitting
            # the basename of this file to be specified prior to generating
            # this file. Unfortunately, this option *ALSO* specifies the
            # basename of the generated executable. While the former is
            # reliably renamable, the former is *NOT* (e.g., due to code
            # signing). Hence, this file is manually renamed without passing
            # this option.
            files.move_file(script_spec_filename,
                            self._pyinstaller_spec_filename)
Beispiel #4
0
    def _init_pyinstaller_command(self) -> None:
        '''
        Initialize the list of all shell words of the PyInstaller command to be
        run.
        '''

        # Defer heavyweight imports.
        from betse.util.io import stderrs
        from betse.util.path import dirs, pathnames
        from betse.util.os.command import cmds
        from betse.util.os.shell import shellstr

        # Relative path of the top-level PyInstaller directory.
        pyinstaller_dirname = 'freeze'

        # Relative path of the PyInstaller spec file converting such
        # platform-independent script into a platform-specific executable.
        self._pyinstaller_spec_filename = pathnames.join(
            pyinstaller_dirname, '.spec')

        # If the frozen executable directory was *NOT* explicitly passed on the
        # command-line, default to a subdirectory of this top-level directory.
        if self.dist_dir is None:
            self.dist_dir = pathnames.join(pyinstaller_dirname, 'dist')
        # Else, canonicalize the passed directory.
        else:
            self.dist_dir = pathnames.canonicalize(self.dist_dir)
        assert isinstance(self.dist_dir,
                          str), ('"{}" not a string.'.format(self.dist_dir))

        # Relative path of the input hooks subdirectory.
        self._pyinstaller_hooks_dirname = pathnames.join(
            pyinstaller_dirname, 'hooks')

        # Relative path of the intermediate build subdirectory.
        pyinstaller_work_dirname = pathnames.join(pyinstaller_dirname, 'build')

        # Create such hooks subdirectory if not found, as failing to do so
        # will induce fatal PyInstaller errors.
        dirs.make_unless_dir(self._pyinstaller_hooks_dirname)

        # List of all shell words of the PyInstaller command to be run,
        # starting with the basename of this command.
        self._pyinstaller_args = []

        # Append all PyInstaller command options common to running such command
        # for both reuse and regeneration of spec files. (Most such options are
        # specific to the latter only and hence omitted.)
        self._pyinstaller_args = [
            # Overwrite existing output paths under the "dist/" subdirectory
            # without confirmation, the default behaviour.
            '--noconfirm',

            # Non-default PyInstaller directories.
            '--workpath=' + shellstr.shell_quote(pyinstaller_work_dirname),
            '--distpath=' + shellstr.shell_quote(self.dist_dir),

            # Non-default log level.
            # '--log-level=DEBUG',
            '--log-level=INFO',
        ]

        # Forward all custom boolean options passed by the user to the current
        # setuptools command (e.g., "--clean") to the "pyinstaller" command.
        if self.clean:
            self._pyinstaller_args.append('--clean')
        if self.debug:
            self._pyinstaller_args.extend((
                '--debug',

                # UPX-based compression uselessly consumes non-trivial time
                # (especially under Windows, where process creation is fairly
                # heavyweight) when freezing debug binaries. To optimize and
                # simplify debugging, such compression is disabled.
                '--noupx',
            ))
            stderrs.output_warning('Enabling bootloader debug messages.')
            stderrs.output_warning('Disabling UPX-based compression.')
        # If *NOT* debugging and UPX is *NOT* found, print a non-fatal warning.
        # While optional, freezing in the absence of UPX produces uncompressed
        # and hence considerably larger executables.
        elif not cmds.is_cmd('upx'):
            stderrs.output_warning(
                'UPX not installed or "upx" not in the current ${PATH}.')
            stderrs.output_warning('Frozen binaries will *NOT* be compressed.')
Beispiel #5
0
    def _run_pytest(self) -> None:
        '''
        Call the :func:`pytest.main` function in the active Python interpreter,
        passed CLI options corresponding to the CLI options passed to this
        setuptools command.
        '''

        # Defer heavyweight imports.
        from betse.util.io import stderrs
        from betse.util.py.module import pymodname

        # List of all shell words to be passed as arguments to "py.test".
        #
        # Whereas the top-level "pytest.ini" configuration file lists options
        # unconditionally passed to *ALL* "py.test" invocations, this list only
        # lists options conditionally applicable to invocations run within the
        # "python3 setup.py test" subcommand.
        #
        # For example, the "--capture=no" option should be conditionally
        # enabled *ONLY* when applying the monkey-patch applied by this
        # setuptools subcommand (namely, here). This option is intentionally
        # omitted from the top-level "pytest.ini" configuration file.
        pytest_args = [
            # When testing interactively, prevent py.test from capturing stdout
            # but *NOT* stderr. By default, py.test captures and delays
            # printing stdout until after test completion. While a possibly
            # suitable default for short-lived unit tests, such capturing is
            # unsuitable for long-lived functional tests.
            #
            # Note that this option is monkey-patched by the _patch_pytest()
            # method to capture only stdout. By default, this option captures
            # neither stdout (which is good) nor stderr (which is bad).
            '--capture=no',

            # When testing interactively, halt testing on the first failure.
            # Permitting multiple failures complicates failure output,
            # especially when every failure after the first is a result of the
            # same underlying issue.
            #
            # When testing non-interactively, testing is typically *NOT* halted
            # on the first failure. Hence, this option is confined to this
            # subcommand rather than added to the general-purpose "pytest.ini"
            # configuration.
            '--maxfail=1',
        ]

        #FIXME: Disable "xdist" if at least one serial test exists. Currently,
        #none do. Theoretically, they could. They once did and certaily could
        #again. Serial functional tests assume that all tests to be serialized
        #are run in the same Python process. "pytest-xdist" currently provides
        #no means of doing so; instead, "pytest-xdist" assigns all tests to
        #arbitrary test slaves and hence Python processes. Until "pytest-xdist"
        #permits tests to be isolated to the same test slave, "pytest-xdist"
        #must *NOT* be enabled. Non-fatal warning to output might resemble:
        #    buputil.output_warning(
        #        'py.test plugin "pytest-xdist" fails to support serial tests.')
        #    buputil.output_warning(
        #        'Tests will *NOT* be parallelized across multiple processors.')

        # True only if the optional third-party "pytest-xdist" plugin is both
        # importable and *NOT* explicitly disabled below (e.g., due to the end
        # user having passed CLI options incompatible with this plugin).
        is_xdist = pymodname.is_module('xdist')

        #FIXME: Disabled for the moment. "xdist" appears to be inexplicably
        #failing with non-human-readable exceptions. The lack of official
        #support for test parallelization in py.test is becoming clear.
        #
        #When "xdist" is (eventually) stabilized, excise this reassignment here
        #*AND* reenable the conditional below.
        is_xdist = False

        #FIXME: Reenable this conditional.
        # If this plugin is unimportable, print a non-fatal warning. Due to the
        # cost of running tests, parallelization is highly recommended.
        # if not is_xdist:
        #     stderrs.output_warning(
        #         'Optional py.test plugin "pytest-xdist" not found.')
        #     stderrs.output_warning(
        #         'Tests will *NOT* be parallelized across multiple processors.')

        # Pass options passed to this subcommand to this py.test command,
        # converting long option names specific to this subcommand (e.g.,
        # "--no-capture") to short option names recognized by py.test (e.g.,
        # "-s"). Sadly, py.test typically recognizes only the latter.
        #
        # If the "-s" option is passed...
        if self.no_capture is not None:
            # If "pytest-xdist" is importable, print a non-fatal warning.
            # This plugin silently ignores all capture redirection CLI options,
            # including "-s", "--no-capture", and "--capture" options. It
            # appears unlikely that this will ever be solved. For details, see:
            # https://github.com/pytest-dev/pytest/issues/680
            if is_xdist:
                # Print a non-fatal warning.
                stderrs.output_warning(
                    'Option "-s" unsupported by py.test plugin "pytest-xdist".'
                )
                stderrs.output_warning(
                    'Tests will *NOT* be parallelized across '
                    'multiple processors.')

                # Disable this plugin. While parallelization is important,
                # respecting end user wishes with respect to capture
                # redirection is paramount and hence takes precedence.
                is_xdist = False

            # Pass the "-s" option to py.test.
            pytest_args.append('-s')
            # pytest_args.append('--capture=no')

        # If any remaining option is passed, forward these option unmodified
        # onto "py.test". For safety, these option's arguments are
        # intentionally *NOT* shell-quoted. Doing so unnecessarily adds an
        # additional level of quoting... which is bad.
        if self.match_name is not None:
            pytest_args.extend((
                '-k',
                self.match_name,
            ))

        # If "pytest-xdist" is both importable and *NOT* explicitly disabled...
        if is_xdist:
            # Notify the user of test parallelization.
            print('Optional py.test plugin "pytest-xdist" found.')
            print(
                'Tests will be parallelized across all available processors.')

            # Instruct "pytest-xdist" to autodetect and parallelize tests to
            # all available processors.
            pytest_args.extend(['-n', 'auto'])

        # Instruct "py.test" of the relative dirname of the top-level directory
        # for this project. On startup, "py.test" internally:
        #
        # * Sets its "rootdir" property to this dirname in absolute form.
        # * Sets its "inifile" property to the concatenation of this dirname
        #   with the basename "pytest.ini" if that top-level configuration file
        #   exists.
        # * Prints the initial values of these properties to stdout.
        #
        # *THIS IS ESSENTIAL.* If "py.test" is *NOT* explicitly passed this
        # relative dirname as an argument, "py.test" typically fails to set
        # these properties to the expected pathnames. For unknown reasons
        # (presumably unresolved "py.test" issues), "py.test" instead sets
        # "rootdir" to the absolute dirname of the current user's home
        # directory and "inifile" to "None". Why? Since no user's home
        # directory contains a "pytest.ini" file, error output resembles:
        #
        #    $ ./test -k test_sim_export --export-sim-conf-dir ~/tmp/yolo
        #    running test
        #    Running py.test with arguments: ['--capture=no', '--maxfail=1', '-k', 'test_sim_export', '--export-sim-conf-dir', '/home/leycec/tmp/yolo']
        #    usage: setup.py [options] [file_or_dir] [file_or_dir] [...]
        #    setup.py: error: unrecognized arguments: --export-sim-conf-dir
        #      inifile: None
        #      rootdir: /home/leycec
        #
        # See the following official documentation for further details,
        # entitled "Initialization: determining rootdir and inifile":
        #     https://docs.pytest.org/en/latest/customize.html
        pytest_args.append('.')

        # import os
        # print('Environment variables:\n{!r}'.format(os.environ))

        # Fork the "py.test" command, propagating its exit status as our own.
        print('Running py.test with arguments: {}'.format(pytest_args))
        sys.exit(self._pytest_public.main(pytest_args))