Import Upstream version 5.20221001

This commit is contained in:
su-fang 2022-11-03 14:00:52 +08:00
commit c05bf1b555
291 changed files with 16039 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
debian/dh-python/
.pc
.coverage
__pycache__
pydist/cache/
*\.1
*\.pyc
*\.swp
debhelper-build-stamp
*\.buildinfo
*\.deb
*\.changes
*\.log
*\.debhelper
*\.substvars
*/*/debian/files
debian/files

17
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,17 @@
default:
image: debian:unstable
tests:
before_script:
- apt-get update
- apt-get -y install --no-install-recommends build-essential debhelper fakeroot flit libjs-jquery pypy python3-all python3-all-dbg python3-all-dev python3-build python3-installer python3-nose2 python3-poetry-core python3-pytest python3-setuptools python3-tomli tox
- apt-get -y install --no-install-recommends python-all python-all-dbg python-setuptools
script:
- make tests
- echo -e '#!/bin/sh\nset -eu\nmake "$@"' > debian/tests/run-installed
- export DH_PYTHON_DIST=$PWD/pydist
- ./debian/tests/dh-python
- ./debian/tests/dh-python2
- ./debian/tests/pybuild
- ./debian/tests/pybuild-py2

67
Makefile Normal file
View File

@ -0,0 +1,67 @@
#!/usr/bin/make -f
INSTALL ?= install
PREFIX ?= /usr/local
MANPAGES ?= pybuild.1 dh_pypy.1 dh_python2.1 dh_python3.1
DVERSION=$(shell dpkg-parsechangelog | sed -rne 's,^Version: (.+),\1,p' || echo 'DEVEL')
VERSION=$(shell dpkg-parsechangelog | sed -rne 's,^Version: ([^-]+).*,\1,p' || echo 'DEVEL')
clean:
make -C tests clean
make -C pydist clean
find . -name '*.py[co]' -delete
find . -name __pycache__ -type d | xargs rm -rf
rm -f .coverage $(MANPAGES)
rm -rf .pybuild
dist:
git archive --format=tar --prefix=dh-python-$(VERSION)/ HEAD \
| xz -9 -c >../dh-python_$(VERSION).orig.tar.xz
install:
$(INSTALL) -m 755 -d $(DESTDIR)$(PREFIX)/bin \
$(DESTDIR)$(PREFIX)/share/debhelper/autoscripts/ \
$(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Sequence/ \
$(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Buildsystem/ \
$(DESTDIR)$(PREFIX)/share/dh-python/dhpython/build \
$(DESTDIR)$(PREFIX)/share/dh-python/dist
$(INSTALL) -m 644 pydist/*_fallback $(DESTDIR)$(PREFIX)/share/dh-python/dist/
$(INSTALL) -m 644 dhpython/*.py $(DESTDIR)$(PREFIX)/share/dh-python/dhpython/
$(INSTALL) -m 644 dhpython/build/*.py $(DESTDIR)$(PREFIX)/share/dh-python/dhpython/build/
$(INSTALL) -m 755 pybuild $(DESTDIR)$(PREFIX)/share/dh-python/
$(INSTALL) -m 755 dh_pypy $(DESTDIR)$(PREFIX)/share/dh-python/
$(INSTALL) -m 755 dh_python2 $(DESTDIR)$(PREFIX)/share/dh-python/
$(INSTALL) -m 755 dh_python3 $(DESTDIR)$(PREFIX)/share/dh-python/
sed -i -e 's/DEVELV/$(DVERSION)/' $(DESTDIR)$(PREFIX)/share/dh-python/pybuild
sed -i -e 's/DEVELV/$(DVERSION)/' $(DESTDIR)$(PREFIX)/share/dh-python/dh_pypy
sed -i -e 's/DEVELV/$(DVERSION)/' $(DESTDIR)$(PREFIX)/share/dh-python/dh_python2
sed -i -e 's/DEVELV/$(DVERSION)/' $(DESTDIR)$(PREFIX)/share/dh-python/dh_python3
$(INSTALL) -m 644 dh/pybuild.pm $(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Buildsystem/
$(INSTALL) -m 644 dh/pypy.pm $(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Sequence/
$(INSTALL) -m 644 dh/python2.pm $(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Sequence/
$(INSTALL) -m 644 dh/python3.pm $(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Sequence/
$(INSTALL) -m 644 autoscripts/* $(DESTDIR)$(PREFIX)/share/debhelper/autoscripts/
%.1: %.rst
rst2man $< > $@
%.htm: %.rst
rst2html $< > $@
manpages: $(MANPAGES)
dist_fallback:
make -C pydist $@
# TESTS
nose:
#nosetests3 --verbose --with-doctest --with-coverage
nose2-3 --verbose --plugin nose2.plugins.doctests --with-doctest
tests: nose
make -C tests
test%:
make -C tests $@
.PHONY: clean tests test% check_versions

192
README.rst Normal file
View File

@ -0,0 +1,192 @@
===========
dh-python
===========
``dh-python`` provides various tools that help packaging Python related files
in Debian.
* ``pybuild`` is a tool that implements ``dh`` sequencer's ``dh_auto_foo``
commands (but it can be used outside ``dh`` as well). It builds and installs
files.
* ``dh_python2`` / ``dh_python3`` / ``dh_pypy`` are tools that take what
``pybuild`` produces and generates runtime dependencies and maintainer
scripts. It fixes some common mistakes, like installing files into
``site-packages`` instead of ``dist-packages``, ``/usr/local/bin/``
shebangs, removes ``.py`` files from ``-dbg`` packages, etc.)
To translate ``requires.txt`` (a file installed in
``dist-packages/foo.egg-info/``) into Debian dependencies, a list of
packages that provide given egg distribution is used. If the dependency
is not found there, ``dpkg -S`` is used (i.e. a given dependency has to be
installed; you need it in ``Build-Depends`` in order to run tests anyway).
See *dependencies* section in ``dh_python3``'s manpage for more details.
* ``dh_python2`` works on ``./debian/python-foo/`` files and other binary
packages that have ``${python:Depends}`` in the ``Depends`` field.
It ignores Python 3.X and PyPy specific directories.
See ``dh_python2`` manpage for more details.
* ``dh_python3`` works on ``./debian/python3-foo/`` files and other binary
packages that have ``${python3:Depends}`` in the ``Depends`` field.
It ignores Python 2.X and PyPy specific directories.
See ``dh_python3`` manpage for more details.
* ``dh_pypy`` works on ``./debian/pypy-foo/`` files and other binary
packages that have ``${pypy:Depends}`` in the ``Depends`` field.
It ignores Python 2.X and Python 3.X specific directories.
See ``dh_pypy`` manpage for more details.
How it works
============
A simplified work flow looks like this:
.. code:: python
# dh_auto_clean stage
for interpreter in REQUESTED_INTERPRETERS:
for version in interpreter.REQUESTED_VERSIONS:
PYBUILD_BEFORE_CLEAN
pybuild --clean
PYBUILD_AFTER_CLEAN
plenty_of_other_dh_foo_tools_invoked_here
# dh_auto_configure stage
for interpreter in REQUESTED_INTERPRETERS:
for version in interpreter.REQUESTED_VERSIONS:
PYBUILD_BEFORE_CONFIGURE
pybuild --configure
PYBUILD_AFTER_CONFIGURE
plenty_of_other_dh_foo_tools_invoked_here
# dh_auto_build stage
for interpreter in REQUESTED_INTERPRETERS:
for version in interpreter.REQUESTED_VERSIONS:
PYBUILD_BEFORE_BUILD
pybuild --build
PYBUILD_AFTER_BUILD
plenty_of_other_dh_foo_tools_invoked_here
# dh_auto_test stage
for interpreter in REQUESTED_INTERPRETERS:
for version in interpreter.REQUESTED_VERSIONS:
PYBUILD_BEFORE_TEST
pybuild --test
PYBUILD_AFTER_TEST
plenty_of_other_dh_foo_tools_invoked_here
# dh_auto_install stage
for interpreter in REQUESTED_INTERPRETERS:
for version in interpreter.REQUESTED_VERSIONS:
PYBUILD_BEFORE_INSTALL
pybuild --install
PYBUILD_AFTER_INSTALL
plenty_of_other_dh_foo_tools_invoked_here
dh_python2
dh_python3
dh_pypy
plenty_of_other_dh_foo_tools_invoked_here
pybuild --$step
---------------
This command is auto-detected, it currently supports distutils, autotools,
cmake and a custom build system where you can define your own set of
commands. Why do we need it? ``dh_auto_foo`` doesn't know each command has to
be invoked for each interpreter and version.
REQUESTED_INTERPRETERS
----------------------
is parsed from ``Build-Depends`` if ``--buildsystem=pybuild`` is set. If it's
not, you have to pass ``--interpreter`` to ``pybuild`` (more in its manpage)
* ``python{3,}-all{,-dev}`` - all CPython interpreters (for packages that
provide public modules / extensions)
* ``python{3,}-all-dbg`` - all CPython debug interpreters (if ``-dbg`` package
is provided)
* ``python{3,}`` - default CPython or closest to default interpreter only (use
this if you build a Python application)
* ``python{3,}-dbg`` - default CPython debug (or closest to the default one)
only
* ``pypy`` - PyPy interpreter
REQUESTED_VERSIONS
------------------
is parsed from ``X-Python{,3}-Version`` and ``Build-Depends`` (the right
``X-*-Version`` is parsed based on interpreters listed in ``Build-Depends``,
see above) See also Debian Python Policy for ``X-Python-Version`` description.
BEFORE and AFTER commands
-------------------------
can be different for each interpreter and/or version, examples:
* ``PYBUILD_AFTER_BUILD_python3.5=rm {destdir}/{build_dir}/foo/bar2X.py``
* ``PYBUILD_BEFORE_INSTALL_python3=touch {destdir}/{install_dir}/foo/bar/__init__.py``
These commands should be used only if overriding ``dh_auto_foo`` is not enough
(example below)
.. code::
override_dh_auto_install:
before_auto_install_commands
dh_auto_install
after_auto_install_commands
See the ``pybuild`` manpage for more details (search for ``BUILD SYSTEM ARGUMENTS``)
overrides
---------
How to override ``pybuild`` autodetected options:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Each ``pybuild`` call can be disabled (for given interpreter, version or
stage). See the ``pybuild`` manpage for more details (search for
``--disable`` description).
* You can pass options in ``override_dh_auto_foo`` via command line options:
.. code::
dh_auto_test -- --system=custom --test-args='{interpreter} setup.py test'
or env. variables:
.. code::
PYBUILD_SYSTEM=custom PYBUILD_TEST_ARGS='{interpreter} setup.py test' dh_auto_test
* You can export env. variables globally at the beginning of debian/rules
.. code::
export PYBUILD_TEST_ARGS={dir}/tests/
How to override dh_python* options:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* via command line, f.e.
.. code::
override_dh_python3:
dh_python3 --shebang=/usr/bin/python3

View File

@ -0,0 +1,6 @@
if command -v py3compile >/dev/null 2>&1; then
py3compile -p #PACKAGE# #ARGS#
fi
if command -v pypy3compile >/dev/null 2>&1; then
pypy3compile -p #PACKAGE# #ARGS# || true
fi

View File

@ -0,0 +1,3 @@
if command -v pycompile >/dev/null 2>&1; then
pycompile -p #PACKAGE# #ARGS#
fi

View File

@ -0,0 +1,5 @@
if command -v pypycompile >/dev/null 2>&1; then
pypycompile -p #PACKAGE# #ARGS#
elif pypy -m py_compile >/dev/null 2>&1; then
dpkg -L #PACKAGE# | grep '\.py$' | pypy -m py_compile - >/dev/null
fi

View File

@ -0,0 +1,6 @@
if command -v py3clean >/dev/null 2>&1; then
py3clean -p #PACKAGE# #ARGS#
else
dpkg -L #PACKAGE# | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e'
find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
fi

View File

@ -0,0 +1,8 @@
if command -v pyclean >/dev/null 2>&1; then
pyclean -p #PACKAGE# #ARGS#
else
dpkg -L #PACKAGE# | grep \.py$ | while read file
do
rm -f "${file}"[co] >/dev/null
done
fi

View File

@ -0,0 +1,6 @@
if command -v pypyclean >/dev/null 2>&1; then
pypyclean -p #PACKAGE# #ARGS#
else
dpkg -L #PACKAGE# | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e'
find /usr/lib/pypy/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
fi

270
dh/pybuild.pm Normal file
View File

@ -0,0 +1,270 @@
# A debhelper build system class for building Python libraries
#
# Copyright: © 2012-2013 Piotr Ożarowski
# TODO:
# * support for dh --parallel
package Debian::Debhelper::Buildsystem::pybuild;
use strict;
use Dpkg::Control;
use Dpkg::Changelog::Debian;
use Debian::Debhelper::Dh_Lib qw(%dh error doit);
use base 'Debian::Debhelper::Buildsystem';
sub DESCRIPTION {
"Python pybuild"
}
sub check_auto_buildable {
my $this=shift;
return doit('pybuild', '--detect', '--really-quiet', '--dir', $this->get_sourcedir());
}
sub new {
my $class=shift;
my $this=$class->SUPER::new(@_);
$this->enforce_in_source_building();
if (!$ENV{'PYBUILD_INTERPRETERS'}) {
if ($ENV{'DEBPYTHON_DEFAULT'}) {
$this->{pydef} = $ENV{'DEBPYTHON_DEFAULT'};}
else {
$this->{pydef} = `pyversions -vd 2>/dev/null`;}
$this->{pydef} =~ s/\s+$//;
if ($ENV{'DEBPYTHON_SUPPORTED'}) {
$this->{pyvers} = $ENV{'DEBPYTHON_SUPPORTED'} =~ s/,/ /r;}
else {
$this->{pyvers} = `pyversions -vr 2>/dev/null`;}
$this->{pyvers} =~ s/\s+$//;
if ($ENV{'DEBPYTHON3_DEFAULT'}) {
$this->{py3def} = $ENV{'DEBPYTHON3_DEFAULT'};}
else {
$this->{py3def} = `py3versions -vd 2>/dev/null`;}
$this->{py3def} =~ s/\s+$//;
if ($ENV{'DEBPYTHON3_SUPPORTED'}) {
$this->{py3vers} = $ENV{'DEBPYTHON3_SUPPORTED'} =~ s/,/ /r;}
else {
$this->{py3vers} = `py3versions -vr 2>/dev/null`;
if ($this->{py3vers} eq "") {
# We swallowed stderr, above
system("py3versions -vr");
die('E: py3versions failed');
}
}
$this->{py3vers} =~ s/\s+$//;
$this->{pypydef} = `pypy -c 'from sys import pypy_version_info as i; print("%s.%s" % (i.major, i.minor))' 2>/dev/null`;
$this->{pypydef} =~ s/\s+$//;
}
return $this;
}
sub configure {
my $this=shift;
foreach my $command ($this->pybuild_commands('configure', @_)) {
doit(@$command);
}
}
sub build {
my $this=shift;
foreach my $command ($this->pybuild_commands('build', @_)) {
doit(@$command);
}
}
sub install {
my $this=shift;
my $destdir=shift;
foreach my $command ($this->pybuild_commands('install', @_)) {
doit(@$command, '--dest-dir', $destdir);
}
}
sub test {
my $this=shift;
foreach my $command ($this->pybuild_commands('test', @_)) {
doit(@$command);
}
}
sub clean {
my $this=shift;
foreach my $command ($this->pybuild_commands('clean', @_)) {
doit(@$command);
}
doit('rm', '-rf', '.pybuild/');
doit('find', '.', '-name', '*.pyc', '-exec', 'rm', '{}', ';');
}
sub pybuild_commands {
my $this=shift;
my $step=shift;
my @options = @_;
my @result;
my $dir = $this->get_sourcedir();
if (not grep {$_ eq '--dir'} @options and $dir ne '.') {
# if --dir is not passed, PYBUILD_DIR can be used
push @options, '--dir', $dir;
}
if (not grep {$_ eq '--verbose'} @options and $dh{QUIET}) {
push @options, '--quiet';
}
my @deps;
if ($ENV{'PYBUILD_INTERPRETERS'}) {
push @result, ['pybuild', "--$step", @options];
}
else {
# get interpreter packages from Build-Depends{,-Indep}:
# NOTE: possible problems with alternative/versioned dependencies
@deps = $this->python_build_dependencies();
# When depends on python{3,}-setuptools-scm, set
# SETUPTOOLS_SCM_PRETEND_VERSION to upstream version
# Without this, setuptools-scm tries to detect current
# version from git tag, which fails for debian tags
# (debian/<version>) sometimes.
if ((grep /(pypy|python[0-9\.]*)-setuptools-scm/, @deps) && !$ENV{'SETUPTOOLS_SCM_PRETEND_VERSION'}) {
my $changelog = Dpkg::Changelog::Debian->new(range => {"count" => 1});
$changelog->load("debian/changelog");
my $version = @{$changelog}[0]->get_version();
$version =~ s/-[^-]+$//; # revision
$version =~ s/^\d+://; # epoch
$version =~ s/~/-/; # ignore tilde versions
$ENV{'SETUPTOOLS_SCM_PRETEND_VERSION'} = $version;
}
# When depends on python{3,}-pbr, set PBR_VERSION to upstream version
# Without this, python-pbr tries to detect current
# version from pkg metadata or git tag, which fails for debian tags
# (debian/<version>) sometimes.
if ((grep /(pypy|python[0-9\.]*)-pbr/, @deps) && !$ENV{'PBR_VERSION'}) {
my $changelog = Dpkg::Changelog::Debian->new(range => {"count" => 1});
$changelog->load("debian/changelog");
my $version = @{$changelog}[0]->get_version();
$version =~ s/-[^-]+$//; # revision
$version =~ s/^\d+://; # epoch
$ENV{'PBR_VERSION'} = $version;
}
my @py2opts = ('pybuild', "--$step");
my @py3opts = ('pybuild', "--$step");
my @pypyopts = ('pybuild', "--$step");
if ($step eq 'test' and $ENV{'PYBUILD_TEST_PYTEST'} ne '1' and
$ENV{'PYBUILD_TEST_NOSE2'} ne '1' and
$ENV{'PYBUILD_TEST_NOSE'} ne '1' and
$ENV{'PYBUILD_TEST_CUSTOM'} ne '1' and
$ENV{'PYBUILD_TEST_TOX'} ne '1') {
if (grep {$_ eq 'python-tox'} @deps and $ENV{'PYBUILD_TEST_TOX'} ne '0') {
push @py2opts, '--test-tox'}
elsif (grep {$_ eq 'python-pytest'} @deps and $ENV{'PYBUILD_TEST_PYTEST'} ne '0') {
push @py2opts, '--test-pytest'}
elsif (grep {$_ eq 'python-nose2'} @deps and $ENV{'PYBUILD_TEST_NOSE2'} ne '0') {
push @py2opts, '--test-nose2'}
elsif (grep {$_ eq 'python-nose'} @deps and $ENV{'PYBUILD_TEST_NOSE'} ne '0') {
push @py2opts, '--test-nose'}
if (grep {$_ eq 'tox'} @deps and $ENV{'PYBUILD_TEST_TOX'} ne '0') {
push @py3opts, '--test-tox'}
elsif (grep {$_ eq 'python3-pytest'} @deps and $ENV{'PYBUILD_TEST_PYTEST'} ne '0') {
push @py3opts, '--test-pytest'}
elsif (grep {$_ eq 'python3-nose2'} @deps and $ENV{'PYBUILD_TEST_NOSE2'} ne '0') {
push @py3opts, '--test-nose2'}
elsif (grep {$_ eq 'python3-nose'} @deps and $ENV{'PYBUILD_TEST_NOSE'} ne '0') {
push @py3opts, '--test-nose'}
if (grep {$_ eq 'pypy-tox'} @deps and $ENV{'PYBUILD_TEST_TOX'} ne '0') {
push @pypyopts, '--test-tox'}
elsif (grep {$_ eq 'pypy-pytest'} @deps and $ENV{'PYBUILD_TEST_PYTEST'} ne '0') {
push @pypyopts, '--test-pytest'}
elsif (grep {$_ eq 'pypy-nose'} @deps and $ENV{'PYBUILD_TEST_NOSE'} ne '0') {
push @pypyopts, '--test-nose'}
}
my $pyall = 0;
my $pyalldbg = 0;
my $py3all = 0;
my $py3alldbg = 0;
my $i = 'python{version}';
# Python
if ($this->{pyvers}) {
if (grep {$_ eq 'python-all' or $_ eq 'python-all-dev'} @deps) {
$pyall = 1;
push @result, [@py2opts, '-i', $i, '-p', $this->{pyvers}, @options];
}
if (grep {$_ eq 'python-all-dbg'} @deps) {
$pyalldbg = 1;
push @result, [@py2opts, '-i', "$i-dbg", '-p', $this->{pyvers}, @options];
}
}
if ($this->{pydef}) {
if (not $pyall and grep {$_ eq 'python' or $_ eq 'python-dev' or
$_ eq 'python2.7' or $_ eq 'python2.7-dev'} @deps) {
push @result, [@py2opts, '-i', $i, '-p', $this->{pydef}, @options];
}
if (not $pyalldbg and grep {$_ eq 'python-dbg' or $_ eq 'python2.7-dbg'} @deps) {
push @result, [@py2opts, '-i', "$i-dbg", '-p', $this->{pydef}, @options];
}
}
# Python 3
if ($this->{py3vers}) {
if (grep {$_ eq 'python3-all' or $_ eq 'python3-all-dev'} @deps) {
$py3all = 1;
push @result, [@py3opts, '-i', $i, '-p', $this->{py3vers}, @options];
}
if (grep {$_ eq 'python3-all-dbg'} @deps) {
$py3alldbg = 1;
push @result, [@py3opts, '-i', "$i-dbg", '-p', $this->{py3vers}, @options];
}
}
if ($this->{py3def}) {
if (not $py3all and grep {$_ eq 'python3' or $_ eq 'python3-dev'} @deps) {
push @result, [@py3opts, '-i', $i, '-p', $this->{py3def}, @options];
}
if (not $py3alldbg and grep {$_ eq 'python3-dbg'} @deps) {
push @result, [@py3opts, '-i', "$i-dbg", '-p', $this->{py3def}, @options];
}
}
# TODO: pythonX.Y → `pybuild -i python{version} -p X.Y`
# PyPy
if ($this->{pypydef} and grep {$_ eq 'pypy'} @deps) {
push @result, [@pypyopts, '-i', 'pypy', '-p', $this->{pypydef}, @options];
}
}
if (!@result) {
use Data::Dumper;
die('E: Please add appropriate interpreter package to Build-Depends, see pybuild(1) for details.' .
'this: ' . Dumper($this) .
'deps: ' . Dumper(\@deps));
}
return @result;
}
sub python_build_dependencies {
my $this=shift;
my @result;
my $c = Dpkg::Control->new(type => CTRL_INFO_SRC);
if ($c->load('debian/control')) {
for my $field (grep /^Build-Depends/, keys %{$c}) {
my $builddeps = $c->{$field};
while ($builddeps =~ /(?:^|[\s,])((pypy|python|tox)[0-9\.]*(-[^\s,\(]+)?)(?:[\s,\(]|$)/g) {
my $dep = $1;
$dep =~ s/:any$//;
if ($dep) {push @result, $dep};
}
}
}
return @result;
}
1

10
dh/pypy.pm Normal file
View File

@ -0,0 +1,10 @@
#! /usr/bin/perl
# debhelper sequence file for dh_pypy
use warnings;
use strict;
use Debian::Debhelper::Dh_Lib;
insert_before("dh_installinit", "dh_pypy");
1

12
dh/python2.pm Normal file
View File

@ -0,0 +1,12 @@
#! /usr/bin/perl
# debhelper sequence file for dh_python2
use warnings;
use strict;
use Debian::Debhelper::Dh_Lib;
insert_before("dh_installinit", "dh_python2");
remove_command("dh_pycentral");
remove_command("dh_pysupport");
1

10
dh/python3.pm Normal file
View File

@ -0,0 +1,10 @@
#! /usr/bin/perl
# debhelper sequence file for dh_python3
use warnings;
use strict;
use Debian::Debhelper::Dh_Lib;
insert_before("dh_installinit", "dh_python3");
1

309
dh_pypy Executable file
View File

@ -0,0 +1,309 @@
#! /usr/bin/python3
# vim: et ts=4 sw=4
# Copyright © 2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import os
import sys
from argparse import ArgumentParser, SUPPRESS
from os.path import exists, join
from shutil import copy as fcopy
from dhpython.debhelper import DebHelper
from dhpython.depends import Dependencies
from dhpython.interpreter import Interpreter, EXTFILE_RE
from dhpython.version import supported, default, VersionRange
from dhpython.pydist import validate as validate_pydist
from dhpython.fs import fix_locations, Scan
from dhpython.option import compiled_regex
from dhpython.tools import pyremove, parse_ns, remove_ns
# initialize script
logging.basicConfig(format='%(levelname).1s: dh_pypy '
'%(module)s:%(lineno)d: %(message)s')
log = logging.getLogger('dhpython')
os.umask(0o22)
DEFAULT = default('pypy')
SUPPORTED = supported('pypy')
class Scanner(Scan):
def handle_ext(self, fpath):
# PyPy doesn't include interpreter version in SONAME,
# its ABI is stable so f.e. PyPy 4.0 has "pypy-26" in SONAME
path, fname = fpath.rsplit('/', 1)
soabi = EXTFILE_RE.search(fname)
if soabi is None:
return
soabi = soabi.groupdict()['soabi']
if soabi is None:
return
self.current_result.setdefault('ext_soabi', set()).add(soabi)
return
def main():
parser = ArgumentParser()
parser.add_argument(
'--version', action='version', version='%prog DEVELV')
parser.add_argument(
'--no-guessing-deps', action='store_false', dest='guess_deps',
help='disable guessing dependencies')
parser.add_argument(
'--skip-private', action='store_true',
help="don't check private directories")
parser.add_argument(
'-v', '--verbose', action='store_true',
default=os.environ.get('DH_VERBOSE') == '1',
help='turn verbose mode on')
# arch=False->arch:all only, arch=True->arch:any only, None->all of them
parser.add_argument(
'-i', '--indep', action='store_false', dest='arch', default=None,
help='act on architecture independent packages')
parser.add_argument(
'-a', '-s', '--arch', action='store_true', dest='arch',
help='act on architecture dependent packages')
parser.add_argument(
'-q', '--quiet', action='store_false', dest='verbose', help='be quiet')
parser.add_argument(
'-p', '--package', action='append', metavar='PACKAGE',
help='act on the package named PACKAGE')
parser.add_argument(
'-N', '--no-package', action='append', metavar='PACKAGE',
help='do not act on the specified package')
parser.add_argument(
'--compile-all', action='store_true',
help='compile all files from given private directory in postinst, not '
'just the ones provided by the package')
parser.add_argument(
'-V', type=VersionRange, dest='vrange', metavar='[X.Y][-][A.B]',
#help='specify list of supported PyPy versions. See pypycompile(1) for '
# 'examples',
help=SUPPRESS)
parser.add_argument(
'-X', '--exclude', action='append', dest='regexpr', type=compiled_regex,
metavar='REGEXPR',
help='exclude items that match given REGEXPR. You may use this option '
'multiple times to build up a list of things to exclude.')
parser.add_argument(
'--accept-upstream-versions', action='store_true',
help='accept upstream versions while translating Python dependencies '
'into Debian ones')
parser.add_argument(
'--depends', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${pypy:Depends}. Use it for missing items in '
'requires.txt.')
parser.add_argument(
'--depends-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Depends}')
parser.add_argument(
'--recommends', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${pypy:Recommends}')
parser.add_argument(
'--recommends-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Recommends}')
parser.add_argument(
'--suggests', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${pypy:Suggests}')
parser.add_argument(
'--suggests-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Suggests}')
parser.add_argument(
'--requires', action='append', metavar='FILE',
help='translate requirements from given file into Debian dependencies '
'and add them to ${pypy:Depends}')
parser.add_argument(
'--namespace', action='append', dest='namespaces', metavar='NAMESPACE',
help='recreate __init__.py files for given namespaces at install time')
parser.add_argument(
'--shebang', metavar='COMMAND',
help='use given command as shebang in scripts')
parser.add_argument(
'--ignore-shebangs', action='store_true',
help='do not translate shebangs into Debian dependencies')
parser.add_argument(
'--ignore-namespace', action='store_true',
help="ignore Egg's namespace_packages.txt file and --namespace option")
parser.add_argument(
'--no-dbg-cleaning', action='store_false', dest='clean_dbg_pkg',
help='do not remove files from debug packages')
parser.add_argument(
'--no-ext-rename', action='store_true',
help='do not add magic tags nor multiarch tuples to extension file '
'names)')
parser.add_argument(
'--no-shebang-rewrite', action='store_true',
help='do not rewrite shebangs')
parser.add_argument('private_dir', nargs='?',
help='Private directory containing Python modules (optional)')
# debhelper options:
parser.add_argument('-O', action='append', help=SUPPRESS)
options = parser.parse_args(os.environ.get('DH_OPTIONS', '').split()
+ sys.argv[1:])
if options.O:
parser.parse_known_args(options.O, options)
private_dir = options.private_dir
if private_dir:
if not private_dir.startswith('/'):
# handle usr/share/foo dirs (without leading slash)
private_dir = '/' + private_dir
# TODO: support more than one private dir at the same time (see :meth:scan)
if options.skip_private:
private_dir = False
if options.verbose:
log.setLevel(logging.DEBUG)
log.debug('version: DEVELV')
log.debug('argv: %s', sys.argv)
log.debug('options: %s', options)
log.debug('supported PyPy versions: %s (default=%s)',
','.join(str(v) for v in SUPPORTED), DEFAULT)
else:
log.setLevel(logging.INFO)
try:
dh = DebHelper(options, impl='pypy')
except Exception as e:
log.error('cannot initialize DebHelper: %s', e)
exit(2)
if not dh.packages:
log.error('no package to act on (pypy-foo or one with ${pypy:Depends} in Depends)')
# exit(7)
if not options.vrange and dh.python_version:
options.vrange = VersionRange(dh.python_version)
interpreter = Interpreter('pypy')
for package, pdetails in dh.packages.items():
log.debug('processing package %s...', package)
interpreter.debug = package.endswith('-dbg')
if not private_dir:
try:
pyremove(interpreter, package, options.vrange)
except Exception as err:
log.error("%s.pyremove: %s", package, err)
exit(5)
fix_locations(package, interpreter, SUPPORTED, options)
stats = Scanner(interpreter, package, private_dir, options).result
dependencies = Dependencies(package, 'pypy', dh.build_depends)
dependencies.parse(stats, options)
if stats['ext_vers']:
dh.addsubstvar(package, 'pypy:Versions',
', '.join(str(v) for v in sorted(stats['ext_vers'])))
ps = package.split('-', 1)
if len(ps) > 1 and ps[0] == 'pypy':
dh.addsubstvar(package, 'pypy:Provides',
', '.join("pypy%s-%s" % (i, ps[1])
for i in sorted(stats['ext_vers'])))
pypyclean_added = False # invoke pypyclean only once in maintainer script
if stats['compile']:
args = ''
if options.vrange:
args += "-V %s" % options.vrange
dh.autoscript(package, 'postinst', 'postinst-pypycompile', args)
dh.autoscript(package, 'prerm', 'prerm-pypyclean', '')
pypyclean_added = True
for pdir, details in sorted(stats['private_dirs'].items()):
if not details.get('compile'):
continue
if not pypyclean_added:
dh.autoscript(package, 'prerm', 'prerm-pypyclean', '')
pypyclean_added = True
args = pdir
ext_for = details.get('ext_vers')
ext_no_version = details.get('ext_no_version')
if ext_for is None and not ext_no_version: # no extension
shebang_versions = list(i.version for i in details.get('shebangs', [])
if i.version and i.version.minor)
if not options.ignore_shebangs and len(shebang_versions) == 1:
# only one version from shebang
args += " -V %s" % shebang_versions[0]
elif options.vrange and options.vrange != (None, None):
args += " -V %s" % options.vrange
elif ext_no_version:
# at least one extension's version not detected
if options.vrange and '-' not in str(options.vrange):
ver = str(options.vrange)
else: # try shebang or default PyPy version
ver = (list(i.version for i in details.get('shebangs', [])
if i.version and i.version.minor) or [None])[0] or DEFAULT
dependencies.depend("pypy%s" % ver)
args += " -V %s" % ver
else:
extensions = sorted(ext_for)
vr = VersionRange(minver=extensions[0], maxver=extensions[-1])
args += " -V %s" % vr
for regex in options.regexpr or []:
args += " -X '%s'" % regex.pattern.replace("'", r"'\''")
dh.autoscript(package, 'postinst', 'postinst-pypycompile', args)
dependencies.export_to(dh)
pydist_file = join('debian', "%s.pydist" % package)
if exists(pydist_file):
if not validate_pydist(pydist_file):
log.warning("%s.pydist file is invalid", package)
else:
dstdir = join('debian', package, 'usr/share/pypy/dist/')
if not exists(dstdir):
os.makedirs(dstdir)
fcopy(pydist_file, join(dstdir, package))
# namespace feature - recreate __init__.py files at install time
if options.ignore_namespace:
nsp = None
else:
nsp = parse_ns(stats['nsp.txt'], options.namespaces)
# note that pypycompile/pypyclean is already added to maintainer scripts
# and it should remain there even if __init__.py was the only .py file
if nsp:
try:
nsp = remove_ns(Interpreter('pypy'), package, nsp,
stats['public_vers'])
except (IOError, OSError) as e:
log.error('cannot remove __init__.py from package: %s', e)
exit(6)
if nsp:
dstdir = join('debian', package, 'usr/share/pypy/ns/')
if not exists(dstdir):
os.makedirs(dstdir)
with open(join(dstdir, package), 'a', encoding='utf-8') as fp:
fp.writelines("%s\n" % i for i in sorted(nsp))
dh.save()
if __name__ == '__main__':
main()

165
dh_pypy.rst Normal file
View File

@ -0,0 +1,165 @@
=========
dh_pypy
=========
---------------------------------------------------------------------------------
calculates PyPy dependencies, adds maintainer scripts to byte compile files, etc.
---------------------------------------------------------------------------------
:Manual section: 1
:Author: Piotr Ożarowski, 2013
SYNOPSIS
========
dh_pypy -p PACKAGE [-V [X.Y][-][A.B]] DIR [-X REGEXPR]
DESCRIPTION
===========
QUICK GUIDE FOR MAINTAINERS
---------------------------
* build-depend on pypy and dh-python,
* add `${pypy:Depends}` to Depends
* build module/application using its standard build system,
* install files to the standard locations,
* add `pypy` to dh's --with option, or:
* call ``dh_pypy`` in the `binary-*` target,
NOTES
-----
dependencies
~~~~~~~~~~~~
dh_pypy tries to translate Python dependencies from the `requires.txt` file
to Debian dependencies. In many cases, this works without any additional
configuration because dh_pypy comes with a build-in mapping of Python module
names to Debian packages that is periodically regenerated from the Debian
archive. By default, the version information in the Python dependencies is
discarded. If you want dh_pypy to generate more strict dependencies (e.g. to
avoid ABI problems), or if the automatic mapping does not work correctly for
your package, you have to provide dh_pypy with additional rules for the
translation of Python module to Debian package dependencies.
For a package *pypy-foo* that depends on a package *pypy-bar*, there are
two files that may provide such rules:
#. If the *pypy-foo* source package ships with a
`debian/pypy-overrides` file, this file is used by dh_pypy
during the build of *pypy-foo*.
#. If the *pypy-bar* source package ships with a
`debian/pypy-bar.pydist` file (and uses dh_pypy), this file
will be included in the binary package as
`/usr/share/dh-python/dist/pypy/pypy-bar`. During the build
of *pypy-foo*, dh_pypy will then find and use the file.
Both files have the same format described in
`/usr/share/doc/dh-python/README.PyDist`. If all you want is to generate
versioned dependencies (and assuming that the *pypy-bar* package provides
the *pybar* Python module), in most cases it will be sufficient to put the line
``pybar pypy-bar; PEP386`` into either of the above files.
namespace feature
~~~~~~~~~~~~~~~~~
dh_pypy parses Egg's namespace_packages.txt files (in addition to
--namespace command line argument(s)) and drops empty __init__.py files from
binary package. pypycompile will regenerate them at install time and pypyclean
will remove them at uninstall time (if they're no longer used in installed
packages). It's still a good idea to provide __init__.py file in one of
binary packages (even if all other packages use this feature).
private dirs
~~~~~~~~~~~~
`/usr/share/foo`, `/usr/share/games/foo`, `/usr/lib/foo` and
`/usr/lib/games/foo` private directories are scanned for Python files by
default (where `foo` is binary package name). If your package ships
Python files in some other directory, add another dh_pypy call in debian/rules
with directory name as an argument - you can use different set of options in
this call. If you need to change options for a private directory that is
checked by default, invoke dh_pypy with --skip-private option and add another
call with a path to this directory and new options.
debug packages
~~~~~~~~~~~~~~
In binary packages which name ends with `-dbg`, all files in
`/usr/lib/pypy/dist-packages/` directory that have extensions different than
`so` or `h` are removed by default. Use --no-dbg-cleaning option to disable
this feature.
overriding supported / default PyPy versions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to override system's list of supported PyPy versions or the
default one (f.e. to build a package that includes symlinks for older version
of PyPy or compile .py files only for given interpreter version), you can do
that via `DEBPYPY_SUPPORTED` and/or `DEBPYPY_DEFAULT` env. variables.
OPTIONS
=======
--version show program's version number and exit
-h, --help show help message and exit
--no-guessing-deps disable guessing dependencies
--no-dbg-cleaning do not remove any files from debug packages
--no-ext-rename do not add magic tags nor multiarch tuples to extension file names
--no-shebang-rewrite do not rewrite shebangs
--skip-private don't check private directories
-v, --verbose turn verbose mode on
-i, --indep act on architecture independent packages
-a, --arch act on architecture dependent packages
-q, --quiet be quiet
-p PACKAGE, --package=PACKAGE act on the package named PACKAGE
-N NO_PACKAGE, --no-package=NO_PACKAGE do not act on the specified package
-X REGEXPR, --exclude=REGEXPR exclude items that match given REGEXPR. You may
use this option multiple times to build up a list of things to exclude.
--compile-all compile all files from given private directory in postinst/rtupdate
not just the ones provided by the package (i.e. do not pass the --package
parameter to py3compile/py3clean)
--accept-upstream-versions accept upstream versions while translating
Python dependencies into Debian ones
--depends=DEPENDS translate given requirements into Debian dependencies
and add them to ${pypy:Depends}. Use it for missing items in requires.txt
--depends-sections=SECTIONS translate requirements from given sections of
requres.txt file into Debian dependencies and add them to ${pypy:Depends}.
--recommends=RECOMMENDS translate given requirements into Debian dependencies
and add them to ${pypy:Recommends}
--recommends-sections=SECTIONS translate requirements from given sections of
requres.txt file into Debian dependencies and add them to ${pypy:Recommends}.
--suggests=SUGGESTS translate given requirements into Debian dependencies
and add them to ${pypy:Suggests}
--suggests-sections=SECTIONS translate requirements from given sections of
requres.txt file into Debian dependencies and add them to ${pypy:Suggests}.
--requires=FILENAME translate requirements from given file(s) into Debian
dependencies and add them to ${pypy:Depends}
--shebang=COMMAND use given command as shebang in scripts
--ignore-shebangs do not translate shebangs into Debian dependencies
SEE ALSO
========
* /usr/share/doc/dh-python/README.PyDist
* pybuild(1)
* http://deb.li/dhpy - most recent version of this document

561
dh_python2 Executable file
View File

@ -0,0 +1,561 @@
#! /usr/bin/python3
# vim: et ts=4 sw=4
# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import os
import re
import sys
from filecmp import dircmp, cmpfiles, cmp as fcmp
from argparse import ArgumentParser, SUPPRESS
from os.path import isdir, islink, exists, join, splitext, realpath
from shutil import copy as fcopy
from dhpython.debhelper import DebHelper
from dhpython.depends import Dependencies
from dhpython.interpreter import Interpreter
from dhpython.fs import fix_locations, Scan
from dhpython.version import supported, default, VersionRange, \
get_requested_versions
from dhpython.pydist import validate as validate_pydist
from dhpython.tools import relative_symlink, so2pyver, parse_ns, remove_ns,\
pyinstall, pyremove
from dhpython.option import compiled_regex
# initialize script
logging.basicConfig(format='%(levelname).1s: dh_python2 '
'%(module)s:%(lineno)d: %(message)s')
log = logging.getLogger('dhpython')
os.umask(0o22)
DEFAULT = default('cpython2')
SUPPORTED = supported('cpython2')
fext = lambda fname: splitext(fname)[-1][1:]
class Scanner(Scan):
def handle_ext(self, fpath):
so_version = so2pyver(fpath)
if so_version:
path, fn = fpath.rsplit('/', 1)
if self.current_pub_version:
if self.current_pub_version != so_version:
log.error('extension linked to libpython%s '
'and shipped in python%s\'s dist-'
'packages: %s',
so_version, self.current_pub_version, fn)
log.warn('public extension linked with '
'libpython%s: %s', so_version, fn)
return so_version
### SHARING FILES ##############################################
def share(package, stats, options):
"""Move files to /usr/share/pyshared/ if possible."""
if package.endswith('-dbg'):
# nothing to share in debug packages
return
interpreter = Interpreter('python')
pubvers = sorted(v for v in stats['public_vers'] if v.major == 2)
if len(pubvers) > 1:
for pos, version1 in enumerate(pubvers):
dir1 = interpreter.sitedir(package, version1)
if not exists(dir1):
continue
for version2 in pubvers[pos + 1:]:
dir2 = interpreter.sitedir(package, version2)
if exists(dir2):
dc = dircmp(dir1, dir2)
share_2x(dir1, dir2, dc)
# elif len(pubvers) == 1:
# move_to_pyshared(interpreter.sitedir(package, pubvers[0]))
# for version in stats['ext_vers']:
# create_ext_links(interpreter.sitedir(package, version))
if options.guess_versions and pubvers:
for version in get_requested_versions('cpython2', options.vrange):
if version not in pubvers:
interpreter.version = version
log.debug('guessing files for %s', interpreter)
versions_without_ext = sorted(set(pubvers) -
stats['ext_vers'])
if not versions_without_ext:
log.error('extension for python%s is missing. '
'Build extensions for all supported Python '
'versions (`pyversions -vr`) or adjust '
'X-Python-Version field or pass '
'--no-guessing-versions to dh_python2',
version)
exit(3)
srcver = versions_without_ext[0]
if srcver in stats['public_vers']:
stats['public_vers'].add(version)
share_2x(interpreter.sitedir(package, srcver),
interpreter.sitedir(package, version))
# remove duplicates
stats['requires.txt'] = set(realpath(i) for i in stats['requires.txt'])
stats['nsp.txt'] = set(realpath(i) for i in stats['nsp.txt'])
# def move_to_pyshared(dir1):
# # dir1 starts with debian/packagename/usr/lib/pythonX.Y/*-packages/
# debian, package, path = dir1.split('/', 2)
# dstdir = join(debian, package, 'usr/share/pyshared/',
# '/'.join(dir1.split('/')[6:]))
#
# for i in os.listdir(dir1):
# fpath1 = join(dir1, i)
# if isdir(fpath1) and not islink(fpath1):
# if any(fn for fn in os.listdir(fpath1) if fext(fn) != 'so'):
# # at least one file that is not an extension
# move_to_pyshared(join(dir1, i))
# else:
# if fext(i) == 'so':
# continue
# fpath2 = join(dstdir, i)
# if not exists(fpath2):
# if not exists(dstdir):
# os.makedirs(dstdir)
# if islink(fpath1):
# fpath1_target = os.readlink(fpath1)
# if isabs(fpath1_target):
# os.symlink(fpath1_target, fpath2)
# else:
# fpath1_target = normpath(join(dir1, fpath1_target))
# relative_symlink(fpath1_target, fpath2)
# os.remove(fpath1)
# else:
# os.rename(fpath1, fpath2)
# relative_symlink(fpath2, fpath1)
#
#
# def create_ext_links(dir1):
# """Create extension symlinks in /usr/lib/pyshared/pythonX.Y.
#
# These symlinks are used to let dpkg detect file conflicts with
# python-support and python-central packages.
# """
#
# debian, package, path = dir1.split('/', 2)
# python, _, module_subpath = path[8:].split('/', 2)
# dstdir = join(debian, package, 'usr/lib/pyshared/', python, module_subpath)
#
# for i in os.listdir(dir1):
# fpath1 = join(dir1, i)
# if isdir(fpath1):
# create_ext_links(fpath1)
# elif fext(i) == 'so':
# fpath2 = join(dstdir, i)
# if exists(fpath2):
# continue
# if not exists(dstdir):
# os.makedirs(dstdir)
# relative_symlink(fpath1, join(dstdir, i))
def create_public_links(dir1, vrange, root=''):
"""Create public module symlinks for given directory."""
debian, package, path = dir1.split('/', 2)
interpreter = Interpreter('python')
versions = get_requested_versions('cpython2', vrange)
for fn in os.listdir(dir1):
fpath1 = join(dir1, fn)
if isdir(fpath1):
create_public_links(fpath1, vrange, join(root, fn))
else:
for version in versions:
dstdir = join(interpreter.sitedir(package, version), root)
if not exists(dstdir):
os.makedirs(dstdir)
relative_symlink(fpath1, join(dstdir, fn))
def share_2x(dir1, dir2, dc=None):
"""Move common files to pyshared and create symlinks in original
locations."""
debian, package, path = dir2.split('/', 2)
# dir1 starts with debian/packagename/usr/lib/pythonX.Y/*-packages/
dstdir = join(debian, package, 'usr/share/pyshared/',
'/'.join(dir1.split('/')[6:]))
if not exists(dstdir) and not islink(dir1):
os.makedirs(dstdir)
if dc is None: # guess/copy mode
if not exists(dir2):
os.makedirs(dir2)
common_dirs = []
common_files = []
for i in os.listdir(dir1):
subdir1 = join(dir1, i)
if isdir(subdir1) and not islink(subdir1):
common_dirs.append([i, None])
else:
# directories with .so files will be blocked earlier
common_files.append(i)
elif islink(dir1):
# skip this symlink in pyshared
# (dpkg has problems with symlinks anyway)
common_dirs = []
common_files = []
else:
common_dirs = dc.subdirs.items()
common_files = dc.common_files
# dircmp returns common names only, lets check files more carefully...
common_files = cmpfiles(dir1, dir2, common_files, shallow=False)[0]
for fn in common_files:
if 'so' in fn.split('.') and not fn.startswith('so'):
# foo.so, bar.so.0.1.2, etc.
# in unlikely case where extensions are exactly the same
continue
fpath1 = join(dir1, fn)
fpath2 = join(dir2, fn)
fpath3 = join(dstdir, fn)
# do not touch symlinks created by previous loop or other tools
if dc and not islink(fpath1):
# replace with a link to pyshared
if not exists(fpath3):
os.rename(fpath1, fpath3)
relative_symlink(fpath3, fpath1)
elif fcmp(fpath3, fpath1, shallow=False):
os.remove(fpath1)
relative_symlink(fpath3, fpath1)
if dc is None: # guess/copy mode
if islink(fpath1):
# ralative links will work as well, it's always the same level
os.symlink(os.readlink(fpath1), fpath2)
else:
if exists(fpath3):
# cannot share it, pyshared contains another copy
fcopy(fpath1, fpath2)
else:
# replace with a link to pyshared
os.rename(fpath1, fpath3)
relative_symlink(fpath3, fpath1)
relative_symlink(fpath3, fpath2)
elif exists(fpath2) and exists(fpath3) and \
fcmp(fpath2, fpath3, shallow=False):
os.remove(fpath2)
relative_symlink(fpath3, fpath2)
for dn, dc in common_dirs:
share_2x(join(dir1, dn), join(dir2, dn), dc)
################################################################
def main():
parser = ArgumentParser()
parser.add_argument(
'--version', action='version', version='%(prog)s DEVELV')
parser.add_argument(
'--no-guessing-versions', action='store_false', dest='guess_versions',
help='disable guessing other supported Python versions')
parser.add_argument(
'--no-guessing-deps', action='store_false', dest='guess_deps',
help='disable guessing dependencies')
parser.add_argument(
'--skip-private', action='store_true',
help="don't check private directories")
parser.add_argument(
'-v', '--verbose', action='store_true',
default=os.environ.get('DH_VERBOSE') == '1',
help='turn verbose mode on')
# arch=False->arch:all only, arch=True->arch:any only, None->all of them
parser.add_argument(
'-i', '--indep', action='store_false', dest='arch', default=None,
help='act on architecture independent packages')
parser.add_argument(
'-a', '-s', '--arch', action='store_true', dest='arch',
help='act on architecture dependent packages')
parser.add_argument(
'-q', '--quiet', action='store_false', dest='verbose',
help='be quiet')
parser.add_argument(
'-p', '--package', action='append', metavar='PACKAGE',
help='act on the package named PACKAGE')
parser.add_argument(
'-N', '--no-package', action='append', metavar='PACKAGE',
help='do not act on the specified package')
parser.add_argument(
'--compile-all', action='store_true',
help='compile all files from given private directory in postinst, not '
'just the ones provided by the package')
parser.add_argument(
'-V', type=VersionRange, dest='vrange', metavar='[X.Y][-][A.B]',
help='specify list of supported Python versions. See pycompile(1) for '
'examples')
parser.add_argument(
'-X', '--exclude', action='append', dest='regexpr', type=compiled_regex,
metavar='REGEXPR',
help='exclude items that match given REGEXPR. You may use this option '
'multiple times to build up a list of things to exclude.')
parser.add_argument(
'--accept-upstream-versions', action='store_true',
help='accept upstream versions while translating Python dependencies '
'into Debian ones')
parser.add_argument(
'--depends', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${python:Depends}. Use it for missing items in '
'requires.txt.')
parser.add_argument(
'--depends-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Depends}')
parser.add_argument(
'--recommends', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${python:Recommends}')
parser.add_argument(
'--recommends-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Recommends}')
parser.add_argument(
'--suggests', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${python:Suggests}')
parser.add_argument(
'--suggests-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Suggests}')
parser.add_argument(
'--requires', action='append', metavar='FILE',
help='translate requirements from given file into Debian dependencies '
'and add them to ${python:Depends}')
parser.add_argument(
'--namespace', action='append', dest='namespaces', metavar='NAMESPACE',
help='recreate __init__.py files for given namespaces at install time')
parser.add_argument(
'--clean-pycentral', action='store_true',
help='generate maintainer script that will remove pycentral files')
parser.add_argument(
'--shebang', metavar='COMMAND',
help='use given command as shebang in scripts')
parser.add_argument(
'--ignore-shebangs', action='store_true',
help='do not translate shebangs into Debian dependencies')
parser.add_argument(
'--ignore-namespace', action='store_true',
help="ignore Egg's namespace_packages.txt file and --namespace option")
parser.add_argument(
'--no-dbg-cleaning', action='store_false', dest='clean_dbg_pkg',
help='do not remove files from debug packages')
parser.add_argument(
'--no-ext-rename', action='store_true',
help='do not add magic tags nor multiarch tuples to extension file '
'names)')
parser.add_argument(
'--no-shebang-rewrite', action='store_true',
help='do not rewrite shebangs')
parser.add_argument('private_dir', nargs='?',
help='Private directory containing Python modules (optional)')
# debhelper options:
parser.add_argument('-O', action='append', help=SUPPRESS)
options = parser.parse_args(os.environ.get('DH_OPTIONS', '').split()
+ sys.argv[1:])
if options.O:
parser.parse_known_args(options.O, options)
if not options.vrange and exists('debian/pyversions'):
log.debug('parsing version range from debian/pyversions')
with open('debian/pyversions', encoding='utf-8') as fp:
for line in fp:
line = line.strip()
if line and not line.startswith('#'):
options.vrange = VersionRange(line)
break
# disable PyDist if dh_pydeb is used
if options.guess_deps:
try:
rules = open('debian/rules', 'r', encoding='utf-8').read()
except IOError:
log.warning('cannot open debian/rules file')
else:
if re.search('\n\s*dh_pydeb', rules) or \
re.search('\n\s*dh\s+[^#]*--with[^#]+pydeb', rules):
log.info('dh_pydeb detected, PyDist feature disabled')
options.guess_deps = False
private_dir = options.private_dir
if private_dir:
if not private_dir.startswith('/'):
# handle usr/share/foo dirs (without leading slash)
private_dir = '/' + private_dir
# TODO: support more than one private dir at the same time (see :meth:scan)
if options.skip_private:
private_dir = False
if options.verbose:
log.setLevel(logging.DEBUG)
log.debug('version: DEVELV')
log.debug('argv: %s', sys.argv)
log.debug('options: %s', options)
log.debug('supported Python versions: %s (default=%s)',
','.join(str(v) for v in SUPPORTED), DEFAULT)
else:
log.setLevel(logging.INFO)
try:
dh = DebHelper(options, impl='cpython2')
except Exception as e:
log.error('cannot initialize DebHelper: %s', e)
exit(2)
if not dh.packages:
log.error('no package to act on (python-foo or one with ${python:Depends} in Depends)')
# exit(7)
if not options.vrange and dh.python_version:
options.vrange = VersionRange(dh.python_version)
interpreter = Interpreter('python')
for package, pdetails in dh.packages.items():
log.debug('processing package %s...', package)
interpreter.debug = package.endswith('-dbg')
if not private_dir:
try:
pyinstall(interpreter, package, options.vrange)
except Exception as err:
log.error("%s.pyinstall: %s", package, err)
exit(4)
try:
pyremove(interpreter, package, options.vrange)
except Exception as err:
log.error("%s.pyremove: %s", package, err)
exit(5)
fix_locations(package, interpreter, SUPPORTED, options)
stats = Scanner(interpreter, package, private_dir, options).result
if not private_dir:
share(package, stats, options)
pyshared_dir = "debian/%s/usr/share/pyshared/" % package
if not stats['public_vers'] and exists(pyshared_dir):
create_public_links(pyshared_dir, options.vrange)
stats = Scanner(interpreter, package, private_dir, options).result
dependencies = Dependencies(package, 'cpython2', dh.build_depends)
dependencies.parse(stats, options)
if stats['public_vers']:
dh.addsubstvar(package, 'python:Versions',
', '.join(str(i) for i in sorted(stats['public_vers'])))
ps = package.split('-', 1)
if len(ps) > 1 and ps[0] == 'python':
dh.addsubstvar(package, 'python:Provides',
', '.join("python%s-%s" % (i, ps[1])
for i in sorted(stats['public_vers'])))
pyclean_added = False # invoke pyclean only once in maintainer script
if stats['compile']:
if options.clean_pycentral:
dh.autoscript(package, 'preinst',
'preinst-pycentral-clean', '')
dh.autoscript(package, 'postinst', 'postinst-pycompile', '')
dh.autoscript(package, 'prerm', 'prerm-pyclean', '')
pyclean_added = True
for pdir, details in sorted(stats['private_dirs'].items()):
if not details.get('compile'):
continue
if not pyclean_added:
dh.autoscript(package, 'prerm', 'prerm-pyclean', '')
pyclean_added = True
args = pdir
ext_for = details.get('ext_vers')
ext_no_version = details.get('ext_no_version')
if ext_for is None and not ext_no_version: # no extension
shebang_versions = list(i.version for i in details.get('shebangs', [])
if i.version and i.version.minor)
if not options.ignore_shebangs and len(shebang_versions) == 1:
# only one version from shebang
args += " -V %s" % shebang_versions[0]
elif options.vrange and options.vrange != (None, None):
args += " -V %s" % options.vrange
elif ext_no_version:
# at least one extension's version not detected
if options.vrange and '-' not in str(options.vrange):
ver = str(options.vrange)
else: # try shebang or default Python version
ver = (list(i.version for i in details.get('shebangs', [])
if i.version and i.version.minor) or [None])[0] or DEFAULT
dependencies.depend("python%s" % ver)
args += " -V %s" % ver
else:
version = ext_for.pop()
args += " -V %s" % version
dependencies.depend("python%s" % version)
for regex in options.regexpr or []:
args += " -X '%s'" % regex.pattern.replace("'", r"'\''")
dh.autoscript(package, 'postinst', 'postinst-pycompile', args)
dependencies.export_to(dh)
pydist_file = join('debian', "%s.pydist" % package)
if exists(pydist_file):
if not validate_pydist(pydist_file):
log.warning("%s.pydist file is invalid", package)
else:
dstdir = join('debian', package, 'usr/share/python/dist/')
if not exists(dstdir):
os.makedirs(dstdir)
fcopy(pydist_file, join(dstdir, package))
bcep_file = join('debian', "%s.bcep" % package)
if exists(bcep_file):
dstdir = join('debian', package, 'usr/share/python/bcep/')
if not exists(dstdir):
os.makedirs(dstdir)
fcopy(bcep_file, join(dstdir, package))
# namespace feature - recreate __init__.py files at install time
if options.ignore_namespace:
nsp = None
else:
nsp = parse_ns(stats['nsp.txt'], options.namespaces)
# note that pycompile/pyclean is already added to maintainer scripts
# and it should remain there even if __init__.py was the only .py file
if nsp:
try:
nsp = remove_ns(Interpreter('python'), package, nsp,
stats['public_vers'])
except (IOError, OSError) as e:
log.error('cannot remove __init__.py from package: %s', e)
exit(6)
if nsp:
dstdir = join('debian', package, 'usr/share/python/ns/')
if not exists(dstdir):
os.makedirs(dstdir)
with open(join(dstdir, package), 'a', encoding='utf-8') as fp:
fp.writelines("%s\n" % i for i in sorted(nsp))
pyshared = join('debian', package, 'usr/share/pyshared/')
if isdir(pyshared) and not os.listdir(pyshared):
# remove empty pyshared directory
os.rmdir(pyshared)
dh.save()
if __name__ == '__main__':
main()

236
dh_python2.rst Normal file
View File

@ -0,0 +1,236 @@
============
dh_python2
============
-----------------------------------------------------------------------------------
calculates Python dependencies, adds maintainer scripts to byte compile files, etc.
-----------------------------------------------------------------------------------
:Manual section: 1
:Author: Piotr Ożarowski, 2012-2013
SYNOPSIS
========
dh_python2 -p PACKAGE [-V [X.Y][-][A.B]] DIR_OR_FILE [-X REGEXPR]
DESCRIPTION
===========
QUICK GUIDE FOR MAINTAINERS
---------------------------
* if necessary, describe supported Python versions via X-Python-Version field
in debian/control,
* build depend on dh-python
* build-depend on python or python-all or python-all-dev (>= 2.6.6-3~),
* build module/application using its standard build system,
remember to build extensions for all supported Python versions (loop over
``pyversions -vr``),
* install files to the *standard* locations, add `--install-layout=deb` to
setup.py's install command if your package is using distutils,
* add `python2` to dh's --with option, or:
* `include /usr/share/cdbs/1/class/python-distutils.mk` in debian/rules and
depend on `cdbs (>= 0.4.90)`, or:
* call ``dh_python2`` in the `binary-*` target,
* add `${python:Depends}` to Depends
NOTES
-----
In order to support more than one Python version in the same binary package,
dh_python2 (unlike dh_pycentral and dh_pysupport) creates symlinks to all
supported Python versions at build time. It means binNMU (or sourceful upload
in case of architecture independent packages) is required once a list of
supported Python version is changed. It's faster and more robust than its
competitors, though.
dependencies
~~~~~~~~~~~~
dh_python2 tries to translate Python dependencies from the `requires.txt` file
to Debian dependencies. In many cases, this works without any additional
configuration because dh_python2 comes with a build-in mapping of Python module
names to Debian packages that is periodically regenerated from the Debian
archive. By default, the version information in the Python dependencies is
discarded. If you want dh_python2 to generate more strict dependencies (e.g. to
avoid ABI problems), or if the automatic mapping does not work correctly for
your package, you have to provide dh_python2 with additional rules for the
translation of Python module to Debian package dependencies.
For a package *python-foo* that depends on a package *python-bar*, there are
two files that may provide such rules:
#. If the *python-foo* source package ships with a
`debian/pydist-overrides` file, this file is used by dh_python
during the build of *python-foo*.
#. If the *python-bar* source package ships with a
`debian/python-bar.pydist` file (and uses dh_python), this file
will be included in the binary package as
`/usr/share/dh-python/dist/cpython2/python-bar`. During the build
of *python-foo*, dh_python will then find and use the file.
Both files have the same format described in
`/usr/share/doc/dh-python/README.PyDist`. If all you want is to generate
versioned dependencies (and assuming that the *python-bar* package provides
the *pybar* Python module), in most cases it will be sufficient to put the line
``pybar python-bar; PEP386`` into either of the above files.
namespace feature
~~~~~~~~~~~~~~~~~
dh_python2 parses Egg's namespace_packages.txt files (in addition to
--namespace command line argument(s)) and drops empty __init__.py files from
binary package. pycompile will regenerate them at install time and pyclean
will remove them at uninstall time (if they're no longer used in installed
packages). It's still a good idea to provide __init__.py file in one of
binary packages (even if all other packages use this feature).
private dirs
~~~~~~~~~~~~
`/usr/share/foo`, `/usr/share/games/foo`, `/usr/lib/foo` and
`/usr/lib/games/foo` private directories are scanned for Python files
by default (where `foo` is binary package name). If your package ships
Python files in some other directory, add another dh_python2 call in
debian/rules with directory name as an argument - you can use different set of
options in this call. If you need to change options (f.e. a list of supported
Python versions) for a private directory that is checked by default, invoke
dh_python2 with --skip-private option and add another call with a path to this
directory and new options.
debug packages
~~~~~~~~~~~~~~
In binary packages which name ends with `-dbg`, all files in
`/usr/lib/python2.X/{site,dist}-packages/` directory
that have extensions different than `so` or `h` are removed by default.
Use --no-dbg-cleaning option to disable this feature.
pyinstall files
~~~~~~~~~~~~~~~
Files listed in debian/pkg.pyinstall file will be installed as public modules
(i.e. into .../dist-packages/ directory) for all requested Python versions
(dh_install doesn't know about python's site- vs. dist-packages issue).
Syntax: ``path/to/file [NAMESPACE] [VERSION_RANGE]``
debian directory is automatically removed from the path, so you can place your
files in debian/ directory and install them from this location (if you want to
install them in "debian" namespace, set NAMESPACE to debian). If NAMESPACE is
set, all listed files will be installed in .../dist-packages/NAMESPACE/
directory.
Examples:
* ``foo.py`` installs .../dist-packages/foo.py for all supported Python versions
* ``foo/bar.py 2.6-`` installs .../dist-packages/foo/bar.py for versions >= 2.6
* ``foo/bar.py spam`` installs .../dist-packages/spam/bar.py
* ``debian/*.py spam.egg 2.5`` installs .../python2.5/site-packages/spam/egg/\*.py
files
pyremove files
~~~~~~~~~~~~~~
If you want to remove some public modules (i.e. files in .../dist-packages/
directory) installed by build system (from all supported Python versions or
only from a subset of these versions), add them to debian/pkg.pyremove file.
Examples:
* ``*.pth`` removes .pth files from .../dist-packages/
* ``bar/baz.py 2.5`` removes .../python2.5/site-packages/bar/baz.py
overriding supported / default Python versions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to override system's list of supported Python versions or the
default one (f.e. to build a package that includes symlinks for older version
of Python or compile .py files only for given interpreter version), you can do
that via `DEBPYTHON_SUPPORTED` and/or `DEBPYTHON_DEFAULT` env. variables.
Example: ``2.5,2.7`` limits the list of supported Python versions to Python 2.5
and Python 2.7.
OPTIONS
=======
--version show program's version number and exit
-h, --help show help message and exit
--no-guessing-versions disable guessing other supported Python versions
--no-guessing-deps disable guessing dependencies
--no-dbg-cleaning do not remove any files from debug packages
--no-ext-rename do not add magic tags nor multiarch tuples to extension file names
--no-shebang-rewrite do not rewrite shebangs
--skip-private don't check private directories
-v, --verbose turn verbose mode on
-i, --indep act on architecture independent packages
-a, --arch act on architecture dependent packages
-q, --quiet be quiet
-p PACKAGE, --package=PACKAGE act on the package named PACKAGE
-N NO_PACKAGE, --no-package=NO_PACKAGE do not act on the specified package
-V VRANGE specify list of supported Python versions. See
pycompile(1) for examples
-X REGEXPR, --exclude=REGEXPR exclude items that match given REGEXPR. You may
use this option multiple times to build up a list of things to exclude.
--compile-all compile all files from given private directory in postinst/rtupdate
not just the ones provided by the package (i.e. do not pass the --package
parameter to pycompile/pyclean)
--accept-upstream-versions accept upstream versions while translating
Python dependencies into Debian ones
--depends=DEPENDS translate given requirements into Debian dependencies
and add them to ${python:Depends}. Use it for missing items in requires.txt
--depends-section=SECTION translate requirements from given sections of
requres.txt file into Debian dependencies and add them to ${python:Depends}.
--recommends=RECOMMENDS translate given requirements into Debian dependencies
and add them to ${python:Recommends}
--recommends-section=SECTION translate requirements from given sections of
requres.txt file into Debian dependencies and add them to ${python:Recommends}.
--suggests=SUGGESTS translate given requirements into Debian dependencies
and add them to ${python:Suggests}
--suggests-section=SECTION translate requirements from given sections of
requres.txt file into Debian dependencies and add them to ${python:Suggests}.
--requires=FILENAME translate requirements from given file(s) into Debian
dependencies and add them to ${python:Depends}
--namespace=NAME use this option (multiple time if necessary) if
namespace_packages.txt is not complete
--ignore-namespace ignore Egg's namespace declaration and
--namespace option. This option will disable removing (and recreating at
install time) empty __init__.py files. Removing namespace_packages.txt from
egg-info directory has the same effect.
--clean-pycentral generate maintainer script that will remove byte code
generated by python-central helper
--shebang=COMMAND use given command as shebang in scripts
--ignore-shebangs do not translate shebangs into Debian dependencies
SEE ALSO
========
* /usr/share/doc/python/python-policy.txt.gz
* /usr/share/doc/dh-python/README.PyDist
* pybuild(1)
* pycompile(1), pyclean(1)
* dh_python3(1), py3compile(1), py3clean(1)
* Wiki page about converting package to dh_python2:
http://wiki.debian.org/Python/TransitionToDHPython2
* http://deb.li/dhp2 - most recent version of this document

284
dh_python3 Executable file
View File

@ -0,0 +1,284 @@
#! /usr/bin/python3
# vim: et ts=4 sw=4
# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import os
import sys
from argparse import ArgumentParser, SUPPRESS
from os.path import exists, join
from shutil import copy as fcopy
from dhpython.debhelper import DebHelper
from dhpython.depends import Dependencies
from dhpython.interpreter import Interpreter, EXTFILE_RE
from dhpython.version import supported, default, Version, VersionRange
from dhpython.pydist import validate as validate_pydist
from dhpython.fs import fix_locations, Scan
from dhpython.option import compiled_regex
from dhpython.tools import pyinstall, pyremove
# initialize script
logging.basicConfig(format='%(levelname).1s: dh_python3 '
'%(module)s:%(lineno)d: %(message)s')
log = logging.getLogger('dhpython')
os.umask(0o22)
DEFAULT = default('cpython3')
SUPPORTED = supported('cpython3')
class Scanner(Scan):
def handle_ext(self, fpath):
path, fname = fpath.rsplit('/', 1)
tagver = EXTFILE_RE.search(fname)
if tagver is None:
# yeah, python3.1 is not covered, but we don't want to
# mess with non-Python libraries, don't we?
return
tagver = tagver.groupdict()['ver']
if tagver is None:
return
tagver = Version("%s.%s" % (tagver[0], tagver[1:]))
return tagver
def main():
parser = ArgumentParser()
parser.add_argument(
'--version', action='version', version='%(prog)s DEVELV')
parser.add_argument(
'--no-guessing-deps', action='store_false', dest='guess_deps',
help='disable guessing dependencies')
parser.add_argument(
'--skip-private', action='store_true',
help="don't check private directories")
parser.add_argument(
'-v', '--verbose', action='store_true',
default=os.environ.get('DH_VERBOSE') == '1',
help='turn verbose mode on')
# arch=False->arch:all only, arch=True->arch:any only, None->all of them
parser.add_argument(
'-i', '--indep', action='store_false', dest='arch', default=None,
help='act on architecture independent packages')
parser.add_argument(
'-a', '-s', '--arch', action='store_true', dest='arch',
help='act on architecture dependent packages')
parser.add_argument(
'-q', '--quiet', action='store_false', dest='verbose', help='be quiet')
parser.add_argument(
'-p', '--package', action='append', metavar='PACKAGE',
help='act on the package named PACKAGE')
parser.add_argument(
'-N', '--no-package', action='append', metavar='PACKAGE',
help='do not act on the specified package')
parser.add_argument(
'--compile-all', action='store_true',
help='compile all files from given private directory in postinst, not '
'just the ones provided by the package')
parser.add_argument(
'-V', type=VersionRange, dest='vrange', metavar='[X.Y][-][A.B]',
help='specify list of supported Python versions. See py3compile(1) for '
'examples')
parser.add_argument(
'-X', '--exclude', action='append', dest='regexpr', type=compiled_regex,
metavar='REGEXPR',
help='exclude items that match given REGEXPR. You may use this option '
'multiple times to build up a list of things to exclude.')
parser.add_argument(
'--accept-upstream-versions', action='store_true',
help='accept upstream versions while translating Python dependencies '
'into Debian ones')
parser.add_argument(
'--depends', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${python3:Depends}. Use it for missing items in '
'requires.txt.')
parser.add_argument(
'--depends-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Depends}')
parser.add_argument(
'--recommends', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${python3:Recommends}')
parser.add_argument(
'--recommends-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Recommends}')
parser.add_argument(
'--suggests', action='append', metavar='REQ',
help='translate given requirements into Debian dependencies and add '
'them to ${python3:Suggests}')
parser.add_argument(
'--suggests-section', action='append', metavar='SECTION',
help='translate requirements from given section into Debian '
'dependencies and add them to ${python3:Suggests}')
parser.add_argument(
'--requires', action='append', metavar='FILE',
help='translate requirements from given file into Debian dependencies '
'and add them to ${python3:Depends}')
parser.add_argument(
'--shebang', metavar='COMMAND',
help='use given command as shebang in scripts')
parser.add_argument(
'--ignore-shebangs', action='store_true',
help='do not translate shebangs into Debian dependencies')
parser.add_argument(
'--no-dbg-cleaning', action='store_false', dest='clean_dbg_pkg',
help='do not remove files from debug packages')
parser.add_argument(
'--no-ext-rename', action='store_true',
help='do not add magic tags nor multiarch tuples to extension file '
'names)')
parser.add_argument(
'--no-shebang-rewrite', action='store_true',
help='do not rewrite shebangs')
parser.add_argument('private_dir', nargs='?',
help='Private directory containing Python modules (optional)')
# debhelper options:
parser.add_argument('-O', action='append', help=SUPPRESS)
options = parser.parse_args(os.environ.get('DH_OPTIONS', '').split()
+ sys.argv[1:])
if options.O:
parser.parse_known_args(options.O, options)
private_dir = options.private_dir
if private_dir:
if not private_dir.startswith('/'):
# handle usr/share/foo dirs (without leading slash)
private_dir = '/' + private_dir
# TODO: support more than one private dir at the same time (see :meth:scan)
if options.skip_private:
private_dir = False
if options.verbose:
log.setLevel(logging.DEBUG)
log.debug('version: DEVELV')
log.debug('argv: %s', sys.argv)
log.debug('options: %s', options)
log.debug('supported Python versions: %s (default=%s)',
','.join(str(v) for v in SUPPORTED), DEFAULT)
else:
log.setLevel(logging.INFO)
try:
dh = DebHelper(options, impl='cpython3')
except Exception as e:
log.error('cannot initialize DebHelper: %s', e)
exit(2)
if not dh.packages:
log.error('no package to act on (python3-foo or one with ${python3:Depends} in Depends)')
# exit(7)
if not options.vrange and dh.python_version:
options.vrange = VersionRange(dh.python_version)
interpreter = Interpreter('python3')
for package, pdetails in dh.packages.items():
log.debug('processing package %s...', package)
interpreter.debug = package.endswith('-dbg')
if not private_dir:
try:
pyinstall(interpreter, package, options.vrange)
except Exception as err:
log.error("%s.pyinstall: %s", package, err)
exit(4)
try:
pyremove(interpreter, package, options.vrange)
except Exception as err:
log.error("%s.pyremove: %s", package, err)
exit(5)
fix_locations(package, interpreter, SUPPORTED, options)
stats = Scanner(interpreter, package, private_dir, options).result
dependencies = Dependencies(package, 'cpython3', dh.build_depends)
dependencies.parse(stats, options)
pyclean_added = False # invoke pyclean only once in maintainer script
if stats['compile']:
args = ''
if options.vrange:
args += "-V %s" % options.vrange
dh.autoscript(package, 'postinst', 'postinst-py3compile', args)
dh.autoscript(package, 'prerm', 'prerm-py3clean', '')
pyclean_added = True
for pdir, details in sorted(stats['private_dirs'].items()):
if not details.get('compile'):
continue
if not pyclean_added:
dh.autoscript(package, 'prerm', 'prerm-py3clean', '')
pyclean_added = True
args = pdir
ext_for = details.get('ext_vers')
ext_no_version = details.get('ext_no_version')
if ext_for is None and not ext_no_version: # no extension
shebang_versions = list(i.version for i in details.get('shebangs', [])
if i.version and i.version.minor)
if not options.ignore_shebangs and len(shebang_versions) == 1:
# only one version from shebang
args += " -V %s" % shebang_versions[0]
elif options.vrange and options.vrange != (None, None):
args += " -V %s" % options.vrange
elif ext_no_version:
# at least one extension's version not detected
if options.vrange and '-' not in str(options.vrange):
ver = str(options.vrange)
else: # try shebang or default Python version
ver = (list(i.version for i in details.get('shebangs', [])
if i.version and i.version.minor) or [None])[0] or DEFAULT
dependencies.depend("python%s" % ver)
args += " -V %s" % ver
else:
extensions = sorted(ext_for)
vr = VersionRange(minver=extensions[0], maxver=extensions[-1])
args += " -V %s" % vr
for regex in options.regexpr or []:
args += " -X '%s'" % regex.pattern.replace("'", r"'\''")
dh.autoscript(package, 'postinst', 'postinst-py3compile', args)
dependencies.export_to(dh)
pydist_file = join('debian', "%s.pydist" % package)
if exists(pydist_file):
if not validate_pydist(pydist_file):
log.warning("%s.pydist file is invalid", package)
else:
dstdir = join('debian', package, 'usr/share/python3/dist/')
if not exists(dstdir):
os.makedirs(dstdir)
fcopy(pydist_file, join(dstdir, package))
bcep_file = join('debian', "%s.bcep" % package)
if exists(bcep_file):
dstdir = join('debian', package, 'usr/share/python3/bcep/')
if not exists(dstdir):
os.makedirs(dstdir)
fcopy(bcep_file, join(dstdir, package))
dh.save()
if __name__ == '__main__':
main()

235
dh_python3.rst Normal file
View File

@ -0,0 +1,235 @@
============
dh_python3
============
-----------------------------------------------------------------------------------
calculates Python dependencies, adds maintainer scripts to byte compile files, etc.
-----------------------------------------------------------------------------------
:Manual section: 1
:Author: Piotr Ożarowski, 2012-2013
SYNOPSIS
========
dh_python3 -p PACKAGE [-V [X.Y][-][A.B]] DIR [-X REGEXPR]
DESCRIPTION
===========
QUICK GUIDE FOR MAINTAINERS
---------------------------
* build depend on dh-python
* build-depend on python3 (Python application) or python3-all (Python module)
or python3-all-dev (Python extension),
* if necessary, describe supported Python 3 versions via X-Python3-Version field
in debian/control,
* build module/application using its standard build system (pybuild wrapper
recommended, see pybuild.1 for more details), remember to build extensions
for all supported Python 3 versions (loop over ``py3versions -vr``),
* install files to the *standard* locations, add `--install-layout=deb` to
setup.py's install command if your package is using distutils,
* add `python3` to dh's --with option, or:
* `include /usr/share/cdbs/1/class/python-distutils.mk` in debian/rules and
depend on `cdbs (>= 0.4.90)`, or:
* call ``dh_python3`` in the `binary-*` target,
* add `${python3:Depends}` to Depends
NOTES
-----
dependencies
~~~~~~~~~~~~
dh_python3 tries to translate Python dependencies from `Requires-Dist`
entries in `dist-info` or `requires.txt` contents in `egg-info` to
Debian dependencies.
In many cases, this works without any additional configuration because
dh_python3 comes with a build-in mapping of Python module names to
Debian packages that is periodically regenerated from the Debian
archive. By default, the version information in the Python dependencies is
discarded. If you want dh_python3 to generate more strict dependencies (e.g. to
avoid ABI problems), or if the automatic mapping does not work correctly for
your package, you have to provide dh_python3 with additional rules for the
translation of Python module to Debian package dependencies.
For a package *python3-foo* that depends on a package *python3-bar*, there are
two files that may provide such rules:
#. If the *python3-foo* source package ships with a
`debian/py3dist-overrides` file, this file is used by dh_python3
during the build of *python3-foo*.
#. If the *python3-bar* source package ships with a
`debian/python3-bar.pydist` file (and uses dh_python3), this file
will be included in the binary package as
`/usr/share/dh-python/dist/cpython3/python3-bar`. During the build
of *python3-foo*, dh_python3 will then find and use the file.
Both files have the same format described in
`/usr/share/doc/dh-python/README.PyDist`. If all you want is to generate
versioned dependencies (and assuming that the *python3-bar* package provides
the *pybar* Python module), in most cases it will be sufficient to put the line
``pybar python3-bar; PEP386`` into either of the above files.
private dirs
~~~~~~~~~~~~
`/usr/share/foo`, `/usr/share/games/foo`, `/usr/lib/foo` and
`/usr/lib/games/foo` private directories are scanned for Python files
by default (where `foo` is binary package name). If your package ships
Python files in some other directory, add another dh_python3 call in
debian/rules with directory name as an argument - you can use different set of
options in this call. If you need to change options (f.e. a list of supported
Python 3 versions) for a private directory that is checked by default, invoke
dh_python3 with --skip-private option and add another call with a path to this
directory and new options.
debug packages
~~~~~~~~~~~~~~
In binary packages which name ends with `-dbg`, all files in
`/usr/lib/python3/dist-packages/` directory
that have extensions different than `so` or `h` are removed by default.
Use --no-dbg-cleaning option to disable this feature.
pyinstall files
~~~~~~~~~~~~~~~
Files listed in debian/pkg.pyinstall file will be installed as public modules
(i.e. into .../dist-packages/ directory) for all requested Python versions.
Syntax: ``path/to/file [NAMESPACE] [VERSION_RANGE]``
debian directory is automatically removed from the path, so you can place your
files in debian/ directory and install them from this location (if you want to
install them in "debian" namespace, set NAMESPACE to debian). If NAMESPACE is
set, all listed files will be installed in .../dist-packages/NAMESPACE/
directory.
Examples:
* ``foo.py`` installs .../dist-packages/foo.py for all supported Python versions
* ``foo/bar.py 3.3-`` installs .../dist-packages/foo/bar.py for versions >= 3.3
* ``foo/bar.py spam`` installs .../dist-packages/spam/bar.py
* ``debian/*.py spam.egg 3.2`` installs .../python3.2/dist-packages/spam/egg/\*.py
files
pyremove files
~~~~~~~~~~~~~~
If you want to remove some public modules (i.e. files in .../dist-packages/
directory) installed by build system (from all supported Python versions or
only from a subset of these versions), add them to debian/pkg.pyremove file.
Examples:
* ``*.pth`` removes .pth files from .../dist-packages/
* ``bar/baz.py 3.2`` removes .../python3.2/dist-packages/bar/baz.py
bcep files
~~~~~~~~~~
Byte-compilation exception patterns can be described in these files. Use it if
you want py3compile to skip specific files. This is the only way to skip .py
files in …/dist-packages/ directory (as `--exclude` passed to py3compile in
postinst is not used in rtupdate scripts and thus this option cannot be used
for non-private modules).
``re|-3.6|/usr/lib/python3/dist-packages/jinja2|.*/async(foo|bar).py``
will skip byte-compilation of `asyncfoo.py` and `asyncbar.py` in
`/usr/lib/python3/dist-packages/jinja2/` directory for each interpreter that
doesn't support `async` keyword (introduced in Python 3.6).
If you want to skip byte-compilation in a subdirectory for all interpreters, use:
``dir|-4.0|/usr/lib/python3/dist-packages/foo/tests/``.
VERSION_RANGE (`-4.0` in the example) is described in `README.PyDist` file.
`debian/python3-foo.bcep` file from source package will be included in the
binary package as `/usr/share/python3/bcep/python3-foo.bcep`
overriding supported / default Python versions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to override system's list of supported Python versions or the
default one (f.e. to build a package that includes symlinks for older version
of Python or compile .py files only for given interpreter version), you can do
that via `DEBPYTHON3_SUPPORTED` and/or `DEBPYTHON3_DEFAULT` env. variables.
Example: ``3.2,3.3`` limits the list of supported Python versions to Python 3.2
and Python 3.3.
OPTIONS
=======
--version show program's version number and exit
-h, --help show help message and exit
--no-guessing-deps disable guessing dependencies
--no-dbg-cleaning do not remove any files from debug packages
--no-ext-rename do not add magic tags nor multiarch tuples to extension file names
--no-shebang-rewrite do not rewrite shebangs
--skip-private don't check private directories
-v, --verbose turn verbose mode on
-i, --indep act on architecture independent packages
-a, --arch act on architecture dependent packages
-q, --quiet be quiet
-p PACKAGE, --package=PACKAGE act on the package named PACKAGE
-N NO_PACKAGE, --no-package=NO_PACKAGE do not act on the specified package
-V VERSION_RANGE specify list of supported Python 3 versions. See
py3compile(1) for examples
-X REGEXPR, --exclude=REGEXPR exclude items that match given REGEXPR. You may
use this option multiple times to build up a list of things to exclude from
byte-compilation in private dirs. See also `bcep files`.
--compile-all compile all files from given private directory in postinst/rtupdate
not just the ones provided by the package (i.e. do not pass the --package
parameter to py3compile/py3clean)
--accept-upstream-versions accept upstream versions while translating
Python dependencies into Debian ones
--depends=DEPENDS translate given requirements into Debian dependencies
and add them to ${python3:Depends}. Use it for missing items in
`requires.txt` / `Requires-Dist`.
--depends-section=SECTION translate requirements from given extra
sections of `requres.txt` / `Requires-Dist` into Debian dependencies
and add them to ${python3:Depends}. May be repeated for multiple
sections.
--recommends=RECOMMENDS translate given requirements into Debian dependencies
and add them to ${python3:Recommends}
--recommends-section=SECTION translate requirements from given extra
sections of `requires.txt` / `Requires-Dist` into Debian dependencies
and add them to ${python3:Recommends}. May be repeated for multiple
sections.
--suggests=SUGGESTS translate given requirements into Debian dependencies
and add them to ${python3:Suggests}
--suggests-section=SECTION translate requirements from given extra
sections of `requires.txt` / `Requires-Dist` into Debian dependencies
and add them to ${python3:Suggests}. May be repeated for multiple
sections.
--requires=FILENAME translate requirements from given file(s) into Debian
dependencies and add them to ${python3:Depends}
--shebang=COMMAND use given command as shebang in scripts
--ignore-shebangs do not translate shebangs into Debian dependencies
SEE ALSO
========
* /usr/share/doc/python3/python-policy.txt.gz
* /usr/share/doc/dh-python/README.PyDist
* pybuild(1)
* py3compile(1), py3clean(1)
* dh_python2(1), pycompile(1), pyclean(1)
* http://deb.li/dhp3 - most recent version of this document

