Example #1
0
    def test_install_requires_version_munging(self):
        """
        Convert a package with a requirement whose version is "munged" by pip.

        Refer to :func:`py2deb.converter.PackageConverter.transform_version()`
        for details about the purpose of this test.
        """
        with TemporaryDirectory() as repository_directory:
            with TemporaryDirectory() as distribution_directory:
                # Create a temporary (and rather trivial :-) Python package.
                with open(os.path.join(distribution_directory, 'setup.py'), 'w') as handle:
                    handle.write(dedent('''
                        from setuptools import setup
                        setup(
                            name='install-requires-munging-test',
                            version='1.0',
                            install_requires=['humanfriendly==1.30.0'],
                        )
                    '''))
                # Run the conversion command.
                converter = self.create_isolated_converter()
                converter.set_repository(repository_directory)
                archives, relationships = converter.convert([distribution_directory])
                # Find the generated *.deb archive.
                pathname = find_package_archive(archives, 'python-install-requires-munging-test')
                # Use deb-pkg-tools to inspect the package metadata.
                metadata, contents = inspect_package(pathname)
                logger.debug("Metadata of generated package: %s", dict(metadata))
                logger.debug("Contents of generated package: %s", dict(contents))
                # Inspect the converted package's dependency.
                assert metadata['Depends'].matches('python-humanfriendly', '1.30'), \
                    "py2deb failed to rewrite version of dependency!"
                assert not metadata['Depends'].matches('python-humanfriendly', '1.30.0'), \
                    "py2deb failed to rewrite version of dependency!"
Example #2
0
 def test_conversion_with_system_package(self):
     """Convert a package and map one of its requirements to a system package."""
     with TemporaryDirectory() as repository_directory:
         with TemporaryDirectory() as distribution_directory:
             # Create a temporary (and rather trivial :-) Python package.
             with open(os.path.join(distribution_directory, 'setup.py'),
                       'w') as handle:
                 handle.write(
                     dedent('''
                     from setuptools import setup
                     setup(
                         name='system-package-conversion-test',
                         version='1.0',
                         install_requires=['dbus-python'],
                     )
                 '''))
             # Run the conversion command.
             converter = self.create_isolated_converter()
             converter.set_repository(repository_directory)
             converter.use_system_package('dbus-python',
                                          fix_name_prefix('python-dbus'))
             archives, relationships = converter.convert(
                 [distribution_directory])
             # Make sure only one archive was generated.
             assert len(archives) == 1
             # Use deb-pkg-tools to inspect the package metadata.
             metadata, contents = inspect_package(archives[0])
             logger.debug("Metadata of generated package: %s",
                          dict(metadata))
             logger.debug("Contents of generated package: %s",
                          dict(contents))
             # Inspect the converted package's dependency.
             assert metadata['Depends'].matches(fix_name_prefix('python-dbus')), \
                 "py2deb failed to rewrite dependency name!"
Example #3
0
    def test_namespace_initialization(self):
        """
        Test namespace package initialization and cleanup.

        This tests the :func:`~py2deb.hooks.initialize_namespaces()` and
        :func:`~py2deb.hooks.cleanup_namespaces()` functions.
        """
        for namespace_style in NAMESPACE_STYLES:
            with TemporaryDirectory() as directory:
                package_name = 'namespace-package-test'
                initialize_namespaces(package_name, directory, TEST_NAMESPACES,
                                      namespace_style)
                self.check_test_namespaces(directory)
                # Increase the reference count of the top level name space.
                initialize_namespaces(package_name, directory,
                                      set([('foo', )]), namespace_style)
                self.check_test_namespaces(directory)
                # Clean up the nested name spaces.
                cleanup_namespaces(package_name, directory, TEST_NAMESPACES)
                # Make sure top level name space is still intact.
                assert os.path.isdir(os.path.join(directory, 'foo'))
                assert os.path.isfile(
                    os.path.join(directory, 'foo', '__init__.py'))
                # Make sure the nested name spaces were cleaned up.
                assert not os.path.isdir(os.path.join(directory, 'foo', 'bar'))
                assert not os.path.isfile(
                    os.path.join(directory, 'foo', 'bar', '__init__.py'))
                assert not os.path.isdir(
                    os.path.join(directory, 'foo', 'bar', 'baz'))
                assert not os.path.isfile(
                    os.path.join(directory, 'foo', 'bar', 'baz',
                                 '__init__.py'))
                # Clean up the top level name space as well.
                cleanup_namespaces(package_name, directory, TEST_NAMESPACES)
                assert not os.path.isdir(os.path.join(directory, 'foo'))
