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
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)
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--'
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(), 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: dist_packages_directories = glob.glob(os.path.join(build_install_prefix, 'lib/python*/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. shared_object_files = self.find_shared_object_files(build_directory) if shared_object_files: # Determine system dependencies by analyzing the linkage of the # *.so file(s) found in the converted package. dependencies += self.find_system_dependencies(shared_object_files) # Make up some control file fields ... :-) architecture = self.determine_package_architecture(shared_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. 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 ['debian-changelog-file-missing', 'embedded-javascript-library', 'extra-license-file', 'unknown-control-interpreter', 'vcs-field-uses-unknown-uri-format']: 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)