113
dhpython/__init__.py Normal file
View File

@ -0,0 +1,113 @@
# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
PKG_PREFIX_MAP = {'cpython2': 'python',
'cpython3': 'python3',
'pypy': 'pypy'}
# minimum version required for compile/clean scripts:
MINPYCDEP = {'cpython2': 'python2:any',
'cpython3': 'python3:any',
'pypy': 'pypy'}
PUBLIC_DIR_RE = {
'cpython2': re.compile(r'.*?/usr/lib/python(2\.\d)(?:/|$)'),
'cpython3': re.compile(r'.*?/usr/lib/python(3(?:\.\d+)?)(?:/|$)'),
'pypy': re.compile(r'.*?/usr/lib/pypy(?:/|$)')}
INTERPRETER_DIR_TPLS = {
'cpython2': r'.*/python2\.\d/',
'cpython3': r'.*/python3(?:\.\d+)?/',
'pypy': r'.*/pypy/'}
MULTIARCH_DIR_TPL = re.compile(
'.*/([a-z][^/-]+-(?:linux|kfreebsd|gnu)(?:-[^/-]+)?)(?:/.*|$)')
# Interpreter site-directories
OLD_SITE_DIRS = {
'cpython2': [
'/usr/local/lib/python{}/site-packages',
'/usr/local/lib/python{}/dist-packages',
'/var/lib/python-support/python{}',
'/usr/lib/pymodules/python{}',
lambda version: '/usr/lib/python{}/site-packages'.format(version)
if version >= '2.6' else None],
'cpython3': [
'/usr/local/lib/python{}/site-packages',
'/usr/local/lib/python{}/dist-packages',
'/usr/lib/python{}/site-packages',
'/usr/lib/python{}/dist-packages',
'/var/lib/python-support/python{}',
'/usr/lib/pymodules/python{}'],
'pypy': [
'/usr/local/lib/pypy/site-packages',
'/usr/local/lib/pypy/dist-packages',
'/usr/lib/pypy/site-packages']}
# PyDist related
PYDIST_DIRS = {
'cpython2': '/usr/share/python/dist/',
'cpython3': '/usr/share/python3/dist/',
'pypy': '/usr/share/pypy/dist/'}
PYDIST_OVERRIDES_FNAMES = {
'cpython2': 'debian/pydist-overrides',
'cpython3': 'debian/py3dist-overrides',
'pypy': 'debian/pypydist-overrides'}
PYDIST_DPKG_SEARCH_TPLS = {
# implementation: (dpkg -S query, regex filter)
'cpython2': ('*/{}-?*.*-info',
r'/(python2\..|pyshared)/.*.(egg|dist)-info$'),
'cpython3': ('*python3/*/{}-?*.*-info', r'.(egg|dist)-info$'),
'pypy': ('*/pypy/dist-packages/{}-?*.*-info', r'.(egg|dist)-info$'),
}
# DebHelper related
DEPENDS_SUBSTVARS = {
'cpython2': '${python:Depends}',
'cpython3': '${python3:Depends}',
'pypy': '${pypy:Depends}',
}
PKG_NAME_TPLS = {
'cpython2': ('python-', 'python2.'),
'cpython3': ('python3-', 'python3.'),
'pypy': ('pypy-',)
}
RT_LOCATIONS = {
'cpython2': '/usr/share/python/runtime.d/',
'cpython3': '/usr/share/python3/runtime.d/',
'pypy': '/usr/share/pypy/runtime.d/',
}
RT_TPLS = {
'cpython2': '''
if [ "$1" = rtupdate ]; then
\tpyclean {pkg_arg} {dname}
\tpycompile {pkg_arg} {args} {dname}
fi''',
'cpython3': '''
if [ "$1" = rtupdate ]; then
\tpy3clean {pkg_arg} {dname}
\tpy3compile {pkg_arg} {args} {dname}
fi''',
'pypy': ''
}