Example #4
0
    def find_system_dependencies(self, shared_object_files):
        """
        (Ab)use dpkg-shlibdeps_ to find dependencies on system libraries.

        :param shared_object_files: The pathnames of the ``*.so`` file(s) contained
                                    in the package (a list of strings).
        :returns: A list of strings in the format of the entries on the
                  ``Depends:`` line of a binary package control file.

        .. _dpkg-shlibdeps: https://www.debian.org/doc/debian-policy/ch-sharedlibs.html#s-dpkg-shlibdeps
        """
        logger.debug("Abusing `dpkg-shlibdeps' to find dependencies on shared libraries ..")
        # Create a fake source package, because `dpkg-shlibdeps' expects this...
        with TemporaryDirectory(prefix='py2deb-dpkg-shlibdeps-') as fake_source_directory:
            # Create the debian/ directory expected in the source package directory.
            os.mkdir(os.path.join(fake_source_directory, 'debian'))
            # Create an empty debian/control file because `dpkg-shlibdeps' requires
            # this (even though it is apparently fine for the file to be empty ;-).
            open(os.path.join(fake_source_directory, 'debian', 'control'), 'w').close()
            # Run `dpkg-shlibdeps' inside the fake source package directory, but
            # let it analyze the *.so files from the actual build directory.
            command = ['dpkg-shlibdeps', '-O', '--warnings=0'] + shared_object_files
            output = execute(*command, directory=fake_source_directory, capture=True, logger=logger)
            expected_prefix = 'shlibs:Depends='
            if not output.startswith(expected_prefix):
                msg = ("The output of dpkg-shlibdeps doesn't match the"
                       " expected format! (expected prefix: %r, output: %r)")
                logger.warning(msg, expected_prefix, output)
                return []
            output = output[len(expected_prefix):]
            dependencies = sorted(dependency.strip() for dependency in output.split(','))
            logger.debug("Dependencies reported by dpkg-shlibdeps: %s", dependencies)
            return dependencies
Example #5
0
    def test_converted_package_installation(self):
        """
        Install a converted package on the test system and verify that it works.

        This test only runs on Travis CI, it's a functional test that uses
        py2deb to convert a Python package to a Debian package, installs
        that package on the local system and verifies that the system wide
        Python installation can successfully import the installed package.
        """
        if os.environ.get('TRAVIS') != 'true':
            self.skipTest(
                "This test should only be run on Travis CI! (set $TRAVIS_CI=true to override)"
            )
        with TemporaryDirectory() as directory:
            version = '1.1.8'
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            archives, relationships = converter.convert(
                ['setproctitle==%s' % version])
            # Find and install the generated *.deb archive.
            pathname = find_package_archive(
                archives, fix_name_prefix('python-setproctitle'))
            execute('dpkg', '--install', pathname, sudo=True)
            # Verify that the installed package can be imported.
            interpreter = '/usr/bin/%s' % python_version()
            output = execute(interpreter,
                             '-c',
                             '; '.join([
                                 'import setproctitle',
                                 'print(setproctitle.__version__)',
                             ]),
                             capture=True)
            assert output == version
Example #6
0
    def test_conversion_of_environment_markers(self):
        """
        Convert a package with installation requirements using environment markers.

        Converts ``weasyprint==0.42`` and sanity checks that the ``cairosvg``
        dependency is present.
        """
        with TemporaryDirectory() as directory:
            # Find our constraints file.
            module_directory = os.path.dirname(os.path.abspath(__file__))
            project_directory = os.path.dirname(module_directory)
            constraints_file = os.path.join(project_directory,
                                            'constraints.txt')
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            # Constrain tinycss2 to avoid Python 2 incompatibilities:
            # https://travis-ci.org/github/paylogic/py2deb/jobs/713388666
            archives, relationships = converter.convert(
                ['--constraint=%s' % constraints_file, 'weasyprint==0.42'])
            # Check that the dependency is present.
            pathname = find_package_archive(
                archives, fix_name_prefix('python-weasyprint'))
            metadata, contents = inspect_package(pathname)
            # Make sure the dependency on cairosvg was added (this confirms
            # that environment markers have been evaluated).
            assert fix_name_prefix(
                'python-cairosvg') in metadata['Depends'].names
