Import Upstream version 5.20221001
This commit is contained in:
commit
c05bf1b555
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
if command -v pycompile >/dev/null 2>&1; then
|
||||
pycompile -p #PACKAGE# #ARGS#
|
||||
fi
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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': ''
|
||||
}
|
|
@ -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]])))
|
|
@ -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)
|
|
@ -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
|
|
@ -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}')
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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'),
|
||||
)
|
|
@ -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")
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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'))
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
9
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and
|
||||
is licensed under the MIT License.
|
|
@ -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:
|
|
@ -0,0 +1 @@
|
|||
3.0 (native)
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
7
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
The Debian packaging is © 2010, Piotr Ożarowski <piotr@debian.org> and
|
||||
is licensed under the MIT License.
|
|
@ -0,0 +1,5 @@
|
|||
Mako python-mako (>= 0.2)
|
||||
SQLAlchemy python-sqlalchemy (>= 0.6)
|
||||
Foo python-foo; PEP386 s/^/2:/
|
||||
Bar python-bar
|
||||
Baz
|
|
@ -0,0 +1 @@
|
|||
debian/spam.py foo 2.5-
|
|
@ -0,0 +1,2 @@
|
|||
foo/spam.py 2.6
|
||||
foo/bar 2.7-
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
3.0 (native)
|
|
@ -0,0 +1 @@
|
|||
print 'SPAM'
|
|
@ -0,0 +1 @@
|
|||
print("you just imported foo from %s" % __file__)
|
|
@ -0,0 +1 @@
|
|||
print("you just imported foo.bar from %s" % __file__)
|
|
@ -0,0 +1 @@
|
|||
print("you just imported foo.baz from %s" % __file__)
|
|
@ -0,0 +1 @@
|
|||
/usr/share/javascript/jquery/jquery.js
|
|
@ -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,
|
||||
)
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
print("I'm __init__.py")
|
|
@ -0,0 +1 @@
|
|||
print("I'm bar")
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
7
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
The Debian packaging is © 2010, Piotr Ożarowski <piotr@debian.org> and
|
||||
is licensed under the MIT License.
|
|
@ -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/
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
3.0 (native)
|
|
@ -0,0 +1 @@
|
|||
print("I'm foo")
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
7
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
The Debian packaging is © 2010, Piotr Ożarowski <piotr@debian.org> and
|
||||
is licensed under the MIT License.
|
|
@ -0,0 +1,2 @@
|
|||
# private module in architecture dependent dir
|
||||
lib/foo.py /usr/lib/python-foo/
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
3.0 (native)
|
|
@ -0,0 +1,5 @@
|
|||
import foo.bar
|
||||
|
||||
class Foo(object):
|
||||
def __init__(self):
|
||||
pass
|
|
@ -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'}
|
||||
)
|
||||
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/python2.5
|
||||
print("I'm bar")
|
|
@ -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
Loading…
Reference in New Issue