99
dhpython/_defaults.py Executable file
View File

@ -0,0 +1,99 @@
#! /usr/bin/python3
# Copyright © 2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
from configparser import ConfigParser
from os import environ
from os.path import exists
from subprocess import Popen, PIPE
SUPPORTED = {
'cpython2': [(2, 7)],
'cpython3': [(3, 8)],
'pypy': [(4, 0)]}
DEFAULT = {
'cpython2': (2, 7),
'cpython3': (3, 8),
'pypy': (4, 0)}
log = logging.getLogger('dhpython')
def cpython_versions(major):
result = [None, None]
ver = '' if major == 2 else '3'
supported = environ.get("DEBPYTHON{}_SUPPORTED".format(ver))
default = environ.get("DEBPYTHON{}_DEFAULT".format(ver))
if not supported or not default:
config = ConfigParser()
config.read("/usr/share/python{}/debian_defaults".format(ver))
if not default:
default = config.get('DEFAULT', 'default-version', fallback='')[6:]
if not supported:
supported = config.get('DEFAULT', 'supported-versions', fallback='')\
.replace('python', '')
if default:
try:
result[0] = tuple(int(i) for i in default.split('.'))
except Exception as err:
log.warn('invalid debian_defaults file: %s', err)
if supported:
try:
result[1] = tuple(tuple(int(j) for j in i.strip().split('.'))
for i in supported.split(','))
except Exception as err:
log.warn('invalid debian_defaults file: %s', err)
return result
def from_file(fpath):
if not exists(fpath):
raise ValueError("missing interpreter: %s" % fpath)
command = "{} --version".format(fpath)
with Popen(command, shell=True, stdout=PIPE) as process:
stdout, stderr = process.communicate()
stdout = str(stdout, 'utf-8')
print(stdout)
cpython2 = cpython_versions(2)
cpython3 = cpython_versions(3)
if cpython2[0]:
DEFAULT['cpython2'] = cpython2[0]
if cpython3[0]:
DEFAULT['cpython3'] = cpython3[0]
if cpython2[1]:
SUPPORTED['cpython2'] = cpython2[1]
if cpython3[1]:
SUPPORTED['cpython3'] = cpython3[1]
#from_file('/usr/bin/pypy')
if __name__ == '__main__':
from sys import argv, stderr
if len(argv) != 3:
print('invalid number of arguments', file=stderr)
exit(1)
if argv[1] == 'default':
print('.'.join(str(i) for i in DEFAULT[argv[2]]))
elif argv[1] == 'supported':
print(','.join(('.'.join(str(i) for i in v) for v in SUPPORTED[argv[2]])))

View File

@ -0,0 +1,42 @@
# Copyright © 2012-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
from glob import glob1
from os.path import dirname
from dhpython.exceptions import RequiredCommandMissingException
log = logging.getLogger('dhpython')
plugins = {}
for i in sorted(i[7:-3] for i in glob1(dirname(__file__), 'plugin_*.py')):
try:
module = __import__("dhpython.build.plugin_%s" % i, fromlist=[i])
module.BuildSystem.NAME = i
module.BuildSystem.is_usable()
plugins[i] = module.BuildSystem
except RequiredCommandMissingException as err:
log.debug("cannot initialize '%s' plugin: Missing command '%s'", i, err)
except Exception as err:
if log.level < logging.INFO:
log.debug("cannot initialize '%s' plugin", i, exc_info=True)
else:
log.debug("cannot initialize '%s' plugin: %s", i, err)

293
dhpython/build/base.py Normal file
View File

@ -0,0 +1,293 @@
# Copyright © 2012-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
from functools import wraps
from glob import glob1
from os import remove, walk
from os.path import exists, isdir, join
from subprocess import Popen, PIPE
from shutil import rmtree, copyfile, copytree
from dhpython.exceptions import RequiredCommandMissingException
from dhpython.tools import execute
try:
from shlex import quote
except ImportError:
# shlex.quote is new in Python 3.3
def quote(s):
if not s:
return "''"
return "'" + s.replace("'", "'\"'\"'") + "'"
log = logging.getLogger('dhpython')
def copy_test_files(dest='{build_dir}',
filelist='{home_dir}/testfiles_to_rm_before_install',
add_to_args=('test', 'tests')):
def _copy_test_files(func):
@wraps(func)
def __copy_test_files(self, context, args, *oargs, **kwargs):
files_to_copy = {'test', 'tests'}
# check debian/pybuild_pythonX.Y.testfiles
for tpl in ('_{i}{v}', '_{i}{m}', ''):
tpl = tpl.format(i=args['interpreter'].name,
v=args['version'],
m=args['version'].major)
fpath = join(args['dir'], 'debian/pybuild{}.testfiles'.format(tpl))
if exists(fpath):
with open(fpath, encoding='utf-8') as fp:
# overwrite files_to_copy if .testfiles file found
files_to_copy = [line.strip() for line in fp.readlines()
if not line.startswith('#')]
break
files_to_remove = set()
for name in files_to_copy:
src_dpath = join(args['dir'], name)
dst_dpath = join(dest.format(**args), name.rsplit('/', 1)[-1])
if exists(src_dpath):
if not exists(dst_dpath):
if isdir(src_dpath):
copytree(src_dpath, dst_dpath)
else:
copyfile(src_dpath, dst_dpath)
files_to_remove.add(dst_dpath + '\n')
if not args['args'] and 'PYBUILD_TEST_ARGS' not in context['ENV']\
and (self.cfg.test_pytest or self.cfg.test_nose) \
and name in add_to_args:
args['args'] = name
if files_to_remove and filelist:
with open(filelist.format(**args), 'a') as fp:
fp.writelines(files_to_remove)
return func(self, context, args, *oargs, **kwargs)
return __copy_test_files
return _copy_test_files
class Base:
"""Base class for build system plugins
:attr REQUIRED_COMMANDS: list of command checked by default in :meth:is_usable,
if one of them is missing, plugin cannot be used.
:type REQUIRED_COMMANDS: list of strings
:attr REQUIRED_FILES: list of files (or glob templates) required by given
build system
:attr OPTIONAL_FILES: dictionary of glob templates (key) and score (value)
used to detect if given plugin is the best one for the job
:type OPTIONAL_FILES: dict (key is a string, value is an int)
:attr SUPPORTED_INTERPRETERS: set of interpreter templates (with or without
{version}) supported by given plugin
"""
DESCRIPTION = ''
REQUIRED_COMMANDS = []
REQUIRED_FILES = []
OPTIONAL_FILES = {}
SUPPORTED_INTERPRETERS = {'python', 'python3', 'python-dbg', 'python3-dbg',
'python{version}', 'python{version}-dbg'}
# files and directories to remove during clean step (other than .pyc):
CLEAN_FILES = {'.pytest_cache', '.coverage'}
def __init__(self, cfg):
self.cfg = cfg
def __repr__(self):
return "BuildSystem(%s)" % self.NAME
@classmethod
def is_usable(cls):
for command in cls.REQUIRED_COMMANDS:
process = Popen(['which', command], stdout=PIPE, stderr=PIPE)
out, err = process.communicate()
if process.returncode != 0:
raise RequiredCommandMissingException(command)
def detect(self, context):
"""Return certainty level that this plugin describes the right build system
This method is using cls.{REQUIRED,OPTIONAL}_FILES only by default,
please extend it in the plugin if more sofisticated methods can be used
for given build system.
:return: 0 <= certainty <= 100
:rtype: int
"""
result = 0
required_files_num = 0
self.DETECTED_REQUIRED_FILES = {} # can be used in the plugin later
for tpl in self.REQUIRED_FILES:
found = False
for ftpl in tpl.split('|'):
res = glob1(context['dir'], ftpl)
if res:
found = True
self.DETECTED_REQUIRED_FILES.setdefault(tpl, []).extend(res)
if found:
required_files_num += 1
# add max 50 points depending on how many required files are available
if self.REQUIRED_FILES:
result += int(required_files_num / len(self.REQUIRED_FILES) * 50)
self.DETECTED_OPTIONAL_FILES = {}
for ftpl, score in self.OPTIONAL_FILES.items():
res = glob1(context['dir'], ftpl)
if res:
result += score
self.DETECTED_OPTIONAL_FILES.setdefault(ftpl, []).extend(res)
if result > 100:
return 100
return result
def clean(self, context, args):
if self.cfg.test_tox:
tox_dir = join(args['dir'], '.tox')
if isdir(tox_dir):
try:
rmtree(tox_dir)
except Exception:
log.debug('cannot remove %s', tox_dir)
for fn in self.CLEAN_FILES:
path = join(context['dir'], fn)
if isdir(path):
try:
rmtree(path)
except Exception:
log.debug('cannot remove %s', path)
elif exists(path):
try:
remove(path)
except Exception:
log.debug('cannot remove %s', path)
for root, dirs, file_names in walk(context['dir']):
for name in dirs:
if name == '__pycache__':
dpath = join(root, name)
log.debug('removing dir: %s', dpath)
try:
rmtree(dpath)
except Exception:
log.debug('cannot remove %s', dpath)
else:
dirs.remove(name)
for fn in file_names:
if fn.endswith(('.pyc', '.pyo')):
fpath = join(root, fn)
log.debug('removing: %s', fpath)
try:
remove(fpath)
except Exception:
log.debug('cannot remove %s', fpath)
def configure(self, context, args):
raise NotImplementedError("configure method not implemented in %s" % self.NAME)
def install(self, context, args):
raise NotImplementedError("install method not implemented in %s" % self.NAME)
def build(self, context, args):
raise NotImplementedError("build method not implemented in %s" % self.NAME)
@copy_test_files()
def test(self, context, args):
if self.cfg.test_nose2:
return 'cd {build_dir}; {interpreter} -m nose2 -v {args}'
elif self.cfg.test_nose:
return 'cd {build_dir}; {interpreter} -m nose -v {args}'
elif self.cfg.test_pytest:
return 'cd {build_dir}; {interpreter} -m pytest {args}'
elif self.cfg.test_tox:
# tox will call pip to install the module. Let it install the
# module inside the virtualenv
pydistutils_cfg = join(args['home_dir'], '.pydistutils.cfg')
if exists(pydistutils_cfg):
remove(pydistutils_cfg)
return 'cd {build_dir}; tox -c {dir}/tox.ini --sitepackages -e py{version.major}{version.minor} {args}'
elif self.cfg.test_custom:
return 'cd {build_dir}; {args}'
elif args['version'] == '2.7' or args['version'] >> '3.1' or args['interpreter'] == 'pypy':
return 'cd {build_dir}; {interpreter} -m unittest discover -v {args}'
def execute(self, context, args, command, log_file=None):
if log_file is False and self.cfg.really_quiet:
log_file = None
command = command.format(**args)
env = dict(context['ENV'])
if 'ENV' in args:
env.update(args['ENV'])
log.info(command)
return execute(command, context['dir'], env, log_file)
def print_args(self, context, args):
cfg = self.cfg
if len(cfg.print_args) == 1 and len(cfg.interpreter) == 1 and '{version}' not in cfg.interpreter[0]:
i = cfg.print_args[0]
if '{' in i:
print(i.format(**args))
else:
print(args.get(i, ''))
else:
for i in cfg.print_args:
if '{' in i:
print(i.format(**args))
else:
print('{} {}: {}'.format(args['interpreter'], i, args.get(i, '')))
def shell_command(func):
@wraps(func)
def wrapped_func(self, context, args, *oargs, **kwargs):
command = kwargs.pop('command', None)
if not command:
command = func(self, context, args, *oargs, **kwargs)
if isinstance(command, int): # final result
return command
if not command:
log.warn('missing command '
'(plugin=%s, method=%s, interpreter=%s, version=%s)',
self.NAME, func.__name__,
args.get('interpreter'), args.get('version'))
return command
if self.cfg.quiet:
log_file = join(args['home_dir'], '{}_cmd.log'.format(func.__name__))
else:
log_file = False
quoted_args = dict((k, quote(v)) if k in ('dir', 'destdir')
or k.endswith('_dir') else (k, v)
for k, v in args.items())
command = command.format(**quoted_args)
output = self.execute(context, args, command, log_file)
if output['returncode'] != 0:
msg = 'exit code={}: {}'.format(output['returncode'], command)
if log_file:
msg += '\nfull command log is available in {}'.format(log_file)
raise Exception(msg)
return True
return wrapped_func

View File

@ -0,0 +1,71 @@
# Copyright © 2012-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from dhpython.build.base import Base, shell_command, copy_test_files
class BuildSystem(Base):
DESCRIPTION = 'CMake build system (using dh_auto_* commands)'
REQUIRED_COMMANDS = ['cmake']
REQUIRED_FILES = ['CMakeLists.txt']
OPTIONAL_FILES = {'cmake_uninstall.cmake': 10, 'CMakeCache.txt': 10}
@shell_command
def clean(self, context, args):
super(BuildSystem, self).clean(context, args)
return 'dh_auto_clean --buildsystem=cmake'
@shell_command
def configure(self, context, args):
return ('dh_auto_configure --buildsystem=cmake'
' --builddirectory={build_dir} --'
# FindPythonInterp:
' -DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/{interpreter}'
' -DPYTHON_LIBRARY:FILEPATH={interpreter.library_file}'
' -DPYTHON_INCLUDE_DIR:PATH={interpreter.include_dir}'
# FindPython:
' -DPython_EXECUTABLE=/usr/bin/{interpreter}'
' -DPython_LIBRARY={interpreter.library_file}'
' -DPython_INCLUDE_DIR={interpreter.include_dir}'
# FindPython3:
' -DPython3_EXECUTABLE=/usr/bin/{interpreter}'
' -DPython3_LIBRARY={interpreter.library_file}'
' -DPython3_INCLUDE_DIR={interpreter.include_dir}'
' {args}')
@shell_command
def build(self, context, args):
return ('dh_auto_build --buildsystem=cmake'
' --builddirectory={build_dir}'
' -- {args}')
@shell_command
def install(self, context, args):
return ('dh_auto_install --buildsystem=cmake'
' --builddirectory={build_dir}'
' --destdir={destdir}'
' -- {args}')
@shell_command
@copy_test_files()
def test(self, context, args):
return ('dh_auto_test --buildsystem=cmake'
' --builddirectory={build_dir}'
' -- {args}')