Example #7
0
    def test_conversion_with_configuration_file(self):
        """
        Convert a group of packages based on the settings in a configuration file.

        Repeats the same test as :func:`test_conversion_of_isolated_packages()`
        but instead of using command line options the conversion process is
        configured using a configuration file.
        """
        # Use a temporary directory as py2deb's repository directory so that we
        # can easily find the *.deb archive generated by py2deb.
        with TemporaryDirectory() as directory:
            configuration_file = os.path.join(directory, 'py2deb.ini')
            with open(configuration_file, 'w') as handle:
                handle.write(
                    dedent('''
                    [py2deb]
                    repository = {repository}
                    name-prefix = pip-accel
                    install-prefix = /usr/lib/pip-accel
                    auto-install = false

                    [alternatives]
                    /usr/bin/pip-accel = /usr/lib/pip-accel/bin/pip-accel

                    [package:pip-accel]
                    no-name-prefix = true

                    [package:coloredlogs]
                    rename = pip-accel-coloredlogs-renamed
                ''',
                           repository=directory))
            # Run the conversion command.
            py2deb('--config=%s' % configuration_file, 'pip-accel==0.12.6')
            # Check the results.
            self.check_converted_pip_accel_packages(directory)
Example #8
0
    def test_pkgutil_namespaces(self):
        """
        Test compatibility with :mod:`pkgutil` style namespace packages.

        This test fails on py2deb <= 4.0 because the two packages involved
        both define the same pkgutil-style namespace package and this
        causes a file conflict that's detected by py2deb, in the form of
        a :exc:`~deb_pkg_tools.checks.DuplicateFilesFound` exception::

            deb_pkg_tools.checks.DuplicateFilesFound: Found 1 duplicate file in 2 package archives!
            -------------------------------------------------------------------------------
            Found 1 conflict between 2 packages:
              1. /tmp/tmpgqz6ettd/python3-backports-functools-lru-cache_1.6.1_all.deb
              2. /tmp/tmpgqz6ettd/python3-configparser_3.7.4_all.deb
            These packages contain 1 conflict:
              1. /usr/lib/python3.6/dist-packages/backports/__init__.py
            -------------------------------------------------------------------------------
        """
        with TemporaryDirectory() as directory:
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            converter.convert([
                'configparser==3.7.4',
                'backports.functools-lru-cache==1.6.1',
            ])
Example #9
0
    def test_conversion_of_binary_package_with_executable(self):
        """
        Convert a package that includes a binary executable file.

        Converts ``uwsgi==2.0.17.1`` and sanity checks the result. The goal of
        this test is to verify that pydeb preserves binary executables instead
        of truncating them as it did until `issue 9`_ was reported.

        .. _issue 9: https://github.com/paylogic/py2deb/issues/9
        """
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            converter.set_install_prefix('/usr/lib/py2deb/uwsgi')
            archives, relationships = converter.convert(['uwsgi==2.0.17.1'])
            # Find the generated *.deb archive.
            pathname = find_package_archive(archives,
                                            fix_name_prefix('python-uwsgi'))
            # Use deb-pkg-tools to inspect the package metadata.
            metadata, contents = inspect_package(pathname)
            logger.debug("Contents of generated package: %s", dict(contents))
            # Find the binary executable file.
            executable = find_file(contents, '/usr/lib/py2deb/uwsgi/bin/uwsgi')
            assert executable.size > 0
Example #10
0
    def test_conversion_of_isolated_packages(self):
        """
        Convert a group of packages with a custom name and installation prefix.

        Converts pip-accel_ and its dependencies to a group of "isolated Debian
        packages" that are installed with a custom name prefix and installation
        prefix and sanity check the result. Also tests the ``--rename=FROM,TO``
        command line option. Performs static checks on the metadata and contents of
        the resulting package archive.

        .. _pip-accel: https://github.com/paylogic/pip-accel
        """
        # Use a temporary directory as py2deb's repository directory so that we
        # can easily find the *.deb archive generated by py2deb.
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            exit_code, output = run_cli(
                main,
                '--repository=%s' % directory,
                '--name-prefix=pip-accel',
                '--install-prefix=/usr/lib/pip-accel',
                # By default py2deb will generate a package called
                # `pip-accel-pip-accel'. The --no-name-prefix=PKG
                # option can be used to avoid this.
                '--no-name-prefix=pip-accel',
                # Strange but valid use case (renaming a dependency):
                # pip-accel-coloredlogs -> pip-accel-coloredlogs-renamed
                '--rename=coloredlogs,pip-accel-coloredlogs-renamed',
                # Also test the update-alternatives integration.
                '--install-alternative=/usr/bin/pip-accel,/usr/lib/pip-accel/bin/pip-accel',
                'pip-accel==0.12.6',
            )
            assert exit_code == 0
            # Check the results.
            self.check_converted_pip_accel_packages(directory)
