├── .gitignore ├── INSTALL.rst ├── LICENSE ├── README.rst ├── TODO.rst ├── debian ├── changelog ├── control ├── copyright ├── install ├── links ├── rules └── source │ └── format ├── overrides └── mako │ └── ctx.json ├── profiles ├── dpt-dep14 │ ├── ctx.json │ ├── debian │ │ ├── gbp.conf │ │ └── tests │ │ │ ├── control │ │ │ └── unittests │ └── hooks │ │ └── post ├── dpt-maint │ ├── ctx.json │ ├── gbp.conf │ ├── post │ └── tests │ │ ├── control │ │ └── unittests ├── dpt │ ├── ctx.json │ ├── debian │ │ ├── gbp.conf │ │ └── tests │ │ │ ├── control │ │ │ └── unittests │ └── hooks │ │ └── post └── openstack │ ├── ctx.json │ └── debian │ └── gbp.conf ├── py2dsp ├── pypi2deb ├── __init__.py ├── cache.py ├── debianize.py ├── decorators.py ├── github.py ├── pypi.py └── tools.py ├── pypi2debian └── templates ├── debian ├── control.tpl ├── copyright.tpl ├── rules.tpl ├── source │ ├── format │ └── options ├── upstream │ └── metadata.tpl └── watch.tpl └── itp.mail /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | __pycache__ 4 | result/ 5 | -------------------------------------------------------------------------------- /INSTALL.rst: -------------------------------------------------------------------------------- 1 | dependencies 2 | ~~~~~~~~~~~~ 3 | * python3 >= 3.4.2 4 | * dh-python 5 | * devscripts (/usr/bin/mk-origtargz) 6 | * python3-aiohttp 7 | * python3-jinja2 8 | * python3-debian 9 | 10 | optional dependencies 11 | ~~~~~~~~~~~~~~~~~~~~~~ 12 | * python3-aioredis or python3-redis (in memory cache used if missing) 13 | * python3-msgpack or python3-simplejson (stdlib's json used if missing) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2015-2018 Piotr Ożarowski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyPI to Debian 2 | ============== 3 | 4 | 5 | * ``py2dsp`` - converts PyPI package to Debian source package 6 | * ``pypi2debian`` - converts PyPI repository to Debian repository 7 | 8 | 9 | features 10 | ~~~~~~~~ 11 | 12 | * uses PyPI metadata 13 | * supports Python 3.x 14 | * guesses build dependencies 15 | * reuses existing Debian package names if already packaged in Debian 16 | * creates -doc package with Sphinx regenerated documentation 17 | * generates ITP email template 18 | * easy to customise (profiles, overrides, templates) 19 | * uses Debian tools to generate files if possible 20 | * integrates with dh-python's tools 21 | * asynchronous 22 | 23 | 24 | examples 25 | ~~~~~~~~ 26 | 27 | * ``py2dsp jinja2`` will download, unpack and debianize jinja2 in ./result/jinja2-2.8/ 28 | * ``py2dsp jinja2-2.8.tar.gz`` will unpack and debianize jinja2 in ./result/jinja2-2.8/ 29 | * ``py2dsp ./result/jinja2-2.8/`` will debianize Jinja2 in provided directory 30 | 31 | if debian/changelog didn't exist, above commands will also create 32 | jinja2_2.8-0~py2deb.mail file with ITP (intend to package) template 33 | 34 | * ``pypi2debian`` will convert PyPI packages to Debian source packages in ./result directory 35 | * ``pypi2debian --build-cmd "sbuild -c unstable"`` as above, but will also try 36 | to build these packages using sbuild (if "unstable" schroot is already set up) 37 | * ``pypi2debian --python3 --classifiers 'Operating System :: POSIX :: Linux'`` 38 | will create only python3-foo packages for all Linux compatible packages. 39 | See `classifiers page`_ for possible ``--classifiers`` values 40 | 41 | .. _classifiers page: https://pypi.python.org/pypi?%3Aaction=list_classifiers 42 | 43 | 44 | overrides 45 | ~~~~~~~~~ 46 | 47 | To override pypi2deb's auto detected values and auto generated files use 48 | overrides mechanism. 49 | 50 | Each package has its own subdirectory in ./overrides dir (can be changed via 51 | ``PYPI2DEB_OVERRIDES_PATH`` env. variable) - complete files, modified templates 52 | (``.tpl`` files) or ``ctx.json`` file can be stored there. 53 | Each file that doesn't have ``.tpl`` extension will be copied to the result 54 | directory, including f.e. debian/patches dir. 55 | 56 | ``ctx.json`` file in overrides/foo/ directory will overwrite auto detected 57 | values (see list below). For the same purpose `py2dsp` section in setup.cfg 58 | file can be used. 59 | 60 | To provide different templates for all packages, point pypi2deb to them via 61 | ``PYPI2DEB_TEMPLATES_PATH`` env. variable. 62 | 63 | To use different PyPI server - set ``PYPI_JSON_URL`` and ``PYPI_XMLRPC_URL`` 64 | env. variables. 65 | 66 | ctx values 67 | ---------- 68 | * `author` - upstream author's name and email 69 | * `binary_arch` - binary package's architecture 70 | * `build_depends` - build dependencies 71 | * `clean_files` - files to remove in clean step 72 | * `copyright` - upstream copyrights (one line) 73 | * `creator` - name and email used in changelog and copyright files 74 | * `deb_copyright` - Debian packaging copyrights 75 | * `deb_license_name` - Debian packaging license name 76 | * `debian_revision` - 0~pypi2deb by default 77 | * `description` - project's raw description 78 | * `distribution` - Debian's suite 79 | * `exports` - variables exported in ``debian/rules`` 80 | * `homepage` - project's homepage 81 | * `interpreters` - supported Python interpreters (python3) 82 | * `license` - license's full text 83 | * `license_name` - license name 84 | * `maintainer` - maintainer's full name and email 85 | * `name` - upstream project name 86 | * `src_name` - debianized upstream project name 87 | * `summary` - package's short description 88 | * `uploaders` - co-maintainers of the package 89 | * `vcs_browser` - URL to web-browsable copy of the Version Control System repository 90 | * `vcs_name` - VCS name, f.e. Git, Svn, etc. 91 | * `vcs_src` - location of the Version Control System 92 | * `version` - upstream version 93 | * `with` - list of dh's ``--with`` extensions 94 | * `docs` 95 | 96 | * `sphinx_dir` - directory with documentation (Sphinx) 97 | * `files` - list of files / dirs to be installed as documentation 98 | * `examples` - list of example files / dirs 99 | * `examples_dir` - relative path to directory with examples 100 | 101 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | * documentation 2 | * tests 3 | * support settings from stdeb.cfg configuration file 4 | * --upload-cmd 5 | * generate result/build.sh if --build-cmd not set 6 | * generate debdiff if .dsc or .changes files are available 7 | * convert upstream version (alpha → ~alpha, etc.) 8 | * generate autopkgtest (DEP-8) 9 | * use aioredis to cache coroutines 10 | * --application (install to private dir, do not prefix binary package with interpreter name, etc.) 11 | * 'Environment :: X11 Applications' or 'Intended Audience :: End Users/Desktop' → private module 12 | * point mk-origtargz to --copyright-file if available in overrides 13 | 14 | debian/copyright 15 | ---------------- 16 | integrate below tools if they're useful 17 | 18 | * licencecheck 19 | * license-reconcile 20 | * /usr/lib/cdbs/licensecheck2dep5 21 | 22 | debian/control 23 | -------------- 24 | * use XB-Python-Egg-Name while generating dependencies 25 | (also in dh_python{2,3}?) 26 | * parse setup.cfg to get build dependencies from 27 | ``[bdist_deb] → build-requires`` or even ``[bdist_rpm]`` 28 | 29 | sphinx docs 30 | ----------- 31 | * use setup.cfg's settings, example: 32 | 33 | | [build_sphinx] 34 | | source-dir = doc/source 35 | | build-dir = doc/build 36 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | pypi2deb (4.20240727) unstable; urgency=medium 2 | 3 | [ Agathe Porte ] 4 | * templates: introduce upstream/metadata (DEP-12) 5 | * introduce profile dpt-dep14 6 | 7 | [ Ananthu C V ] 8 | * fix: no package summary in pypi failing py2dsp 9 | 10 | [ Paul Wise ] 11 | * Set the default build-backend to setuptools.build_meta:__legacy__ 12 | 13 | [ Piotr Ożarowski ] 14 | * DPT profile: set DPT as Maintainer and the ID of the person calling py2dsp 15 | as uploader (closes: #1064952) 16 | * Allow HTTP_PROXY and other environment variables (closes: #1016023) 17 | * Drop nose from suggested packages (closes: #1071981) 18 | 19 | -- Piotr Ożarowski Sat, 27 Jul 2024 15:59:51 +0200 20 | 21 | pypi2deb (3.20240228) unstable; urgency=medium 22 | 23 | [ Piotr Ożarowski ] 24 | * dpt: ignore egg-info directories while importing files into git repository 25 | 26 | [ Sandro Tosi ] 27 | * remove myself 28 | 29 | -- Sandro Tosi Wed, 28 Feb 2024 01:29:33 -0500 30 | 31 | pypi2deb (3.20230219) unstable; urgency=medium 32 | 33 | [ Sandro Tosi ] 34 | * debian/rules 35 | - use the same reportbug trick to make sure the internal version is in sync 36 | with the package version 37 | * debian/watch 38 | - point Homepage to the salsa repo 39 | * debian/control 40 | - depends on python3-debian >= 0.1.45, fixes a bug that creates an extra 41 | empty line at the end of d/changelog, upsetting lintian; Closes: #1015238 42 | * feat(control.tpl): run autopkgtest via autopkgtest-pkg-pybuild; 43 | Closes: #1015927 44 | * feat: add support for pyproject.toml; Closes: #1015237 45 | * feat(hooks/post): reuse git/gbp functionalities when building the intial 46 | git repo, instead of manual commands 47 | 48 | [ Nicolas Dandrimont ] 49 | * pypi: use the urls key to find download URLs; Closes: #1015888 50 | 51 | [ Agathe Porte ] 52 | * github: remove trailing `/` in repo url 53 | * github: add some logs while performing operations 54 | * control.tpl: remove python3 Recommends/Suggests 55 | * github: fix wrong tag download URL 56 | * dpt: unittests: use py3versions -s (instead of -r) 57 | * profiles: introduce dpt-maint, where DPT is set to Maintainer 58 | * use async/await keywords instead of yield from; Closes: #1029749 59 | 60 | -- Sandro Tosi Sun, 19 Feb 2023 15:28:32 -0500 61 | 62 | pypi2deb (3.20220721) unstable; urgency=medium 63 | 64 | [ Agathe Porte ] 65 | * github: use last tag if no release found 66 | 67 | [ Sandro Tosi ] 68 | * template: bump Standards-Version to 4.6.1.0 69 | 70 | -- Sandro Tosi Thu, 21 Jul 2022 14:10:30 -0400 71 | 72 | pypi2deb (3.20220707) unstable; urgency=medium 73 | 74 | [ Piotr Ożarowski ] 75 | * Add Sandro Tosi to co-maintainers 76 | * Update VCS-* fields to point to Salsa 77 | 78 | [ Sandro Tosi ] 79 | * template: set Standards-Version to 4.5.0 80 | * template: set dh compat to 13, via debhelper-compat b-d; Closes: #966104 81 | * profiles: PAPT and DPMT converged into DPT, so update the profiles 82 | accordingly 83 | * Remove support for python2; Closes: #966103 84 | * When using a local tarball, copy it in the destination directory 85 | * template: set Standards-Version to 4.5.1 86 | * template: bump d/watch version to 4 87 | * recognize both 'Apache 2' and 'Apache 2.0' 88 | * hook: tag upstream version after import, as done by gbp import-orig 89 | * py2dsp: remove spurious debian/files, left behind dpkg-buildpackage due to 90 | #845436 91 | * py2dsp: When using a local tarball, gather name and version from it 92 | * template: Copy d-devel and d-python when submitting a new ITP 93 | * template: Use a single X-Debbugs-Cc field, with 2 addresses 94 | * unpack: remove the upstream debian/ directory, if present 95 | * Fetch upstream tarball from GitHub 96 | * Add rudimentary support for autopkgtests 97 | * Create repo on salsa if SALSA_TOKEN is setup in ~/.devscripts 98 | * template: bump Standards-Version to 4.6.0.1 99 | * Advertise py2dsp as the tool that created the debianization (in commit msg 100 | and d/changelog) 101 | * Create the salsa repo enabling the tagpending webhook too 102 | * build doc using execute_after 103 | * set github in context even if we dont pass a file on the command-line 104 | * Push the initial packaging to salsa (if the repo is created) and track 105 | upstream branches 106 | * debian/control: 107 | - run wrap-and-sort -at 108 | - bump Standards-Version to 4.6.1.0 (no changes needed) 109 | * debian/watch 110 | - drop d/watch, not needed for a native package 111 | 112 | [ Agathe Porte ] 113 | * debian: update debhelper from v9 to v13 114 | * control.tpl: add Rules-Requires-Root: no 115 | * debianize.py: remove copyright symbol 116 | 117 | -- Sandro Tosi Thu, 07 Jul 2022 22:53:34 -0400 118 | 119 | pypi2deb (2.20180804) unstable; urgency=medium 120 | 121 | [ Oliver Okrongli ] 122 | * py2dsp: Support downloading specific version (e.g. 'pytest/3.1.1') 123 | * Fix set-typed values in ctx override files 124 | 125 | [ Alexandros Afentoulis ] 126 | * Use new Python Package Index URL: https://pypi.org 127 | 128 | [ Piotr Ożarowski ] 129 | * Update DPMT/PAPT profiles to reflect move to salsa / gbp 130 | 131 | -- Piotr Ożarowski Sat, 04 Aug 2018 14:56:57 +0800 132 | 133 | pypi2deb (2.20180318) unstable; urgency=medium 134 | 135 | * Fix Python 3.6 / latest aiohttp compatibility 136 | * Do not try to clean/check build dependencies while building source package 137 | * Standards-Version bumped to 4.1.3 (no changes needed) 138 | 139 | -- Piotr Ożarowski Sun, 18 Mar 2018 21:49:31 +0100 140 | 141 | pypi2deb (1.20170623) unstable; urgency=medium 142 | 143 | * New release: 144 | - add PYPI2DEB_SPHINX_DIRS 145 | - aiohttp 2.X compatible 146 | - minor fixes 147 | * Standards-Version bumped to 4.0.0 (no changes needed) 148 | 149 | -- Piotr Ożarowski Fri, 23 Jun 2017 19:29:46 +0200 150 | 151 | pypi2deb (1.20160809) unstable; urgency=medium 152 | 153 | * New release: 154 | - add support for pre and post hooks (scripts that are invoked before and 155 | after creating debian directory) 156 | - add DPMT post hook that initializes git(-dpm) repository 157 | - read overrides also from ./overrides directory (if present) 158 | - add --build option to py2dsp (to build also binary package, not just 159 | source one) 160 | - some smaller improvements 161 | 162 | -- Piotr Ożarowski Tue, 09 Aug 2016 21:00:43 +0200 163 | 164 | pypi2deb (1.20151008) unstable; urgency=medium 165 | 166 | * Initial release (closes: #801334) 167 | 168 | -- Piotr Ożarowski Thu, 08 Oct 2015 20:39:55 +0200 169 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: pypi2deb 2 | Section: python 3 | Priority: optional 4 | Maintainer: Piotr Ożarowski 5 | Build-Depends: debhelper-compat (= 13), 6 | dh-python, 7 | python3 (>= 3.4.2), 8 | Standards-Version: 4.6.1.0 9 | Homepage: https://salsa.debian.org/python-team/tools/pypi2deb 10 | Vcs-Git: https://salsa.debian.org/python-team/tools/pypi2deb.git 11 | Vcs-Browser: https://salsa.debian.org/python-team/tools/pypi2deb 12 | 13 | Package: pypi2deb 14 | Architecture: all 15 | Depends: build-essential, 16 | devscripts, 17 | dh-python, 18 | python3-aiohttp (>= 0.17), 19 | python3-debian (>= 0.1.45), 20 | python3-github, 21 | python3-jinja2, 22 | python3-redis, 23 | ${misc:Depends}, 24 | ${python3:Depends}, 25 | Recommends: python3-msgpack, 26 | Suggests: cython, 27 | cython3, 28 | python-all-dev, 29 | python-pytest, 30 | python-setuptools, 31 | python3-all-dev, 32 | python3-pytest, 33 | python3-setuptools, 34 | python3-sphinx, 35 | Description: PyPI to Debian converter 36 | This package provides these tools: 37 | * py2dsp - converts PyPI/GitHub package to Debian source package 38 | * pypi2debian - converts PyPI repository to Debian repository 39 | . 40 | Features: 41 | * uses PyPI metadata 42 | * supports Python 3.x 43 | * guesses build dependencies 44 | * reuses existing Debian package names if already packaged in Debian 45 | * creates -doc package with Sphinx regenerated documentation 46 | * generates ITP email template 47 | * easy to customise (profiles, overrides, templates) 48 | * uses Debian tools to generate files if possible 49 | * integrates with dh-python's tools 50 | * asynchronous 51 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: pypi2deb 3 | Upstream-Contact: Piotr Ożarowski 4 | Source: https://github.com/p1otr/pypi2deb 5 | 6 | Files: * 7 | Copyright: 2015-2018 Piotr Ożarowski 8 | License: Expat 9 | 10 | License: Expat 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | . 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | . 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | overrides /usr/share/pypi2deb/ 2 | profiles /usr/share/pypi2deb/ 3 | pypi2deb /usr/share/pypi2deb/ 4 | templates /usr/share/pypi2deb/ 5 | py2dsp /usr/share/pypi2deb/ 6 | pypi2debian /usr/share/pypi2deb/ 7 | -------------------------------------------------------------------------------- /debian/links: -------------------------------------------------------------------------------- 1 | /usr/share/pypi2deb/py2dsp /usr/bin/py2dsp 2 | /usr/share/pypi2deb/pypi2debian /usr/bin/pypi2debian 3 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #! /usr/bin/make -f 2 | 3 | include /usr/share/dpkg/pkg-info.mk 4 | 5 | VERSION := $(shell python3 -c "import pypi2deb; print(pypi2deb.VERSION)") 6 | 7 | %: 8 | dh $@ --with python3 9 | 10 | override_dh_auto_clean: 11 | # Test if versions are synchronized (only if releasing); this will bomb if not synced 12 | if [ "$(DEB_DISTRIBUTION)" != "UNRELEASED" -a "$(VERSION)" != "$(DEB_VERSION)" ] ; \ 13 | then \ 14 | echo 'Please update VERSION variable in pypi2deb/__init__.py'; exit 1 ; \ 15 | fi 16 | 17 | dh_auto_clean 18 | find . -type d -name __pycache__ -print0 | xargs --null --no-run-if-empty rm -rf 19 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /overrides/mako/ctx.json: -------------------------------------------------------------------------------- 1 | {"interpreters": ["python3", "pypy"]} 2 | -------------------------------------------------------------------------------- /profiles/dpt-dep14/ctx.json: -------------------------------------------------------------------------------- 1 | ../dpt/ctx.json -------------------------------------------------------------------------------- /profiles/dpt-dep14/debian/gbp.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | pristine-tar = True 3 | debian-branch = debian/latest 4 | upstream-branch = upstream/latest -------------------------------------------------------------------------------- /profiles/dpt-dep14/debian/tests/control: -------------------------------------------------------------------------------- 1 | ../../../dpt/debian/tests/control -------------------------------------------------------------------------------- /profiles/dpt-dep14/debian/tests/unittests: -------------------------------------------------------------------------------- 1 | ../../../dpt/debian/tests/unittests -------------------------------------------------------------------------------- /profiles/dpt-dep14/hooks/post: -------------------------------------------------------------------------------- 1 | ../../dpt/hooks/post -------------------------------------------------------------------------------- /profiles/dpt-maint/ctx.json: -------------------------------------------------------------------------------- 1 | {"debian_revision": "1", 2 | "distribution": "unstable", 3 | "message": "Initial release; Closes: #NNNNNN", 4 | "maintainer": "Debian Python Team ", 5 | "uploaders": "{creator}", 6 | "vcs_browser": "https://salsa.debian.org/python-team/packages/{src_name}", 7 | "vcs_name": "Git", 8 | "vcs_src": "https://salsa.debian.org/python-team/packages/{src_name}.git"} 9 | -------------------------------------------------------------------------------- /profiles/dpt-maint/gbp.conf: -------------------------------------------------------------------------------- 1 | ../dpt/debian/gbp.conf -------------------------------------------------------------------------------- /profiles/dpt-maint/post: -------------------------------------------------------------------------------- 1 | ../dpt/hooks/post -------------------------------------------------------------------------------- /profiles/dpt-maint/tests/control: -------------------------------------------------------------------------------- 1 | ../../dpt/debian/tests/control -------------------------------------------------------------------------------- /profiles/dpt-maint/tests/unittests: -------------------------------------------------------------------------------- 1 | ../../dpt/debian/tests/unittests -------------------------------------------------------------------------------- /profiles/dpt/ctx.json: -------------------------------------------------------------------------------- 1 | {"debian_revision": "1", 2 | "distribution": "unstable", 3 | "maintainer": "Debian Python Team ", 4 | "uploaders": "{creator}", 5 | "vcs_browser": "https://salsa.debian.org/python-team/packages/{src_name}", 6 | "vcs_name": "Git", 7 | "vcs_src": "https://salsa.debian.org/python-team/packages/{src_name}.git"} 8 | -------------------------------------------------------------------------------- /profiles/dpt/debian/gbp.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | pristine-tar = True 3 | debian-branch = debian/master 4 | filter = [ '*.egg-info' ] 5 | -------------------------------------------------------------------------------- /profiles/dpt/debian/tests/control: -------------------------------------------------------------------------------- 1 | # use this document to learn how to write autopkgtests 2 | # https://salsa.debian.org/ci-team/autopkgtest/-/blob/master/doc/README.package-tests.rst 3 | 4 | ## Enable this test if the package ship a module and it has tests available 5 | # Tests: unittests 6 | # Depends: @, 7 | # @builddeps@, 8 | # Restrictions: allow-stderr 9 | 10 | ## Enable this test if the package ships a command-line tool 11 | # Test-Command: COMMAND-LINE-TOOL --help 12 | # Depends: @ 13 | -------------------------------------------------------------------------------- /profiles/dpt/debian/tests/unittests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -efu 3 | 4 | echo "ERROR: Adapt this autopkgtests script to the package and remove this line" ; exit 1 5 | 6 | pys="$(py3versions -s 2> /dev/null)" 7 | 8 | cp -a tests "$AUTOPKGTEST_TMP" 9 | cd "$AUTOPKGTEST_TMP" 10 | 11 | for py in $pys; do 12 | echo "=== $py ===" 13 | $py -m pytest tests 2>&1 14 | done 15 | -------------------------------------------------------------------------------- /profiles/dpt/hooks/post: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | NAME=$1 6 | VERSION=$2 7 | REVISION=$3 8 | PY2DST_VERSION=$4 9 | TARBALL=`find .. -maxdepth 1 -type f -name "${NAME}_${VERSION}.orig.tar.*" -printf %f -quit` 10 | 11 | # do nothing if .git directory exists 12 | test \! -d .git || exit 0 13 | # make sure needed tools are installed 14 | test -x "/usr/bin/pristine-tar" || (echo "E: please install pristine-tar package"; exit 1) 15 | test -x "/usr/bin/gbp" || (echo "E: please install git-buildpackage package"; exit 1) 16 | 17 | git init --initial-branch=debian/master 18 | git remote add origin ssh://git@salsa.debian.org/python-team/packages/${NAME}.git 19 | gbp import-orig "../$TARBALL" --pristine-tar --upstream-version=${VERSION} 20 | 21 | git add debian/* 22 | git commit -a -m "add initial Debian packaging; autogenerated by py2dsp/${PY2DST_VERSION}" 23 | 24 | if grep -q SALSA_TOKEN $HOME/.devscripts 25 | then 26 | salsa --group python-team/packages --kgb --irc-channel=debian-python-changes --tagpending create_repo ${NAME} 27 | echo 'I: repository created on Salsa.debian.org' 28 | git push --all --set-upstream 29 | git push --tags 30 | echo 'I: pushed to salsa the current package and tags' 31 | else 32 | echo 'I: to create remote repository go to https://salsa.debian.org/python-team/packages and click on "New project" link' 33 | echo ' (https://salsa.debian.org/projects/new?namespace_id=9360)' 34 | fi 35 | 36 | echo "I: to push (after reviewing all files) changes to DPT's repo:" 37 | echo " git push --set-upstream origin --all" 38 | -------------------------------------------------------------------------------- /profiles/openstack/ctx.json: -------------------------------------------------------------------------------- 1 | {"debian_revision": "1", 2 | "distribution": "unstable", 3 | "message": "Initial release (closes: #NNNNNN)", 4 | "maintainer": "PKG OpenStack ", 5 | "uploaders": "{creator}", 6 | "vcs_browser": "http://anonscm.debian.org/gitweb/?p=openstack/{src_name}.git", 7 | "vcs_name": "Git", 8 | "vcs_src": "git://anonscm.debian.org/openstack/{src_name}.git"} 9 | -------------------------------------------------------------------------------- /profiles/openstack/debian/gbp.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | upstream-branch = master 3 | debian-branch = debian/unstable 4 | upstream-tag = %(version)s 5 | compression = xz 6 | 7 | [git-buildpackage] 8 | export-dir = ../build-area/ 9 | 10 | -------------------------------------------------------------------------------- /py2dsp: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | # vim: et ts=4 sw=4 3 | # Copyright © 2015-2018 Piotr Ożarowski 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import logging 24 | import argparse 25 | import asyncio 26 | import sys 27 | import shutil 28 | from os import environ, getcwd, makedirs, unlink 29 | from os.path import abspath, exists, isdir, join 30 | from shutil import rmtree 31 | from pypi2deb import VERSION 32 | from pypi2deb.debianize import debianize 33 | from pypi2deb.github import github_download 34 | from pypi2deb.pypi import get_pypi_info, parse_pypi_info, download 35 | from pypi2deb.tools import execute, unpack, parse_filename, pkg_name 36 | 37 | logging.basicConfig(format='%(levelname).1s: py2dsp ' 38 | '%(module)s:%(lineno)d: %(message)s') 39 | log = logging.getLogger('py2dsp') 40 | DESCRIPTION = 'Python source package to Debian source package converter' 41 | 42 | 43 | async def main(args): 44 | log.debug('args: %s', args) 45 | if not exists(args.root): 46 | makedirs(args.root) 47 | if exists(args.name): # file or dir 48 | fpath = abspath(args.name) 49 | fname = fpath.rstrip('/').rsplit('/', 1)[-1] 50 | parsed = parse_filename(fname) 51 | version = parsed.get('version') 52 | name = parsed.get('name') or args.name 53 | ctx = await get_pypi_info(args.pypi_search if args.pypi_search else name) 54 | ctx = parse_pypi_info(ctx) 55 | ctx['name'], ctx['version'] = name, version 56 | if args.github: 57 | ctx['github'] = args.github 58 | shutil.copy(fpath, args.root) 59 | else: # download from PyPI 60 | parsed = parse_filename(args.name) 61 | requested_version = parsed.get('version') 62 | name = parsed.get('name') or args.name 63 | ctx = await get_pypi_info(args.pypi_search if args.pypi_search else name, requested_version) 64 | ctx = parse_pypi_info(ctx) 65 | if not ctx: 66 | log.error('invalid name: %s', args.name) 67 | exit(1) 68 | #name = ctx['name'] 69 | version = ctx['version'] 70 | if args.github: 71 | # Use the version parsed from the PyPI API response to download from GitHub 72 | ctx['github'] = args.github 73 | log.debug(f"Calling github_download with {name}, {args.github}, {version}, {args.root}") 74 | fname = await github_download(name, args.github, version=version, destdir=args.root) 75 | else: 76 | # Use the requested version to get a richer response from the PyPI API if no version was requested 77 | fname = await download(name, version=requested_version, destdir=args.root) 78 | fpath = join(args.root, fname) 79 | 80 | ctx['root'] = args.root 81 | src_name = ctx['src_name'] = pkg_name(name) 82 | ctx['distribution'] = args.distribution 83 | ctx['debian_revision'] = args.revision 84 | if isdir(fpath): 85 | dpath = fpath 86 | else: 87 | dirname = '{}-{}'.format(src_name, version) 88 | log.debug(f"Unpacking {fpath}") 89 | dpath = unpack(fpath, args.root, dirname) 90 | 91 | await debianize(dpath, ctx, args.profile) 92 | await execute(['dpkg-buildpackage', '-S', '-us', '-uc', '-nc', '-d', 93 | '-I.git', '-i.git'], dpath) 94 | # workaround for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=845436 95 | unlink(join(dpath, 'debian/files')) 96 | if args.build: 97 | await execute(['dpkg-buildpackage', '-b'], dpath) 98 | 99 | if args.clean: 100 | rmtree(dpath) 101 | 102 | 103 | if __name__ == '__main__': 104 | usage = '%(prog)s NAME [OPTIONS]' 105 | parser = argparse.ArgumentParser(usage=usage, 106 | description=DESCRIPTION) 107 | parser.add_argument('-v', '--verbose', action='store_true', 108 | default=environ.get('PY2DSP_VERBOSE') == '1', 109 | help='turn verbose mode on') 110 | parser.add_argument('-q', '--quiet', action='store_true', 111 | default=environ.get('PY2DSP_QUIET') == '1', 112 | help='be quiet') 113 | parser.add_argument('--version', action='version', 114 | version='%(prog)s {}'.format(VERSION)) 115 | 116 | parser.add_argument('--root', action='store', metavar='DIR', 117 | default=environ.get('DESTDIR', 118 | join(getcwd(), 'result')), 119 | help='destination directory [default: ./result]') 120 | parser.add_argument('--clean', action='store_true', 121 | default=environ.get('PY2DSP_CLEAN', '0') == '1', 122 | help='remove name-version directory after creating source package') 123 | parser.add_argument('--build', action='store_true', 124 | default=environ.get('PY2DSP_BUILD', '0') == '1', 125 | help='build binary package') 126 | parser.add_argument('--application', action='store_true', 127 | default=environ.get('PY2DSP_APPLICATION', '0') == '1', 128 | help='this is an application rather than module') 129 | parser.add_argument('--profile', action='store', 130 | help='load default values from profile.json file (if available)') 131 | parser.add_argument('--github', '--gh', default=None, 132 | help='fetch the package from GitHub instead of PyPI') 133 | parser.add_argument('--pypi-search', default=None, 134 | help='specify the PyPI search term instead of the source package name') 135 | 136 | changelog = parser.add_argument_group('changelog', 'debian/changelog specific settings') 137 | changelog.add_argument('--distribution', action='store', 138 | default=environ.get('PY2DSP_DISTRIBUTION', 'UNRELEASED'), 139 | help='targetted Debian suite') 140 | changelog.add_argument('--revision', action='store', 141 | default=environ.get('PY2DSP_REVISION', '0~py2deb'), 142 | help='Debian changelog revision') 143 | changelog.add_argument('-m', '--message', action='store', 144 | default=environ.get('PY2DSP_MESSAGE', 'converte0~py2deb'), 145 | help='Debian changelog message') 146 | 147 | parser.add_argument('name', default=None, nargs='?', 148 | help='Python source name or tarball') 149 | 150 | args = parser.parse_args() 151 | if not args.name and args.github: 152 | args.name = args.github.split('/')[-1] 153 | 154 | if args.verbose: 155 | logging.getLogger('pypi2deb').setLevel(logging.DEBUG) 156 | log.setLevel(logging.DEBUG) 157 | elif args.quiet: 158 | logging.getLogger('pypi2deb').setLevel(logging.ERROR) 159 | log.setLevel(logging.ERROR) 160 | else: 161 | logging.getLogger('pypi2deb').setLevel(logging.INFO) 162 | log.setLevel(logging.INFO) 163 | log.debug('version: {}'.format(VERSION)) 164 | log.debug(sys.argv) 165 | 166 | if args.profile in ('dpmt', 'papt'): 167 | logging.warning("'dpmt' and 'papt' profiles have been replaced by the 'dpt' profile") 168 | args.profile = 'dpt' 169 | 170 | try: 171 | asyncio.run(main(args)) 172 | except Exception as e: 173 | log.error(e, exc_info=args.verbose) 174 | exit(2) 175 | -------------------------------------------------------------------------------- /pypi2deb/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2015-2018 Piotr Ożarowski 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import sys 22 | from os import environ 23 | from os.path import join, dirname, abspath 24 | 25 | TEMPLATES_PATH = environ.get('PYPI2DEB_TEMPLATES_PATH', 26 | abspath(join(dirname(__file__), '..', 'templates'))) 27 | OVERRIDES_PATH = environ.get('PYPI2DEB_OVERRIDES_PATH', 28 | abspath(join(dirname(__file__), '..', 'overrides'))) 29 | PROFILES_PATH = environ.get('PYPI2DEB_PROFILES_PATH', 30 | abspath(join(dirname(__file__), '..', 'profiles'))) 31 | VERSION = '4.20240727' 32 | # Add path to dh-python's private library 33 | # (yeah, it's not stable enough to make it public one, fortunatly 34 | # author of pypi2deb and dh-python know each other ;) 35 | sys.path.append('/usr/share/dh-python/') 36 | -------------------------------------------------------------------------------- /pypi2deb/cache.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2015-2018 Piotr Ożarowski 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import logging 22 | 23 | __all__ = ['load', 'dump'] 24 | 25 | try: 26 | import msgpack as _serializer 27 | NAMESPACE = 'P2D:' 28 | except ImportError: 29 | NAMESPACE = 'P2D-j:' 30 | try: 31 | import simplejson as _serializer 32 | except ImportError: 33 | import json as _serializer 34 | try: 35 | import redis 36 | except ImportError: 37 | conn = None 38 | else: 39 | conn = redis.Redis() 40 | try: 41 | conn.ping() 42 | except redis.ConnectionError: 43 | conn = None 44 | if not conn: 45 | 46 | class _FallbackCache(dict): 47 | def setex(self, key, data, ttl=None): 48 | self[key] = data 49 | 50 | def get(self, key, default=None): 51 | return super(_FallbackCache, self).get(key, default) 52 | 53 | conn = _FallbackCache() 54 | 55 | 56 | log = logging.getLogger('pypi2deb') 57 | 58 | 59 | def load(key, default=None): 60 | result = conn.get(NAMESPACE + key) 61 | if result is None: 62 | return default 63 | try: 64 | return _serializer.loads(result, encoding='utf-8') 65 | except Exception as err: 66 | exc_info = log.level <= logging.DEBUG 67 | log.warn('cannot load cache (%s): %s', key, err, exc_info=exc_info) 68 | return default 69 | 70 | 71 | def dump(key, data, ttl=3600): 72 | exc_info = log.level <= logging.DEBUG 73 | try: 74 | data = _serializer.dumps(data) 75 | except Exception as err: 76 | log.warn('cannot serialize cache (%s): %s', key, err, exc_info=exc_info) 77 | return 78 | try: 79 | conn.setex(NAMESPACE + key, data, ttl) 80 | except Exception as err: 81 | log.warn('cannot dump cache (%s): %s', key, err, exc_info=exc_info) 82 | -------------------------------------------------------------------------------- /pypi2deb/debianize.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2015-2018 Piotr Ożarowski 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import logging 22 | import pathlib 23 | import tomllib 24 | 25 | from configparser import ConfigParser 26 | from datetime import datetime 27 | from os import access, chmod, environ, listdir, makedirs, walk, X_OK 28 | from os.path import abspath, exists, isdir, join, dirname 29 | from shutil import copy 30 | 31 | from pypi2deb import VERSION, OVERRIDES_PATH, PROFILES_PATH, TEMPLATES_PATH 32 | from pypi2deb.tools import execute 33 | 34 | from jinja2 import Environment, FileSystemLoader 35 | from debian.changelog import Changelog, Version, get_maintainer 36 | try: 37 | from simplejson import load, dump 38 | except ImportError: 39 | from json import load, dump 40 | 41 | from dhpython import PKG_PREFIX_MAP 42 | from dhpython.pydist import guess_dependency, parse_pydep 43 | 44 | 45 | log = logging.getLogger('pypi2deb') 46 | # The location of sphinx-based documentation can be configured via the 47 | # environment variable PYPI2DEB_SPHINX_DIRS (colon-separated directories). 48 | # Disable sphinx support by assigning an empty string to this environment 49 | # variable. 50 | try: 51 | SPHINX_DIR = set(environ['PYPI2DEB_SPHINX_DIRS'].split(":")) 52 | except KeyError: 53 | SPHINX_DIR = {'docs', 'doc', 'doc/build'} 54 | INTERPRETER_MAP = {value: key for key, value in PKG_PREFIX_MAP.items()} 55 | VERSIONED_I_MAP = {'python': 'python2'} 56 | DESC_STOP_KEYWORDS = {'changelog', 'changes', 'license', 'requirements', 57 | 'installation'} 58 | 59 | 60 | def _copy_static_files(src_dir, debian_dir): 61 | """Copy static templates/overrides from source to destination""" 62 | for root, dirs, file_names in walk(src_dir): 63 | for fn in file_names: 64 | if fn.endswith(('.tpl', '.swp')): 65 | continue 66 | dst_dir = root.replace(src_dir, debian_dir) 67 | if not exists(dst_dir): 68 | makedirs(dst_dir) 69 | if not exists(join(dst_dir, fn)): 70 | copy(join(root, fn), join(dst_dir, fn)) 71 | 72 | 73 | async def debianize(dpath, ctx, profile=None): 74 | update_ctx(dpath, ctx) 75 | 76 | setupcfg_fpath = join(dpath, 'setup.cfg') 77 | if exists(setupcfg_fpath): 78 | upstream_cfg = ConfigParser() 79 | upstream_cfg.read(setupcfg_fpath) 80 | if 'py2dsp' in upstream_cfg: 81 | ctx.update(upstream_cfg['py2dsp'].items()) 82 | 83 | override_paths = [TEMPLATES_PATH] 84 | 85 | # profile overrides 86 | if profile: 87 | if isdir(profile): # --profile a_dir 88 | override_paths.append(profile) 89 | elif exists(profile): # --profile a_file 90 | with open(profile) as fp: 91 | set_keys = [key for (key, value) in ctx.items() if isinstance(value, set)] 92 | ctx.update(load(fp)) 93 | # recover sets from Json lists 94 | for set_key in set_keys: 95 | if not isinstance(ctx[set_key], set): 96 | ctx[set_key] = set(ctx[set_key]) 97 | else: # --profile name 98 | profile_dpath = join(PROFILES_PATH, profile) 99 | if isdir(profile_dpath): 100 | override_paths.append(profile_dpath) 101 | 102 | # package specific overrides... in global overrides dir 103 | o_dpath = join(OVERRIDES_PATH, ctx['name'].lower()) 104 | isdir(o_dpath) and override_paths.append(o_dpath) 105 | # ... in local ./overrides 106 | o_dpath = join('overrides', ctx['name'].lower()) 107 | isdir(o_dpath) and override_paths.append(o_dpath) 108 | 109 | # handle overrides 110 | debian_dir = join(dpath, 'debian') 111 | for o_dpath in override_paths: 112 | fpath = join(o_dpath, 'ctx.json') 113 | if exists(fpath): 114 | with open(fpath) as fp: 115 | set_keys = [key for (key, value) in ctx.items() if isinstance(value, set)] 116 | ctx.update(load(fp)) 117 | # recover sets from Json lists 118 | for set_key in set_keys: 119 | if not isinstance(ctx[set_key], set): 120 | ctx[set_key] = set(ctx[set_key]) 121 | # invoke pre hooks 122 | fpath = abspath(join(o_dpath, 'hooks', 'pre')) 123 | if exists(fpath) and access(fpath, X_OK): 124 | _dump_ctx(ctx) 125 | code = await execute([fpath, ctx['src_name'], 126 | ctx['version'], ctx['debian_revision']], 127 | cwd=dpath) 128 | if code != 0: 129 | raise Exception("pre hook for %s failed with %d return code" % ( 130 | ctx['name'], code)) 131 | for o_dpath in reversed(override_paths): 132 | # copy static files 133 | deb_dpath = join(o_dpath, 'debian') 134 | if isdir(deb_dpath): 135 | _copy_static_files(deb_dpath, debian_dir) 136 | 137 | for key in ('vcs_src', 'vcs_browser', 'uploaders'): 138 | if key in ctx: 139 | ctx[key] = ctx[key].format(**ctx) 140 | ctx['debian_version'] = "{}-{}".format(ctx['version'], ctx['debian_revision']) 141 | 142 | # Jinja setup: set templates directories 143 | templates_dir = [dpath] # use existing dir as a template dir as well 144 | templates_dir.extend(override_paths) 145 | templates_dir.append(TEMPLATES_PATH) 146 | env = Environment(loader=FileSystemLoader(templates_dir)) 147 | 148 | # render debian dir files (note that order matters) 149 | docs(dpath, ctx, env) 150 | control(dpath, ctx, env) 151 | rules(dpath, ctx, env) 152 | chmod(join(dpath, 'debian', 'rules'), 0o755) 153 | initial_release = await changelog(dpath, ctx, env) 154 | if initial_release: 155 | itp_mail(dpath, ctx, env) 156 | copyright(dpath, ctx, env) 157 | watch(dpath, ctx, env) 158 | # Currently only Github is supported for DEP-12 159 | if 'github' in ctx: 160 | upstream__metadata(dpath, ctx, env) 161 | clean(dpath, ctx, env) 162 | 163 | # invoke post hooks 164 | for o_dpath in override_paths: 165 | fpath = join(o_dpath, 'hooks', 'post') 166 | if exists(fpath) and access(fpath, X_OK): 167 | _dump_ctx(ctx) 168 | code = await execute([fpath, ctx['src_name'], 169 | ctx['version'], ctx['debian_revision'], 170 | VERSION], 171 | cwd=dpath) 172 | if code != 0: 173 | raise Exception("post hook for %s failed with %d return code" % ( 174 | ctx['name'], code)) 175 | 176 | 177 | def update_ctx(dpath, ctx): 178 | ctx.setdefault('exports', {}) 179 | ctx.setdefault('build_depends', set()) 180 | maintainer, email = get_maintainer() 181 | ctx['creator'] = '{} <{}>'.format(maintainer, email) 182 | if 'maintainer' not in ctx: 183 | ctx['maintainer'] = '{} <{}>'.format(maintainer, email) 184 | if 'debian_revision' not in ctx: 185 | ctx['debian_revision'] = '0~pypi2deb' 186 | 187 | ctx['binary_arch'] = 'all' 188 | ctx.setdefault('clean_files', set()) 189 | for root, dirs, file_names in walk(dpath): 190 | if any(fname.endswith(('.c', '.cpp', '.pyx')) for fname in file_names): 191 | ctx['binary_arch'] = 'any' 192 | 193 | for fname in file_names: 194 | if fname.endswith('.pyx'): 195 | if 'python3' in ctx['interpreters']: 196 | ctx['build_depends'].add('cython3') 197 | for ext in ('c', 'cpp'): 198 | fname_c = fname[:-3] + ext 199 | if fname_c in file_names: 200 | ctx['clean_files'].add(join(root.replace(dpath, '.'), fname_c)) 201 | 202 | 203 | def _dump_ctx(ctx): 204 | """dump ctx in JSON format so that hooks can use it""" 205 | try: 206 | fpath = join(ctx['root'], '{src_name}_{version}-{debian_revision}.ctx'.format(**ctx)) 207 | serializable_ctx = {key: (value if not isinstance(value, set) else tuple(value)) 208 | for (key, value) in ctx.items()} 209 | with open(fpath, 'w') as fp: 210 | dump(serializable_ctx, fp, indent=' ') 211 | except Exception: 212 | log.debug('cannot dump ctx', exc_info=True) 213 | 214 | 215 | def docs(dpath, ctx, env): 216 | docs = ctx.setdefault('docs', {}) 217 | for path in SPHINX_DIR: 218 | if exists(join(dpath, path, 'Makefile')) and exists(join(dpath, path, 'conf.py')): 219 | docs['sphinx_dir'] = path 220 | ctx['build_depends'].add('python3-sphinx') 221 | docs.setdefault('files', []).append('.pybuild/docs/*') 222 | for fn in listdir(dpath): 223 | if fn.lower().startswith('readme'): 224 | docs.setdefault('files', []).append(fn) 225 | if fn.lower() == 'examples': 226 | docs['examples_dir'] = fn 227 | if docs: 228 | if 'sphinx_dir' in docs: 229 | # i.e. we have binary packages with docs 230 | docs_pkg = 'python-{}-doc' 231 | elif 'python3' in ctx['interpreters']: 232 | docs_pkg = 'python3-{}' 233 | docs_pkg = docs_pkg.format(ctx['src_name']) 234 | if 'examples_dir' in docs: 235 | # TODO: should we extend this file only if it exists? 236 | with open(join(dpath, 'debian', docs_pkg + '.examples'), 'w') as fp: 237 | fp.write("{}/*\n".format(docs['examples_dir'])) 238 | if 'files' in docs: 239 | # TODO: should we extend this file only if it exists? 240 | with open(join(dpath, 'debian', docs_pkg + '.docs'), 'w') as fp: 241 | for fn in docs['files']: 242 | fp.write("{}\n".format(fn)) 243 | 244 | 245 | def _render_template(func): 246 | name = func.__name__.replace('__', '/') 247 | 248 | def _template(dpath, ctx, env, *args, **kwargs): 249 | fpath = join(dpath, 'debian', name) 250 | if exists(fpath): 251 | log.debug('debian/%s already exist, skipping', name) 252 | return 253 | ctx = func(dpath, ctx, env, *args, **kwargs) 254 | tpl = env.get_template('debian/{}.tpl'.format(name)) 255 | 256 | if not isdir(dirname(fpath)): 257 | makedirs(dirname(fpath)) 258 | 259 | with open(fpath, 'w', encoding='utf-8') as fp: 260 | fp.write(tpl.render(ctx)) 261 | return _template 262 | 263 | 264 | @_render_template 265 | def control(dpath, ctx, env): 266 | desc = [] 267 | code_line = False 268 | first_line = True 269 | for line in ctx['description'].split('\n'): 270 | if first_line: 271 | if line.lower() == ctx['name'].lower(): 272 | continue 273 | if not line.strip().replace('=', '').replace('-', '').replace('~', ''): 274 | continue 275 | first_line = False 276 | if not line.strip(): 277 | desc.append(' .') 278 | else: 279 | if line.startswith(('* ', '>>> ', '... ', '.. ', '$ ')) \ 280 | or code_line or line == '...': 281 | if line.startswith('>>> '): 282 | # next line should get extra space char as well 283 | code_line = True 284 | elif code_line: 285 | code_line = False 286 | line = ' ' + line 287 | elif line.strip().lower() in DESC_STOP_KEYWORDS: 288 | break 289 | line = line.replace('\t', ' ') 290 | desc.append(' ' + line) 291 | for key, value in { 292 | 'short_desc': ctx['summary'][:80], 293 | 'long_desc': '\n'.join(desc), 294 | }.items(): 295 | if key not in ctx: 296 | ctx[key] = value 297 | 298 | req = set() 299 | if 'requires' in ctx: 300 | for impl in ctx['interpreters']: 301 | impl = INTERPRETER_MAP.get(impl, impl) 302 | try: 303 | dependency = guess_dependency(impl, line) 304 | if dependency: 305 | ctx['build_depends'].add(dependency) 306 | except Exception as err: 307 | log.warn('cannot parse build dependency: %s', err) 308 | else: 309 | for i in listdir(dpath): 310 | if i.endswith('.egg-info') and exists(join(dpath, i, 'requires.txt')): 311 | req.add(join(dpath, i, 'requires.txt')) 312 | if i == 'requirements.txt': 313 | req.add(join(dpath, 'requirements.txt')) 314 | 315 | for fpath in req: 316 | for impl in ctx['interpreters']: 317 | impl = INTERPRETER_MAP.get(impl, impl) 318 | try: 319 | for i in parse_pydep(impl, fpath)['depends']: 320 | ctx['build_depends'].add(i) 321 | except Exception as err: 322 | log.warn('cannot parse build dependency: %s', err) 323 | 324 | pyproject_file = pathlib.Path(dpath, 'pyproject.toml') 325 | if pyproject_file.exists(): 326 | pyproject_toml = tomllib.loads(pyproject_file.read_text()) 327 | if 'build-system' in pyproject_toml: 328 | # https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/#fallback-behaviour 329 | if 'build-backend' not in pyproject_toml['build-system']: 330 | log.info("Unable to detect a build backend in pyproject.toml, falling back to setuptools") 331 | pyproject_toml['build-system']['build-backend'] = \ 332 | "setuptools.build_meta:__legacy__" 333 | ctx['pybuild_depends'] = 'pybuild-plugin-pyproject' 334 | # static list of build backends and what package to install to use them 335 | backends = { 336 | 'flit': 'flit', 337 | 'hatchling': 'python3-hatchling', 338 | 'mesonpy': 'python3-mesonpy', 339 | 'pdm': 'python3-pdm-pep517', 340 | 'poetry': 'python3-poetry-core', 341 | 'setuptools': 'python3-setuptools', 342 | 'sipbuild': 'python3-sipbuild', 343 | } 344 | for _backend, _package in backends.items(): 345 | if _backend in pyproject_toml['build-system']['build-backend']: 346 | ctx['pybuild_depends'] += f', {_package}' 347 | else: 348 | log.info("Unable to detect a build system via pyproject.toml, falling back to setup.py") 349 | 350 | # either there's only a setup.py or we couldnt detect a build backend via pyproject.toml 351 | if exists(join(dpath, 'setup.py')) and 'pybuild_depends' not in ctx: 352 | ctx['pybuild_depends'] = 'dh-python' 353 | with open(join(dpath, 'setup.py')) as fp: 354 | for line in fp: 355 | if line.startswith('#'): 356 | continue 357 | if 'setuptools' in line: 358 | for interpreter in ctx['interpreters']: 359 | ctx['build_depends'].add('{}-setuptools'.format(interpreter)) 360 | 361 | if 'python3' in ctx['interpreters']: 362 | ctx['build_depends'].add( 363 | 'python3-all%s' % ('-dev' if ctx['binary_arch'] == 'any' else '')) 364 | if 'pypy' in ctx['interpreters']: 365 | ctx['build_depends'].add('pypy') 366 | 367 | return ctx 368 | 369 | 370 | @_render_template 371 | def rules(dpath, ctx, env): 372 | ctx['with'] = ','.join(VERSIONED_I_MAP.get(i, i) for i in ctx['interpreters']) 373 | if ctx.get('docs', {}).get('sphinx_dir'): 374 | ctx['with'] += ',sphinxdoc' 375 | 376 | # if package install a script in /usr/bin/ - ship it only in python3-foo package 377 | if exists(join(dpath, 'setup.py')) and len(ctx['interpreters']) > 1: 378 | with open(join(dpath, 'setup.py')) as fp: 379 | for line in fp: 380 | if 'console_scripts' in line: 381 | for interpreter in ctx['interpreters']: 382 | if interpreter == 'python3': 383 | continue 384 | ipreter = VERSIONED_I_MAP.get(interpreter, interpreter) 385 | ctx['exports']['PYBUILD_AFTER_INSTALL_{}'.format(ipreter)] = 'rm -rf {destdir}/usr/bin/' 386 | break 387 | 388 | return ctx 389 | 390 | 391 | async def changelog(dpath, ctx, env): 392 | change = ctx.get('message', f"Initial release, autogenerated by py2dsp/{VERSION}; Closes: #NNNNNN") 393 | version = ctx['debian_version'] 394 | distribution = ctx.get('distribution', 'UNRELEASED') 395 | 396 | fpath = join(dpath, 'debian', 'changelog') 397 | if exists(fpath): 398 | with open(fpath, encoding='utf-8') as fp: 399 | line = fp.readline() 400 | if ctx['version'] in line or 'UNRELEASED' in line: 401 | log.debug('changelog doesn\'t need an update') 402 | return 403 | else: 404 | await execute(['dch', '--force-distribution', '--distribution', distribution, 405 | '--newversion', version, '-m', change], cwd=dpath) 406 | return 407 | 408 | now = datetime.utcnow() 409 | changelog = Changelog() 410 | changelog.new_block(package=ctx['src_name'], 411 | version=Version(version), 412 | distributions=distribution, 413 | urgency='low', 414 | author=ctx['creator'], 415 | date=now.strftime('%a, %d %b %Y %H:%M:%S +0000')) 416 | changelog.add_change('') 417 | changelog.add_change(' * {}'.format(change)) 418 | changelog.add_change('') 419 | 420 | with open(fpath, 'w', encoding='utf-8') as fp: 421 | changelog.write_to_open_file(fp) 422 | return True 423 | 424 | 425 | @_render_template 426 | def copyright(dpath, ctx, env): 427 | if not ctx.get('deb_copyright'): 428 | ctx['deb_copyright'] = "{} {}".format(datetime.now().year, ctx['creator']) 429 | if ctx['license_name'] in ('Apache 2', 'Apache 2.0'): 430 | ctx['license_name'] = 'Apache2' 431 | ctx['license'] = ' See /usr/share/common-licenses/Apache-2.0' 432 | if not ctx.get('deb_license_name'): 433 | ctx['deb_license_name'] = ctx['license_name'] 434 | ctx['deb_license'] = ctx.get('license', '') 435 | if not ctx.get('deb_license') and ctx['deb_license_name']: 436 | ctx['deb_license'] = '' 437 | fpath = '/usr/share/common-licenses/{}'.format(ctx['deb_license_name']) 438 | if exists(fpath): 439 | license = [] 440 | with open(fpath, encoding='utf-8') as fp: 441 | for line in fp: 442 | line = line.rstrip() 443 | license.append(' {}'.format(line) if line else ' .') 444 | ctx['deb_license'] = '\n'.join(license) 445 | 446 | ctx.setdefault('copyright', '') 447 | 448 | if not ctx.get('license'): 449 | license = [] 450 | for fn in listdir(dpath): 451 | if not fn.lower().startswith('license'): 452 | continue 453 | with open(join(dpath, fn), 'r') as fp: 454 | for line in fp: 455 | line = line.rstrip() 456 | if not line: 457 | license.append(' .') 458 | else: 459 | license.append(' ' + line) 460 | if not ctx['copyright'] and line.lower().startswith('copyright '): 461 | ctx['copyright'] = line[10:] 462 | if license: 463 | ctx['license'] = '\n'.join(license) 464 | break 465 | if not ctx.get('copyright'): 466 | ctx['copyright'] = ctx['author'] 467 | return ctx 468 | 469 | 470 | @_render_template 471 | def watch(dpath, ctx, env): 472 | return ctx 473 | 474 | @_render_template 475 | def upstream__metadata(dpath, ctx, env): 476 | """Render debian/upstream/metadata according to DEP-12.""" 477 | return ctx 478 | 479 | 480 | def itp_mail(dpath, ctx, env): 481 | fpath = join(ctx['root'], '{src_name}_{version}-{debian_revision}.mail'.format(**ctx)) 482 | if exists(fpath): 483 | return 484 | tpl = env.get_template('itp.mail') 485 | with open(fpath, 'w', encoding='utf-8') as fp: 486 | fp.write(tpl.render(ctx)) 487 | 488 | 489 | def clean(dpath, ctx, env): 490 | old = set() 491 | fpath = join(dpath, 'debian', 'clean') 492 | if exists(fpath): 493 | with open(fpath) as fp: 494 | for line in fp: 495 | old.add(line.strip()) 496 | 497 | new = set() 498 | for i in ctx['clean_files']: 499 | if i not in old: 500 | new.add(i) 501 | 502 | if new: 503 | with open(fpath, 'a') as fp: 504 | for fn in new: 505 | fp.write("\n{}".format(fn)) 506 | -------------------------------------------------------------------------------- /pypi2deb/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2015-2018 Piotr Ożarowski 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import logging 22 | from functools import wraps 23 | from pypi2deb.cache import load as _cache_load, dump as _cache_dump 24 | 25 | log = logging.getLogger('pypi2deb') 26 | 27 | 28 | def cache(ttl=3600, key=None, prefix=None): 29 | """Cache decorated function's result. 30 | 31 | :param key: static cache key 32 | :param prefix: prepend it to the autogenerated key 33 | """ 34 | 35 | def _cache(func): 36 | func_name = func.__name__ 37 | 38 | @wraps(func) 39 | def __cache(*args, **kwargs): 40 | if key: 41 | cache_key = key 42 | else: 43 | cache_key = "%s:%s:%s" % (func_name, args, kwargs) 44 | if prefix: 45 | cache_key = "%s:%s" % (prefix, cache_key) 46 | 47 | res = _cache_load(cache_key) 48 | if res is None: 49 | res = func(*args, **kwargs) 50 | if res is not None: 51 | _cache_dump(cache_key, res, ttl) 52 | 53 | return res 54 | return __cache 55 | return _cache 56 | -------------------------------------------------------------------------------- /pypi2deb/github.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2021 Sandro Tosi 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # PyGithub doesnt support (yet) asyncio, and there's only a prototype of a github 22 | # client supporting it, so we're gonna do it the old way 23 | 24 | import logging 25 | from os.path import join, exists 26 | 27 | import aiohttp 28 | from github import Github 29 | from github.GithubException import UnknownObjectException 30 | 31 | log = logging.getLogger('pypi2deb') 32 | 33 | 34 | async def github_download(name, github_url, version=None, destdir='.'): 35 | g = Github() 36 | repo_name = github_url.replace('https://github.com/', '').rstrip('/') 37 | log.debug(f"Calling github get_repo with arg {repo_name}") 38 | repo = g.get_repo(repo_name) 39 | 40 | if not name: 41 | name = repo.name 42 | 43 | try: 44 | tag_name = repo.get_latest_release().tag_name 45 | except UnknownObjectException: 46 | # Some projects do not use Github Releases, check the latest tag instead 47 | tag_name = repo.get_tags()[0].name 48 | 49 | if not version: 50 | # TODO: are there other special cases? vx.y.z tag gets rewritten as x.y.z 51 | version = tag_name.lstrip('v') 52 | 53 | # cant use this for now, chk https://github.com/PyGithub/PyGithub/issues/1871 54 | # download_url = latest_release.tarball_url 55 | # so let's "forge" the right URL here 56 | download_url = f"{github_url}/archive/refs/tags/{tag_name}.tar.gz" 57 | 58 | fname = f'{name}_{version}.orig.tar.gz' 59 | 60 | fpath = join(destdir, fname) 61 | if exists(fpath): 62 | return fname 63 | 64 | async with aiohttp.ClientSession(trust_env=True) as session: 65 | log.debug(f"fetching upstream tarball from {download_url}") 66 | response = await session.get(download_url) 67 | with open(fpath, 'ba') as fp: 68 | data = await response.read() 69 | fp.write(data) 70 | 71 | return fname 72 | -------------------------------------------------------------------------------- /pypi2deb/pypi.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2015-2018 Piotr Ożarowski 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import logging 22 | from os import environ 23 | from os.path import exists, join 24 | from xmlrpc.client import ServerProxy 25 | # from aioxmlrpc.client import ServerProxy # TODO: package it in Debian 26 | import aiohttp 27 | 28 | from pypi2deb.decorators import cache 29 | from pypi2deb.tools import pkg_name, execute 30 | 31 | PYPI_JSON_URL = environ.get('PYPI_JSON_URL', 'https://pypi.org/pypi') 32 | PYPI_XMLRPC_URL = environ.get('PYPI_XMLRPC_URL', 'https://pypi.org/pypi') 33 | log = logging.getLogger('pypi2deb') 34 | 35 | 36 | async def get_pypi_info(name, version=None): 37 | url = PYPI_JSON_URL + '/' + name 38 | if version: 39 | url += '/' + version 40 | url += '/json' 41 | 42 | async with aiohttp.ClientSession(trust_env=True) as session: 43 | try: 44 | response = await session.get(url) 45 | except Exception as err: 46 | log.error('invalid project name: {} ({})'.format(name, err)) 47 | return 48 | try: 49 | result = await response.json() 50 | except Exception as err: 51 | log.warn('cannot download %s %s details from PyPI: %r', name, version, err) 52 | return 53 | return result 54 | 55 | 56 | def parse_pypi_info(data): 57 | if not data: 58 | return {} 59 | 60 | for key, value in list(data['info'].items()): 61 | if value == 'UNKNOWN': 62 | data['info'][key] = '' 63 | 64 | info = data['info'] 65 | result = { 66 | 'name': info['name'], 67 | 'version': info['version'], 68 | 'description': info['description'].replace('\r\n', '\n'), 69 | 'license_name': info['license'], 70 | 'author': '{author} <{author_email}>'.format(**info), 71 | 'homepage': info['home_page'], 72 | } 73 | if 'requires' in info: # see f.e. qutebrowser 74 | result['requires'] = info['requires'] 75 | 76 | summary = info.get('summary', 'FIXME').replace(' ', ' ') 77 | 78 | unwanted_prefixes = {'a', 'an', 'the', 'is', result['name'].lower()} 79 | while True: 80 | if ' ' not in summary: 81 | break 82 | prefix, rest = summary.split(maxsplit=1) 83 | if prefix.lower() in unwanted_prefixes: 84 | summary = rest 85 | else: 86 | break 87 | if summary.endswith('.'): 88 | summary = summary[:-1] 89 | result['summary'] = summary 90 | 91 | classifiers = data['info']['classifiers'] 92 | result['interpreters'] = set() 93 | for i in classifiers: 94 | if i.startswith('Programming Language :: Python :: 3'): 95 | result['interpreters'].add('python3') 96 | # if 'Programming Language :: Python :: Implementation :: PyPy' in classifiers: 97 | # result['interpreters'].add('pypy') 98 | if not result['interpreters']: 99 | # assume it's Python 3 only 100 | result['interpreters'].add('python3') 101 | 102 | return result 103 | 104 | 105 | def parse_pkg_info(fpath): 106 | """Parse PKG-INFO file""" 107 | raise NotImplementedError() # FIXME 108 | 109 | 110 | async def download(name, version=None, destdir='.'): 111 | details = await get_pypi_info(name, version) 112 | if not details: 113 | raise Exception('cannot get PyPI project details for {}'.format(name)) 114 | 115 | download_version = version or details['info']['version'] 116 | 117 | package_urls = details['urls'] 118 | if not package_urls: 119 | log.debug('missing release of %s %s on PyPI', name, download_version) 120 | raise Exception('missing release') 121 | 122 | try: 123 | release = next((i for i in package_urls if i['python_version'] == 'source')) 124 | except StopIteration: 125 | release = None 126 | 127 | if not release: 128 | available_files = ", ".join("{} (package type: {})".format(f["filename"], f["packagetype"]) for f in package_urls) 129 | other_versions_text = "" 130 | if not version and "releases" in details: 131 | other_available_versions = [v for v in details["releases"] if v != download_version] 132 | if other_available_versions: 133 | other_versions_text = " Some other versions found for {}: {}{}.".format( 134 | name, 135 | ", ".join(other_available_versions[:5]), 136 | " and others" if len(other_available_versions) > 5 else "" 137 | ) 138 | else: 139 | other_versions_text = " No other versions for {} found.".format(name) 140 | raise Exception('Source package for {} version {} not available on PyPI. Available files: {}.{}'.format(name, download_version, available_files, other_versions_text)) 141 | 142 | orig_ext = ext = release['filename'].replace('{}-{}.'.format(name, download_version), '') 143 | if ext not in {'tar.gz', 'tar.bz2', 'tar.xz'}: 144 | ext = 'tar.xz' 145 | 146 | fname = '{}_{}.orig.{}'.format(pkg_name(name), download_version, ext) 147 | 148 | fpath = join(destdir, fname) 149 | if exists(fpath): 150 | return fname 151 | 152 | async with aiohttp.ClientSession() as session: 153 | log.debug(f"fetching upstream tarball from {release['url']}") 154 | response = await session.get(release['url']) 155 | with open(fpath if ext == orig_ext else join(destdir, release['filename']), 'wb') as fp: 156 | data = await response.read() 157 | fp.write(data) 158 | 159 | if orig_ext != ext: 160 | cmd = ['mk-origtargz', '--rename', '--compression', 'xz', 161 | '--package', pkg_name(details['info']['name']), '--version', download_version, 162 | '--directory', destdir, 163 | '--repack', join(destdir, release['filename'])] 164 | # TODO: add --copyright-file if overriden copyright file is available 165 | await execute(cmd) 166 | 167 | return fname 168 | 169 | 170 | @cache() 171 | def list_packages(classifiers=None): 172 | client = ServerProxy(PYPI_XMLRPC_URL) 173 | if not classifiers: 174 | # packages = await client.list_packages() 175 | packages = client.list_packages() 176 | packages = dict.fromkeys(packages) 177 | else: 178 | # packages = await client.browse(classifiers) 179 | packages = client.browse(classifiers) 180 | # browse returns all versions, use the latest one only 181 | # TODO: use LooseVersion 182 | packages = dict(sorted(packages)) 183 | return packages 184 | -------------------------------------------------------------------------------- /pypi2deb/tools.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2015-2018 Piotr Ożarowski 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import asyncio 22 | import logging 23 | import os 24 | import re 25 | import tarfile 26 | from datetime import datetime 27 | from os.path import exists, join 28 | from shlex import split 29 | from shutil import rmtree 30 | from pypi2deb.decorators import cache 31 | from dhpython.pydist import load, safe_name 32 | 33 | 34 | FILENAME_RE = re.compile(r''' 35 | (?:.*/)? 36 | (?P[a-zA-Z-].*) 37 | [-_] 38 | (?P[0-9][A-Za-z0-9\-.+]*?) 39 | (?: 40 | \. 41 | (?:orig\.)? 42 | (?P(?:tar(?:\.[a-z0-9]+)?)|(?:zip)) 43 | )?$ 44 | ''', re.VERBOSE) 45 | log = logging.getLogger('pypi2deb') 46 | 47 | 48 | def unpack(fpath, destdir='.', dname=None): 49 | if dname: 50 | dst_dpath = join(destdir, dname) 51 | if exists(dst_dpath): 52 | log.debug('{} already exists, no need to unpack'.format(dname)) 53 | return dst_dpath 54 | else: 55 | dst_dpath = None 56 | 57 | fname = fpath.rsplit('/', 1)[-1] 58 | if '.tar.' in fname: 59 | with tarfile.open(fpath, 'r') as tar: 60 | members = [] 61 | for memb in tar.getmembers(): 62 | if memb.name.startswith('/') or '../' in memb.name: 63 | log.warn('skipping invalid file name: %s', memb.name) 64 | continue 65 | members.append(memb) 66 | 67 | dirname = members[0].name 68 | if not members[0].isdir(): 69 | destdir = dst_dpath = join(destdir, dname or 'extracted') 70 | dirname = dname = None 71 | if exists(destdir): 72 | log.debug('{} already exists, no need to unpack'.format(destdir)) 73 | return destdir 74 | elif not dst_dpath: 75 | dst_dpath = join(destdir, dirname) 76 | 77 | tar.extractall(destdir, members) 78 | if dname and dname != dirname: 79 | os.rename(join(destdir, dirname), dst_dpath) 80 | 81 | upstream_debian = os.path.join(dst_dpath, 'debian') 82 | if os.path.exists(upstream_debian): 83 | rmtree(upstream_debian) 84 | return dst_dpath 85 | 86 | 87 | def parse_filename(name): 88 | match = FILENAME_RE.match(name) 89 | return match.groupdict() if match else {} 90 | 91 | 92 | async def execute(command, cwd=None, env=None, log_output=None): 93 | """Execute external shell commad. 94 | 95 | :param cdw: currennt working directory 96 | :param env: environment 97 | :param log_output: 98 | * opened log file or path to this file, or 99 | * None if output should be redirectored to stdout/stderr 100 | """ 101 | env = env or os.environ 102 | close = False 103 | if log_output: 104 | if isinstance(log_output, str): 105 | close = True 106 | log_output = open(log_output, 'a', encoding='utf-8') 107 | log_output.write('\n# command executed on {}'.format(datetime.now().isoformat())) 108 | log_output.write('\n$ {}\n'.format(command)) 109 | log_output.flush() 110 | 111 | if isinstance(command, str): 112 | command = split(command) 113 | log.debug('invoking: %s in %s', command, cwd) 114 | 115 | create = asyncio.create_subprocess_exec(*command, stdout=log_output, stderr=log_output, 116 | cwd=cwd, env=env) 117 | proc = await create 118 | await proc.wait() 119 | close and log_output.close() 120 | 121 | return proc.returncode 122 | 123 | 124 | def pkg_name(name): 125 | names = _load_package_names() 126 | name = safe_name(name).lower() 127 | if name in names: 128 | return names[name] 129 | result = name.lower().replace('-python', '').replace('python-', '') 130 | if result.endswith('.py'): 131 | result = result[:-3] 132 | result = re.sub('[^a-z0-9.-]', '-', result) 133 | return result 134 | 135 | 136 | @cache() 137 | def _load_package_names(): 138 | result = {} 139 | try: 140 | data = load('cpython3') 141 | except Exception as err: 142 | log.warn('cannot load pydist names: %s', err) 143 | data = {} 144 | else: 145 | for key, details in data.items(): 146 | result[key.lower()] = details[0]['dependency'].replace('python3-', '') 147 | return result 148 | -------------------------------------------------------------------------------- /pypi2debian: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | # vim: et ts=4 sw=4 3 | # Copyright © 2015-2018 Piotr Ożarowski 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import argparse 24 | import asyncio 25 | import logging 26 | import sys 27 | try: 28 | from asyncio import JoinableQueue as Queue # Python 3.4 29 | except ImportError: # Python 3.5 30 | from asyncio import Queue 31 | from os import environ, getcwd, makedirs 32 | from os.path import exists, join 33 | 34 | from pypi2deb import VERSION 35 | from pypi2deb.debianize import debianize 36 | from pypi2deb.pypi import list_packages 37 | from pypi2deb.pypi import get_pypi_info, parse_pypi_info, download 38 | from pypi2deb.tools import unpack, pkg_name, execute 39 | 40 | logging.basicConfig(format='%(levelname).1s: pypi2debian ' 41 | '%(module)s:%(lineno)d: %(message)s') 42 | log = logging.getLogger('pypi2debian') 43 | DESCRIPTION = 'Python Package Index to Debian repository converter' 44 | 45 | 46 | class Converter: 47 | def __init__(self, args, loop=None): 48 | self.args = args 49 | self.loop = loop or asyncio.get_event_loop() 50 | self.queue = Queue(loop=self.loop) 51 | self.build_src_queue = Queue(loop=self.loop) 52 | self.build_bin_queue = Queue(loop=self.loop) 53 | 54 | def __enter__(self): 55 | return self 56 | 57 | def __exit__(self, exc_type, exc_value, traceback): 58 | if exc_type is None: 59 | self.loop.run_until_complete(self.run()) 60 | # self.loop.close() 61 | 62 | async def run(self): 63 | stats_worker = asyncio.Task(self.stats_worker(), loop=self.loop) 64 | workers = [asyncio.Task(self.worker(), loop=self.loop) 65 | for _ in range(int(self.args.jobs))] 66 | build_src_workers = [asyncio.Task(self.build_src_worker(), loop=self.loop) 67 | for _ in range(int(self.args.src_jobs))] 68 | build_bin_workers = [asyncio.Task(self.build_bin_worker(), loop=self.loop) 69 | for _ in range(int(self.args.bin_jobs))] 70 | try: 71 | await self.queue.join() 72 | await self.build_src_queue.join() 73 | await self.build_bin_queue.join() 74 | finally: 75 | stats_worker.cancel() 76 | for w in workers: 77 | w.cancel() 78 | for w in build_src_workers: 79 | w.cancel() 80 | for w in build_bin_workers: 81 | w.cancel() 82 | 83 | def convert(self, name, version): 84 | self.queue.put_nowait((name, version)) 85 | 86 | def build_src(self, name, version, ctx): 87 | self.build_src_queue.put_nowait((name, version, ctx)) 88 | 89 | def build_bin(self, name, version, ctx): 90 | self.build_bin_queue.put_nowait((name, version, ctx)) 91 | 92 | async def stats_worker(self): 93 | while True: 94 | await asyncio.sleep(10) 95 | log.info('* pending conversion jobs: %s, src jobs: %s, build jobs: %s', 96 | self.queue.qsize(), self.build_src_queue.qsize(), 97 | self.build_bin_queue.qsize()) 98 | 99 | async def worker(self): 100 | args = self.args 101 | while True: 102 | name, version = await self.queue.get() 103 | try: 104 | try: 105 | ctx = await get_pypi_info(name, version) 106 | ctx = parse_pypi_info(ctx) 107 | except Exception as err: 108 | log.error('%s %s: cannot load details from PyPI: %r', name, version, err) 109 | continue 110 | if not ctx: 111 | log.error('%s %s: cannot find details on PyPI', name, version) 112 | continue 113 | if not version: 114 | version = ctx['version'] 115 | ctx['src_name'] = pkg_name(name) 116 | ctx['debian_revision'] = '0~pypi2deb' 117 | 118 | dsc_path = join(args.root, '{src_name}_{version}-{debian_revision}.dsc'.format(**ctx)) 119 | ctx['dsc'] = dsc_path # could be used in build step 120 | if exists(dsc_path): 121 | log.debug('%s %s: skipping - dsc file already exists', name, version) 122 | continue 123 | 124 | if args.no_pypy: 125 | ctx['interpreters'].discard('pypy') 126 | if args.pypy: 127 | ctx['interpreters'] = ctx['interpreters'] & {'pypy'} 128 | if args.python3: 129 | ctx['interpreters'] = ctx['interpreters'] & {'python3'} 130 | if not ctx['interpreters']: 131 | log.debug('%s %s: no matching interpreter is supported', name, version) 132 | continue 133 | 134 | try: 135 | fname = await download(name, version, destdir=args.root) 136 | except Exception as err: 137 | log.error('%s %s: cannot download from PyPI: %r', name, version, err) 138 | continue 139 | 140 | fpath = join(args.root, fname) 141 | 142 | ctx['root'] = args.root 143 | dirname = '{}-{}'.format(ctx['src_name'], version) 144 | try: 145 | dpath = unpack(fpath, args.root, dirname) 146 | except Exception as err: 147 | log.error('%s %s: cannot unpack sources: %r', name, version, err) 148 | continue 149 | ctx['src_dir'] = dpath 150 | 151 | # debianize sources 152 | try: 153 | await debianize(dpath, ctx, args.profile) 154 | except Exception as err: 155 | log.warn('%s %s: conversion failed with: %r', name, version, err) 156 | continue 157 | 158 | # create Debian source package 159 | if args.build_src_cmd: 160 | self.build_src(name, version, ctx) 161 | except Exception as err: 162 | log.error('conversion failure (%s %s)', name, version, exc_info=True) 163 | finally: 164 | self.queue.task_done() 165 | 166 | async def build_src_worker(self): 167 | args = self.args 168 | while True: 169 | name, version, ctx = await self.build_src_queue.get() 170 | try: 171 | command = args.build_src_cmd.format(**ctx) 172 | log_path = join(args.root, '{src_name}_{version}-{debian_revision}_source.log'.format(**ctx)) 173 | res = await execute(command, ctx['src_dir'], log_output=log_path) 174 | if res != 0: 175 | log.error('%s %s: creating source package failed with return code %d', 176 | name, version, res) 177 | elif args.build_cmd: 178 | # build the package - separate queue, usually one build at a time 179 | self.build_bin(name, version, ctx) 180 | except Exception as err: 181 | log.error('%s %s: creating source package failed with: %r', 182 | name, version, err) 183 | self.build_src_queue.task_done() 184 | 185 | async def build_bin_worker(self): 186 | args = self.args 187 | while True: 188 | name, version, ctx = await self.build_bin_queue.get() 189 | try: 190 | command = args.build_cmd.format(**ctx) 191 | log_path = join(args.root, '{src_name}_{version}-{debian_revision}_build.log'.format(**ctx)) 192 | res = await execute(command, ctx['src_dir'], log_output=log_path) 193 | if res != 0: 194 | log.error('%s %s: building binary failed with return code %d', 195 | name, version, res) 196 | except Exception as err: 197 | log.error('%s %s: building binary package failed with: %r', 198 | name, version, err, exc_info=True) 199 | self.build_bin_queue.task_done() 200 | 201 | 202 | if __name__ == '__main__': 203 | usage = '%(prog)s [OPTIONS]' 204 | parser = argparse.ArgumentParser(usage=usage, 205 | description=DESCRIPTION) 206 | parser.add_argument('-v', '--verbose', action='store_true', 207 | default=environ.get('PYPI2DEB_VERBOSE') == '1', 208 | help='turn verbose mode on') 209 | parser.add_argument('-q', '--quiet', action='store_true', 210 | default=environ.get('PYPI2DEB_QUIET') == '1', 211 | help='be quiet') 212 | parser.add_argument('--version', action='version', 213 | version='%(prog)s {}'.format(VERSION)) 214 | 215 | parser.add_argument('--root', action='store', metavar='DIR', 216 | default=environ.get('DESTDIR', 217 | join(getcwd(), 'result')), 218 | help='destination directory [default: ./result]') 219 | parser.add_argument('--clean', action='store_true', 220 | default=environ.get('PY2DSP_CLEAN', '0') == '1', 221 | help='remove name-version directory after creating source package') 222 | 223 | parser.add_argument('--profile', action='store', 224 | help='load default values from profile.json file (if available)') 225 | 226 | filters = parser.add_argument_group('filters') 227 | filters.add_argument('-c', '--classifiers', action='append', metavar='TAG', 228 | default=[], help='tag used to select packages for conversion' 229 | ' (can be passed several times)') 230 | filters.add_argument('--python3', action='store_true', 231 | default=environ.get('PYPI2DEB_PYTHON3') == '1', 232 | help='limit to Python 3.X packages only') 233 | # filters.add_argument('--pypy', action='store_true', 234 | # default=environ.get('PYPI2DEB_PYPY') == '1', 235 | # help='limit to PyPy packages only') 236 | # filters.add_argument('--no-pypy', action='store_true', 237 | # default=environ.get('PYPI2DEB_NO_PYPY') == '1', 238 | # help='do not generate pypy- packages') 239 | 240 | commands = parser.add_argument_group('commands', 'override commands') 241 | commands.add_argument('--build-src-cmd', action='store', metavar='COMMAND', 242 | help='command to build source package', 243 | default='dpkg-buildpackage -S -uc -us -nc -d -I.git -i.git') 244 | commands.add_argument('--build-cmd', action='store', metavar='COMMAND', 245 | help='build command, none by default') 246 | 247 | jobs = parser.add_argument_group('jobs') 248 | jobs.add_argument('-j', '--jobs', default=8, metavar='INT', 249 | help='number of conversion jobs to run simultaneously') 250 | jobs.add_argument('--src-jobs', default=4, metavar='INT', 251 | help='number of source package build jobs to run simultaneously') 252 | jobs.add_argument('--bin-jobs', default=1, metavar='INT', 253 | help='number of binary build jobs to run simultaneously') 254 | 255 | args = parser.parse_args() 256 | args.pypy = False 257 | args.no_pypy = True 258 | 259 | if args.verbose: 260 | logging.getLogger('pypi2deb').setLevel(logging.DEBUG) 261 | log.setLevel(logging.DEBUG) 262 | elif args.quiet: 263 | logging.getLogger('pypi2deb').setLevel(logging.ERROR) 264 | log.setLevel(logging.ERROR) 265 | else: 266 | logging.getLogger('pypi2deb').setLevel(logging.INFO) 267 | log.setLevel(logging.INFO) 268 | 269 | if args.profile in ('dpmt', 'papt'): 270 | logging.warning("'dpmt' and 'papt' profiles have been replaced by the 'dpt' profile") 271 | args.profile = 'dpt' 272 | 273 | if args.python3: 274 | args.classifiers.append('Programming Language :: Python :: 3') 275 | elif args.pypy: 276 | args.classifiers.append('Programming Language :: Python :: Implementation :: PyPy') 277 | 278 | log.debug('version: {}'.format(VERSION)) 279 | log.debug(sys.argv) 280 | log.debug('args: %s', args) 281 | 282 | if not exists(args.root): 283 | makedirs(args.root) 284 | 285 | packages = list_packages(args.classifiers) 286 | 287 | with Converter(args) as converter: 288 | for name, version in packages.items(): 289 | converter.convert(name, version) 290 | -------------------------------------------------------------------------------- /templates/debian/control.tpl: -------------------------------------------------------------------------------- 1 | Source: {{src_name}} 2 | Section: python 3 | Priority: optional 4 | Maintainer: {{maintainer}} 5 | {%- if uploaders %} 6 | Uploaders: {{uploaders}}{% endif %} 7 | Build-Depends: debhelper-compat (= 13), 8 | {{pybuild_depends}}, 9 | {%- for dependency in build_depends|sort %} 10 | {{dependency}},{% endfor %} 11 | Standards-Version: 4.6.2.0 12 | Testsuite: autopkgtest-pkg-pybuild 13 | {%- if homepage %} 14 | Homepage: {{homepage}}{% endif %} 15 | {%- if vcs_name and vcs_src %} 16 | Vcs-{{vcs_name}}: {{vcs_src}}{% endif %} 17 | {%- if vcs_browser %} 18 | Vcs-Browser: {{vcs_browser}}{% endif %} 19 | Rules-Requires-Root: no 20 | {%- if 'python3' in interpreters %} 21 | 22 | Package: python3-{{src_name.replace('python-', '')}} 23 | Architecture: {{binary_arch}} 24 | Depends: ${misc:Depends}, ${python3:Depends},{% if binary_arch == 'any' %} ${shlibs:Depends},{% endif %} 25 | {%- for dependency in python3_depends %} 26 | {{dependency}},{% endfor %} 27 | Description: {{short_desc}} 28 | {{long_desc}}{% endif %} 29 | {%- if 'pypy' in interpreters %} 30 | 31 | Package: pypy-{{src_name.replace('python-', '')}} 32 | Architecture: {{binary_arch}} 33 | Depends: ${misc:Depends}, ${pypy:Depends},{% if binary_arch == 'any' %} ${shlibs:Depends},{% endif %} 34 | {%- for dependency in pypy2_depends %} 35 | {{dependency}},{% endfor %} 36 | Recommends: ${pypy:Recommends} 37 | Suggests: ${pypy:Suggests} 38 | Description: {{short_desc}} 39 | {{long_desc}}{% endif %} 40 | {%- if docs and 'sphinx_dir' in docs %} 41 | 42 | Package: python-{{src_name.replace('python-', '')}}-doc 43 | Section: doc 44 | Architecture: all 45 | Depends: ${misc:Depends}, ${sphinxdoc:Depends} 46 | Description: documentation for the {{name}} Python library 47 | This package provides documentation for {{name}} {% endif %} 48 | -------------------------------------------------------------------------------- /templates/debian/copyright.tpl: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: {{name}} 3 | Upstream-Contact: {{author}} 4 | Source: {{github if github else homepage}} 5 | 6 | Files: * 7 | Copyright: {{copyright}} 8 | License: {{license_name}} 9 | 10 | Files: debian/* 11 | Copyright: {{deb_copyright}} 12 | License: {{deb_license_name}} 13 | 14 | License: {{license_name}} 15 | {{license}} 16 | {%- if license_name != deb_license_name %} 17 | 18 | License: {{deb_license_name}} 19 | {{deb_license}} 20 | {% endif %} 21 | -------------------------------------------------------------------------------- /templates/debian/rules.tpl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/make -f 2 | 3 | export PYBUILD_NAME={{src_name.replace('python-', '')}} 4 | {%- for key, value in exports.items() | sort %} 5 | export {{key}}={{value}}{% endfor %} 6 | %: 7 | dh $@ --with {{with}} --buildsystem=pybuild 8 | {%- if docs and docs.sphinx_dir %} 9 | 10 | execute_after_dh_auto_build-indep: 11 | ifeq (,$(filter nodoc,$(DEB_BUILD_OPTIONS))) 12 | cd {{docs.sphinx_dir}} && \ 13 | PYTHONPATH=$(CURDIR) http_proxy='http://127.0.0.1:9/' https_proxy='https://127.0.0.1:9/' \ 14 | sphinx-build -N -E -T -b html . $(CURDIR)/.pybuild/docs/html/ 15 | rm -rf $(CURDIR)/.pybuild/docs/html/.doctrees 16 | endif 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /templates/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /templates/debian/source/options: -------------------------------------------------------------------------------- 1 | extend-diff-ignore="^[^/]+.(egg-info|dist-info)/" 2 | -------------------------------------------------------------------------------- /templates/debian/upstream/metadata.tpl: -------------------------------------------------------------------------------- 1 | {%- if github %}Bug-Database: {{github}}/issues 2 | Bug-Submit: {{github}}/issues/new 3 | Repository: {{github}}.git 4 | Repository-Browse: {{github}} 5 | {% endif %} -------------------------------------------------------------------------------- /templates/debian/watch.tpl: -------------------------------------------------------------------------------- 1 | version=4 2 | {%- if github %} 3 | opts="pgpmode=none, filenamemangle=s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%@PACKAGE@-$1.tar.gz%" \ 4 | {{github}}/tags (?:.*?/)?v?(\d[\d.]*)\.tar\.gz 5 | {% else %} 6 | # try also https://pypi.debian.net/{{name}}/watch 7 | opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ 8 | https://pypi.debian.net/{{name}}/{{name}}-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /templates/itp.mail: -------------------------------------------------------------------------------- 1 | From: {{creator}} 2 | To: Debian Bug Tracking System 3 | Subject: ITP: {{src_name}} -- {{short_desc}} 4 | 5 | Package: wnpp 6 | Severity: wishlist 7 | X-Debbugs-Cc: debian-devel@lists.debian.org, :debian-python@lists.debian.org 8 | Owner: {{maintainer}} 9 | 10 | * Package name : {{src_name}} 11 | Version : {{version}} 12 | Upstream Author : {{author}} 13 | * URL : {{homepage}} 14 | * License : {{license_name}} 15 | Programming Lang: Python 16 | Description : {{short_desc}} 17 | 18 | Binary package names:{% if 'python3' in interpreters %} python3-{{src_name}}{% endif %} 19 | {%- if 'pypy' in interpreters %} pypy-{{src_name}}{% endif %} 20 | 21 | {{long_desc}} 22 | --------------------------------------------------------------------------------