View File

@ -0,0 +1,48 @@
# Copyright © 2012-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from dhpython.build.base import Base, shell_command, copy_test_files
class BuildSystem(Base):
DESCRIPTION = 'use --*-args options to configure this system'
SUPPORTED_INTERPRETERS = True # all interpreters
@shell_command
def clean(self, context, args):
super(BuildSystem, self).clean(context, args)
return args['args']
@shell_command
def configure(self, context, args):
return args['args']
@shell_command
def build(self, context, args):
return args['args']
@shell_command
def install(self, context, args):
return args['args']
@shell_command
@copy_test_files()
def test(self, context, args):
return args['args'] or super(BuildSystem, self).test(context, args)

View File

@ -0,0 +1,121 @@
# Copyright © 2012-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
from glob import glob1
from os import remove
from os.path import exists, isdir, join
from shutil import rmtree
from dhpython.build.base import Base, shell_command, copy_test_files
log = logging.getLogger('dhpython')
_setup_tpl = 'setup.py|setup-3.py'
def create_pydistutils_cfg(func):
"""distutils doesn't have sane command-line API - this decorator creates
.pydistutils.cfg file to workaround it
hint: if you think this is plain stupid, please don't read
distutils/setuptools/distribute sources
"""
def wrapped_func(self, context, args, *oargs, **kwargs):
fpath = join(args['home_dir'], '.pydistutils.cfg')
if not exists(fpath):
with open(fpath, 'w', encoding='utf-8') as fp:
lines = ['[clean]\n',
'all=1\n',
'[build]\n',
'build_lib={}\n'.format(args['build_dir']),
'[install]\n',
'force=1\n',
'install_layout=deb\n',
'install_scripts=$base/bin\n',
'install_lib={}\n'.format(args['install_dir']),
'prefix=/usr\n']
log.debug('pydistutils config file:\n%s', ''.join(lines))
fp.writelines(lines)
context['ENV']['HOME'] = args['home_dir']
return func(self, context, args, *oargs, **kwargs)
wrapped_func.__name__ = func.__name__
return wrapped_func
class BuildSystem(Base):
DESCRIPTION = 'Distutils build system'
SUPPORTED_INTERPRETERS = {'python', 'python3', 'python{version}',
'python-dbg', 'python3-dbg', 'python{version}-dbg',
'pypy'}
REQUIRED_FILES = [_setup_tpl]
OPTIONAL_FILES = {'setup.cfg': 1,
'requirements.txt': 1,
'PKG-INFO': 10,
'*.egg-info': 10}
CLEAN_FILES = Base.CLEAN_FILES | {'build'}
def detect(self, context):
result = super(BuildSystem, self).detect(context)
if _setup_tpl in self.DETECTED_REQUIRED_FILES:
context['args']['setup_py'] = self.DETECTED_REQUIRED_FILES[_setup_tpl][0]
else:
context['args']['setup_py'] = 'setup.py'
return result
@shell_command
@create_pydistutils_cfg
def clean(self, context, args):
super(BuildSystem, self).clean(context, args)
if exists(args['interpreter'].binary()):
return '{interpreter} {setup_py} clean {args}'
return 0 # no need to invoke anything
@shell_command
@create_pydistutils_cfg
def configure(self, context, args):
return '{interpreter} {setup_py} config {args}'
@shell_command
@create_pydistutils_cfg
def build(self, context, args):
return '{interpreter.binary_dv} {setup_py} build {args}'
@shell_command
@create_pydistutils_cfg
def install(self, context, args):
# remove egg-info dirs from build_dir
for fname in glob1(args['build_dir'], '*.egg-info'):
fpath = join(args['build_dir'], fname)
rmtree(fpath) if isdir(fpath) else remove(fpath)
return '{interpreter.binary_dv} {setup_py} install --root {destdir} {args}'
@shell_command
@create_pydistutils_cfg
@copy_test_files()
def test(self, context, args):
if not self.cfg.custom_tests:
fpath = join(args['dir'], args['setup_py'])
with open(fpath, 'rb') as fp:
if fp.read().find(b'test_suite') > 0:
# TODO: is that enough to detect if test target is available?
return '{interpreter} {setup_py} test {args}'
return super(BuildSystem, self).test(context, args)

View File

@ -0,0 +1,182 @@
# Copyright © 2012-2020 Piotr Ożarowski <piotr@debian.org>
# © 2020 Scott Kitterman <scott@kitterman.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from fnmatch import fnmatch
from pathlib import Path
import csv
import logging
import os
import os.path as osp
import shutil
import sysconfig
try:
import tomli
except ModuleNotFoundError:
# Plugin still works, only needed for autodetection
pass
try:
from flit.install import Installer
except ImportError:
Installer = object
from dhpython.build.base import Base, shell_command
log = logging.getLogger('dhpython')
class DebianInstaller(Installer):
def install_directly(self, destdir, installdir):
"""Install a module/package into package directory, and create its
scripts.
"""
if installdir[:1] == os.sep:
installdir = installdir[1:]
vars_ = sysconfig.get_config_vars()
vars_['base'] = destdir + vars_['base']
try:
dirs = sysconfig.get_paths(scheme='deb_system', vars=vars_)
except KeyError:
# Debian hasn't patched sysconfig schemes until 3.10
# TODO: Introduce a version check once sysconfig is patched.
dirs = sysconfig.get_paths(scheme='posix_prefix', vars=vars_)
dirs['purelib'] = dirs['platlib'] = osp.join(destdir, installdir)
os.makedirs(dirs['purelib'], exist_ok=True)
os.makedirs(dirs['scripts'], exist_ok=True)
dst = osp.join(dirs['purelib'], osp.basename(self.module.path))
if osp.lexists(dst):
if osp.isdir(dst) and not osp.islink(dst):
shutil.rmtree(dst)
else:
os.unlink(dst)
src = str(self.module.path)
if self.module.is_package:
log.info("Installing package %s -> %s", src, dst)
shutil.copytree(src, dst)
self._record_installed_directory(dst)
else:
log.info("Installing file %s -> %s", src, dst)
shutil.copy2(src, dst)
self.installed_files.append(dst)
scripts = self.ini_info.entrypoints.get('console_scripts', {})
if scripts:
log.info("Installing scripts to %s", dirs['scripts'])
self.install_scripts(scripts, dirs['scripts'])
log.info("Writing dist-info %s", dirs['purelib'])
self.write_dist_info(dirs['purelib'])
# Remove direct_url.json - contents are not useful or reproduceable
for path in Path(dirs['purelib']).glob("*.dist-info/direct_url.json"):
path.unlink()
# Remove build path from RECORD files
for path in Path(dirs['purelib']).glob("*.dist-info/RECORD"):
with open(path) as f:
reader = csv.reader(f)
records = list(reader)
with open(path, 'w') as f:
writer = csv.writer(f)
for path, hash_, size in records:
path = path.replace(destdir, '')
if fnmatch(path, "*.dist-info/direct_url.json"):
continue
writer.writerow([path, hash_, size])
class BuildSystem(Base):
DESCRIPTION = 'Flit build system'
SUPPORTED_INTERPRETERS = {'python3', 'python{version}'}
REQUIRED_FILES = ['pyproject.toml']
OPTIONAL_FILES = {}
CLEAN_FILES = Base.CLEAN_FILES | {'build'}
def detect(self, context):
"""Return certainty level that this plugin describes the right build
system
This method uses cls.{REQUIRED}_FILES (pyroject.toml) as well as
checking to see if build-backend is set to flit_core.buildapi.
Score is 85 if both are present (to allow manually setting distutils to
score higher if set).
:return: 0 <= certainty <= 100
:rtype: int
"""
if Installer is object:
return 0
result = super().detect(context)
try:
with open('pyproject.toml', 'rb') as f:
pyproject = tomli.load(f)
if pyproject.get('build-system', {}).get('build-backend') == \
'flit_core.buildapi':
result += 35
else:
# Not a flit built package
result = 0
except NameError:
# No toml, no autdetection
result = 0
except FileNotFoundError:
# Not a pep517 package
result = 0
if result > 100:
return 100
return result
def clean(self, context, args):
super().clean(context, args)
if osp.exists(args['interpreter'].binary()):
log.debug("removing '%s' (and everything under it)",
args['build_dir'])
osp.isdir(args['build_dir']) and shutil.rmtree(args['build_dir'])
return 0 # no need to invoke anything
def configure(self, context, args):
# Flit does not support binary extensions
return 0 # Not needed for flit
def build(self, context, args):
my_dir = Path(args['dir'])
install_kwargs = {'user': False, 'symlink': False, 'deps': 'none'}
DebianInstaller.from_ini_path(my_dir / 'pyproject.toml',
**install_kwargs).install_directly(
args['build_dir'], '')
# These get byte compiled too, although it's not logged.
return 0 # Not needed for flit
def install(self, context, args):
my_dir = Path(args['dir'])
install_kwargs = {'user': False, 'symlink': False, 'deps': 'none'}
DebianInstaller.from_ini_path(my_dir / 'pyproject.toml',
**install_kwargs).install_directly(
args['destdir'],
args['install_dir'])
return 0 # Not needed for flit'
@shell_command
def test(self, context, args):
return super().test(context, args)

View File

@ -0,0 +1,195 @@
# Copyright © 2012-2020 Piotr Ożarowski <piotr@debian.org>
# © 2020 Scott Kitterman <scott@kitterman.com>
# © 2021 Stuart Prescott <stuart@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from pathlib import Path
import logging
import os.path as osp
import shutil
import sysconfig
try:
import tomli
except ModuleNotFoundError:
# Plugin still works, only needed for autodetection
pass
try:
from installer import install
from installer.destinations import SchemeDictionaryDestination
from installer.sources import WheelFile
except ModuleNotFoundError:
SchemeDictionaryDestination = WheelFile = install = None
from dhpython.build.base import Base, shell_command
log = logging.getLogger('dhpython')
class BuildSystem(Base):
DESCRIPTION = 'Generic PEP517 build system'
SUPPORTED_INTERPRETERS = {'python3', 'python{version}'}
REQUIRED_FILES = ['pyproject.toml']
OPTIONAL_FILES = {}
CLEAN_FILES = Base.CLEAN_FILES | {'build'}
def detect(self, context):
"""Return certainty level that this plugin describes the right build
system
This method uses cls.{REQUIRED}_FILES (pyroject.toml) only; any
other PEP517 compliant builder (such as the flit) builder should
indicate higher specificity than this plugin.
:return: 0 <= certainty <= 100
:rtype: int
"""
result = super().detect(context)
# Temporarily reduce the threshold while we're in beta
result -= 20
try:
with open('pyproject.toml', 'rb') as f:
pyproject = tomli.load(f)
if pyproject.get('build-system', {}).get('build-backend'):
result += 10
else:
# Not a PEP517 built package
result = 0
except NameError:
# No toml, no autdetection
result = 0
except FileNotFoundError:
# Not a PEP517 package
result = 0
if result > 100:
return 100
return result
def clean(self, context, args):
super().clean(context, args)
if osp.exists(args['interpreter'].binary()):
log.debug("removing '%s' (and everything under it)",
args['build_dir'])
osp.isdir(args['build_dir']) and shutil.rmtree(args['build_dir'])
return 0 # no need to invoke anything
def configure(self, context, args):
if install is None:
raise Exception("PEP517 plugin dependencies are not available. "
"Please Build-Depend on pybuild-plugin-pyproject.")
# No separate configure step
return 0
def build(self, context, args):
self.build_step1(context, args)
self.build_step2(context, args)
@shell_command
def build_step1(self, context, args):
""" build a wheel using the PEP517 builder defined by upstream """
log.info('Building wheel for %s with "build" module',
args['interpreter'])
args['ENV']['FLIT_NO_NETWORK'] = '1'
return ('{interpreter} -m build '
'--skip-dependency-check --no-isolation --wheel '
'--outdir ' + args['home_dir'] +
' {args}'
)
def build_step2(self, context, args):
""" unpack the wheel into pybuild's normal """
log.info('Unpacking wheel built for %s with "installer" module',
args['interpreter'])
# FIXME: setuptools would use scripts-X.Y; this could use usr/bin?
scripts = f'{args["build_dir"]}/scripts-{args["interpreter"].version}'
if osp.exists(scripts):
log.warning('Scripts directory already exists, skipping unpack. '
'Is the Python package being built twice?')
return
destination = SchemeDictionaryDestination(
{
'platlib': args['build_dir'],
'purelib': args['build_dir'],
'scripts': scripts,
#FIXME is this the right dest for data?
'data': args['build_dir']
},
interpreter=args['interpreter'].binary_dv,
script_kind='posix',
)
# FIXME this next step will unpack every single wheel file it finds
# which is probably ok since each wheel is built in a separate
# directory; but perhaps it should only accept the correctly named
# wheels that match the current interpreter?
# python-packaging has relevant utilities in
# - packaging/utils.py::parse_wheel_filename
# - packaging/tags.py (although it is current-interpreter-centric)
wheels = Path(args['home_dir']).glob('*.whl')
for wheel in wheels:
if wheel.name.startswith('UNKNOWN'):
raise Exception(f'UNKNOWN wheel found: {wheel.name}. Does '
'pyproject.toml specify a build-backend?')
with WheelFile.open(wheel) as source:
install(
source=source,
destination=destination,
additional_metadata={},
)
def install(self, context, args):
log.info('Copying package built for %s to destdir',
args['interpreter'])
try:
paths = sysconfig.get_paths(scheme='deb_system')
except KeyError:
# Debian hasn't patched sysconfig schemes until 3.10
# TODO: Introduce a version check once sysconfig is patched.
paths = sysconfig.get_paths(scheme='posix_prefix')
# start by copying the scripts
for script_dir in Path(args['build_dir']).glob('scripts-*'):
target_dir = args['destdir'] + paths['scripts']
log.debug('Copying scripts directory contents from %s -> %s',
script_dir, target_dir)
shutil.copytree(
script_dir,
target_dir,
dirs_exist_ok=True,
)
# then copy the modules
module_dir = args['build_dir']
target_dir = args['destdir'] + args['install_dir']
log.debug('Copying module contents from %s -> %s',
module_dir, target_dir)
shutil.copytree(
module_dir,
target_dir,
ignore=shutil.ignore_patterns('scripts-*'),
dirs_exist_ok=True,
)
@shell_command
def test(self, context, args):
scripts = f'{args["build_dir"]}/scripts-{args["interpreter"].version}'
if osp.exists(scripts):
context['ENV']['PATH'] = f"{scripts}:{context['ENV']['PATH']}"
return super().test(context, args)

295
dhpython/debhelper.py Normal file
View File

@ -0,0 +1,295 @@
# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import re
from os import makedirs, chmod, environ
from os.path import basename, exists, join, dirname
from sys import argv
from dhpython import DEPENDS_SUBSTVARS, PKG_NAME_TPLS, RT_LOCATIONS, RT_TPLS
log = logging.getLogger('dhpython')
parse_dep = re.compile('''[,\s]*
(?P<name>[^ :]+)(?::any)?
\s*
\(?(?P<version>([>=<]{2,}|=)\s*[^\)]+)?\)?
\s*
(?:\[(?P<arch>[^\]]+)\])?
''', re.VERBOSE).match
def build_options(**options):
"""Build an Options object from kw options"""
default_options = {
'arch': None,
'package': [],
'no_package': [],
}
built_options = default_options
built_options.update(options)
return type('Options', (object,), built_options)
class DebHelper:
"""Reinvents the wheel / some dh functionality (Perl is ugly ;-P)"""
def __init__(self, options, impl='cpython3'):
self.options = options
self.packages = {}
self.build_depends = {}
self.python_version = None
# Note that each DebHelper instance supports ONE interpreter type only
# it's not possible to mix cpython2, cpython3 and pypy here
self.impl = impl
skip_tpl = set()
for name, tpls in PKG_NAME_TPLS.items():
if name != impl:
skip_tpl.update(tpls)
skip_tpl = tuple(skip_tpl)
substvar = DEPENDS_SUBSTVARS[impl]
pkgs = options.package
skip_pkgs = options.no_package
try:
with open('debian/control', 'r', encoding='utf-8') as fp:
paragraphs = [{}]
field = None
for lineno, line in enumerate(fp, 1):
if line.startswith('#'):
continue
if not line.strip():
if paragraphs[-1]:
paragraphs.append({})
field = None
continue
if line[0].isspace(): # Continuation
paragraphs[-1][field] += line.rstrip()
continue
if not ':' in line:
raise Exception(
'Unable to parse line %i in debian/control: %s'
% (lineno, line))
field, value = line.split(':', 1)
field = field.lower()
paragraphs[-1][field] = value.strip()
except IOError:
raise Exception('cannot find debian/control file')
# Trailing new lines?
if not paragraphs[-1]:
paragraphs.pop()
if len(paragraphs) < 2:
raise Exception('Unable to parse debian/control, found less than '
'2 paragraphs')
self.source_name = paragraphs[0]['source']
if self.impl == 'cpython3' and 'x-python3-version' in paragraphs[0]:
self.python_version = paragraphs[0]['x-python3-version']
if len(self.python_version.split(',')) > 2:
raise ValueError('too many arguments provided for '
'X-Python3-Version: min and max only.')
elif self.impl == 'cpython2':
if 'x-python-version' in paragraphs[0]:
self.python_version = paragraphs[0]['x-python-version']
elif 'xs-python-version' in paragraphs[0]:
self.python_version = paragraphs[0]['xs-python-version']
build_depends = []
for field in ('build-depends', 'build-depends-indep',
'build-depends-arch'):
if field in paragraphs[0]:
build_depends.append(paragraphs[0][field])
build_depends = ', '.join(build_depends)
for dep1 in build_depends.split(','):
for dep2 in dep1.split('|'):
details = parse_dep(dep2)
if details:
details = details.groupdict()
if details['arch']:
architectures = details['arch'].split()
else:
architectures = [None]
for arch in architectures:
self.build_depends.setdefault(
details['name'], {})[arch] = details['version']
for paragraph_no, paragraph in enumerate(paragraphs[1:], 2):
if 'package' not in paragraph:
raise Exception('Unable to parse debian/control, paragraph %i '
'missing Package field' % paragraph_no)
binary_package = paragraph['package']
if skip_tpl and binary_package.startswith(skip_tpl):
log.debug('skipping package: %s', binary_package)
continue
if pkgs and binary_package not in pkgs:
continue
if skip_pkgs and binary_package in skip_pkgs:
continue
pkg = {
'substvars': {},
'autoscripts': {},
'rtupdates': [],
'arch': paragraph['architecture'],
}
if (options.arch is False and pkg['arch'] != 'all' or
options.arch is True and pkg['arch'] == 'all'):
# TODO: check also if arch matches current architecture:
continue
if not binary_package.startswith(PKG_NAME_TPLS[impl]):
# package doesn't have common prefix (python-, python3-, pypy-)
# so lets check if Depends/Recommends contains the
# appropriate substvar
if (substvar not in paragraph.get('depends', '')
and substvar not in paragraph.get('recommends', '')):
log.debug('skipping package %s (missing %s in '
'Depends/Recommends)',
binary_package, substvar)
continue
# Operate on binary_package
self.packages[binary_package] = pkg
fp.close()
log.debug('source=%s, binary packages=%s', self.source_name,
list(self.packages.keys()))
def addsubstvar(self, package, name, value):
"""debhelper's addsubstvar"""
self.packages[package]['substvars'].setdefault(name, []).append(value)
def autoscript(self, package, when, template, args):
"""debhelper's autoscript"""
self.packages[package]['autoscripts'].setdefault(when, {})\
.setdefault(template, []).append(args)
def add_rtupdate(self, package, value):
self.packages[package]['rtupdates'].append(value)
def save_autoscripts(self):
for package, settings in self.packages.items():
autoscripts = settings.get('autoscripts')
if not autoscripts:
continue
for when, templates in autoscripts.items():
fn = "debian/%s.%s.debhelper" % (package, when)
if exists(fn):
with open(fn, 'r', encoding='utf-8') as datafile:
data = datafile.read()
else:
data = ''
new_data = ''
for tpl_name, args in templates.items():
for i in args:
# try local one first (useful while testing dh_python3)
fpath = join(dirname(__file__), '..',
"autoscripts/%s" % tpl_name)
if not exists(fpath):
fpath = "/usr/share/debhelper/autoscripts/%s" % tpl_name
with open(fpath, 'r', encoding='utf-8') as tplfile:
tpl = tplfile.read()
if self.options.compile_all and args:
# TODO: should args be checked to contain dir name?
tpl = tpl.replace('-p #PACKAGE#', '')
elif settings['arch'] == 'all':
tpl = tpl.replace('#PACKAGE#', package)
else:
arch = environ['DEB_HOST_ARCH']
tpl = tpl.replace('#PACKAGE#', '%s:%s' % (package, arch))
tpl = tpl.replace('#ARGS#', i)
if tpl not in data and tpl not in new_data:
new_data += "\n%s" % tpl
if new_data:
data += '\n# Automatically added by {}'.format(basename(argv[0])) +\
'{}\n# End automatically added section\n'.format(new_data)
fp = open(fn, 'w', encoding='utf-8')
fp.write(data)
fp.close()
def save_substvars(self):
for package, settings in self.packages.items():
substvars = settings.get('substvars')
if not substvars:
continue
fn = "debian/%s.substvars" % package
if exists(fn):
with open(fn, 'r', encoding='utf-8') as datafile:
data = datafile.read()
else:
data = ''
for name, values in substvars.items():
p = data.find("%s=" % name)
if p > -1: # parse the line and remove it from data
e = data[p:].find('\n')
line = data[p + len("%s=" % name):
p + e if e > -1 else None]
items = [i.strip() for i in line.split(',') if i]
if e > -1 and data[p + e:].strip():
data = "%s\n%s" % (data[:p], data[p + e:])
else:
data = data[:p]
else:
items = []
for j in values:
if j not in items:
items.append(j)
if items:
if data:
data += '\n'
data += "%s=%s\n" % (name, ', '.join(items))
data = data.replace('\n\n', '\n')
if data:
fp = open(fn, 'w', encoding='utf-8')
fp.write(data)
fp.close()
def save_rtupdate(self):
for package, settings in self.packages.items():
pkg_arg = '' if self.options.compile_all else "-p %s" % package
values = settings.get('rtupdates')
if not values:
continue
d = 'debian/{}/{}'.format(package, RT_LOCATIONS[self.impl])
if not exists(d):
makedirs(d)
fn = "%s/%s.rtupdate" % (d, package)
if exists(fn):
data = open(fn, 'r', encoding='utf-8').read()
else:
data = "#! /bin/sh\nset -e"
for dname, args in values:
cmd = RT_TPLS[self.impl].format(pkg_arg=pkg_arg,
dname=dname,
args=args)
if cmd not in data:
data += "\n%s" % cmd
if data:
fp = open(fn, 'w', encoding='utf-8')
fp.write(data)
fp.close()
chmod(fn, 0o755)
def save(self):
self.save_substvars()
self.save_autoscripts()
self.save_rtupdate()

281
dhpython/depends.py Normal file
View File

@ -0,0 +1,281 @@
# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
from functools import partial
from os.path import exists, join
from dhpython import PKG_PREFIX_MAP, MINPYCDEP
from dhpython.pydist import parse_pydep, parse_requires_dist, guess_dependency
from dhpython.version import default, supported, VersionRange
log = logging.getLogger('dhpython')
class Dependencies:
"""Store relations (dependencies, etc.) between packages."""
def __init__(self, package, impl='cpython3', bdep=None):
self.impl = impl
self.package = package
bdep = self.bdep = bdep or {}
self.is_debug_package = dbgpkg = package.endswith('-dbg')
# TODO: move it to PyPy and CPython{2,3} classes
self.ipkg_vtpl = 'python%s-dbg' if dbgpkg else 'python%s'
if impl == 'cpython3':
self.ipkg_tpl = 'python3-dbg' if dbgpkg else 'python3'
elif impl == 'cpython2':
self.ipkg_tpl = 'python2-dbg' if dbgpkg else 'python2'
elif impl == 'pypy':
self.ipkg_tpl = 'pypy-dbg' if dbgpkg else 'pypy'
self.ipkg_vtpl = 'pypy%s-dbg' if dbgpkg else 'pypy%s'
if impl == 'pypy':
self.ipkg_tpl_ma = self.ipkg_tpl
self.ipkg_vtpl_ma = self.ipkg_vtpl
else:
self.ipkg_tpl_ma = self.ipkg_tpl + ':any'
self.ipkg_vtpl_ma = self.ipkg_vtpl + ':any'
self.python_dev_in_bd = 'python-dev' in bdep or\
'python-all-dev' in bdep or\
'python2-dev' in bdep or\
'python2-all-dev' in bdep or\
'python2.7-dev' in bdep or\
'python3-dev' in bdep or\
'python3-all-dev' in bdep
self.depends = set()
self.recommends = []
self.suggests = []
self.enhances = []
self.breaks = []
self.rtscripts = []
def export_to(self, dh):
"""Fill in debhelper's substvars."""
prefix = PKG_PREFIX_MAP.get(self.impl, 'misc')
for i in sorted(self.depends):
dh.addsubstvar(self.package, '{}:Depends'.format(prefix), i)
for i in sorted(self.recommends):
dh.addsubstvar(self.package, '{}:Recommends'.format(prefix), i)
for i in sorted(self.suggests):
dh.addsubstvar(self.package, '{}:Suggests'.format(prefix), i)
for i in sorted(self.enhances):
dh.addsubstvar(self.package, '{}:Enhances'.format(prefix), i)
for i in sorted(self.breaks):
dh.addsubstvar(self.package, '{}:Breaks'.format(prefix), i)
for i in sorted(self.rtscripts):
dh.add_rtupdate(self.package, i)
def __str__(self):
return "D=%s; R=%s; S=%s; E=%s, B=%s; RT=%s" %\
(self.depends, self.recommends, self.suggests,
self.enhances, self.breaks, self.rtscripts)
def depend(self, value):
if value and value not in self.depends:
self.depends.add(value)
def recommend(self, value):
if value and value not in self.recommends:
self.recommends.append(value)
def suggest(self, value):
if value and value not in self.suggests:
self.suggests.append(value)
def enhance(self, value):
if value and value not in self.enhances:
self.enhances.append(value)
def break_(self, value):
if value and value not in self.breaks:
self.breaks.append(value)
def rtscript(self, value):
if value not in self.rtscripts:
self.rtscripts.append(value)
def parse(self, stats, options):
log.debug('generating dependencies for package %s', self.package)
tpl = self.ipkg_tpl
vtpl = self.ipkg_vtpl
tpl_ma = self.ipkg_tpl_ma
vtpl_ma = self.ipkg_vtpl_ma
vrange = options.vrange
if vrange and any((stats['compile'], stats['public_vers'],
stats['ext_vers'], stats['ext_no_version'],
stats['shebangs'])):
if any((stats['compile'], stats['public_vers'], stats['shebangs'])):
tpl_tmp = tpl_ma
else:
tpl_tmp = tpl
minv = vrange.minver
# note it's an open interval (i.e. do not add 1 here!):
maxv = vrange.maxver
if minv == maxv:
self.depend(vtpl % minv)
minv = maxv = None
if minv:
self.depend("%s (>= %s~)" % (tpl_tmp, minv))
if maxv:
self.depend("%s (<< %s)" % (tpl_tmp, maxv))
if self.impl == 'cpython2' and stats['public_vers']:
# additional Depends to block python package transitions
sorted_vers = sorted(stats['public_vers'])
minv = sorted_vers[0]
maxv = sorted_vers[-1]
if minv <= default(self.impl):
self.depend("%s (>= %s~)" % (tpl_ma, minv))
if maxv >= default(self.impl):
self.depend("%s (<< %s)" % (tpl_ma, maxv + 1))
if self.impl == 'pypy' and stats.get('ext_soabi'):
# TODO: make sure alternative is used only for the same extension names
# ie. for foo.ABI1.so, foo.ABI2.so, bar.ABI3,so, bar.ABI4.so generate:
# pypy-abi-ABI1 | pypy-abi-ABI2, pypy-abi-ABI3 | pypy-abi-ABI4
self.depend('|'.join(soabi.replace('-', '-abi-')
for soabi in sorted(stats['ext_soabi'])))
if stats['ext_vers']:
# TODO: what about extensions with stable ABI?
sorted_vers = sorted(stats['ext_vers'])
minv = sorted_vers[0]
maxv = sorted_vers[-1]
#self.depend('|'.join(vtpl % i for i in stats['ext_vers']))
if minv <= default(self.impl):
self.depend("%s (>= %s~)" % (tpl, minv))
if maxv >= default(self.impl):
self.depend("%s (<< %s)" % (tpl, maxv + 1))
# make sure py{,3}compile binary is available
if stats['compile'] and self.impl in MINPYCDEP:
self.depend(MINPYCDEP[self.impl])
for ipreter in stats['shebangs']:
self.depend("%s%s" % (ipreter, '' if self.impl == 'pypy' else ':any'))
supported_versions = supported(self.impl)
default_version = default(self.impl)
for private_dir, details in stats['private_dirs'].items():
versions = list(i.version for i in details.get('shebangs', []) if i.version and i.version.minor)
for v in versions:
if v in supported_versions:
self.depend(vtpl_ma % v)
else:
log.info('dependency on %s (from shebang) ignored'
' - it\'s not supported anymore', vtpl % v)
# /usr/bin/python{,3} shebang → add python{,3} to Depends
if any(True for i in details.get('shebangs', []) if i.version is None or i.version.minor is None):
self.depend(tpl_ma)
extensions = False
if self.python_dev_in_bd:
extensions = sorted(details.get('ext_vers', set()))
#self.depend('|'.join(vtpl % i for i in extensions))
if extensions:
self.depend("%s (>= %s~)" % (tpl, extensions[0]))
self.depend("%s (<< %s)" % (tpl, extensions[-1] + 1))
elif details.get('ext_no_version'):
# assume unrecognized extension was built for default interpreter version
self.depend("%s (>= %s~)" % (tpl, default_version))
self.depend("%s (<< %s)" % (tpl, default_version + 1))
if details.get('compile'):
if self.impl in MINPYCDEP:
self.depend(MINPYCDEP[self.impl])
args = ''
if extensions:
args += "-V %s" % VersionRange(minver=extensions[0], maxver=extensions[-1])
elif len(versions) == 1: # only one version from shebang
#if versions[0] in supported_versions:
args += "-V %s" % versions[0]
# ... otherwise compile with default version
elif details.get('ext_no_version'):
# assume unrecognized extension was built for default interpreter version
args += "-V %s" % default_version
elif vrange:
args += "-V %s" % vrange
if vrange.minver == vrange.maxver:
self.depend(vtpl % vrange.minver)
else:
if vrange.minver: # minimum version specified
self.depend("%s (>= %s~)" % (tpl_ma, vrange.minver))
if vrange.maxver: # maximum version specified
self.depend("%s (<< %s)" % (tpl_ma, vrange.maxver + 1))
for regex in options.regexpr or []:
args += " -X '%s'" % regex.pattern.replace("'", r"'\''")
self.rtscript((private_dir, args))
section_options = {
'depends_sec': options.depends_section,
'recommends_sec': options.recommends_section,
'suggests_sec': options.suggests_section,
}
guess_deps = partial(guess_dependency, impl=self.impl, bdep=self.bdep,
accept_upstream_versions=options.accept_upstream_versions)
if options.guess_deps:
for fn in stats['requires.txt']:
# TODO: should options.recommends and options.suggests be
# removed from requires.txt?
deps = parse_pydep(self.impl, fn, bdep=self.bdep, **section_options)
[self.depend(i) for i in deps['depends']]
[self.recommend(i) for i in deps['recommends']]
[self.suggest(i) for i in deps['suggests']]
for fpath in stats['egg-info']:
with open(fpath, 'r', encoding='utf-8') as fp:
for line in fp:
if line.startswith('Requires: '):
req = line[10:].strip()
self.depend(guess_deps(req=req))
for fpath in stats['dist-info']:
deps = parse_requires_dist(self.impl, fpath, bdep=self.bdep,
**section_options)
[self.depend(i) for i in deps['depends']]
[self.recommend(i) for i in deps['recommends']]
[self.suggest(i) for i in deps['suggests']]
# add dependencies from --depends
for item in options.depends or []:
self.depend(guess_deps(req=item))
# add dependencies from --recommends
for item in options.recommends or []:
self.recommend(guess_deps(req=item))
# add dependencies from --suggests
for item in options.suggests or []:
self.suggest(guess_deps(req=item))
# add dependencies from --requires
for fn in options.requires or []:
fpath = join('debian', self.package, fn)
if not exists(fpath):
fpath = fn
if not exists(fpath):
log.warn('cannot find requirements file: %s', fn)
continue
deps = parse_pydep(self.impl, fpath, bdep=self.bdep, **section_options)
[self.depend(i) for i in deps['depends']]
[self.recommend(i) for i in deps['recommends']]
[self.suggest(i) for i in deps['suggests']]
log.debug(self)

23
dhpython/exceptions.py Normal file
View File

@ -0,0 +1,23 @@
# Copyright © 2022 Stefano Rivera <stefanor@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
class RequiredCommandMissingException(Exception):
pass

586
dhpython/fs.py Normal file
View File