Example #11
0
    def test_bytecode_generation(self):
        """
        Test byte code generation and cleanup.

        This tests the :func:`~py2deb.hooks.generate_bytecode_files()` and
        :func:`~py2deb.hooks.cleanup_bytecode_files()` functions.
        """
        with TemporaryDirectory() as directory:
            # Generate a Python file.
            python_file = os.path.join(directory, 'test.py')
            with open(python_file, 'w') as handle:
                handle.write('print(42)\n')
            # Generate the byte code file.
            generate_bytecode_files('bytecode-test', [python_file])
            # Make sure a byte code file was generated.
            bytecode_files = list(find_bytecode_files(python_file))
            assert len(bytecode_files) > 0 and all(os.path.isfile(fn) for fn in bytecode_files), \
                "Failed to generate Python byte code file!"
            # Sneak a random file into the __pycache__ directory to test the
            # error handling in cleanup_bytecode_files().
            cache_directory = os.path.join(directory, '__pycache__')
            random_file = os.path.join(cache_directory, 'random-file')
            if HAS_PEP_3147:
                touch(random_file)
            # Cleanup the byte code file.
            cleanup_bytecode_files('bytecode-test', [python_file])
            assert len(bytecode_files) > 0 and all(not os.path.isfile(fn) for fn in bytecode_files), \
                "Failed to cleanup Python byte code file!"
            if HAS_PEP_3147:
                assert os.path.isfile(random_file), \
                    "Byte code cleanup removed unrelated file!"
                os.unlink(random_file)
                cleanup_bytecode_files('test-package', [python_file])
                assert not os.path.isdir(cache_directory), \
                    "Failed to clean up __pycache__ directory!"
Example #12
0
    def test_conversion_of_extras(self):
        """
        Convert a package with extras.

        Converts ``raven[flask]==3.6.0`` and sanity checks the result.
        """
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            archives, relationships = converter.convert([
                # Flask 1.0 drops Python 2.6 compatibility so we explicitly
                # include an older version to prevent raven[flask] from pulling
                # in the latest version of flask, causing this test to fail.
                'flask==0.12.4',
                'raven[flask]==3.6.0',
            ])
            # Check that a relationship with the extra in the package name was generated.
            expression = '%s (= 3.6.0)' % fix_name_prefix('python-raven-flask')
            assert expression in relationships
            # Check that a package with the extra in the filename was generated.
            archive = find_package_archive(
                archives, fix_name_prefix('python-raven-flask'))
            assert archive
            # Use deb-pkg-tools to inspect the package metadata.
            metadata, contents = inspect_package(archive)
            logger.debug("Metadata of generated package: %s", dict(metadata))
            # Check that a "Provides" field was added.
            assert metadata['Provides'].matches(
                fix_name_prefix('python-raven'))
Example #13
0
    def test_custom_conversion_command(self):
        """
        Convert a simple Python package that requires a custom conversion command.

        Converts Fabric and sanity checks the result. For details please refer
        to :func:`py2deb.converter.PackageConverter.set_conversion_command()`.
        """
        if sys.version_info[0] == 3:
            self.skipTest("Fabric is not Python 3.x compatible")
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            converter.set_conversion_command('Fabric', 'rm -Rf paramiko')
            converter.convert(['--no-deps', 'Fabric==0.9.0'])
            # Find the generated Debian package archive.
            archives = glob.glob('%s/*.deb' % directory)
            logger.debug("Found generated archive(s): %s", archives)
            pathname = find_package_archive(archives,
                                            fix_name_prefix('python-fabric'))
            # Use deb-pkg-tools to inspect the generated package.
            metadata, contents = inspect_package(pathname)
            # Check for the two *.py files that should be installed by the package.
            for filename, entry in contents.items():
                if filename.startswith(
                        '/usr/lib') and not entry.permissions.startswith('d'):
                    assert 'fabric' in filename.lower()
                    assert 'paramiko' not in filename.lower()
Example #14
0
 def test_pre_removal_hook(self):
     """Test the :func:`~py2deb.hooks.pre_removal_hook()` function."""
     with TemporaryDirectory() as directory:
         self.run_post_install_hook(directory)
         pre_removal_hook(package_name='prerm-test-package',
                          alternatives=set(),
                          modules_directory=directory,
                          namespaces=self.test_namespaces)
         assert not os.path.isdir(os.path.join(directory, 'foo'))
Example #15
0
    def test_conversion_of_package_with_dependencies(self):
        """
        Convert a non trivial Python package with several dependencies.

        Converts deb-pkg-tools_ to a Debian package archive and sanity checks the
        result. Performs static checks on the metadata (dependencies) of the
        resulting package archive.

        .. _deb-pkg-tools: https://pypi.python.org/pypi/deb-pkg-tools
        """
        # Use a temporary directory as py2deb's repository directory so that we
        # can easily find the *.deb archive generated by py2deb.
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            py2deb('--repository=%s' % directory, 'deb-pkg-tools==1.22')
            # Find the generated Debian package archives.
            archives = glob.glob('%s/*.deb' % directory)
            logger.debug("Found generated archive(s): %s", archives)
            # Make sure the expected dependencies have been converted.
            converted_dependencies = set(
                parse_filename(a).name for a in archives)
            expected_dependencies = set([
                'python-cached-property',
                'python-chardet',
                'python-coloredlogs',
                'python-deb-pkg-tools',
                'python-debian',
                'python-executor',
                'python-humanfriendly',
                'python-six',
            ])
            assert expected_dependencies.issubset(converted_dependencies)
            # Use deb-pkg-tools to inspect ... deb-pkg-tools :-)
            pathname = find_package_archive(archives, 'python-deb-pkg-tools')
            metadata, contents = inspect_package(pathname)
            logger.debug("Metadata of generated package: %s", dict(metadata))
            logger.debug("Contents of generated package: %s", dict(contents))
            # Make sure the dependencies defined in `stdeb.cfg' have been preserved.
            for configured_dependency in [
                    'apt', 'apt-utils', 'dpkg-dev', 'fakeroot', 'gnupg',
                    'lintian'
            ]:
                logger.debug("Checking configured dependency %s ..",
                             configured_dependency)
                assert metadata['Depends'].matches(
                    configured_dependency) is not None
            # Make sure the dependencies defined in `setup.py' have been preserved.
            expected_dependencies = [
                'python-chardet', 'python-coloredlogs', 'python-debian',
                'python-executor', 'python-humanfriendly'
            ]
            for python_dependency in expected_dependencies:
                logger.debug("Checking Python dependency %s ..",
                             python_dependency)
                assert metadata['Depends'].matches(
                    python_dependency) is not None
Example #16
0
 def test_python_requirements_fallback(self):
     """Test the fall-back implementation of the ``python_requirements`` property."""
     with TemporaryDirectory() as directory:
         # Run the conversion command.
         converter = self.create_isolated_converter()
         converter.set_repository(directory)
         packages = list(converter.get_source_distributions(['coloredlogs==6.0']))
         coloredlogs_package = next(p for p in packages if p.python_name == 'coloredlogs')
         assert any(p.key == 'humanfriendly' for p in coloredlogs_package.python_requirements)
         assert any(p.key == 'humanfriendly' for p in coloredlogs_package.python_requirements_fallback)
Example #17
0
    def test_conversion_of_extras(self):
        """
        Convert a package with extras.

        Converts ``raven[flask]==3.6.0`` and sanity checks the result.
        """
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            archives, relationships = converter.convert(['raven[flask]==3.6.0'])
            # Check that a relationship with the extra in the package name was generated.
            assert relationships == ['python-raven-flask (= 3.6.0)']
            # Check that a package with the extra in the filename was generated.
            assert find_package_archive(archives, 'python-raven-flask')
Example #18
0
    def test_duplicate_files_check(self):
        """
        Ensure that `py2deb` checks for duplicate file conflicts within dependency sets.

        Converts a version of Fabric that bundles Paramiko but also includes
        Paramiko itself in the dependency set, thereby causing a duplicate file
        conflict, to verify that `py2deb` recognizes duplicate file conflicts.
        """
        if sys.version_info[0] == 3:
            self.skipTest("Fabric is not Python 3.x compatible")
        with TemporaryDirectory() as directory:
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            self.assertRaises(DuplicateFilesFound,
                              converter.convert,
                              ['Fabric==0.9.0', 'Paramiko==1.14.0'])