@ -0,0 +1,586 @@
# Copyright © 2013-2019 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import difflib
import hashlib
import logging
import os
import re
import sys
from filecmp import cmp as cmpfile
from glob import glob
from os.path import (lexists, exists, getsize, isdir, islink, join, realpath,
relpath, split, splitext)
from shutil import rmtree
from stat import ST_MODE, S_IXUSR, S_IXGRP, S_IXOTH
from dhpython import MULTIARCH_DIR_TPL
from dhpython.tools import fix_shebang, clean_egg_name
from dhpython.interpreter import Interpreter
log = logging.getLogger('dhpython')
def fix_locations(package, interpreter, versions, options):
"""Move files to the right location."""
# make a copy since we change version later
interpreter = Interpreter(interpreter)
for version in versions:
interpreter.version = version
dstdir = interpreter.sitedir(package)
for srcdir in interpreter.old_sitedirs(package):
if isdir(srcdir):
# TODO: what about relative symlinks?
log.debug('moving files from %s to %s', srcdir, dstdir)
share_files(srcdir, dstdir, interpreter, options)
try:
os.removedirs(srcdir)
except OSError:
pass
# do the same with debug locations
dstdir = interpreter.sitedir(package, gdb=True)
for srcdir in interpreter.old_sitedirs(package, gdb=True):
if isdir(srcdir):
log.debug('moving files from %s to %s', srcdir, dstdir)
share_files(srcdir, dstdir, interpreter, options)
try:
os.removedirs(srcdir)
except OSError:
pass
# move files from /usr/include/pythonX.Y/ to …/pythonX.Ym/
if interpreter.symlinked_include_dir:
srcdir = "debian/%s%s" % (package, interpreter.symlinked_include_dir)
if srcdir and isdir(srcdir):
dstdir = "debian/%s%s" % (package, interpreter.include_dir)
log.debug('moving files from %s to %s', srcdir, dstdir)
share_files(srcdir, dstdir, interpreter, options)
try:
os.removedirs(srcdir)
except OSError:
pass
def share_files(srcdir, dstdir, interpreter, options):
"""Try to move as many files from srcdir to dstdir as possible."""
cleanup_actions = []
for i in os.listdir(srcdir):
fpath1 = join(srcdir, i)
if not lexists(fpath1): # removed in rename_ext
continue
if i.endswith('.pyc'): # f.e. when tests were invoked on installed files
os.remove(fpath1)
continue
if not options.no_ext_rename and splitext(i)[-1] == '.so':
# try to rename extension here as well (in :meth:`scan` info about
# Python version is gone)
version = interpreter.parse_public_dir(srcdir)
if version and version is not True:
fpath1 = Scan.rename_ext(fpath1, interpreter, version)
i = split(fpath1)[-1]
if srcdir.endswith(".dist-info"):
if i == 'LICENSE' or i.startswith('LICENSE.'):
os.remove(fpath1)
cleanup_actions.append((remove_from_RECORD, ([i],)))
continue
elif isdir(fpath1) and i == 'license_files':
cleanup_actions.append((
remove_from_RECORD,
([
relpath(license, srcdir)
for license in glob(join(srcdir, i, '**'))
],)
))
rmtree(fpath1)
continue
fpath2 = join(dstdir, i)
if not isdir(fpath1) and not exists(fpath2):
# do not rename directories here - all .so files have to be renamed first
os.renames(fpath1, fpath2)
continue
if islink(fpath1):
# move symlinks without changing them if they point to the same place
if not exists(fpath2):
os.renames(fpath1, fpath2)
elif realpath(fpath1) == realpath(fpath2):
os.remove(fpath1)
elif isdir(fpath1):
share_files(fpath1, fpath2, interpreter, options)
elif cmpfile(fpath1, fpath2, shallow=False):
os.remove(fpath1)
elif i.endswith(('.abi3.so', '.abi4.so')) and interpreter.parse_public_dir(srcdir):
log.warning('%s differs from previous one, removing anyway (%s)', i, srcdir)
os.remove(fpath1)
elif srcdir.endswith(".dist-info"):
# dist-info file that differs... try merging
if i == "WHEEL":
if merge_WHEEL(fpath1, fpath2):
cleanup_actions.append((fix_merged_RECORD, ()))
os.remove(fpath1)
elif i == "RECORD":
merge_RECORD(fpath1, fpath2)
os.remove(fpath1)
else:
log.warn("No merge driver for dist-info file %s", i)
else:
# The files differed so we cannot collapse them.
log.warn('Paths differ: %s and %s', fpath1, fpath2)
if options.verbose and not i.endswith(('.so', '.a')):
with open(fpath1) as fp1:
fromlines = fp1.readlines()
with open(fpath2) as fp2:
tolines = fp2.readlines()
diff = difflib.unified_diff(fromlines, tolines, fpath1, fpath2)
sys.stderr.writelines(diff)
for action, args in cleanup_actions:
action(dstdir, *args)
try:
os.removedirs(srcdir)
except OSError:
pass
## Functions to merge parts of the .dist-info metadata directory together
def missing_lines(src, dst):
"""Find all the lines in the text file src that are not in dst"""
with open(dst) as fh:
current = {k: None for k in fh.readlines()}
missing = []
with open(src) as fh:
for line in fh.readlines():
if line not in current:
missing.append(line)
return missing
def merge_WHEEL(src, dst):
"""Merge the source .dist-info/WHEEL file into the destination
Note that after editing the WHEEL file, the sha256 included in
the .dist-info/RECORD file will be incorrect and will need fixing
using the fix_merged_RECORD() function.
"""
log.debug("Merging WHEEL file %s into %s", src, dst)
missing = missing_lines(src, dst)
with open(dst, "at") as fh:
for line in missing:
if line.startswith("Tag: "):
fh.write(line)
else:
log.warn("WHEEL merge discarded line %s", line)
return len(missing)
def merge_RECORD(src, dst):
"""Merge the source .dist-info/RECORD file into the destination"""
log.debug("Merging RECORD file %s into %s", src, dst)
missing = missing_lines(src, dst)
with open(dst, "at") as fh:
for line in missing:
fh.write(line)
return len(missing)
def fix_merged_RECORD(distdir):
"""Update the checksum for .dist-info/WHEEL in .dist-info/RECORD
After merging the .dist-info/WHEEL file, the sha256 recorded for it will be
wrong in .dist-info/RECORD, so edit that file to ensure that it is fixed.
The output is sorted for reproducibility.
"""
log.debug("Fixing RECORD file in %s", distdir)
record_path = join(distdir, "RECORD")
wheel_path = join(distdir, "WHEEL")
wheel_dir = split(split(record_path)[0])[1]
wheel_relpath = join(wheel_dir, "WHEEL")
with open(wheel_path, "rb") as fh:
wheel_sha256 = hashlib.sha256(fh.read()).hexdigest();
wheel_size = getsize(wheel_path)
contents = [
"{name},sha256={sha256sum},{size}\n".format(
name=wheel_relpath,
sha256sum=wheel_sha256,
size=wheel_size,
)]
with open(record_path) as fh:
for line in fh.readlines():
if not line.startswith(wheel_relpath):
contents.append(line)
# now write out the updated record
with open(record_path, "wt") as fh:
fh.writelines(sorted(contents))
def remove_from_RECORD(distdir, files):
"""Remove all specified dist-info files from RECORD"""
log.debug("Removing %r from RECORD in %s", files, distdir)
record = join(distdir, "RECORD")
parent_dir = split(distdir)[1]
names = [join(parent_dir, name) for name in files]
lines = []
with open(record) as fh:
lines = fh.readlines()
filtered = [line for line in lines if not line.split(',', 1)[0] in names]
if lines == filtered:
log.warn("Unable to remove %r from RECORD in %s, not found",
files, distdir)
with open(record, 'wt') as fh:
fh.writelines(sorted(filtered))
class Scan:
UNWANTED_DIRS = re.compile(r'.*/__pycache__(/.*)?$')
UNWANTED_FILES = re.compile(r'.*\.py[co]$')
def __init__(self, interpreter, package, dpath=None, options=None):
self.interpreter = interpreter
self.impl = interpreter.impl
self.package = package
if not dpath:
self.proot = "debian/%s" % self.package
else:
dpath = dpath.strip('/')
self.proot = join('debian', self.package, dpath)
self.dpath = dpath
del dpath
self.options = options
self.result = {'requires.txt': set(),
'egg-info': set(),
'dist-info': set(),
'nsp.txt': set(),
'shebangs': set(),
'public_vers': set(),
'private_dirs': {},
'compile': False,
'ext_vers': set(),
'ext_no_version': set()}
for root, dirs, file_names in os.walk(self.proot):
if interpreter.should_ignore(root):
del dirs[:]
continue
self.current_private_dir = self.current_pub_version = None
version = interpreter.parse_public_dir(root)
if version:
self.current_dir_is_public = True
if version is True:
version = None
else:
self.current_pub_version = version
else:
self.current_dir_is_public = False
if self.current_dir_is_public:
if root.endswith('-packages'):
if version is not None:
self.result['public_vers'].add(version)
for name in dirs:
if name in ('test', 'tests') or name.startswith('.'):
log.debug('removing dist-packages/%s', name)
rmtree(join(root, name))
dirs.remove(name)
else:
self.current_private_dir = self.check_private_dir(root)
if not self.current_private_dir:
# i.e. not a public dir and not a private dir
if self.is_bin_dir(root):
self.handle_bin_dir(root, file_names)
else: # not a public, private or bin directory
# continue with a subdirectory
continue
for name in dirs:
dpath = join(root, name)
if self.is_unwanted_dir(dpath):
rmtree(dpath)
dirs.remove(name)
continue
if self.is_dist_dir(root):
self.handle_dist_dir(root, file_names)
continue
if self.is_egg_dir(root):
self.handle_egg_dir(root, file_names)
continue
# check files
for fn in sorted(file_names):
# sorted() to make sure .so files are handled before .so.foo
fpath = join(root, fn)
if self.is_unwanted_file(fpath):
log.debug('removing unwanted: %s', fpath)
os.remove(fpath)
continue
if self.is_egg_file(fpath):
self.handle_egg_file(fpath)
continue
if not exists(fpath):
# possibly removed while handling .so symlinks
if islink(fpath) and '.so.' in split(fpath)[-1]:
# dangling symlink to (now removed/renamed) .so file
# which wasn't removed yet (see test203's quux.so.0)
log.info('removing dangling symlink: %s', fpath)
os.remove(fpath)
continue
fext = splitext(fn)[-1][1:]
if fext == 'so':
if not self.options.no_ext_rename:
fpath = self.rename_ext(fpath, interpreter, version)
ver = self.handle_ext(fpath)
ver = ver or version
if ver:
self.current_result.setdefault('ext_vers', set()).add(ver)
else:
self.current_result.setdefault('ext_no_version', set()).add(fpath)
if self.current_private_dir:
if exists(fpath) and fext != 'so':
mode = os.stat(fpath)[ST_MODE]
if mode & S_IXUSR or mode & S_IXGRP or mode & S_IXOTH:
if (options.no_shebang_rewrite or
fix_shebang(fpath, self.options.shebang)) and \
not self.options.ignore_shebangs:
try:
res = Interpreter.from_file(fpath)
except Exception as e:
log.debug('cannot parse shebang %s: %s', fpath, e)
else:
self.current_result.setdefault('shebangs', set()).add(res)
if fext == 'py' and self.handle_public_module(fpath) is not False:
self.current_result['compile'] = True
if not dirs and not self.current_private_dir:
try:
os.removedirs(root)
except OSError:
pass
log.debug("package %s details = %s", package, self.result)
@property
def current_result(self):
if self.current_private_dir:
return self.result['private_dirs'].setdefault(self.current_private_dir, {})
return self.result
def is_unwanted_dir(self, dpath):
return self.__class__.UNWANTED_DIRS.match(dpath)
def is_unwanted_file(self, fpath):
if self.__class__.UNWANTED_FILES.match(fpath):
return True
if self.current_dir_is_public and self.is_dbg_package\
and self.options.clean_dbg_pkg\
and splitext(fpath)[-1][1:] not in ('so', 'h'):
return True
@property
def private_dirs_to_check(self):
if self.dpath:
# scan private directory *only*
return [self.dpath]
if self.dpath is False:
result = []
else:
result = [i % self.package for i in (
'usr/lib/%s',
'usr/lib/games/%s',
'usr/share/%s',
'usr/share/games/%s')]
return result
@property
def is_dbg_package(self):
#return self.interpreter.debug
return self.package.endswith('-dbg')
def check_private_dir(self, dpath):
"""Return private dir's root if it's a private dir."""
for i in self.private_dirs_to_check:
if dpath.startswith(join('debian', self.package, i)):
return '/' + i
@staticmethod
def rename_ext(fpath, interpreter, current_pub_version=None):
"""Add multiarch triplet, etc. Return new name.
This method is invoked for all .so files in public or private directories.
"""
# current_pub_version - version parsed from dist-packages (True if unversioned)
# i.e. if it's not None - it's a public dist-packages directory
path, fname = fpath.rsplit('/', 1)
if current_pub_version is not None and islink(fpath):
# replace symlinks with extensions in dist-packages directory
dstfpath = fpath
links = set()
while islink(dstfpath):
links.add(dstfpath)
dstfpath = join(path, os.readlink(dstfpath))
if exists(dstfpath) and '.so.' in split(dstfpath)[-1]:
# rename .so.$FOO symlinks, remove other ones
for lpath in links:
log.info('removing symlink: %s', lpath)
os.remove(lpath)
log.info('renaming %s to %s', dstfpath, fname)
os.rename(dstfpath, fpath)
if MULTIARCH_DIR_TPL.match(fpath):
# ignore /lib/i386-linux-gnu/, /usr/lib/x86_64-kfreebsd-gnu/, etc.
return fpath
new_fn = interpreter.check_extname(fname, current_pub_version)
if new_fn:
# TODO: what about symlinks pointing to this file
new_fpath = join(path, new_fn)
if exists(new_fpath):
log.warn('destination file exist, '
'cannot rename %s to %s', fname, new_fn)
else:
log.info('renaming %s to %s', fname, new_fn)
os.rename(fpath, new_fpath)
return new_fpath
return fpath
def handle_ext(self, fpath):
"""Handle .so file, return its version if detected."""
def handle_public_module(self, fpath):
pass
def is_bin_dir(self, dpath):
"""Check if dir is one from PATH ones."""
# dname = debian/packagename/usr/games
spath = dpath.strip('/').split('/', 4)
if len(spath) > 4:
return False # assume bin directories don't have subdirectories
if dpath.endswith(('/sbin', '/bin', '/usr/games')):
# /(s)bin or /usr/(s)bin or /usr/games
return True
def handle_bin_dir(self, dpath, file_names):
if self.options.no_shebang_rewrite or self.options.ignore_shebangs:
return
for fn in file_names:
fpath = join(dpath, fn)
if fix_shebang(fpath, self.options.shebang):
try:
res = Interpreter.from_file(fpath)
except Exception as e:
log.debug('cannot parse shebang %s: %s', fpath, e)
else:
self.result['shebangs'].add(res)
def is_egg_dir(self, dname):
"""Check if given directory contains egg-info."""
return dname.endswith('.egg-info')
def handle_egg_dir(self, dpath, file_names):
path, dname = dpath.rsplit('/', 1)
if self.is_dbg_package and self.options.clean_dbg_pkg:
rmtree(dpath)
return
clean_name = clean_egg_name(dname)
if clean_name != dname:
if exists(join(path, clean_name)):
log.info('removing %s (%s is already available)', dname, clean_name)
rmtree(dpath)
return
else:
log.info('renaming %s to %s', dname, clean_name)
os.rename(dpath, join(path, clean_name))
dname = clean_name
dpath = join(path, dname)
if file_names:
if 'requires.txt' in file_names:
self.result['requires.txt'].add(join(dpath, 'requires.txt'))
if 'namespace_packages.txt' in file_names:
self.result['nsp.txt'].add(join(dpath, 'namespace_packages.txt'))
if 'SOURCES.txt' in file_names:
os.remove(join(dpath, 'SOURCES.txt'))
file_names.remove('SOURCES.txt')
def is_egg_file(self, fpath):
"""Check if given file contains egg-info."""
return fpath.endswith('.egg-info')
def handle_egg_file(self, fpath):
root, name = fpath.rsplit('/', 1)
clean_name = clean_egg_name(name)
if clean_name != name:
if exists(join(root, clean_name)):
log.info('removing %s (%s is already available)',
name, clean_name)
os.remove(fpath)
else:
log.info('renaming %s to %s', name, clean_name)
os.rename(fpath, join(root, clean_name))
self.result['egg-info'].add(join(root, clean_name))
def is_dist_dir(self, dname):
"""Check if given directory contains dist-info."""
return dname.endswith('.dist-info')
def handle_dist_dir(self, dpath, file_names):
path, dname = dpath.rsplit('/', 1)
if self.is_dbg_package and self.options.clean_dbg_pkg:
rmtree(dpath)
return
if file_names:
if 'METADATA' in file_names:
self.result['dist-info'].add(join(dpath, 'METADATA'))
def cleanup(self):
if self.is_dbg_package and self.options.clean_dbg_pkg:
# remove empty directories in -dbg packages
proot = self.proot + '/usr/lib'
for root, dirs, file_names in os.walk(proot, topdown=False):
if '-packages/' in root and not file_names:
try:
os.removedirs(root)
except Exception:
pass

576
dhpython/interpreter.py Normal file
View File

@ -0,0 +1,576 @@
# Copyright © 2012-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import os
import re
from os.path import exists, join, split
from dhpython import INTERPRETER_DIR_TPLS, PUBLIC_DIR_RE, OLD_SITE_DIRS
SHEBANG_RE = re.compile(r'''
(?:\#!\s*){0,1} # shebang prefix
(?P<path>
.*?/bin/.*?)?
(?P<name>
python|pypy)
(?P<version>
\d[\.\d]*)?
(?P<debug>
-dbg)?
(?P<options>.*)
''', re.VERBOSE)
EXTFILE_RE = re.compile(r'''
(?P<name>.*?)
(?:\.
(?P<stableabi>abi\d+)
|(?:\.
(?P<soabi>
(?P<impl>cpython|pypy)
-
(?P<ver>\d{2,})
(?P<flags>[a-z]*)
)?
(?:
(?:(?<!\.)-)? # minus sign only if soabi is defined
(?P<multiarch>[^/]*?)
)?
))?
(?P<debug>_d)?
\.so$''', re.VERBOSE)
log = logging.getLogger('dhpython')
class Interpreter:
"""
:attr path: /usr/bin/ in most cases
:attr name: pypy or python (even for python3 and python-dbg) or empty string
:attr version: interpreter's version
:attr debug: -dbg version of the interpreter
:attr impl: implementation (cpytho2, cpython3 or pypy)
:attr options: options parsed from shebang
:type path: str
:type name: str
:type version: Version or None
:type debug: bool
:type impl: str
:type options: tuple
"""
path = '/usr/bin/'
name = 'python'
version = None
debug = False
impl = ''
options = ()
_cache = {}
def __init__(self, value=None, path=None, name=None, version=None,
debug=None, impl=None, options=None):
params = locals()
del params['self']
del params['value']
if isinstance(value, Interpreter):
for key in params.keys():
if params[key] is None:
params[key] = getattr(value, key)
elif value:
if value.replace('.', '').isdigit() and not version:
# version string
params['version'] = Version(value)
else:
# shebang or other string
for key, val in self.parse(value).items():
# prefer values passed to constructor over shebang ones:
if params[key] is None:
params[key] = val
for key, val in params.items():
if val is not None:
setattr(self, key, val)
elif key == 'version':
setattr(self, key, val)
def __setattr__(self, name, value):
if name == 'name':
if value not in ('python', 'pypy', ''):
raise ValueError("interpreter not supported: %s" % value)
if value == 'python':
if self.version:
if self.version.major == 3:
self.__dict__['impl'] = 'cpython3'
else:
self.__dict__['impl'] = 'cpython2'
elif value == 'pypy':
self.__dict__['impl'] = 'pypy'
elif name == 'version' and value is not None:
value = Version(value)
if not self.impl and self.name == 'python':
if value.major == 3:
self.impl = 'cpython3'
else:
self.impl = 'cpython2'
if name in ('path', 'name', 'impl', 'options') and value is None:
pass
elif name == 'debug':
self.__dict__[name] = bool(value)
else:
self.__dict__[name] = value
def __repr__(self):
result = self.path
if not result.endswith('/'):
result += '/'
result += self._vstr(self.version)
if self.options:
result += ' ' + ' '.join(self.options)
return result
def __str__(self):
return self._vstr(self.version)
def _vstr(self, version=None, consider_default_ver=False):
if self.impl == 'pypy':
# TODO: will Debian support more than one PyPy version?
return self.name
version = version or self.version or ''
if consider_default_ver and (not version or version == self.default_version):
version = '3' if self.impl == 'cpython3' else '2'
if self.debug:
return 'python{}-dbg'.format(version)
return self.name + str(version)
def binary(self, version=None):
return '{}{}'.format(self.path, self._vstr(version))
@property
def binary_dv(self):
"""Like binary(), but returns path to default intepreter symlink
if version matches default one for given implementation.
"""
return '{}{}'.format(self.path, self._vstr(consider_default_ver=True))
@property
def default_version(self):
if self.impl:
return default(self.impl)
@staticmethod
def parse(shebang):
"""Return dict with parsed shebang
>>> sorted(Interpreter.parse('/usr/bin/python3.2-dbg').items())
[('debug', '-dbg'), ('name', 'python'), ('options', ()), ('path', '/usr/bin/'), ('version', '3.2')]
>>> sorted(Interpreter.parse('#! /usr/bin/python3.2').items())
[('debug', None), ('name', 'python'), ('options', ()), ('path', '/usr/bin/'), ('version', '3.2')]
>>> sorted(Interpreter.parse('/usr/bin/python3.2-dbg --foo --bar').items())
[('debug', '-dbg'), ('name', 'python'), ('options', ('--foo', '--bar')),\
('path', '/usr/bin/'), ('version', '3.2')]
"""
result = SHEBANG_RE.search(shebang)
if not result:
return {}
result = result.groupdict()
if 'options' in result:
# TODO: do we need "--key value" here?
result['options'] = tuple(result['options'].split())
if result['name'] == 'python' and result['version'] is None:
result['version'] = '2'
return result
@classmethod
def from_file(cls, fpath):
"""Read file's shebang and parse it."""
interpreter = Interpreter()
with open(fpath, 'rb') as fp:
data = fp.read(96)
if b"\0" in data:
raise ValueError('cannot parse binary file')
# make sure only first line is checkeed
data = str(data, 'utf-8').split('\n')[0]
if not data.startswith('#!'):
raise ValueError("doesn't look like a shebang: %s" % data)
parsed = cls.parse(data)
if not parsed:
raise ValueError("doesn't look like a shebang: %s" % data)
for key, val in parsed.items():
setattr(interpreter, key, val)
return interpreter
def sitedir(self, package=None, version=None, gdb=False):
"""Return path to site-packages directory.
Note that returned path is not the final location of .py files
>>> i = Interpreter('python')
>>> i.sitedir(version='3.1')
'/usr/lib/python3/dist-packages/'
>>> i.sitedir(version='2.5')
'/usr/lib/python2.5/site-packages/'
>>> i.sitedir(version=Version('2.7'))
'/usr/lib/python2.7/dist-packages/'
>>> i.sitedir(version='3.1', gdb=True, package='python3-foo')
'debian/python3-foo/usr/lib/debug/usr/lib/python3/dist-packages/'
>>> i.sitedir(version=Version('3.2'))
'/usr/lib/python3/dist-packages/'
"""
try:
version = Version(version or self.version)
except Exception as err:
raise ValueError("cannot find valid version: %s" % err)
if self.impl == 'pypy':
path = '/usr/lib/pypy/dist-packages/'
elif version << Version('2.6'):
path = "/usr/lib/python%s/site-packages/" % version
elif version << Version('3.0'):
path = "/usr/lib/python%s/dist-packages/" % version
else:
path = '/usr/lib/python3/dist-packages/'
if gdb:
path = "/usr/lib/debug%s" % path
if package:
path = "debian/%s%s" % (package, path)
return path
def old_sitedirs(self, package=None, version=None, gdb=False):
"""Return deprecated paths to site-packages directories."""
try:
version = Version(version or self.version)
except Exception as err:
raise ValueError("cannot find valid version: %s" % err)
result = []
for item in OLD_SITE_DIRS.get(self.impl, []):
if isinstance(item, str):
result.append(item.format(version))
else:
res = item(version)
if res is not None:
result.append(res)
if gdb:
result = ['/usr/lib/debug{}'.format(i) for i in result]
if self.impl.startswith('cpython'):
result.append('/usr/lib/debug/usr/lib/pyshared/python{}'.format(version))
if package:
result = ['debian/{}{}'.format(package, i) for i in result]
return result
def parse_public_dir(self, path):
"""Return version assigned to site-packages path
or True is it's unversioned public dir."""
match = PUBLIC_DIR_RE[self.impl].match(path)
if match:
vers = match.groups(0)
if vers and vers[0]:
return Version(vers)
return True
def should_ignore(self, path):
"""Return True if path is used by another interpreter implementation."""
cache_key = 'should_ignore_{}'.format(self.impl)
if cache_key not in self.__class__._cache:
expr = [v for k, v in INTERPRETER_DIR_TPLS.items() if k != self.impl]
regexp = re.compile('|'.join('({})'.format(i) for i in expr))
self.__class__._cache[cache_key] = regexp
else:
regexp = self.__class__._cache[cache_key]
return regexp.search(path)
def cache_file(self, fpath, version=None):
"""Given path to a .py file, return path to its .pyc/.pyo file.
This function is inspired by Python 3.2's imp.cache_from_source.
:param fpath: path to file name
:param version: Python version
>>> i = Interpreter('python')
>>> i.cache_file('foo.py', Version('3.1'))
'foo.pyc'
>>> i.cache_file('bar/foo.py', '3.8') # doctest: +SKIP
'bar/__pycache__/foo.cpython-38.pyc'
"""
version = Version(version or self.version)
last_char = 'o' if '-O' in self.options else 'c'
if version <= Version('3.1'):
return fpath + last_char
fdir, fname = split(fpath)
if not fname.endswith('.py'):
fname += '.py'
return join(fdir, '__pycache__', "%s.%s.py%s" %
(fname[:-3], self.magic_tag(version), last_char))
def magic_number(self, version=None):
"""Return magic number."""
version = Version(version or self.version)
if self.impl == 'cpython2':
return ''
result = self._execute('import imp; print(imp.get_magic())', version)
return eval(result)
def magic_tag(self, version=None):
"""Return Python magic tag (used in __pycache__ dir to tag files).
>>> i = Interpreter('python')
>>> i.magic_tag(version='3.8') # doctest: +SKIP
'cpython-38'
"""
version = Version(version or self.version)
if self.impl.startswith('cpython') and version << Version('3.2'):
return ''
return self._execute('import imp; print(imp.get_tag())', version)
def multiarch(self, version=None):
"""Return multiarch tag."""
version = Version(version or self.version)
try:
soabi, multiarch = self._get_config(version)[:2]
except Exception:
log.debug('cannot get multiarch', exc_info=True)
# interpreter without multiarch support
return ''
return multiarch
def stableabi(self, version=None):
version = Version(version or self.version)
# stable ABI was introduced in Python 3.3
if self.impl == 'cpython3' and version >> Version('3.2'):
return 'abi{}'.format(version.major)
def soabi(self, version=None):
"""Return SOABI flag (used to in .so files)."""
version = Version(version or self.version)
# NOTE: it's not the same as magic_tag
try:
soabi, multiarch = self._get_config(version)[:2]
except Exception:
log.debug('cannot get soabi', exc_info=True)
# interpreter without soabi support
return ''
return soabi
@property
def include_dir(self):
"""Return INCLUDE_DIR path.
>>> Interpreter('python2.7').include_dir # doctest: +SKIP
'/usr/include/python2.7'
>>> Interpreter('python3.8-dbg').include_dir # doctest: +SKIP
'/usr/include/python3.8d'
"""
if self.impl == 'pypy':
return '/usr/lib/pypy/include'
try:
result = self._get_config()[2]
if result:
return result
except Exception:
result = ''
log.debug('cannot get include path', exc_info=True)
result = '/usr/include/{}'.format(self.name)
version = self.version
if self.debug:
if version >= '3.8':
result += 'd'
elif version << '3.3':
result += '_d'
else:
result += 'dm'
else:
if version >= '3.8':
pass
elif version >> '3.2':
result += 'm'
elif version == '3.2':
result += 'mu'
return result
@property
def symlinked_include_dir(self):
"""Return path to symlinked include directory."""
if self.impl in ('cpython2', 'pypy') or self.debug \
or self.version >> '3.7' or self.version << '3.3':
# these interpreters do not provide symlink,
# others provide it in libpython3.X-dev
return
try:
result = self._get_config()[2]
if result:
if result.endswith('m'):
return result[:-1]
else:
# there's include_dir, but no "m"
return
except Exception:
result = '/usr/include/{}'.format(self.name)
log.debug('cannot get include path', exc_info=True)
return result
@property
def library_file(self):
"""Return libfoo.so file path."""
if self.impl == 'pypy':
return ''
libpl, ldlibrary = self._get_config()[3:5]
if ldlibrary.endswith('.a'):
# python3.1-dbg, python3.2, python3.2-dbg returned static lib
ldlibrary = ldlibrary.replace('.a', '.so')
if libpl and ldlibrary:
return join(libpl, ldlibrary)
raise Exception('cannot find library file for {}'.format(self))
def check_extname(self, fname, version=None):
"""Return extension file name if file can be renamed."""
if not version and not self.version:
return
version = Version(version or self.version)
if '/' in fname:
fdir, fname = fname.rsplit('/', 1) # in case full path was passed
else:
fdir = ''
info = EXTFILE_RE.search(fname)
if not info:
return
info = info.groupdict()
if info['ver'] and (not version or version.minor is None):
# get version from soabi if version is not set of only major
# version number is set
version = Version("%s.%s" % (info['ver'][0], info['ver'][1]))
if info['stableabi']:
# files with stable ABI in name don't need changes
return
if info['debug'] and self.debug is False:
# do not change Python 2.X extensions already marked as debug
# (the other way around is acceptable)
return
if info['soabi'] and info['multiarch']:
# already tagged, nothing we can do here
return
try:
soabi, multiarch = self._get_config(version)[:2]
except Exception:
log.debug('cannot get soabi/multiarch', exc_info=True)
return
if info['soabi'] and soabi and info['soabi'] != soabi:
return
tmp_soabi = info['soabi'] or soabi
tmp_multiarch = info['multiarch'] or multiarch
result = info['name']
if result.endswith('module') and result != 'module' and (
self.impl == 'cpython3' and version >> '3.2' or
self.impl == 'cpython2' and version == '2.7'):
result = result[:-6]
if tmp_soabi:
result = "{}.{}".format(result, tmp_soabi)
if tmp_multiarch and not (self.impl == 'cpython3' and version << '3.3') and tmp_multiarch not in soabi:
result = "{}-{}".format(result, tmp_multiarch)
elif self.impl == 'cpython2' and version == '2.7' and tmp_multiarch:
result = "{}.{}".format(result, tmp_multiarch)
if self.debug and self.impl == 'cpython2':
result += '_d'
result += '.so'
if fname == result:
return
return join(fdir, result)
def suggest_pkg_name(self, name):
"""Suggest binary package name with for given library name
>>> Interpreter('python3.1').suggest_pkg_name('foo')
'python3-foo'
>>> Interpreter('python3.8').suggest_pkg_name('foo_bar')
'python3-foo-bar'
>>> Interpreter('python2.7-dbg').suggest_pkg_name('bar')
'python-bar-dbg'
"""
name = name.replace('_', '-')
if self.impl == 'pypy':
return 'pypy-{}'.format(name)
version = '3' if self.impl == 'cpython3' else ''
result = 'python{}-{}'.format(version, name)
if self.debug:
result += '-dbg'
return result
def _get_config(self, version=None):
version = Version(version or self.version)
# sysconfig module is available since Python 3.2
# (also backported to Python 2.7)
if self.impl == 'pypy' or self.impl.startswith('cpython') and (
version >> '2.6' and version << '3'
or version >> '3.1' or version == '3'):
cmd = 'import sysconfig as s;'
else:
cmd = 'from distutils import sysconfig as s;'
cmd += 'print("__SEP__".join(i or "" ' \
'for i in s.get_config_vars('\
'"SOABI", "MULTIARCH", "INCLUDEPY", "LIBPL", "LDLIBRARY")))'
conf_vars = self._execute(cmd, version).split('__SEP__')
if conf_vars[1] in conf_vars[0]:
# Python >= 3.5 includes MILTIARCH in SOABI
conf_vars[0] = conf_vars[0].replace("-%s" % conf_vars[1], '')
try:
conf_vars[1] = os.environ['DEB_HOST_MULTIARCH']
except KeyError:
pass
return conf_vars
def _execute(self, command, version=None, cache=True):
version = Version(version or self.version)
exe = "{}{}".format(self.path, self._vstr(version))
command = "{} -c '{}'".format(exe, command.replace("'", "\'"))
if cache and command in self.__class__._cache:
return self.__class__._cache[command]
if not exists(exe):
raise Exception("cannot execute command due to missing "
"interpreter: %s" % exe)
output = execute(command)
if output['returncode'] != 0:
log.debug(output['stderr'])
raise Exception('{} failed with status code {}'.format(command, output['returncode']))
result = output['stdout'].splitlines()
if len(result) == 1:
result = result[0]
if cache:
self.__class__._cache[command] = result
return result
# due to circular imports issue
from dhpython.tools import execute
from dhpython.version import Version, default

70
dhpython/markers.py Normal file
View File

@ -0,0 +1,70 @@
# Copyright © 2022 Stefano Rivera <stefanor@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
Handle Environment Markers
https://www.python.org/dev/peps/pep-0508/#environment-markers
TODO: Ideally replace with the packaging library, but the API is currently
private: https://github.com/pypa/packaging/issues/496
"""
import re
SIMPLE_ENV_MARKER_RE = re.compile(r'''
(?P<marker>[a-z_]+)
\s*
(?P<op><=?|>=?|[=!~]=|===)
\s*
(?P<quote>['"])
(?P<value>.*) # Could contain additional markers
(?P=quote)
''', re.VERBOSE)
COMPLEX_ENV_MARKER_RE = re.compile(r'''
(?:\s|\))
(?:and|or)
(?:\s|\()
''', re.VERBOSE)
class ComplexEnvironmentMarker(Exception):
pass
def parse_environment_marker(marker):
"""Parse a simple marker of <= 1 environment restriction"""
marker = marker.strip()
if marker.startswith('(') and marker.endswith(')'):
marker = marker[1:-1].strip()
m = COMPLEX_ENV_MARKER_RE.search(marker)
if m:
raise ComplexEnvironmentMarker()
m = SIMPLE_ENV_MARKER_RE.match(marker)
if not m:
raise ComplexEnvironmentMarker()
return (
m.group('marker'),
m.group('op'),
m.group('value'),
)

30
dhpython/option.py Normal file
View File

@ -0,0 +1,30 @@
# -*- coding: UTF-8 -*-
# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
def compiled_regex(string):
"""argparse regex type"""
try:
return re.compile(string)
except re.error:
raise ValueError("regular expression is not valid")

692
dhpython/pydist.py Normal file
View File

@ -0,0 +1,692 @@
# Copyright © 2010-2020 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import email
import logging
import platform
import os
import re
from functools import partial
from os.path import exists, isdir, join
from subprocess import PIPE, Popen
if __name__ == '__main__':
import sys
sys.path.append(os.path.abspath(join(os.path.dirname(__file__), '..')))
from dhpython import PKG_PREFIX_MAP, PUBLIC_DIR_RE,\
PYDIST_DIRS, PYDIST_OVERRIDES_FNAMES, PYDIST_DPKG_SEARCH_TPLS
from dhpython.markers import ComplexEnvironmentMarker, parse_environment_marker
from dhpython.tools import memoize
from dhpython.version import get_requested_versions, Version
log = logging.getLogger('dhpython')
PYDIST_RE = re.compile(r"""
(?P<name>[A-Za-z][A-Za-z0-9_.-]*) # Python distribution name
\s*
(?P<vrange>(?:-?\d\.\d+(?:-(?:\d\.\d+)?)?)?) # version range
\s*
(?P<dependency>(?:[a-z][^;]*)?) # Debian dependency
(?: # optional upstream version -> Debian version translator
;\s*
(?P<standard>PEP386)? # PEP-386 mode
\s*
(?P<rules>(?:s|tr|y).*)? # translator rules
)?
""", re.VERBOSE)
REQUIRES_RE = re.compile(r'''
(?P<name>[A-Za-z][A-Za-z0-9_.-]*) # Python distribution name
\s*
(?P<enabled_extras>(?:\[[^\]]*\])?) # ignored for now
\s*
\(? # optional parenthesis
(?: # optional minimum/maximum version
(?P<operator><=?|>=?|==|!=|~=)
\s*
(?P<version>(\w|[-.*])+)
(?: # optional interval minimum/maximum version
\s*
,
\s*
(?P<operator2><=?|>=?|==|!=)
\s*
(?P<version2>(\w|[-.])+)
)?
)?
\)? # optional closing parenthesis
\s*
(?:; # optional environment markers
(?P<environment_marker>.+)
)?
''', re.VERBOSE)
EXTRA_RE = re.compile(r'''
;
\s*
extra
\s*
==
\s*
(?P<quote>['"])
(?P<section>[a-zA-Z0-9-_.]+)
(?P=quote)
''', re.VERBOSE)
REQ_SECTIONS_RE = re.compile(r'''
^
\[
(?P<section>[a-zA-Z0-9-_.]+)?
\s*
(?::
(?P<environment_marker>.+)
)?
\]
\s*
$
''', re.VERBOSE)
DEB_VERS_OPS = {
'==': '=',
'<': '<<',
'>': '>>',
'~=': '>=',
}
def validate(fpath):
"""Check if pydist file looks good."""
with open(fpath, encoding='utf-8') as fp:
for line in fp:
line = line.strip()
if line.startswith('#') or not line:
continue
if not PYDIST_RE.match(line):
log.error('invalid pydist data in file %s: %s',
fpath.rsplit('/', 1)[-1], line)
return False
return True
@memoize
def load(impl):
"""Load information about installed Python distributions.
:param impl: interpreter implementation, f.e. cpython2, cpython3, pypy
:type impl: str
"""
fname = PYDIST_OVERRIDES_FNAMES.get(impl)
if exists(fname):
to_check = [fname] # first one!
else:
to_check = []
dname = PYDIST_DIRS.get(impl)
if isdir(dname):
to_check.extend(join(dname, i) for i in os.listdir(dname))
fbdir = os.environ.get('DH_PYTHON_DIST', '/usr/share/dh-python/dist/')
fbname = join(fbdir, '{}_fallback'.format(impl))
if exists(fbname): # fall back generated at dh-python build time
to_check.append(fbname) # last one!
result = {}
for fpath in to_check:
with open(fpath, encoding='utf-8') as fp:
for line in fp:
line = line.strip()
if line.startswith('#') or not line:
continue
dist = PYDIST_RE.search(line)
if not dist:
raise Exception('invalid pydist line: %s (in %s)' % (line, fpath))
dist = dist.groupdict()
name = safe_name(dist['name'])
dist['versions'] = get_requested_versions(impl, dist['vrange'])
dist['dependency'] = dist['dependency'].strip()
if dist['rules']:
dist['rules'] = dist['rules'].split(';')
else:
dist['rules'] = []
result.setdefault(name, []).append(dist)
return result
def guess_dependency(impl, req, version=None, bdep=None,
accept_upstream_versions=False):
bdep = bdep or {}
log.debug('trying to find dependency for %s (python=%s)',
req, version)
if isinstance(version, str):
version = Version(version)
# some upstreams have weird ideas for distribution name...
name, rest = re.compile('([^!><=~ \(\)\[;]+)(.*)').match(req).groups()
# TODO: check stdlib and dist-packaged for name.py and name.so files
req = safe_name(name) + rest
data = load(impl)
req_d = REQUIRES_RE.match(req)
if not req_d:
log.info('please ask dh_python3 author to fix REQUIRES_RE '
'or your upstream author to fix requires.txt')
raise Exception('requirement is not valid: %s' % req)
req_d = req_d.groupdict()
env_marker_alts = ''
if req_d['environment_marker']:
action = check_environment_marker_restrictions(
req,
req_d['environment_marker'],
impl)
if action is False:
return
elif action is True:
pass
else:
env_marker_alts = ' ' + action
name = req_d['name']
details = data.get(name.lower())
if details:
log.debug("dependency: module seems to be installed")
for item in details:
if version and version not in item.get('versions', version):
# rule doesn't match version, try next one
continue
if not item['dependency']:
log.debug("dependency: requirement ignored")
return # this requirement should be ignored
if item['dependency'].endswith(')'):
# no need to translate versions if version is hardcoded in
# Debian dependency
log.debug("dependency: requirement already has hardcoded version")
return item['dependency'] + env_marker_alts
if req_d['operator'] == '==' and req_d['version'].endswith('*'):
# Translate "== 1.*" to "~= 1.0"
req_d['operator'] = '~='
req_d['version'] = req_d['version'].replace('*', '0')
log.debug("dependency: translated wildcard version to semver limit")
if req_d['version'] and (item['standard'] or item['rules']) and\
req_d['operator'] not in (None, '!='):
o = _translate_op(req_d['operator'])
v = _translate(req_d['version'], item['rules'], item['standard'])
d = "%s (%s %s)%s" % (
item['dependency'], o, v, env_marker_alts)
if req_d['version2'] and req_d['operator2'] not in (None,'!='):
o2 = _translate_op(req_d['operator2'])
v2 = _translate(req_d['version2'], item['rules'], item['standard'])
d += ", %s (%s %s)%s" % (
item['dependency'], o2, v2, env_marker_alts)
elif req_d['operator'] == '~=':
o2 = '<<'
v2 = _translate(_max_compatible(req_d['version']), item['rules'], item['standard'])
d += ", %s (%s %s)%s" % (
item['dependency'], o2, v2, env_marker_alts)
log.debug("dependency: constructed version")
return d
elif accept_upstream_versions and req_d['version'] and \
req_d['operator'] not in (None,'!='):
o = _translate_op(req_d['operator'])
d = "%s (%s %s)%s" % (
item['dependency'], o, req_d['version'], env_marker_alts)
if req_d['version2'] and req_d['operator2'] not in (None,'!='):
o2 = _translate_op(req_d['operator2'])
d += ", %s (%s %s)%s" % (
item['dependency'], o2, req_d['version2'],
env_marker_alts)
elif req_d['operator'] == '~=':
o2 = '<<'
d += ", %s (%s %s)%s" % (
item['dependency'], o2,
_max_compatible(req_d['version']), env_marker_alts)
log.debug("dependency: constructed upstream version")
return d
else:
if item['dependency'] in bdep:
if None in bdep[item['dependency']] and bdep[item['dependency']][None]:
log.debug("dependency: included in build-deps with limits ")
return "{} ({}){}".format(
item['dependency'], bdep[item['dependency']][None],
env_marker_alts)
# if arch in bdep[item['dependency']]:
# TODO: handle architecture specific dependencies from build depends
# (current architecture is needed here)
log.debug("dependency: included in build-deps")
return item['dependency'] + env_marker_alts
# search for Egg metadata file or directory (using dpkg -S)
dpkg_query_tpl, regex_filter = PYDIST_DPKG_SEARCH_TPLS[impl]
dpkg_query = dpkg_query_tpl.format(ci_regexp(safe_name(name)))
log.debug("invoking dpkg -S %s", dpkg_query)
process = Popen(('/usr/bin/dpkg', '-S', dpkg_query),
stdout=PIPE, stderr=PIPE)
stdout, stderr = process.communicate()
if process.returncode == 0:
result = set()
stdout = str(stdout, 'utf-8')
for line in stdout.split('\n'):
if not line.strip():
continue
pkg, path = line.split(':', 1)
if regex_filter and not re.search(regex_filter, path):
continue
result.add(pkg)
if len(result) > 1:
log.error('more than one package name found for %s dist', name)
elif not result:
log.debug('dpkg -S did not find package for %s', name)
else:
log.debug('dependency: found a result with dpkg -S')
return result.pop() + env_marker_alts
else:
log.debug('dpkg -S did not find package for %s: %s', name, stderr)
pname = sensible_pname(impl, name)
log.info('Cannot find package that provides %s. '
'Please add package that provides it to Build-Depends or '
'add "%s %s" line to %s or add proper '
'dependency to Depends by hand and ignore this info.',
name, safe_name(name), pname, PYDIST_OVERRIDES_FNAMES[impl])
# return pname
def check_environment_marker_restrictions(req, marker_str, impl):
"""Check wither we should include or skip a dependency based on its
environment markers.
Returns: True - to keep a dependency
False - to skip it
str - to append "| foo" to generated dependencies
"""
if impl != 'cpython3':
log.info('Ignoring environment markers for non-Python 3.x: %s', req)
return False
try:
marker, op, value = parse_environment_marker(marker_str)
except ComplexEnvironmentMarker:
log.info('Ignoring complex environment marker: %s', req)
return False
# TODO: Use dynamic values when building arch-dependent
# binaries, otherwise static values
# TODO: Hurd values?
supported_values = {
'implementation_name': ('cpython', 'pypy'),
'os_name': ('posix',),
'platform_system': ('GNU/kFreeBSD', 'Linux'),
'platform_machine': (platform.machine(),),
'platform_python_implementation': ('CPython', 'PyPy'),
'sys_platform': (
'gnukfreebsd8', 'gnukfreebsd9', 'gnukfreebsd10',
'gnukfreebsd11', 'gnukfreebsd12', 'gnukfreebsd13',
'linux'),
}
if marker in supported_values:
sv = supported_values[marker]
if op in ('==', '!='):
if ((op == '==' and value not in sv)
or (op == '!=' and value in sv)):
log.debug('Skipping requirement (%s != %s): %s',
value, sv, req)
return False
else:
log.info(
'Skipping requirement with unhandled environment marker '
'comparison: %s', req)
return False
elif marker in ('python_version', 'python_full_version',
'implementation_version'):
# TODO: Replace with full PEP-440 parser
env_ver = value
split_ver = value.split('.')
if marker == 'python_version':
version_parts = 2
elif marker == 'python_full_version':
version_parts = 3
else:
version_parts = len(split_ver)
if '*' in env_ver:
if split_ver.index('*') != len(split_ver) -1:
log.info('Skipping requirement with intermediate wildcard: %s',
req)
return False
split_ver.pop()
env_ver = '.'.join(split_ver)
if op == '==':
if marker == 'python_full_version':
marker = 'python_version'
version_parts = 2
else:
op == '=~'
elif op == '!=':
if marker == 'python_full_version':
marker = 'python_version'
version_parts = 2
else:
log.info('Ignoring wildcard != requirement, not '
'representable in Debian: %s', req)
return True
else:
log.info('Skipping requirement with %s on a wildcard: %s',
op, req)
return False
int_ver = []
for ver_part in split_ver:
if ver_part.isdigit():
int_ver.append(int(ver_part))
else:
env_ver = '.'.join(str(x) for x in int_ver)
log.info('Truncating unparseable version %s to %s in %s',
value, env_ver, req)
break
if len(int_ver) < version_parts:
int_ver.append(0)
env_ver += '.0'
next_ver = int_ver.copy()
next_ver[version_parts - 1] += 1
next_ver = '.'.join(str(x) for x in next_ver)
prev_ver = int_ver.copy()
prev_ver[version_parts - 1] -= 1
prev_ver = '.'.join(str(x) for x in prev_ver)
if op == '<':
if int_ver <= [3, 0, 0]:
return False
return '| python3 (>> {})'.format(env_ver)
elif op == '<=':
return '| python3 (>> {})'.format(next_ver)
elif op == '>=':
if int_ver < [3, 0, 0]:
return True
return '| python3 (<< {})'.format(env_ver)
elif op == '>':
if int_ver < [3, 0, 0]:
return True
return '| python3 (<< {})'.format(next_ver)
elif op in ('==', '==='):
# === is arbitrary equality (PEP 440)
if marker == 'python_version' or op == '==':
return '| python3 (<< {}) | python3 (>> {})'.format(
env_ver, next_ver)
else:
log.info(
'Skipping requirement with %s environment marker, cannot '
'model in Debian deps: %s', op, req)
return False
elif op == '~=': # Compatible equality (PEP 440)
ceq_next_ver = int_ver[:2]
ceq_next_ver[1] += 1
ceq_next_ver = '.'.join(str(x) for x in ceq_next_ver)
return '| python3 (<< {}) | python3 (>> {})'.format(
env_ver, ceq_next_ver)
elif op == '!=':
log.info('Ignoring != comparison in environment marker, cannot '
'model in Debian deps: %s', req)
return True
elif marker == 'extra':
# Handled in section logic of parse_requires_dist()
return True
else:
log.info('Skipping requirement with unknown environment marker: %s',
marker)
return False
return True
def parse_pydep(impl, fname, bdep=None, options=None,
depends_sec=None, recommends_sec=None, suggests_sec=None):
depends_sec = depends_sec or []
recommends_sec = recommends_sec or []
suggests_sec = suggests_sec or []
public_dir = PUBLIC_DIR_RE[impl].match(fname)
ver = None
if public_dir and public_dir.groups() and len(public_dir.group(1)) != 1:
ver = public_dir.group(1)
guess_deps = partial(guess_dependency, impl=impl, version=ver, bdep=bdep,
accept_upstream_versions=getattr(
options, 'accept_upstream_versions', False))
result = {'depends': [], 'recommends': [], 'suggests': []}
modified = section = False
env_action = True
processed = []
with open(fname, 'r', encoding='utf-8') as fp:
for line in fp:
line = line.strip()
if not line or line.startswith('#'):
processed.append(line)
continue
if line.startswith('['):
m = REQ_SECTIONS_RE.match(line)
if not m:
log.info('Skipping section %s, unable to parse header',
line)
processed.append(line)
section = object()
continue
section = m.group('section')
env_action = True
if m.group('environment_marker'):
env_action = check_environment_marker_restrictions(
line,
m.group('environment_marker'),
impl)
processed.append(line)
continue
if section:
if section in depends_sec:
result_key = 'depends'
elif section in recommends_sec:
result_key = 'recommends'
elif section in suggests_sec:
result_key = 'suggests'
else:
processed.append(line)
continue
else:
result_key = 'depends'
dependency = None
if env_action:
dependency = guess_deps(req=line)
if dependency and isinstance(env_action, str):
dependency = ', '.join(
part.strip() + ' ' + env_action
for part in dependency.split(','))
if dependency:
result[result_key].append(dependency)
modified = True
else:
processed.append(line)
if modified and public_dir:
with open(fname, 'w', encoding='utf-8') as fp:
fp.writelines(i + '\n' for i in processed)
return result
def parse_requires_dist(impl, fname, bdep=None, options=None, depends_sec=None,
recommends_sec=None, suggests_sec=None):
"""Extract dependencies from a dist-info/METADATA file"""
depends_sec = depends_sec or []
recommends_sec = recommends_sec or []
suggests_sec = suggests_sec or []
public_dir = PUBLIC_DIR_RE[impl].match(fname)
ver = None
if public_dir and public_dir.groups() and len(public_dir.group(1)) != 1:
ver = public_dir.group(1)
guess_deps = partial(guess_dependency, impl=impl, version=ver, bdep=bdep,
accept_upstream_versions=getattr(
options, 'accept_upstream_versions', False))
result = {'depends': [], 'recommends': [], 'suggests': []}
section = None
with open(fname, 'r', encoding='utf-8') as fp:
metadata = email.message_from_string(fp.read())
requires = metadata.get_all('Requires-Dist', [])
for req in requires:
m = EXTRA_RE.search(req)
result_key = 'depends'
if m:
section = m.group('section')
if section:
if section in depends_sec:
result_key = 'depends'
elif section in recommends_sec:
result_key = 'recommends'
elif section in suggests_sec:
result_key = 'suggests'
else:
continue
dependency = guess_deps(req=req)
if dependency:
result[result_key].append(dependency)
return result
def safe_name(name):
"""Emulate distribute's safe_name."""
return re.compile('[^A-Za-z0-9.]+').sub('_', name).lower()
def sensible_pname(impl, egg_name):
"""Guess Debian package name from Egg name."""
egg_name = safe_name(egg_name).replace('_', '-')
if egg_name.startswith('python-'):
egg_name = egg_name[7:]
return '{}-{}'.format(PKG_PREFIX_MAP[impl], egg_name.lower())
def ci_regexp(name):
"""Return case insensitive dpkg -S regexp."""
return ''.join("[%s%s]" % (i.upper(), i) if i.isalpha() else i for i in name.lower())
PRE_VER_RE = re.compile(r'[-.]?(alpha|beta|rc|dev|a|b|c)')
GROUP_RE = re.compile(r'\$(\d+)')
def _pl2py(pattern):
"""Convert Perl RE patterns used in uscan to Python's
>>> print(_pl2py('foo$3'))
foo\g<3>
"""
return GROUP_RE.sub(r'\\g<\1>', pattern)
def _max_compatible(version):
"""Return the maximum version compatible with `version` in PEP440 terms,
used by ~= requires version specifiers.
https://www.python.org/dev/peps/pep-0440/#compatible-release
>>> _max_compatible('2.2')
'3'
>>> _max_compatible('1.4.5')
'1.5'
>>> _max_compatible('1.3.alpha4')
'2'
>>> _max_compatible('2.1.3.post5')
'2.2'
"""
v = Version(version)
v.serial = None
v.releaselevel = None
if v.micro is not None:
v.micro = None
return str(v + 1)
v.minor = None
return str(v + 1)
def _translate(version, rules, standard):
"""Translate Python version into Debian one.
>>> _translate('1.C2betac', ['s/c//gi'], None)
'1.2beta'
>>> _translate('5-fooa1.2beta3-fooD',
... ['s/^/1:/', 's/-foo//g', 's:([A-Z]):+$1:'], 'PEP386')
'1:5~a1.2~beta3+D'
>>> _translate('x.y.x.z', ['tr/xy/ab/', 'y,z,Z,'], None)
'a.b.a.Z'
"""
for rule in rules:
# uscan supports s, tr and y operations
if rule.startswith(('tr', 'y')):
# Note: no support for escaped separator in the pattern
pos = 1 if rule.startswith('y') else 2
tmp = rule[pos + 1:].split(rule[pos])
version = version.translate(str.maketrans(tmp[0], tmp[1]))
elif rule.startswith('s'):
# uscan supports: g, u and x flags
tmp = rule[2:].split(rule[1])
pattern = re.compile(tmp[0])
count = 1
if tmp[2:]:
flags = tmp[2]
if 'g' in flags:
count = 0
if 'i' in flags:
pattern = re.compile(tmp[0], re.I)
version = pattern.sub(_pl2py(tmp[1]), version, count)
else:
log.warn('unknown rule ignored: %s', rule)
if standard == 'PEP386':
version = PRE_VER_RE.sub(r'~\g<1>', version)
return version
def _translate_op(operator):
"""Translate Python version operator into Debian one.
>>> _translate_op('==')
'='
>>> _translate_op('<')
'<<'
>>> _translate_op('<=')
'<='
"""
return DEB_VERS_OPS.get(operator, operator)
if __name__ == '__main__':
impl = os.environ.get('IMPL', 'cpython3')
for i in sys.argv[1:]:
if os.path.isfile(i):
try:
print(', '.join(parse_pydep(impl, i)['depends']))
except Exception as err:
log.error('%s: cannot guess (%s)', i, err)
else:
try:
print(guess_dependency(impl, i) or '')
except Exception as err:
log.error('%s: cannot guess (%s)', i, err)

340
dhpython/tools.py Normal file
View File

@ -0,0 +1,340 @@
# -*- coding: UTF-8 -*-
# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import os
import re
import locale
from datetime import datetime
from glob import glob
from pickle import dumps
from shutil import rmtree
from os.path import exists, getsize, isdir, islink, join, split
from subprocess import Popen, PIPE
log = logging.getLogger('dhpython')
EGGnPTH_RE = re.compile(r'(.*?)(-py\d\.\d(?:-[^.]*)?)?(\.egg-info|\.pth)$')
SHAREDLIB_RE = re.compile(r'NEEDED.*libpython(\d\.\d)')
def relpath(target, link):
"""Return relative path.
>>> relpath('/usr/share/python-foo/foo.py', '/usr/bin/foo', )
'../share/python-foo/foo.py'
"""
t = target.split('/')
l = link.split('/')
while l and l[0] == t[0]:
del l[0], t[0]
return '/'.join(['..'] * (len(l) - 1) + t)
def relative_symlink(target, link):
"""Create relative symlink."""
return os.symlink(relpath(target, link), link)
def move_file(fpath, dstdir):
"""Move file to dstdir. Works with symlinks (including relative ones)."""
if isdir(fpath):
dname = split(fpath)[-1]
for fn in os.listdir(fpath):
move_file(join(fpath, fn), join(dstdir, dname))
if islink(fpath):
dstpath = join(dstdir, split(fpath)[-1])
relative_symlink(os.readlink(fpath), dstpath)
os.remove(fpath)
else:
os.rename(fpath, dstdir)
def move_matching_files(src, dst, pattern, sub=None, repl=''):
"""Move files (preserving path) that match given pattern.
move_matching_files('foo/bar/', 'foo/baz/', 'spam/.*\.so$')
will move foo/bar/a/b/c/spam/file.so to foo/baz/a/b/c/spam/file.so
:param sub: regular expression for path part that will be replaced with `repl`
:param repl: replacement for `sub`
"""
match = re.compile(pattern).search
if sub:
sub = re.compile(sub).sub
repl = repl or ''
for root, dirs, filenames in os.walk(src):
for fn in filenames:
spath = join(root, fn)
if match(spath):
if sub is not None:
spath = sub(repl, spath)
dpath = join(dst, relpath(spath, src))
os.renames(spath, dpath)
def fix_shebang(fpath, replacement=None):
"""Normalize file's shebang.
:param replacement: new shebang command (path to interpreter and options)
"""
try:
interpreter = Interpreter.from_file(fpath)
except Exception as err:
log.debug('fix_shebang (%s): %s', fpath, err)
return None
if not replacement and interpreter.version == '2':
# we'll drop /usr/bin/python symlink from python package at some point
replacement = '/usr/bin/python2'
if interpreter.debug:
replacement += '-dbg'
elif not replacement and interpreter.path != '/usr/bin/': # f.e. /usr/local/* or */bin/env
interpreter.path = '/usr/bin'
replacement = repr(interpreter)
if replacement:
log.info('replacing shebang in %s', fpath)
try:
with open(fpath, 'rb') as fp:
fcontent = fp.readlines()
except IOError:
log.error('cannot open %s', fpath)
return False
# do not catch IOError here, the file is zeroed at this stage so it's
# better to fail
with open(fpath, 'wb') as fp:
fp.write(("#! %s\n" % replacement).encode('utf-8'))
fp.writelines(fcontent[1:])
return True
def so2pyver(fpath):
"""Return libpython version file is linked to or None.
:rtype: tuple
:returns: Python version
"""
cmd = "readelf -Wd '%s'" % fpath
process = Popen(cmd, stdout=PIPE, shell=True)
encoding = locale.getdefaultlocale()[1] or 'utf-8'
match = SHAREDLIB_RE.search(str(process.stdout.read(), encoding=encoding))
if match:
return Version(match.groups()[0])
def clean_egg_name(name):
"""Remove Python version and platform name from Egg files/dirs.
>>> clean_egg_name('python_pipeline-0.1.3_py3k-py3.1.egg-info')
'python_pipeline-0.1.3_py3k.egg-info'
>>> clean_egg_name('Foo-1.2-py2.7-linux-x86_64.egg-info')
'Foo-1.2.egg-info'
"""
match = EGGnPTH_RE.match(name)
if match and match.group(2) is not None:
return ''.join(match.group(1, 3))
return name
def parse_ns(fpaths, other=None):
"""Parse namespace_packages.txt files."""
result = set(other or [])
for fpath in fpaths:
with open(fpath, 'r', encoding='utf-8') as fp:
for line in fp:
if line:
result.add(line.strip())
return result
def remove_ns(interpreter, package, namespaces, versions):
"""Remove empty __init__.py files for requested namespaces."""
if not isinstance(namespaces, set):
namespaces = set(namespaces)
keep = set()
for ns in namespaces:
for version in versions:
fpath = join(interpreter.sitedir(package, version), *ns.split('.'))
fpath = join(fpath, '__init__.py')
if not exists(fpath):
continue
if getsize(fpath) != 0:
log.warning('file not empty, cannot share %s namespace', ns)
keep.add(ns)
break
# return a set of namespaces that should be handled by pycompile/pyclean
result = namespaces - keep
# remove empty __init__.py files, if available
for ns in result:
for version in versions:
dpath = join(interpreter.sitedir(package, version), *ns.split('.'))
fpath = join(dpath, '__init__.py')
if exists(fpath):
os.remove(fpath)
if not os.listdir(dpath):
os.rmdir(dpath)
# clean pyshared dir as well
dpath = join('debian', package, 'usr/share/pyshared', *ns.split('.'))
fpath = join(dpath, '__init__.py')
if exists(fpath):
os.remove(fpath)
if not os.listdir(dpath):
os.rmdir(dpath)
return result
def execute(command, cwd=None, env=None, log_output=None, shell=True):
"""Execute external shell command.
:param cdw: current working directory
:param env: environment
:param log_output:
* opened log file or path to this file, or
* None if output should be included in the returned dict, or
* False if output should be redirected to stdout/stderr
"""
args = {'shell': shell, 'cwd': cwd, 'env': env}
close = False
if log_output is False:
pass
elif log_output is None:
args.update(stdout=PIPE, stderr=PIPE)
elif log_output:
if isinstance(log_output, str):
close = True
log_output = open(log_output, 'a', encoding='utf-8')
log_output.write('\n# command executed on {}'.format(datetime.now().isoformat()))
log_output.write('\n$ {}\n'.format(command))
log_output.flush()
args.update(stdout=log_output, stderr=log_output)
log.debug('invoking: %s', command)
with Popen(command, **args) as process:
stdout, stderr = process.communicate()
close and log_output.close()
return dict(returncode=process.returncode,
stdout=stdout and str(stdout, 'utf-8'),
stderr=stderr and str(stderr, 'utf-8'))
class memoize:
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args, **kwargs):
key = dumps((args, kwargs))
if key not in self.cache:
self.cache[key] = self.func(*args, **kwargs)
return self.cache[key]
def pyinstall(interpreter, package, vrange):
"""Install local files listed in pkg.pyinstall files as public modules."""
srcfpath = "./debian/%s.pyinstall" % package
if not exists(srcfpath):
return
impl = interpreter.impl
versions = get_requested_versions(impl, vrange)
for line in open(srcfpath, encoding='utf-8'):
if not line or line.startswith('#'):
continue
details = INSTALL_RE.match(line)
if not details:
raise ValueError("unrecognized line: %s" % line)
details = details.groupdict()
if details['module']:
details['module'] = details['module'].replace('.', '/')
myvers = versions & get_requested_versions(impl, details['vrange'])
if not myvers:
log.debug('%s.pyinstall: no matching versions for line %s',
package, line)
continue
files = glob(details['pattern'])
if not files:
raise ValueError("missing file(s): %s" % details['pattern'])
for fpath in files:
fpath = fpath.lstrip('/.')
if details['module']:
dstname = join(details['module'], split(fpath)[1])
elif fpath.startswith('debian/'):
dstname = fpath[7:]
else:
dstname = fpath
for version in myvers:
dstfpath = join(interpreter.sitedir(package, version), dstname)
dstdir = split(dstfpath)[0]
if not exists(dstdir):
os.makedirs(dstdir)
if exists(dstfpath):
os.remove(dstfpath)
os.link(fpath, dstfpath)
def pyremove(interpreter, package, vrange):
"""Remove public modules listed in pkg.pyremove file."""
srcfpath = "./debian/%s.pyremove" % package
if not exists(srcfpath):
return
impl = interpreter.impl
versions = get_requested_versions(impl, vrange)
for line in open(srcfpath, encoding='utf-8'):
if not line or line.startswith('#'):
continue
details = REMOVE_RE.match(line)
if not details:
raise ValueError("unrecognized line: %s: %s" % (package, line))
details = details.groupdict()
myvers = versions & get_requested_versions(impl, details['vrange'])
if not myvers:
log.debug('%s.pyremove: no matching versions for line %s',
package, line)
for version in myvers:
site_dirs = interpreter.old_sitedirs(package, version)
site_dirs.append(interpreter.sitedir(package, version))
for sdir in site_dirs:
files = glob(sdir + '/' + details['pattern'])
for fpath in files:
if isdir(fpath):
rmtree(fpath)
else:
os.remove(fpath)
from dhpython.interpreter import Interpreter
from dhpython.version import Version, get_requested_versions, RANGE_PATTERN
INSTALL_RE = re.compile(r"""
(?P<pattern>.+?) # file pattern
(?:\s+ # optional Python module name:
(?P<module>[A-Za-z][A-Za-z0-9_.]*)?
)?
\s* # optional version range:
(?P<vrange>%s)?$
""" % RANGE_PATTERN, re.VERBOSE)
REMOVE_RE = re.compile(r"""
(?P<pattern>.+?) # file pattern
\s* # optional version range:
(?P<vrange>%s)?$
""" % RANGE_PATTERN, re.VERBOSE)

457
dhpython/version.py Normal file
View File