Example #19
0
    def test_conversion_of_binary_package(self):
        """
        Convert a package that includes a ``*.so`` file (a shared object file).

        Converts ``setproctitle==1.1.8`` and sanity checks the result. The goal
        of this test is to verify that pydeb properly handles packages with
        binary components (including dpkg-shlibdeps_ magic). This explains why
        I chose the setproctitle_ package:

        1. This package is known to require a compiled shared object file for
           proper functioning.

        2. Despite requiring a compiled shared object file the package is
           fairly lightweight and has little dependencies so including this
           test on every run of the test suite won't slow things down so much
           that it becomes annoying.

        3. The package is documented to support Python 3.x as well which means
           we can run this test on all supported Python versions.

        .. _setproctitle: https://pypi.org/project/setproctitle/
        .. _dpkg-shlibdeps: https://manpages.debian.org/dpkg-shlibdeps
        """
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            archives, relationships = converter.convert(
                ['setproctitle==1.1.8'])
            # Find the generated *.deb archive.
            pathname = find_package_archive(
                archives, fix_name_prefix('python-setproctitle'))
            # Use deb-pkg-tools to inspect the package metadata.
            metadata, contents = inspect_package(pathname)
            logger.debug("Metadata of generated package: %s", dict(metadata))
            logger.debug("Contents of generated package: %s", dict(contents))
            # Make sure the package's architecture was properly set.
            assert metadata['Architecture'] != 'all'
            # Make sure the shared object file is included in the package.
            assert find_file(contents, '/usr/lib/*/setproctitle*.so')
            # Make sure a dependency on libc was added (this shows that
            # dpkg-shlibdeps was run successfully).
            assert 'libc6' in metadata['Depends'].names
Example #20
0
 def check_python_callback(self, expression):
     """Test for Python callback logic manipulating the build of a package."""
     with TemporaryDirectory() as repository_directory:
         # Run the conversion command.
         converter = self.create_isolated_converter()
         converter.set_repository(repository_directory)
         converter.set_python_callback(expression)
         converter.set_name_prefix('callback-test')
         archives, relationships = converter.convert(['naturalsort'])
         # Find the generated *.deb archive.
         pathname = find_package_archive(archives, 'callback-test-naturalsort')
         # Use deb-pkg-tools to inspect the package metadata.
         metadata, contents = inspect_package(pathname)
         logger.debug("Metadata of generated package: %s", dict(metadata))
         logger.debug("Contents of generated package: %s", dict(contents))
         # Inspect the converted package's dependency.
         assert metadata['Breaks'].matches('callback-test-natsort'), \
             "Result of Python callback not visible?!"
         assert metadata['Replaces'].matches('callback-test-natsort'), \
             "Result of Python callback not visible?!"
Example #21
0
    def test_conversion_of_environment_markers(self):
        """
        Convert a package with installation requirements using environment markers.

        Converts ``weasyprint==0.42`` and sanity checks that the ``cairosvg``
        dependency is present.
        """
        if sys.version_info[:2] == (2, 6):
            self.skipTest("WeasyPrint 0.42 is not Python 2.6 compatible")
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            archives, relationships = converter.convert(['weasyprint==0.42'])
            # Check that the dependency is present.
            pathname = find_package_archive(archives, 'python-weasyprint')
            metadata, contents = inspect_package(pathname)
            # Make sure the dependency on cairosvg was added (this confirms
            # that environment markers have been evaluated).
            assert 'python-cairosvg' in metadata['Depends'].names
Example #22
0
    def test_conversion_of_extras(self):
        """
        Convert a package with extras.

        Converts ``raven[flask]==3.6.0`` and sanity checks the result.
        """
        with TemporaryDirectory() as directory:
            # Run the conversion command.
            converter = self.create_isolated_converter()
            converter.set_repository(directory)
            archives, relationships = converter.convert([
                # Flask 1.0 drops Python 2.6 compatibility so we explicitly
                # include an older version to prevent raven[flask] from pulling
                # in the latest version of flask, causing this test to fail.
                'flask==0.12.4',
                'raven[flask]==3.6.0',
            ])
            # Check that a relationship with the extra in the package name was generated.
            expression = '%s (= 3.6.0)' % fix_name_prefix('python-raven-flask')
            assert expression in relationships
            # Check that a package with the extra in the filename was generated.
            assert find_package_archive(archives,
                                        fix_name_prefix('python-raven-flask'))