@ -0,0 +1,457 @@
# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import re
from os.path import exists
from dhpython import _defaults
RANGE_PATTERN = r'(-)?(\d\.\d+)(?:(-)(\d\.\d+)?)?'
RANGE_RE = re.compile(RANGE_PATTERN)
VERSION_RE = re.compile(r'''
(?P<major>\d+)\.?
(?P<minor>\d+)?\.?
(?P<micro>\d+)?[.\s]?
(?P<releaselevel>alpha|beta|candidate|final)?[.\s]?
(?P<serial>\d+)?''', re.VERBOSE)
log = logging.getLogger('dhpython')
Interpreter = None
class Version:
# TODO: Upgrade to PEP-440
def __init__(self, value=None, major=None, minor=None, micro=None,
releaselevel=None, serial=None):
"""Construct a new instance.
>>> Version(major=0, minor=0, micro=0, releaselevel=0, serial=0)
Version('0.0')
>>> Version('0.0')
Version('0.0')
"""
if isinstance(value, (tuple, list)):
value = '.'.join(str(i) for i in value)
if isinstance(value, Version):
for name in ('major', 'minor', 'micro', 'releaselevel', 'serial'):
setattr(self, name, getattr(value, name))
return
comp = locals()
del comp['self']
del comp['value']
if value:
match = VERSION_RE.match(value)
for name, value in match.groupdict().items() if match else []:
if value is not None and comp[name] is None:
comp[name] = value
for name, value in comp.items():
if name != 'releaselevel' and value is not None:
value = int(value)
setattr(self, name, value)
if self.major is None:
raise ValueError('major component is required')
def __str__(self):
"""Return major.minor or major string.
>>> str(Version(major=3, minor=2, micro=1, releaselevel='final', serial=4))
'3.2'
>>> str(Version(major=2))
'2'
"""
result = str(self.major)
if self.minor is not None:
result += '.{}'.format(self.minor)
return result
def __hash__(self):
return hash(repr(self))
def __repr__(self):
"""Return full version string.
>>> repr(Version(major=3, minor=2, micro=1, releaselevel='final', serial=4))
"Version('3.2.1.final.4')"
>>> repr(Version(major=2))
"Version('2')"
"""
result = "Version('{}".format(self)
for name in ('micro', 'releaselevel', 'serial'):
value = getattr(self, name)
if not value:
break
result += '.{}'.format(value)
return result + "')"
def __add__(self, other):
"""Return next version.
>>> Version('3.1') + 1
Version('3.2')
>>> Version('2') + '1'
Version('3')
"""
result = Version(self)
if self.minor is None:
result.major += int(other)
else:
result.minor += int(other)
return result
def __sub__(self, other):
"""Return previous version.
>>> Version('3.1') - 1
Version('3.0')
>>> Version('3') - '1'
Version('2')
"""
result = Version(self)
if self.minor is None:
result.major -= int(other)
new = result.major
else:
result.minor -= int(other)
new = result.minor
if new < 0:
raise ValueError('cannot decrease version further')
return result
def __eq__(self, other):
try:
other = Version(other)
except Exception:
return False
return self.__cmp(other) == 0
def __lt__(self, other):
return self.__cmp(other) < 0
def __le__(self, other):
return self.__cmp(other) <= 0
def __gt__(self, other):
return self.__cmp(other) > 0
def __ge__(self, other):
return self.__cmp(other) >= 0
def __lshift__(self, other):
"""Compare major.minor or major only (if minor is not set).
>>> Version('2.6') << Version('2.7')
True
>>> Version('2.6') << Version('2.6.6')
False
>>> Version('3') << Version('2')
False
>>> Version('3.1') << Version('2')
False
>>> Version('2') << Version('3.2.1.alpha.3')
True
"""
if not isinstance(other, Version):
other = Version(other)
if self.minor is None or other.minor is None:
return self.__cmp(other, ignore='minor') < 0
else:
return self.__cmp(other, ignore='micro') < 0
def __rshift__(self, other):
"""Compare major.minor or major only (if minor is not set).
>>> Version('2.6') >> Version('2.7')
False
>>> Version('2.6.7') >> Version('2.6.6')
False
>>> Version('3') >> Version('2')
True
>>> Version('3.1') >> Version('2')
True
>>> Version('2.1') >> Version('3.2.1.alpha.3')
False
"""
if not isinstance(other, Version):
other = Version(other)
if self.minor is None or other.minor is None:
return self.__cmp(other, ignore='minor') > 0
else:
return self.__cmp(other, ignore='micro') > 0
def __cmp(self, other, ignore=None):
if not isinstance(other, Version):
other = Version(other)
for name in ('major', 'minor', 'micro', 'releaselevel', 'serial'):
if name == ignore:
break
value1 = getattr(self, name) or 0
value2 = getattr(other, name) or 0
if name == 'releaselevel':
rmap = {'alpha': -3, 'beta': -2, 'candidate': -1, 'final': 0}
value1 = rmap.get(value1, 0)
value2 = rmap.get(value2, 0)
if value1 == value2:
continue
return (value1 > value2) - (value1 < value2)
return 0
class VersionRange:
def __init__(self, value=None, minver=None, maxver=None):
if minver:
self.minver = Version(minver)
else:
self.minver = None
if maxver:
self.maxver = Version(maxver)
else:
self.maxver = None
if value:
minver, maxver = self.parse(value)
if minver and self.minver is None:
self.minver = minver
if maxver and self.maxver is None:
self.maxver = maxver
def __bool__(self):
if self.minver is not None or self.maxver is not None:
return True
return False
def __str__(self):
"""Return version range string from given range.
>>> str(VersionRange(minver='3.4'))
'3.4-'
>>> str(VersionRange(minver='3.4', maxver='3.6'))
'3.4-3.6'
>>> str(VersionRange(minver='3.4', maxver='4.0'))
'3.4-4.0'
>>> str(VersionRange(maxver='3.7'))
'-3.7'
>>> str(VersionRange(minver='3.5', maxver='3.5'))
'3.5'
>>> str(VersionRange())
'-'
"""
if self.minver is None is self.maxver:
return '-'
if self.minver == self.maxver:
return str(self.minver)
elif self.minver is None:
return '-{}'.format(self.maxver)
elif self.maxver is None:
return '{}-'.format(self.minver)
else:
return '{}-{}'.format(self.minver, self.maxver)
def __repr__(self):
"""Return version range string.
>>> repr(VersionRange('5.0-'))
"VersionRange(minver='5.0')"
>>> repr(VersionRange('3.0-3.5'))
"VersionRange(minver='3.0', maxver='3.5')"
"""
result = 'VersionRange('
if self.minver is not None:
result += "minver='{}'".format(self.minver)
if self.maxver is not None:
result += ", maxver='{}'".format(self.maxver)
result = result.replace('(, ', '(')
return result + ")"
@staticmethod
def parse(value):
"""Return minimum and maximum Python version from given range.
>>> VersionRange.parse('3.0-')
(Version('3.0'), None)
>>> VersionRange.parse('3.1-3.3')
(Version('3.1'), Version('3.3'))
>>> VersionRange.parse('3.2-4.0')
(Version('3.2'), Version('4.0'))
>>> VersionRange.parse('-3.7')
(None, Version('3.7'))
>>> VersionRange.parse('3.2')
(Version('3.2'), Version('3.2'))
>>> VersionRange.parse('') == VersionRange.parse('-')
True
>>> VersionRange.parse('>= 4.0')
(Version('4.0'), None)
"""
if value in ('', '-'):
return None, None
match = RANGE_RE.match(value)
if not match:
try:
minv, maxv = VersionRange._parse_pycentral(value)
except Exception:
raise ValueError("version range is invalid: %s" % value)
else:
groups = match.groups()
if list(groups).count(None) == 3: # only one version is allowed
minv = Version(groups[1])
return minv, minv
minv = maxv = None
if groups[0]: # maximum version only
maxv = groups[1]
else:
minv = groups[1]
maxv = groups[3]
minv = Version(minv) if minv else None
maxv = Version(maxv) if maxv else None
if maxv and minv and minv > maxv:
raise ValueError("version range is invalid: %s" % value)
return minv, maxv
@staticmethod
def _parse_pycentral(value):
"""Parse X-Python3-Version.
>>> VersionRange._parse_pycentral('>= 3.1')
(Version('3.1'), None)
>>> VersionRange._parse_pycentral('<< 4.0')
(None, Version('4.0'))
>>> VersionRange._parse_pycentral('3.1')
(Version('3.1'), Version('3.1'))
>>> VersionRange._parse_pycentral('3.1, 3.2')
(Version('3.1'), None)
"""
minv = maxv = None
hardcoded = set()
for item in value.split(','):
item = item.strip()
match = re.match('>=\s*([\d\.]+)', item)
if match:
minv = "%.3s" % match.group(1)
continue
match = re.match('<<\s*([\d\.]+)', item)
if match:
maxv = "%.3s" % match.group(1)
continue
match = re.match('^[\d\.]+$', item)
if match:
hardcoded.add("%.3s" % match.group(0))
if len(hardcoded) == 1:
ver = hardcoded.pop()
return Version(ver), Version(ver)
if not minv and hardcoded:
# yeah, no maxv!
minv = sorted(hardcoded)[0]
return Version(minv) if minv else None, Version(maxv) if maxv else None
def default(impl):
"""Return default interpreter version for given implementation."""
if impl not in _defaults.DEFAULT:
raise ValueError("interpreter implementation not supported: %r" % impl)
ver = _defaults.DEFAULT[impl]
return Version(major=ver[0], minor=ver[1])
def supported(impl):
"""Return list of supported interpreter versions for given implementation."""
if impl not in _defaults.SUPPORTED:
raise ValueError("interpreter implementation not supported: %r" % impl)
versions = _defaults.SUPPORTED[impl]
return [Version(major=v[0], minor=v[1]) for v in versions]
def get_requested_versions(impl, vrange=None, available=None):
"""Return a set of requested and supported Python versions.
:param impl: interpreter implementation
:param available: if set to `True`, return installed versions only,
if set to `False`, return requested versions that are not installed.
By default returns all requested versions.
:type available: bool
>>> sorted(get_requested_versions('cpython3', '')) == sorted(supported('cpython3'))
True
>>> sorted(get_requested_versions('cpython3', '-')) == sorted(supported('cpython3'))
True
>>> get_requested_versions('cpython3', '>= 5.0')
set()
"""
if isinstance(vrange, str):
vrange = VersionRange(vrange)
if not vrange:
versions = set(supported(impl))
else:
minv = Version(major=0, minor=0) if vrange.minver is None else vrange.minver
maxv = Version(major=99, minor=99) if vrange.maxver is None else vrange.maxver
if minv == maxv:
versions = set([minv] if minv in supported(impl) else tuple())
else:
versions = set(v for v in supported(impl) if minv <= v < maxv)
if available is not None:
# to avoid circular imports
global Interpreter
if Interpreter is None:
from dhpython.interpreter import Interpreter
if available:
interpreter = Interpreter(impl=impl)
versions = set(v for v in versions
if exists(interpreter.binary(v)))
elif available is False:
interpreter = Interpreter(impl=impl)
versions = set(v for v in versions
if not exists(interpreter.binary(v)))
return versions
def build_sorted(versions, impl='cpython3'):
"""Return sorted list of versions in a build friendly order.
i.e. default version, if among versions, is sorted last.
>>> build_sorted([(2, 6), (3, 4), default('cpython3'), (3, 6), (2, 7)])[-1] == default('cpython3')
True
>>> build_sorted(('3.2', (3, 0), '3.1'))
[Version('3.0'), Version('3.1'), Version('3.2')]
"""
default_ver = default(impl)
result = sorted(Version(v) for v in versions)
try:
result.remove(default_ver)
except ValueError:
pass
else:
result.append(default_ver)
return result

591
pybuild Executable file
View File

@ -0,0 +1,591 @@
#! /usr/bin/python3
# vim: et ts=4 sw=4
# Copyright © 2012-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import logging
import argparse
import re
import sys
from os import environ, getcwd, makedirs, remove
from os.path import abspath, exists, isdir, join
from shutil import rmtree
INTERP_VERSION_RE = re.compile(r'^python(?P<version>3\.\d+)(?P<dbg>-dbg)?$')
logging.basicConfig(format='%(levelname).1s: pybuild '
'%(module)s:%(lineno)d: %(message)s')
log = logging.getLogger('dhpython')
def main(cfg):
log.debug('cfg: %s', cfg)
from dhpython import build, PKG_PREFIX_MAP
from dhpython.debhelper import DebHelper, build_options
from dhpython.version import Version, build_sorted, get_requested_versions
from dhpython.interpreter import Interpreter
from dhpython.tools import execute, move_matching_files
if cfg.list_systems:
for name, Plugin in sorted(build.plugins.items()):
print(name, '\t', Plugin.DESCRIPTION)
exit(0)
nocheck = False
if 'DEB_BUILD_OPTIONS' in environ:
nocheck = 'nocheck' in environ['DEB_BUILD_OPTIONS']
if not nocheck and 'DEB_BUILD_PROFILES' in environ:
nocheck = 'nocheck' in environ['DEB_BUILD_PROFILES']
env = environ.copy()
# set some defaults in environ to make the build reproducible
env.setdefault('LC_ALL', 'C.UTF-8')
env.setdefault('CCACHE_DIR', abspath('.pybuild/ccache'))
env.setdefault('no_proxy', 'localhost')
if 'http_proxy' not in env:
env['http_proxy'] = 'http://127.0.0.1:9/'
elif not env['http_proxy']:
del env['http_proxy'] # some tools don't like empty var.
if 'https_proxy' not in env:
env['https_proxy'] = 'https://127.0.0.1:9/'
elif not env['https_proxy']:
del env['https_proxy'] # some tools don't like empty var.
if 'DEB_PYTHON_INSTALL_LAYOUT' not in env:
env['DEB_PYTHON_INSTALL_LAYOUT'] = 'deb'
arch_data = {}
if exists('/usr/bin/dpkg-architecture'):
res = execute('/usr/bin/dpkg-architecture')
for line in res['stdout'].splitlines():
key, value = line.strip().split('=', 1)
arch_data[key] = value
# Set _PYTHON_HOST_PLATFORM to ensure debugging symbols on, f.e. i386
# emded a constant name regardless of the 32/64-bit kernel.
host_platform = '{DEB_HOST_ARCH_OS}-{DEB_HOST_ARCH}'.format(**arch_data)
# it's not called amd64 in Python
host_platform = host_platform.replace('amd64', 'x86_64')
env.setdefault('_PYTHON_HOST_PLATFORM', host_platform)
if arch_data['DEB_BUILD_ARCH'] != arch_data['DEB_HOST_ARCH']:
# support cross compiling Python 3.X extensions, see #892931
env.setdefault('_PYTHON_SYSCONFIGDATA_NAME',
'_sysconfigdata__' + arch_data["DEB_HOST_MULTIARCH"])
# Selected on command line?
selected_plugin = cfg.system
# Selected by build_dep?
if not selected_plugin:
dh = DebHelper(build_options())
for build_dep in dh.build_depends:
if build_dep.startswith('pybuild-plugin-'):
selected_plugin = build_dep.split('-', 2)[2]
break
if selected_plugin:
certainty = 99
Plugin = build.plugins.get(selected_plugin)
if not Plugin:
log.error('unrecognized build system: %s', selected_plugin)
exit(10)
plugin = Plugin(cfg)
context = {'ENV': env, 'args': {}, 'dir': cfg.dir}
plugin.detect(context)
else:
plugin, certainty, context = None, 0, None
for Plugin in build.plugins.values():
try:
tmp_plugin = Plugin(cfg)
except Exception as err:
log.warn('cannot initialize %s plugin: %s', Plugin.NAME,
err, exc_info=cfg.verbose)
continue
tmp_context = {'ENV': env, 'args': {}, 'dir': cfg.dir}
tmp_certainty = tmp_plugin.detect(tmp_context)
log.debug('Plugin %s: certainty %i', Plugin.NAME, tmp_certainty)
if tmp_certainty and tmp_certainty > certainty:
plugin, certainty, context = tmp_plugin, tmp_certainty, tmp_context
del Plugin
if not plugin:
log.error('cannot detect build system, please use --system option'
' or set PYBUILD_SYSTEM env. variable')
exit(11)
if plugin.SUPPORTED_INTERPRETERS is not True:
# if versioned interpreter was requested and selected plugin lists
# versioned ones as supported: extend list of supported interpreters
# with this interpreter
tpls = {i for i in plugin.SUPPORTED_INTERPRETERS if '{version}' in i}
if tpls:
for ipreter in cfg.interpreter:
m = INTERP_VERSION_RE.match(ipreter)
if m:
ver = m.group('version')
updated = set(tpl.format(version=ver) for tpl in tpls)
updated and plugin.SUPPORTED_INTERPRETERS.update(updated)
for interpreter in cfg.interpreter:
if plugin.SUPPORTED_INTERPRETERS is not True and interpreter not in plugin.SUPPORTED_INTERPRETERS:
log.error('interpreter %s not supported by %s', interpreter, plugin)
exit(12)
log.debug('detected build system: %s (certainty: %s%%)', plugin.NAME, certainty)
if cfg.detect_only:
if not cfg.really_quiet:
print(plugin.NAME)
exit(0)
versions = cfg.versions
if not versions:
if len(cfg.interpreter) == 1:
i = cfg.interpreter[0]
m = INTERP_VERSION_RE.match(i)
if m:
log.debug('defaulting to version hardcoded in interpreter name')
versions = [m.group('version')]
else:
IMAP = {v: k for k, v in PKG_PREFIX_MAP.items()}
if i in IMAP:
versions = build_sorted(get_requested_versions(
IMAP[i], available=True), impl=IMAP[i])
if versions and '{version}' not in i:
versions = versions[-1:] # last one, the default one
if not versions: # still no luck
log.debug('defaulting to all supported Python 3.X versions')
versions = build_sorted(get_requested_versions(
'cpython3', available=True), impl='cpython3')
versions = [Version(v) for v in versions]
def get_option(name, interpreter=None, version=None, default=None):
if interpreter:
# try PYBUILD_NAME_python3.3-dbg (or hardcoded interpreter)
i = interpreter.format(version=version or '')
opt = "PYBUILD_{}_{}".format(name.upper(), i)
if opt in environ:
return environ[opt]
# try PYBUILD_NAME_python3-dbg (if not checked above)
if '{version}' in interpreter and version:
i = interpreter.format(version=version.major)
opt = "PYBUILD_{}_{}".format(name.upper(), i)
if opt in environ:
return environ[opt]
# try PYBUILD_NAME
opt = "PYBUILD_{}".format(name.upper())
if opt in environ:
return environ[opt]
# try command line args
return getattr(cfg, name, default) or default
def get_args(context, step, version, interpreter):
i = interpreter.format(version=version)
ipreter = Interpreter(i)
home_dir = [ipreter.impl, str(version)]
if ipreter.debug:
home_dir.append('dbg')
if cfg.name:
home_dir.append(cfg.name)
home_dir = '.pybuild/{}'.format('_'.join(home_dir))
build_dir = get_option('build_dir', interpreter, version,
default=join(home_dir, 'build'))
destdir = context['destdir'].format(version=version, interpreter=i)
if cfg.name:
package = ipreter.suggest_pkg_name(cfg.name)
else:
package = 'PYBUILD_NAME_not_set'
if cfg.name and destdir.rstrip('/').endswith('debian/tmp'):
destdir = "debian/{}".format(package)
destdir = abspath(destdir)
args = dict(context['args'])
args.update({
'package': package,
'interpreter': ipreter,
'version': version,
'args': get_option("%s_args" % step, interpreter, version, ''),
'dir': abspath(context['dir'].format(version=version, interpreter=i)),
'destdir': destdir,
'build_dir': abspath(build_dir.format(version=version, interpreter=i)),
# versioned dist-packages even for Python 3.X - dh_python3 will fix it later
# (and will have a chance to compare files)
'install_dir': get_option('install_dir', interpreter, version,
'/usr/lib/python{version}/dist-packages'
).format(version=version, interpreter=i),
'home_dir': abspath(home_dir)})
if interpreter == 'pypy':
args['install_dir'] = '/usr/lib/pypy/dist-packages/'
env = dict(args.get('ENV', {}))
pp = env.get('PYTHONPATH', context['ENV'].get('PYTHONPATH'))
pp = pp.split(':') if pp else []
if step in {'build', 'test'}:
if step == 'test':
args['test_dir'] = join(args['destdir'], args['install_dir'].lstrip('/'))
if args['test_dir'] not in pp:
pp.append(args['test_dir'])
if args['build_dir'] not in pp:
pp.append(args['build_dir'])
# cross compilation support for Python 2.x
if (version.major == 2 and
arch_data.get('DEB_BUILD_ARCH') != arch_data.get('DEB_HOST_ARCH')):
pp.insert(0, ('/usr/lib/python{0}/plat-{1[DEB_HOST_MULTIARCH]}'
).format(version, arch_data))
env['PYTHONPATH'] = ':'.join(pp)
# cross compilation support for Python <= 3.8 (see above)
if version.major == 3:
name = '_PYTHON_SYSCONFIGDATA_NAME'
value = env.get(name, context['ENV'].get(name, ''))
if version << '3.8' and value.startswith('_sysconfigdata_')\
and not value.startswith('_sysconfigdata_m'):
value = env[name] = "_sysconfigdata_m%s" % value[15:]
# update default from main() for -dbg interpreter
if value and ipreter.debug and not value.startswith('_sysconfigdata_d'):
env[name] = "_sysconfigdata_d%s" % value[15:]
args['ENV'] = env
if not exists(args['build_dir']):
makedirs(args['build_dir'])
return args
def is_disabled(step, interpreter, version):
i = interpreter
prefix = "{}/".format(step)
disabled = (get_option('disable', i, version) or '').split()
for item in disabled:
if item in (step, '1'):
log.debug('disabling {} step for {} {}'.format(step, i, version))
return True
if item.startswith(prefix):
disabled.append(item[len(prefix):])
if i in disabled or str(version) in disabled or \
i.format(version=version) in disabled or \
i.format(version=version.major) in disabled:
log.debug('disabling {} step for {} {}'.format(step, i, version))
return True
return False
def run(func, interpreter, version, context):
step = func.__func__.__name__
args = get_args(context, step, version, interpreter)
env = dict(context['ENV'])
if 'ENV' in args:
env.update(args['ENV'])
before_cmd = get_option('before_{}'.format(step), interpreter, version)
if before_cmd:
if cfg.quiet:
log_file = join(args['home_dir'], 'before_{}_cmd.log'.format(step))
else:
log_file = False
command = before_cmd.format(**args)
log.info(command)
output = execute(command, context['dir'], env, log_file)
if output['returncode'] != 0:
msg = 'exit code={}: {}'.format(output['returncode'], command)
raise Exception(msg)
fpath = join(args['home_dir'], 'testfiles_to_rm_before_install')
if step == 'install' and exists(fpath):
with open(fpath) as fp:
for line in fp:
path = line.strip('\n')
if exists(path):
if isdir(path):
rmtree(path)
else:
remove(path)
remove(fpath)
result = func(context, args)
after_cmd = get_option('after_{}'.format(step), interpreter, version)
if after_cmd:
if cfg.quiet:
log_file = join(args['home_dir'], 'after_{}_cmd.log'.format(step))
else:
log_file = False
command = after_cmd.format(**args)
log.info(command)
output = execute(command, context['dir'], env, log_file)
if output['returncode'] != 0:
msg = 'exit code={}: {}'.format(output['returncode'], command)
raise Exception(msg)
return result
def move_to_ext_destdir(i, version, context):
"""Move built C extensions from the general destdir to ext_destdir"""
args = get_args(context, 'install', version, interpreter)
ext_destdir = get_option('ext_destdir', i, version)
if ext_destdir:
move_matching_files(args['destdir'], ext_destdir,
get_option('ext_pattern', i, version),
get_option('ext_sub_pattern', i, version),
get_option('ext_sub_repl', i, version))
func = None
if cfg.clean_only:
func = plugin.clean
elif cfg.configure_only:
func = plugin.configure
elif cfg.build_only:
func = plugin.build
elif cfg.install_only:
func = plugin.install
elif cfg.test_only:
func = plugin.test
elif cfg.print_args:
func = plugin.print_args
### one function for each interpreter at a time mode ###
if func:
step = func.__func__.__name__
if step == 'test' and nocheck:
exit(0)
failure = False
for i in cfg.interpreter:
ipreter = Interpreter(interpreter.format(version=versions[0]))
iversions = build_sorted(versions, impl=ipreter.impl)
if '{version}' not in i and len(versions) > 1:
log.info('limiting Python versions to %s due to missing {version}'
' in interpreter string', str(versions[-1]))
iversions = versions[-1:] # just the default or closest to default
for version in iversions:
if is_disabled(step, i, version):
continue
c = dict(context)
c['dir'] = get_option('dir', i, version, cfg.dir)
c['destdir'] = get_option('destdir', i, version, cfg.destdir)
try:
run(func, i, version, c)
except Exception as err:
log.error('%s: plugin %s failed with: %s',
step, plugin.NAME, err, exc_info=cfg.verbose)
# try to build/test other interpreters/versions even if
# one of them fails to make build logs more verbose:
failure = True
if step not in ('build', 'test'):
exit(13)
if step == 'install':
move_to_ext_destdir(i, version, c)
if failure:
# exit with a non-zero return code if at least one build/test failed
exit(13)
exit(0)
### all functions for interpreters in batches mode ###
try:
context_map = {}
for i in cfg.interpreter:
ipreter = Interpreter(interpreter.format(version=versions[0]))
iversions = build_sorted(versions, impl=ipreter.impl)
if '{version}' not in i and len(versions) > 1:
log.info('limiting Python versions to %s due to missing {version}'
' in interpreter string', str(versions[-1]))
iversions = versions[-1:] # just the default or closest to default
for version in iversions:
key = (i, version)
if key in context_map:
c = context_map[key]
else:
c = dict(context)
c['dir'] = get_option('dir', i, version, cfg.dir)
c['destdir'] = get_option('destdir', i, version, cfg.destdir)
context_map[key] = c
if not is_disabled('clean', i, version):
run(plugin.clean, i, version, c)
if not is_disabled('configure', i, version):
run(plugin.configure, i, version, c)
if not is_disabled('build', i, version):
run(plugin.build, i, version, c)
if not is_disabled('install', i, version):
run(plugin.install, i, version, c)
move_to_ext_destdir(i, version, c)
if not nocheck and not is_disabled('test', i, version):
run(plugin.test, i, version, c)
except Exception as err:
log.error('plugin %s failed: %s', plugin.NAME, err,
exc_info=cfg.verbose)
exit(14)
def parse_args(argv):
usage = '%(prog)s [ACTION] [BUILD SYSTEM ARGS] [DIRECTORIES] [OPTIONS]'
parser = argparse.ArgumentParser(usage=usage)
parser.add_argument('-v', '--verbose', action='store_true',
default=environ.get('PYBUILD_VERBOSE') == '1',
help='turn verbose mode on')
parser.add_argument('-q', '--quiet', action='store_true',
default=environ.get('PYBUILD_QUIET') == '1',
help='doesn\'t show external command\'s output')
parser.add_argument('-qq', '--really-quiet', action='store_true',
default=environ.get('PYBUILD_RQUIET') == '1',
help='be quiet')
parser.add_argument('--version', action='version', version='%(prog)s DEVELV')
action = parser.add_argument_group('ACTION', '''The default is to build,
install and test the library using detected build system version by
version. Selecting one of following actions, will invoke given action
for all versions - one by one - which (contrary to the default action)
in some build systems can overwrite previous results.''')
action.add_argument('--detect', action='store_true', dest='detect_only',
help='return the name of detected build system')
action.add_argument('--clean', action='store_true', dest='clean_only',
help='clean files using auto-detected build system specific methods')
action.add_argument('--configure', action='store_true', dest='configure_only',
help='invoke configure step for all requested Python versions')
action.add_argument('--build', action='store_true', dest='build_only',
help='invoke build step for all requested Python versions')
action.add_argument('--install', action='store_true', dest='install_only',
help='invoke install step for all requested Python versions')
action.add_argument('--test', action='store_true', dest='test_only',
help='invoke tests for auto-detected build system')
action.add_argument('--list-systems', action='store_true',
help='list available build systems and exit')
action.add_argument('--print', action='append', dest='print_args',
help="print pybuild's internal parameters")
arguments = parser.add_argument_group('BUILD SYSTEM ARGS', '''
Additional arguments passed to the build system.
--system=custom requires complete command.''')
arguments.add_argument('--before-clean', metavar='CMD',
help='invoked before the clean command')
arguments.add_argument('--clean-args', metavar='ARGS')
arguments.add_argument('--after-clean', metavar='CMD',
help='invoked after the clean command')
arguments.add_argument('--before-configure', metavar='CMD',
help='invoked before the configure command')
arguments.add_argument('--configure-args', metavar='ARGS')
arguments.add_argument('--after-configure', metavar='CMD',
help='invoked after the configure command')
arguments.add_argument('--before-build', metavar='CMD',
help='invoked before the build command')
arguments.add_argument('--build-args', metavar='ARGS')
arguments.add_argument('--after-build', metavar='CMD',
help='invoked after the build command')
arguments.add_argument('--before-install', metavar='CMD',
help='invoked before the install command')
arguments.add_argument('--install-args', metavar='ARGS')
arguments.add_argument('--after-install', metavar='CMD',
help='invoked after the install command')
arguments.add_argument('--before-test', metavar='CMD',
help='invoked before the test command')
arguments.add_argument('--test-args', metavar='ARGS')
arguments.add_argument('--after-test', metavar='CMD',
help='invoked after the test command')
tests = parser.add_argument_group('TESTS', '''\
unittest\'s discover is used by default (if available)''')
tests.add_argument('--test-nose', action='store_true',
default=environ.get('PYBUILD_TEST_NOSE') == '1',
help='use nose module in --test step')
tests.add_argument('--test-nose2', action='store_true',
default=environ.get('PYBUILD_TEST_NOSE2') == '1',
help='use nose2 module in --test step')
tests.add_argument('--test-pytest', action='store_true',
default=environ.get('PYBUILD_TEST_PYTEST') == '1',
help='use pytest module in --test step')
tests.add_argument('--test-tox', action='store_true',
default=environ.get('PYBUILD_TEST_TOX') == '1',
help='use tox in --test step')
tests.add_argument('--test-custom', action='store_true',
default=environ.get('PYBUILD_TEST_CUSTOM') == '1',
help='use custom command in --test step')
dirs = parser.add_argument_group('DIRECTORIES')
dirs.add_argument('-d', '--dir', action='store', metavar='DIR',
default=environ.get('PYBUILD_DIR', getcwd()),
help='source files directory - base for other relative dirs [default: CWD]')
dirs.add_argument('--dest-dir', action='store', metavar='DIR', dest='destdir',
default=environ.get('DESTDIR', 'debian/tmp'),
help='destination directory [default: debian/tmp]')
dirs.add_argument('--ext-dest-dir', action='store', metavar='DIR', dest='ext_destdir',
default=environ.get('PYBUILD_EXT_DESTDIR'),
help='destination directory for .so files')
dirs.add_argument('--ext-pattern', action='store', metavar='PATTERN',
default=environ.get('PYBUILD_EXT_PATTERN', r'\.so(\.[^/]*)?$'),
help='regular expression for files that should be moved'
' if --ext-dest-dir is set [default: .so files]')
dirs.add_argument('--ext-sub-pattern', action='store', metavar='PATTERN',
default=environ.get('PYBUILD_EXT_SUB_PATTERN'),
help='pattern to change --ext-pattern\'s filename or path')
dirs.add_argument('--ext-sub-repl', action='store', metavar='PATTERN',
default=environ.get('PYBUILD_EXT_SUB_REPL'),
help='replacement for match from --ext-sub-pattern,'
' empty string by default')
dirs.add_argument('--install-dir', action='store', metavar='DIR',
help='installation directory [default: .../dist-packages]')
dirs.add_argument('--name', action='store',
default=environ.get('PYBUILD_NAME'),
help='use this name to guess destination directories')
limit = parser.add_argument_group('LIMITATIONS')
limit.add_argument('-s', '--system',
default=environ.get('PYBUILD_SYSTEM'),
help='select a build system [default: auto-detection]')
limit.add_argument('-p', '--pyver', action='append', dest='versions',
help='''build for Python VERSION.
This option can be used multiple times
[default: all supported Python 3.X versions]''')
limit.add_argument('-i', '--interpreter', action='append',
help='change interpreter [default: python{version}]')
limit.add_argument('--disable', metavar='ITEMS',
help='disable action, interpreter or version')
args = parser.parse_args()
if not args.interpreter:
args.interpreter = environ.get('PYBUILD_INTERPRETERS', 'python{version}').split()
if not args.versions:
args.versions = environ.get('PYBUILD_VERSIONS', '').split()
else:
# add support for -p `pyversions -rv`
versions = []
for version in args.versions:
versions.extend(version.split())
args.versions = versions
if args.test_nose or args.test_nose2 or args.test_pytest or args.test_tox\
or args.system == 'custom':
args.custom_tests = True
else:
args.custom_tests = False
return args
if __name__ == '__main__':
cfg = parse_args(sys.argv)
if cfg.really_quiet:
cfg.quiet = True
log.setLevel(logging.CRITICAL)
elif cfg.verbose:
log.setLevel(logging.DEBUG)
else:
log.setLevel(logging.INFO)
log.debug('version: DEVELV')
log.debug(sys.argv)
main(cfg)
# let dh/cdbs clean the .pybuild dir
# rmtree(join(cfg.dir, '.pybuild'))

312
pybuild.rst Normal file
View File