Example #23
0
    def convert(self):
        """
        Convert current package from Python package to Debian package.

        :returns: The pathname of the generated ``*.deb`` archive.
        """
        with TemporaryDirectory(prefix='py2deb-build-') as build_directory:

            # Prepare the absolute pathname of the Python interpreter on the
            # target system. This pathname will be embedded in the first line
            # of executable scripts (including the post-installation and
            # pre-removal scripts).
            python_executable = '/usr/bin/%s' % python_version()

            # Unpack the binary distribution archive provided by pip-accel inside our build directory.
            build_install_prefix = os.path.join(
                build_directory, self.converter.install_prefix.lstrip('/'))
            self.converter.pip_accel.bdists.install_binary_dist(
                members=self.transform_binary_dist(python_executable),
                prefix=build_install_prefix,
                python=python_executable,
                virtualenv_compatible=False,
            )

            # Determine the directory (at build time) where the *.py files for
            # Python modules are located (the site-packages equivalent).
            if self.has_custom_install_prefix:
                build_modules_directory = os.path.join(build_install_prefix,
                                                       'lib')
            else:
                # The /py*/ pattern below is intended to match both /pythonX.Y/ and /pypyX.Y/.
                dist_packages_directories = glob.glob(
                    os.path.join(build_install_prefix,
                                 'lib/py*/dist-packages'))
                if len(dist_packages_directories) != 1:
                    msg = "Expected to find a single 'dist-packages' directory inside converted package!"
                    raise Exception(msg)
                build_modules_directory = dist_packages_directories[0]

            # Determine the directory (at installation time) where the *.py
            # files for Python modules are located.
            install_modules_directory = os.path.join(
                '/', os.path.relpath(build_modules_directory, build_directory))

            # Execute a user defined command inside the directory where the Python modules are installed.
            command = self.converter.scripts.get(self.python_name.lower())
            if command:
                execute(command,
                        directory=build_modules_directory,
                        logger=logger)

            # Determine the package's dependencies, starting with the currently
            # running version of Python and the Python requirements converted
            # to Debian packages.
            dependencies = [python_version()] + self.debian_dependencies

            # Check if the converted package contains any compiled *.so files.
            object_files = find_object_files(build_directory)
            if object_files:
                # Strip debugging symbols from the object files.
                strip_object_files(object_files)
                # Determine system dependencies by analyzing the linkage of the
                # *.so file(s) found in the converted package.
                dependencies += find_system_dependencies(object_files)

            # Make up some control file fields ... :-)
            architecture = self.determine_package_architecture(object_files)
            control_fields = unparse_control_fields(
                dict(package=self.debian_name,
                     version=self.debian_version,
                     maintainer=self.debian_maintainer,
                     description=self.debian_description,
                     architecture=architecture,
                     depends=dependencies,
                     priority='optional',
                     section='python'))

            # Automatically add the Mercurial global revision id when available.
            if self.vcs_revision:
                control_fields['Vcs-Hg'] = self.vcs_revision

            # Apply user defined control field overrides from `stdeb.cfg'.
            control_fields = self.load_control_field_overrides(control_fields)

            # Create the DEBIAN directory.
            debian_directory = os.path.join(build_directory, 'DEBIAN')
            os.mkdir(debian_directory)

            # Generate the DEBIAN/control file.
            control_file = os.path.join(debian_directory, 'control')
            logger.debug("Saving control file fields to %s: %s", control_file,
                         control_fields)
            with open(control_file, 'wb') as handle:
                control_fields.dump(handle)

            # Lintian is a useful tool to find mistakes in Debian binary
            # packages however Lintian checks from the perspective of a package
            # included in the official Debian repositories. Because py2deb
            # doesn't and probably never will generate such packages some
            # messages emitted by Lintian are useless (they merely point out
            # how the internals of py2deb work). Because of this we silence
            # `known to be irrelevant' messages from Lintian using overrides.
            if self.converter.lintian_ignore:
                overrides_directory = os.path.join(
                    build_directory,
                    'usr',
                    'share',
                    'lintian',
                    'overrides',
                )
                overrides_file = os.path.join(overrides_directory,
                                              self.debian_name)
                os.makedirs(overrides_directory)
                with open(overrides_file, 'w') as handle:
                    for tag in self.converter.lintian_ignore:
                        handle.write('%s: %s\n' % (self.debian_name, tag))

            # Find the alternatives relevant to the package we're building.
            alternatives = set(
                (link, path) for link, path in self.converter.alternatives
                if os.path.isfile(
                    os.path.join(build_directory, path.lstrip('/'))))

            # Generate post-installation and pre-removal maintainer scripts.
            self.generate_maintainer_script(
                filename=os.path.join(debian_directory, 'postinst'),
                python_executable=python_executable,
                function='post_installation_hook',
                package_name=self.debian_name,
                alternatives=alternatives,
                modules_directory=install_modules_directory,
                namespaces=self.namespaces)
            self.generate_maintainer_script(
                filename=os.path.join(debian_directory, 'prerm'),
                python_executable=python_executable,
                function='pre_removal_hook',
                package_name=self.debian_name,
                alternatives=alternatives,
                modules_directory=install_modules_directory,
                namespaces=self.namespaces)

            # Enable a user defined Python callback to manipulate the resulting
            # binary package before it's turned into a *.deb archive (e.g.
            # manipulate the contents or change the package metadata).
            if self.converter.python_callback:
                logger.debug("Invoking user defined Python callback ..")
                self.converter.python_callback(self.converter, self,
                                               build_directory)
                logger.debug("User defined Python callback finished!")

            return build_package(directory=build_directory,
                                 check_package=self.converter.lintian_enabled,
                                 copy_files=False)
Example #24
0
 def test_post_install_hook(self):
     """Test the :func:`~py2deb.hooks.post_installation_hook()` function."""
     with TemporaryDirectory() as directory:
         self.run_post_install_hook(directory)
         self.check_test_namespaces(directory)
Example #25
0
 def test_post_install_hook(self):
     """Test the :func:`~py2deb.hooks.post_installation_hook()` function."""
     for namespace_style in NAMESPACE_STYLES:
         with TemporaryDirectory() as directory:
             self.run_post_install_hook(directory, namespace_style)
             self.check_test_namespaces(directory)
Example #26
0
    def test_conversion_of_simple_package(self):
        """
        Convert a simple Python package without any dependencies.

        Converts coloredlogs_ and sanity checks the result. Performs several static
        checks on the metadata and contents of the resulting package archive.

        .. _coloredlogs: https://pypi.org/project/coloredlogs
        """
        # Use a temporary directory as py2deb's repository directory so that we
        # can easily find the *.deb archive generated by py2deb.
        with TemporaryDirectory() as directory:
            # Run the conversion twice to check that existing archives are not overwritten.
            last_modified_time = 0
            for i in range(2):
                # Prepare a control file to be patched.
                control_file = os.path.join(directory, 'control')
                with open(control_file, 'w') as handle:
                    handle.write('Depends: vim\n')
                # Run the conversion command.
                exit_code, output = run_cli(
                    main,
                    '--verbose',
                    '--yes',
                    '--repository=%s' % directory,
                    '--report-dependencies=%s' % control_file,
                    'coloredlogs==0.5',
                )
                assert exit_code == 0
                # Check that the control file was patched.
                control_fields = load_control_file(control_file)
                assert control_fields['Depends'].matches('vim')
                assert control_fields['Depends'].matches(
                    fix_name_prefix('python-coloredlogs'), '0.5')
                # Find the generated Debian package archive.
                archives = glob.glob('%s/*.deb' % directory)
                logger.debug("Found generated archive(s): %s", archives)
                assert len(archives) == 1
                # Verify that existing archives are not overwritten.
                if not last_modified_time:
                    # Capture the last modified time of the archive in the first iteration.
                    last_modified_time = os.path.getmtime(archives[0])
                else:
                    # Verify the last modified time of the archive in the second iteration.
                    assert last_modified_time == os.path.getmtime(archives[0])
                # Use deb-pkg-tools to inspect the generated package.
                metadata, contents = inspect_package(archives[0])
                logger.debug("Metadata of generated package: %s",
                             dict(metadata))
                logger.debug("Contents of generated package: %s",
                             dict(contents))
                # Check the package metadata.
                assert metadata['Package'] == fix_name_prefix(
                    'python-coloredlogs')
                assert metadata['Version'].startswith('0.5')
                assert metadata['Architecture'] == 'all'
                # There should be exactly one dependency: some version of Python.
                assert metadata['Depends'].matches(python_version())
                # Don't care about the format here as long as essential information is retained.
                assert 'Peter Odding' in metadata['Maintainer']
                assert '*****@*****.**' in metadata['Maintainer']
                # Check the package contents.
                # Check for the two *.py files that should be installed by the package.
                assert find_file(
                    contents,
                    '/usr/lib/py*/dist-packages/coloredlogs/__init__.py')
                assert find_file(
                    contents,
                    '/usr/lib/py*/dist-packages/coloredlogs/converter.py')
                # Make sure the file ownership and permissions are sane.
                archive_entry = find_file(
                    contents,
                    '/usr/lib/py*/dist-packages/coloredlogs/__init__.py')
                assert archive_entry.owner == 'root'
                assert archive_entry.group == 'root'
                assert archive_entry.permissions == '-rw-r--r--'