@ -0,0 +1,312 @@
=========
pybuild
=========
----------------------------------------------------------------------------------------------------
invokes various build systems for requested Python versions in order to build modules and extensions
----------------------------------------------------------------------------------------------------
:Manual section: 1
:Author: Piotr Ożarowski, 2012-2019
SYNOPSIS
========
pybuild [ACTION] [BUILD SYSTEM ARGUMENTS] [DIRECTORIES] [OPTIONS]
DEBHELPER COMMAND SEQUENCER INTEGRATION
=======================================
* build depend on `dh-python`,
* build depend on all supported Python interpreters, pybuild will use it to create
a list of interpreters to build for.
Recognized dependencies:
- `python3-all-dev` - for Python extensions that work with Python 3.X interpreters,
- `python3-all-dbg` - as above, add this one if you're building -dbg packages,
- `python3-all` - for Python modules that work with Python 3.X interpreters,
- `python3-dev` - builds an extension for default Python 3.X interpreter
(useful for private extensions, use python3-all-dev for public ones),
- `python3` - as above, used if headers files are not needed to build private module,
- `python-all-dev` - for Python extensions that work with obsolete Python 2.X interpreters,
- `python-all-dbg` - as above, add this one if you're building -dbg packages,
- `python-all` - for Python modules that work with obsolete Python 2.X interpreters,
- `pypy` - for PyPy 2.X interpreter.
* add `--buildsystem=pybuild` to dh's arguments in debian/rules,
* if more than one binary package is build:
add debian/python-foo.install files, or
`export PYBUILD_NAME=modulename` (modulename will be used to guess binary
package prefixes), or
`export PYBUILD_DESTDIR` env. variables in debian/rules
* add `--with=python3` or `--with=python3,python2,pypy` to dh's arguments in debian/rules
(see proper helper's manpage for more details) or add `dh-sequence-python3`
(`dh-sequence-python2` for Python 2.X, `dh-sequence-pypy` for PyPy) to Build-Depends
debian/rules file example::
#! /usr/bin/make -f
export PYBUILD_NAME=foo
%:
dh $@ --with python3 --buildsystem=pybuild
OPTIONS
=======
Most options can be set (in addition to command line) via environment
variables. PyBuild will check:
* PYBUILD_OPTION_VERSIONED_INTERPRETER (f.e. PYBUILD_CLEAN_ARGS_python3.2)
* PYBUILD_OPTION_INTERPRETER (f.e. PYBUILD_CONFIGURE_ARGS_python3-dbg)
* PYBUILD_OPTION (f.e. PYBUILD_INSTALL_ARGS)
optional arguments
------------------
-h, --help show this help message and exit
-v, --verbose turn verbose mode on
-q, --quiet doesn't show external command's output
-qq, --really-quiet be quiet
--version show program's version number and exit
ACTION
------
The default is to build, install and test the library using detected build
system version by version. Selecting one of following actions, will invoke
given action for all versions - one by one - which (contrary to the default
action) in some build systems can overwrite previous results.
--detect
return the name of detected build system
--clean
clean files using auto-detected build system specific methods
--configure
invoke configure step for all requested Python versions
--build
invoke build step for all requested Python versions
--install
invoke install step for all requested Python versions
--test
invoke tests for auto-detected build system
--list-systems
list available build systems and exit
--print
print pybuild's internal parameters
TESTS
-----
unittest's discover from standard library is used in test step by default.
--test-nose
use nose module in test step, remember to add python-nose and/or
python3-nose to Build-Depends
--test-nose2
use nose2 module in test step, remember to add python-nose2 and/or
python3-nose2 to Build-Depends
--test-pytest
use pytest module in test step, remember to add python-pytest and/or
python3-pytest to Build-Depends
--test-tox
use tox command in test step, remember to add tox
to Build-Depends. Requires tox.ini file
--test-custom
use a custom command in the test step. The full test command is then
specified with `--test-args` or by setting the `PYBUILD_TEST_ARGS`
environment variable. Remember to add any needed packages to run the
tests to Build-Depends.
testfiles
~~~~~~~~~
Tests are invoked from within build directory to make sure newly built
files are tested instead of source files. If test suite requires other files
in this directory, you can list them in `debian/pybuild.testfiles` file
(you can also use `debian/pybuild_pythonX.testfiles` or
`debian/pybuild_pythonX.Y.testfiles`) and files listed there will be copied
before test step and removed before install step.
By default only `test` and `tests` directories are copied to build directory.
BUILD SYSTEM ARGUMENTS
----------------------
Additional arguments passed to the build system.
--system=custom requires complete command in --foo-args parameters.
--before-clean COMMAND
invoked before the clean command
--clean-args ARGUMENTS
arguments added to clean command generated by build system plugin
--after-clean COMMAND
invoked after the clean command
--before-configure COMMAND
invoked before the configure command
--configure-args ARGUMENTS
arguments added to configure command generated by build system plugin
--after-configure COMMAND
invoked after the configure command
--before-build COMMAND
invoked before the build command
--build-args ARGUMENTS
arguments added to build command generated by build system plugin
--after-build COMMAND
invoked after the build command
--before-install COMMAND
invoked before the install command
--install-args ARGUMENTS
arguments added to install command generated by build system plugin
--after-install COMMAND
invoked after the install command
--before-test COMMAND
invoked before the test command
--test-args ARGUMENTS
arguments added to test command generated by build system plugin
--after-test COMMAND
invoked after the test command
variables that can be used in `ARGUMENTS` and `COMMAND`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `{version}` will be replaced with current Python version,
you can also use `{version.major}`, `{version.minor}`, etc.
* `{interpreter}` will be replaced with current interpreter,
you can also use `{interpreter.include_dir}`
* `{dir}` will be replaced with sources directory,
* `{destdir}` will be replaced with destination directory,
* `{home_dir}` will be replaced with temporary HOME directory,
where plugins can keep their data
(.pybuild/interpreter_version/ by default),
* `{build_dir}` will be replaced with build directory
* `{install_dir}` will be replaced with install directory.
* `{package}` will be replaced with suggested package name,
if --name (or PYBUILD_NAME) is set to `foo`, this variable
will be replaced to `python-foo`, `python3-foo` or `pypy-foo`
depending on interpreter which is used in given iteration.
DIRECTORIES
-----------
-d DIR, --dir DIR
set source files directory - base for other relative dirs
[by default: current working directory]
--dest-dir DIR
set destination directory [default: debian/tmp]
--ext-dest-dir DIR
set destination directory for .so files
--ext-pattern PATTERN
regular expression for files that should be moved if --ext-dest-dir is set
[default: `\.so(\.[^/]*)?$`]
--ext-sub-pattern PATTERN
regular expression for part of path/filename matched in --ext-pattern
that should be removed or replaced with --ext-sub-repl
--ext-sub-repl PATTERN
replacement for matches in --ext-sub-pattern
--install-dir DIR
set installation directory [default: .../dist-packages]
--name NAME
use this name to guess destination directories
(depending on interpreter, "foo" sets debian/python-foo,
debian/python3-foo, debian/python3-foo-dbg, etc.)
This overrides --dest-dir.
variables that can be used in `DIR`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `{version}` will be replaced with current Python version,
* `{interpreter}` will be replaced with selected interpreter.
LIMITATIONS
-----------
-s SYSTEM, --system SYSTEM
select a build system [default: auto-detection]
-p VERSIONS, --pyver VERSIONS
build for Python VERSIONS. This option can be used multiple times.
Versions can be separated by space character.
The default is all Python 3.X supported versions.
-i INTERPRETER, --interpreter INTERPRETER
change interpreter [default: python{version}]
--disable ITEMS
disable action, interpreter, version or any mix of them.
Note that f.e. python3 and python3-dbg are two different interpreters,
--disable test/python3 doesn't disable python3-dbg's tests.
disable examples
~~~~~~~~~~~~~~~~
* `--disable test/python3.9-dbg` - disables tests for python3.9-dbg
* `--disable '3.8 3.9'` - disables all actions for version 3.8 and 3.9
* `PYBUILD_DISABLE=python3.9` - disables all actions for Python 3.9
* `PYBUILD_DISABLE_python3.3=test` - disables tests for Python 3.3
* `PYBUILD_DISABLE=test/python3.3` - same as above
* `PYBUILD_DISABLE=configure/python3 2.4 pypy` - disables configure
action for all python3 interpreters, all actions for version 2.4, and
all actions for pypy
PLUGINS
-------
pybuild supports multiple build system plugins. By default it is
automatically selected. These systems are currently supported::
* distutils (most commonly used)
* cmake
* flit
* pyproject
* custom
flit plugin
~~~~~~~~~~~
The flit plugin can be used to build Debian packages based on PEP 517
metadata in `pyproject.toml` when flit is the upstream build system. These
can be identified by the presence of a `build-backend = "flit_core.buildapi"`
element in `pyproject.toml`. The flit plugin only supports python3. To use
this plugin::
* build depend on `flit` and either
* build depend on `python3-tomli` so flit can be automatically selected or
* add `export PYBUILD_SYSTEM=flit` to debian/rules to manually select
debian/rules file example::
#! /usr/bin/make -f
export PYBUILD_NAME=foo
export PYBUILD_SYSTEM=flit (needed if python3-tomli is not installed)
%:
dh $@ --with python3 --buildsystem=pybuild
pyproject
~~~~~~~~~
The pyproject plugin drives the new PEP-517 standard interface for
building Python packages, upstream. This is configured via
`pyproject.toml`.
This plugin is expected to replace the distutils and flit plugins in the
future.
The entry points generated by the package are created during the build step
(other plugins make the entry points during the install step); the entry
points are available in PATH during the test step, permitting them to be
called from tests.
To use this plugin:
* build depend on `pybuild-plugin-pyproject` as well as any build tools
specified by upstream in `pyproject.toml`.
ENVIRONMENT
===========
As described above in OPTIONS, pybuild can be configured by `PYBUILD_`
prefixed environment variables.
Tests are skipped if `nocheck` is in the `DEB_BUILD_OPTIONS` or
`DEB_BUILD_PROFILES` environment variables.
`DESTDIR` provides a default a default value to the `--dest-dir` option.
Pybuild will export `http_proxy=http://127.0.0.1:9/`,
`https_proxy=https://127.0.0.1:9/`, and `no_proxy=localhost` to
hopefully block attempts by the package's build-system to access the
Internet.
If network access to a loopback interface is needed and blocked by this,
export empty `http_proxy` and `https_proxy` variables before calling
pybuild.
If not set, `LC_ALL`, `CCACHE_DIR`, `DEB_PYTHON_INSTALL_LAYOUT`,
`_PYTHON_HOST_PLATFORM`, `_PYTHON_SYSCONFIGDATA_NAME`, will all be set
to appropriate values, before calling the package's build script.
SEE ALSO
========
* dh_python2(1)
* dh_python3(1)
* https://wiki.debian.org/Python/Pybuild
* http://deb.li/pybuild - most recent version of this document

16
pydist/Makefile Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/make -f
FALLBACK_FLAGS = $(shell dpkg-vendor --is ubuntu && echo '--ubuntu')
clean:
rm -rf cache
#rm -f dist_fallback
rm -f README.PyDist.html
dist_fallback:
python3 ./generate_fallback_list.py $(FALLBACK_FLAGS)
README.PyDist.html: README.PyDist
rst2html $< $@
.PHONY: clean

122
pydist/README.PyDist Normal file
View File

@ -0,0 +1,122 @@
============
PyDist files
============
DISTNAME [VRANGE] [DEPENDENCY][; [PEP386] [RULES]]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PyDist files help tools like dh_python2/3 to translate Python dependencies
(from setup.py's install_requires or egg's requires.txt file) to Debian
dependencies if given Python distribution file / directory is not installed
(hint: add proper package to Build-Depends and PyDist file might not be needed)
or if detection is not correct.
Before checking for package that provides installed egg-info file or directory
dh_python3 is checking these locations for overrides:
* debian/py3dist-overrides
* /usr/share/python3/dist/*
* /usr/share/dh-python/dist/cpython3_fallback
debian/python3-foo.pydist is copied into /usr/share/python3/dist/ automatically.
For Python 2.X it's adequately: pydist-overrides, /usr/share/python/dist/* and
/usr/share/dh-python/dist/cpython2_fallback
For PyPy it's adequately: pypydist-overrides, /usr/share/pypy/dist/* and
/usr/share/dh-python/dist/pypy_fallback
*NOTE:* There's no need to add an override if build-depending on a package that
provides searched egg-info results in correctly recognized dependency.
Required fields:
~~~~~~~~~~~~~~~~
DISTNAME
````````
Python distribution name (you can find it at the beginning of .egg-info
file/directory name that your package provides).
Examples:
* SQLAlchemy
* Jinja2
* numpy
Optional fields:
~~~~~~~~~~~~~~~~
VRANGE
``````
Python version or version range the line applies to.
Examples:
* 2.6 (Python 2.6 only)
* 2.5- (Python 2.5 and newer)
* 2.5-2.7 (Python 2.5 or 2.6)
* -2.7 (Python 2.6 or older)
* 3.1 (Python 3.1 only)
* 3.1- (Python 3.1 and newer)
* 3.1-3.3 (Python 3.1 or 3.2)
* -3.4 (Python 3.3 or older)
DEPENDENCY
``````````
Debian dependency, multiple packages or versions are allowed.
If not set, given Python distribution name will be ignored.
Examples:
* python-mako
* python-jinja2 | python (>= 2.6)
* python-sqlalchemy (>= 0.5), python-sqlalchemy (<< 0.6)
* python3-mako
* python3-jinja2 | python3 (>= 3.0)
* python3-sqlalchemy (>= 0.5), python3-sqlalchemy (<< 0.6)
PEP386
``````
Standards flag: upstream uses versioning schema described in PEP 386.
RULES
`````
Rules needed to translate upstream version to Debian one. If PEP386 is
set, its rules will be applied later. Multiple rules are allowed, separate them
with a space.
Examples:
* s/^/2:/
* s/alpha/~alpha/ s/^/1:/
Notes:
~~~~~~
You can use multiple lines if binary package provides more than one Python
distribution or if you want to specify different dependencies for each Python
version or version range.
If you use dh_python2, it will install debian/binary_package_name.pydist file
to /usr/share/dh-python/dist/cpython2/binary_package_name automatically.
If you use dh_python3, it will install debian/binary_package_name.pydist file
to /usr/share/dh-python/dist/cpython3/binary_package_name automatically.
Complete examples:
~~~~~~~~~~~~~~~~~~
* SQLAlchemy python-sqlalchemy (>= 0.5), python-sqlalchemy (<< 0.6)
* Mako python-mako; PEP386
* foo -2.5 python-oldfoo; s/^/3:/
* foo 2.5- python-foo; PEP386
* Bar 2.6-
* SQLAlchemy python3-sqlalchemy (>= 0.5), python3-sqlalchemy (<< 0.6)
* Mako python3-mako; PEP386
* foo -3.2 python3-oldfoo; s/^/3:/
* foo 3.2- python3-foo; PEP386
* Bar 2.6-
.. vim: ft=rst

28
pydist/cpython2_fallback Normal file
View File

@ -0,0 +1,28 @@
Arriero arriero
DisplayCAL displaycal
Pillow python-pil
Pmw python-pmw
VirtualMailManager vmm
argparse python (>= 2.7) | python-argparse
cinfony python-cinfony
dvcs_autosync dvcs-autosync
gjots2 gjots2
grokmirror grokmirror
keepkey python-keepkey
live_wrapper live-wrapper
mini_buildd python-mini-buildd
mozilla_devscripts mozilla-devscripts
nemu python-nemu
neuroshare python-neuroshare
pil python-pil
pip python-pip
postnews postnews
python python
python_passfd python-passfd
python_unshare python-unshare
setuptools python-pkg-resources
six python-six
sphinx_patchqueue python-sphinx-patchqueue
vamos undertaker
vland vland
wsgiref python (>= 2.5) | python-wsgiref

4755
pydist/cpython3_fallback Normal file

File diff suppressed because it is too large Load Diff

154
pydist/generate_fallback_list.py Executable file
View File

@ -0,0 +1,154 @@
#! /usr/bin/python3
# Copyright © 2010-2015 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
import sys
try:
from distro_info import DistroInfo # python3-distro-info package
except ImportError:
DistroInfo = None
from gzip import decompress
from os import chdir, mkdir
from os.path import dirname, exists, isdir, join, split
from urllib.request import urlopen
if '--ubuntu' in sys.argv and DistroInfo:
SOURCES = [
'http://archive.ubuntu.com/ubuntu/dists/%s/Contents-amd64.gz' %
DistroInfo('ubuntu').devel(),
]
else:
SOURCES = [
'http://ftp.debian.org/debian/dists/unstable/main/Contents-all.gz',
'http://ftp.debian.org/debian/dists/unstable/main/Contents-amd64.gz',
]
IGNORED_PKGS = {'python-setuptools', 'python3-setuptools', 'pypy-setuptools'}
OVERRIDES = {
'cpython2': {
'python': 'python',
'setuptools': 'python-pkg-resources',
'wsgiref': 'python (>= 2.5) | python-wsgiref',
'argparse': 'python (>= 2.7) | python-argparse',
# not recognized due to .pth file (egg-info is in PIL/ and not in *-packages/)
'pil': 'python-pil',
'Pillow': 'python-pil'},
'cpython3': {
'pil': 'python3-pil',
'Pillow': 'python3-pil',
'pylint': 'pylint',
'setuptools': 'python3-pkg-resources',
'argparse': 'python3 (>= 3.2)'},
'pypy': {}
}
public_egg = re.compile(r'''
/usr/
(
(?P<cpython2>
(lib/python2\.[0-9]/((site)|(dist))-packages)|
(share/python-support/[^/]+)
)|
(?P<cpython3>
(lib/python3/dist-packages)
)|
(?P<pypy>
(lib/pypy/dist-packages)
)
)
/[^/]*\.(dist|egg)-info
''', re.VERBOSE).match
skip_sensible_names = True if '--skip-sensible-names' in sys.argv else False
chdir(dirname(__file__))
if isdir('../dhpython'):
sys.path.append('..')
else:
sys.path.append('/usr/share/dh-python/dhpython/')
from dhpython.pydist import sensible_pname
data = ''
if not isdir('cache'):
mkdir('cache')
for source in SOURCES:
cache_fpath = join('cache', split(source)[-1])
if not exists(cache_fpath):
with urlopen(source) as fp:
source_data = fp.read()
with open(cache_fpath, 'wb') as fp:
fp.write(source_data)
else:
with open(cache_fpath, 'rb') as fp:
source_data = fp.read()
try:
data += str(decompress(source_data), encoding='UTF-8')
except UnicodeDecodeError as e: # Ubuntu
data += str(decompress(source_data), encoding='ISO-8859-15')
result = {
'cpython2': {},
'cpython3': {},
'pypy': {}}
# Contents file doesn't contain comment these days
is_header = not data.startswith('bin')
for line in data.splitlines():
if is_header:
if line.startswith('FILE'):
is_header = False
continue
try:
path, desc = line.rsplit(maxsplit=1)
except ValueError:
# NOTE(jamespage) some lines in Ubuntu are not parseable.
continue
path = '/' + path.rstrip()
section, pkg_name = desc.rsplit('/', 1)
if pkg_name in IGNORED_PKGS:
continue
match = public_egg(path)
if match:
egg_name = [i.split('-', 1)[0] for i in path.split('/')
if i.endswith(('.egg-info', '.dist-info'))][0]
if egg_name.endswith('.egg'):
egg_name = egg_name[:-4]
impl = next(key for key, value in match.groupdict().items() if value)
if skip_sensible_names and\
sensible_pname(impl, egg_name) == pkg_name:
continue
processed = result[impl]
if egg_name not in processed:
processed[egg_name] = pkg_name
for impl, details in result.items():
with open('{}_fallback'.format(impl), 'w') as fp:
overrides = OVERRIDES[impl]
lines = []
for egg, value in overrides.items():
lines.append('{} {}\n'.format(egg, value))
lines.extend(
'{} {}\n'.format(egg, pkg) for egg, pkg in details.items() if egg not in overrides
)
fp.writelines(sorted(lines))

14
pydist/pypy_fallback Normal file
View File

@ -0,0 +1,14 @@
Wand pypy-wand
appdirs pypy-appdirs
asn1crypto pypy-asn1crypto
enum34 pypy-enum34
ipaddress pypy-ipaddress
packaging pypy-packaging
pretend pypy-pretend
pyasn1 pypy-pyasn1
pyparsing pypy-pyparsing2
rawkit pypy-rawkit
scandir pypy-scandir
six pypy-six
stem pypy-stem
vanguards vanguards

20
tests/Makefile Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/make -f
# enable or disable tests here:
#TESTS := test101 test201 test202 test203 test204 test205 test206 test207 test301 test302 test303 test304 test305 test306 testpb01 testpb02 testpb03 testpb04 testpb05 testpb06
TESTS := test101 test301 test302 test303 test304 test305 test306
all: $(TESTS)
test%:
make -C t$* run
make -C t$* check
clean-test%:
make -C t$* clean
clean: $(TESTS:%=clean-%)
rm -f *\.dsc *\.tar\.gz *\.build *\.changes *\.deb *\.buildinfo
@find . -prune -name '*.egg-info' -exec rm -rf '{}' ';' || true
.PHONY: clean

23
tests/common.mk Normal file
View File

@ -0,0 +1,23 @@
#!/usr/bin/make -f
export DEBPYTHON_DEFAULT ?= $(shell python3 ../../dhpython/_defaults.py default cpython2)
export DEBPYTHON_SUPPORTED ?= $(shell python3 ../../dhpython/_defaults.py supported cpython2)
export DEBPYTHON3_DEFAULT ?= $(shell python3 ../../dhpython/_defaults.py default cpython3)
export DEBPYTHON3_SUPPORTED ?= $(shell python3 ../../dhpython/_defaults.py supported cpython3)
export DEBPYPY_DEFAULT ?= $(shell python3 ../../dhpython/_defaults.py default pypy)
export DEBPYPY_SUPPORTED ?= $(shell python3 ../../dhpython/_defaults.py supported pypy)
export DEB_HOST_MULTIARCH=my_multiarch-triplet
export DEB_HOST_ARCH ?= $(shell dpkg-architecture -qDEB_HOST_ARCH)
export DH_INTERNAL_OPTIONS=
all: run check
run: clean
@echo ============================================================
@echo ==== TEST: `basename $$PWD`
dpkg-buildpackage -b -us -uc \
--no-check-builddeps \
--check-command="../test-package-show-info"
clean-common:
./debian/rules clean

18
tests/common.py Normal file
View File

@ -0,0 +1,18 @@
class FakeOptions:
def __init__(self, **kwargs):
opts = {
'depends': (),
'depends_section': (),
'guess_deps': False,
'no_ext_rename': False,
'recommends': (),
'recommends_section': (),
'requires': (),
'suggests': (),
'suggests_section': (),
'vrange': None,
'accept_upstream_versions': False,
}
opts.update(kwargs)
for k, v in opts.items():
setattr(self, k, v)

12
tests/t101/Makefile Normal file
View File

@ -0,0 +1,12 @@
#!/usr/bin/make -f
include ../common.mk
check:
test -f debian/pypy-foo/usr/lib/pypy/dist-packages/foo.py
test ! -d debian/pypy-foo/usr/lib/pypy/site-packages
test ! -d debian/pypy-foo/usr/lib/pypy/dist-packages/__pycache__
grep -q pypycompile debian/pypy-foo/DEBIAN/postinst
grep -q pypyclean debian/pypy-foo/DEBIAN/prerm
clean:
./debian/rules clean

View File

@ -0,0 +1,5 @@
foo (1.2.3) unstable; urgency=low
* Initial release
-- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200

1
tests/t101/debian/compat Normal file
View File

@ -0,0 +1 @@
9

13
tests/t101/debian/control Normal file
View File

@ -0,0 +1,13 @@
Source: foo
Section: python
Priority: optional
Maintainer: Piotr Ożarowski <piotr@debian.org>
Build-Depends: debhelper (>= 7.0.50~)
# , dh-python
Standards-Version: 3.9.4
Package: pypy-foo
Architecture: all
Depends: ${pypy3:Depends}, ${shlibs:Depends}, ${misc:Depends}
Description: package with public PyPy modules
example package #1

View File

@ -0,0 +1,2 @@
The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and
is licensed under the MIT License.

18
tests/t101/debian/rules Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_install:
dh_install
DH_VERBOSE=1 ../../dh_pypy
override_dh_auto_build:
override_dh_auto_test:
override_dh_auto_install:
mkdir -p debian/pypy-foo/usr/lib/pypy/site-packages/__pycache__
echo "print('foo')" > debian/pypy-foo/usr/lib/pypy/site-packages/foo.py
touch debian/pypy-foo/usr/lib/pypy/site-packages/__pycache__/foo.pypy-20.pyc
override_dh_auto_clean:

View File

@ -0,0 +1 @@
3.0 (native)

19
tests/t201/Makefile Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/make -f
include ../common.mk
DPY=$(DEBPYTHON_DEFAULT)
check:
grep -q "Recommends: .*python-mako" debian/python-foo/DEBIAN/control
#grep -q 'python-foo (>= 2:0.1~rc2)' debian/python-foo/DEBIAN/control
if [ -x /usr/bin/python2.6 ]; then\
test -f debian/python-foo/usr/lib/python2.6/dist-packages/foo/__init__.p;\
test ! -f debian/python-foo/usr/lib/python2.6/dist-packages/foo/spam.py;\
fi
grep -qe "Depends: .*python2\(:any\)\? (<<" debian/python-foo/DEBIAN/control
[ "`readlink debian/python-foo/usr/lib/python$(DPY)/dist-packages/foo/absolute_link_to_tmp`" = "/tmp" ]
[ "`readlink debian/python-foo/usr/lib/python$(DPY)/dist-packages/foo/link_to_parent_dir`" = ".." ]
grep -q 'pycompile -p python-foo\s*$$' debian/python-foo/DEBIAN/postinst
clean: clean-common
rm -rf lib/Foo.egg-info

View File

@ -0,0 +1,5 @@
foo (0.1.1) unstable; urgency=low
* Initial release
-- Piotr Ożarowski <piotr@debian.org> Sat, 27 Feb 2010 20:42:17 +0100

1
tests/t201/debian/compat Normal file
View File

@ -0,0 +1 @@
7

21
tests/t201/debian/control Normal file
View File

@ -0,0 +1,21 @@
Source: foo
Section: python
Priority: optional
Maintainer: Piotr Ożarowski <piotr@debian.org>
Build-Depends: debhelper (>= 7.0.50~)
Build-Depends-Indep: python-all
Standards-Version: 3.9.0
XS-Python-Version: >= 2.4
Package: python-foo
Architecture: all
Depends: ${python:Depends}, ${misc:Depends}
Recommends: ${python:Recommends}
Suggests: ${python:Suggests}
Enhances: ${python:Enhances}
Breaks: foo,
${python:Breaks}
Provides: ${python:Provides}
XB-Python-Version: ${python:Versions}
Description: foo to rule them all
example package #1

View File

@ -0,0 +1,2 @@
The Debian packaging is © 2010, Piotr Ożarowski <piotr@debian.org> and
is licensed under the MIT License.

View File

@ -0,0 +1,5 @@
Mako python-mako (>= 0.2)
SQLAlchemy python-sqlalchemy (>= 0.6)
Foo python-foo; PEP386 s/^/2:/
Bar python-bar
Baz

View File

@ -0,0 +1 @@
debian/spam.py foo 2.5-

View File

@ -0,0 +1,2 @@
foo/spam.py 2.6
foo/bar 2.7-

23
tests/t201/debian/rules Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/make -f
%:
dh $@ --buildsystem=python_distutils
override_dh_pysupport:
override_dh_install:
dh_install
find debian/ -name jquery.js -exec \
ln -fs /usr/share/javascript/jquery/jquery.js '{}' \;
find debian/ -name foo -type d -exec \
ln -s /tmp/ '{}/absolute_link_to_tmp' \;
find debian/ -name foo -type d -exec \
ln -s .. '{}/link_to_parent_dir' \;
DH_VERBOSE=1 ../../dh_python2\
--depends 'SQLAlchemy >= 0.6.1'\
--recommends Mako\
--suggests 'Foo >= 0.1rc2'\
--suggests 'bar >= 1.0'
clean:
dh_clean
rm -rf build

View File

@ -0,0 +1 @@
3.0 (native)

View File

@ -0,0 +1 @@
print 'SPAM'

View File

@ -0,0 +1 @@
print("you just imported foo from %s" % __file__)

View File

@ -0,0 +1 @@
print("you just imported foo.bar from %s" % __file__)

View File

@ -0,0 +1 @@
print("you just imported foo.baz from %s" % __file__)

1
tests/t201/lib/foo/jquery.js vendored Symbolic link
View File

@ -0,0 +1 @@
/usr/share/javascript/jquery/jquery.js

18
tests/t201/setup.py Normal file
View File

@ -0,0 +1,18 @@
#! /usr/bin/python
# -*- coding: UTF-8 -*-
from distutils.core import setup
setup(name='Foo',
version='0.1',
description="Foo to rule them all",
long_description="TODO",
keywords='foo bar baz',
author='Piotr Ożarowski',
author_email='piotr@debian.org',
url='http://www.debian.org/',
license='MIT',
package_dir={'': 'lib'},
packages=['foo'],
package_data = {'foo': ['jquery.js']},
zip_safe=False,
)

11
tests/t202/Makefile Normal file
View File

@ -0,0 +1,11 @@
#!/usr/bin/make -f
include ../common.mk
clean: clean-common
check:
test -f debian/python-foo/usr/lib/python2.7/dist-packages/foo.py
test -f debian/python-foo/usr/lib/python2.7/dist-packages/bar/bar.py
test \! -f debian/python-foo/usr/lib/python2.7/dist-packages/tests/__init__.py
grep -q 'pycompile -p python-foo\s*$$' debian/python-foo/DEBIAN/postinst
grep -q 'pyclean -p python-foo\s*$$' debian/python-foo/DEBIAN/prerm

1
tests/t202/__init__.py Normal file
View File

@ -0,0 +1 @@
print("I'm __init__.py")

1
tests/t202/bar.py Normal file
View File

@ -0,0 +1 @@
print("I'm bar")

View File

@ -0,0 +1,5 @@
foo (0.1.1) unstable; urgency=low
* Initial release
-- Piotr Ożarowski <piotr@debian.org> Sat, 27 Feb 2010 20:42:17 +0100

1
tests/t202/debian/compat Normal file
View File

@ -0,0 +1 @@
7

18
tests/t202/debian/control Normal file
View File

@ -0,0 +1,18 @@
Source: foo
Section: python
Priority: optional
Maintainer: Piotr Ożarowski <piotr@debian.org>
Build-Depends: debhelper (>= 7.0.50~)
Build-Depends-Indep: python-all
Standards-Version: 3.9.1
XS-Python-Version: >= 2.1
Package: python-foo
Architecture: all
Depends: ${python:Depends}, ${misc:Depends}
Recommends: ${python:Recommends}
Suggests: ${python:Suggests}
Enhances: ${python:Enhances}
Breaks: ${python:Breaks}
Description: foo to rule them all
example package #2

View File

@ -0,0 +1,2 @@
The Debian packaging is © 2010, Piotr Ożarowski <piotr@debian.org> and
is licensed under the MIT License.

View File

@ -0,0 +1,4 @@
foo.py /usr/share/pyshared/
__init__.py /usr/share/pyshared/bar/
__init__.py /usr/share/pyshared/tests/
bar.py /usr/share/pyshared/bar/

12
tests/t202/debian/rules Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/make -f
%:
dh $@ --buildsystem=python_distutils
override_dh_pysupport:
override_dh_install:
dh_install
DH_VERBOSE=1 ../../dh_python2
clean:
dh_clean

View File

@ -0,0 +1 @@
3.0 (native)

1
tests/t202/foo.py Normal file
View File

@ -0,0 +1 @@
print("I'm foo")

0
tests/t202/setup.py Normal file
View File

28
tests/t203/Makefile Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/make -f
include ../common.mk
DPY=$(DEBPYTHON_DEFAULT)
ifeq ($(DPY),2.7)
TRIPLET=.$(DEB_HOST_MULTIARCH)
endif
check:
grep -q "pycompile -p python-foo:$(DEB_HOST_ARCH) /usr/lib/python-foo -V $(DPY)"\
debian/python-foo/DEBIAN/postinst
grep -q "pyclean -p python-foo:$(DEB_HOST_ARCH) /usr/lib/python-foo -V $(DPY)"\
debian/python-foo/DEBIAN/prerm
test -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/bar$(TRIPLET).so
test ! -f debian/python-foo/usr/share/pyshared/foo/bar.so
test ! -f debian/python-foo/usr/share/pyshared/foo/bar$(TRIPLET).so
test -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/spam$(TRIPLET).so
test ! -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/spam.so.0.1
test -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/baz$(TRIPLET).so
test ! -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/baz.so.0.1
test ! -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/baz.so.0.1.2
test -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/quux$(TRIPLET).so
test ! -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/quux.so.0
test ! -L debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/quux.so.0
test ! -f debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/quux.so.0.0.0
clean: clean-common
rm -rf lib/Foo.egg-info build

View File

@ -0,0 +1,5 @@
foo (0.1.1) unstable; urgency=low
* Initial release
-- Piotr Ożarowski <piotr@debian.org> Sun, 19 Dec 2010 19:40:33 +0100

1
tests/t203/debian/compat Normal file
View File

@ -0,0 +1 @@
7

19
tests/t203/debian/control Normal file
View File

@ -0,0 +1,19 @@
Source: foo
Section: python
Priority: optional
Maintainer: Piotr Ożarowski <piotr@debian.org>
Build-Depends: debhelper (>= 7.0.50~), python-all-dev
Standards-Version: 3.9.1
X-Python-Version: >= 2.6
Package: python-foo
Architecture: any
Depends: ${python:Depends}, ${shlibs:Depends}, ${misc:Depends}
Recommends: ${python:Recommends}
Suggests: ${python:Suggests}
Enhances: ${python:Enhances}
Breaks: ${python:Breaks}
Provides: ${python:Provides}
XB-Python-Version: ${python:Versions}
Description: foo to rule them all
example package #3 - Python extension

View File

@ -0,0 +1,2 @@
The Debian packaging is © 2010, Piotr Ożarowski <piotr@debian.org> and
is licensed under the MIT License.

View File

@ -0,0 +1,2 @@
# private module in architecture dependent dir
lib/foo.py /usr/lib/python-foo/

40
tests/t203/debian/rules Executable file
View File

@ -0,0 +1,40 @@
#!/usr/bin/make -f
DPY=$(DEBPYTHON_DEFAULT)
ifeq (,$(wildcard /usr/bin/python2.6))
# /usr/bin/python2.6 is not available, test 2.7 only
export DEBPYTHON_SUPPORTED=2.7
endif
%:
dh $@ --buildsystem=python_distutils
override_dh_pysupport:
override_dh_install:
dh_install
# install also as private extension
dh_install debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/bar.so \
/usr/lib/python-foo/
# ... and under versioned name with a symlink
cp debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/bar.so \
debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/spam.so.0
dh_link /usr/lib/python${DPY}/dist-packages/foo/spam.so.0 \
/usr/lib/python${DPY}/dist-packages/foo/spam.so
# ... and with multiple symlinks
cp debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/bar.so \
debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/baz.so.0.1.2
dh_link /usr/lib/python${DPY}/dist-packages/foo/baz.so.0.1.2 \
/usr/lib/python${DPY}/dist-packages/foo/baz.so.0.1
dh_link /usr/lib/python${DPY}/dist-packages/foo/baz.so.0.1 \
/usr/lib/python${DPY}/dist-packages/foo/baz.so
# ... second style of multiple symlinks
cp debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/bar.so \
debian/python-foo/usr/lib/python${DPY}/dist-packages/foo/quux.so.0.0.0
dh_link /usr/lib/python${DPY}/dist-packages/foo/quux.so.0.0.0 \
/usr/lib/python${DPY}/dist-packages/foo/quux.so.0
dh_link /usr/lib/python${DPY}/dist-packages/foo/quux.so.0.0.0 \
/usr/lib/python${DPY}/dist-packages/foo/quux.so
# ... and complex multiple symlinks
DH_VERBOSE=1 DEB_HOST_MULTIARCH=my_multiarch-triplet ../../dh_python2
clean:
dh_clean

View File

@ -0,0 +1 @@
3.0 (native)

View File

0
tests/t203/lib/bar.c Normal file
View File

5
tests/t203/lib/foo.py Normal file
View File

@ -0,0 +1,5 @@
import foo.bar
class Foo(object):
def __init__(self):
pass

13
tests/t203/setup.py Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python
from distutils.core import setup, Extension
setup(name="distutils-test",
version = "0.1",
author="jbailey",
author_email="jbailey@debian.org",
url="http://www.python.org/sigs/distutils-sig/",
ext_modules=[Extension('foo/bar', ['lib/bar.c'])],
#py_modules=['package'],
packages = ["foo"],
package_dir = {'foo': 'lib'}
)

13
tests/t204/Makefile Normal file
View File

@ -0,0 +1,13 @@
#!/usr/bin/make -f
include ../common.mk
clean: clean-common
check:
grep -q python2.6 debian/foo/usr/share/foo/foo.py
grep -q Depends:.*python debian/foo/DEBIAN/control
#grep -q python2.5 debian/foo/usr/share/bar/bar.py
#grep -q Depends:.*python2.5 debian/foo/DEBIAN/control
grep -q python2.4 debian/foo/usr/share/foo/baz.py
test ! -x debian/foo/usr/share/foo/baz.py
grep -q Depends:.*python2.4 debian/foo/DEBIAN/control && false || true

2
tests/t204/bar.py Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/python2.5
print("I'm bar")

2
tests/t204/baz.py Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/python2.4
print("I'm baz - not executable")

Some files were not shown because too many files have changed in this diff Show More