Commit d7d61d5f authored by Jakub Ružička's avatar Jakub Ružička
Browse files

packaging: refactor and drop pbr

Drop problematic pbr in favor of recommended modern python packaging setup:

https://packaging.python.org/tutorials/packaging-projects/

Versioning is now handled by miniver without introducing external dep:

https://github.com/jbweston/miniver

I've also updated and extended setup.cfg with more metadata for better
integration on PyPI and elsewhere.

Fixes: #41
parent 7d2adccf
Pipeline #80119 passed with stage
in 1 minute and 53 seconds
......@@ -4,6 +4,7 @@
*.egg-info
/AUTHORS
/ChangeLog
/build
/dist
/pkg
/site
......@@ -24,15 +24,19 @@ unit:
script:
- tox -e unit
self:
integration:
stage: test
script:
- tox -e self
- tox -e integration
integration:
self:
stage: test
script:
- tox -e integration
# `python3 -m build` causes issues in tox in docker container
# use py.test directly
- pip3 install -r requirements.txt
- python3 setup.py develop --user
- py.test-3 tests/self
# docs published to GitLab Pages: https://packaging.pages.nic.cz/apkg/
......
import pbr.version
version_info = pbr.version.VersionInfo('apkg')
try:
__version__ = version_info.version_string()
except AttributeError:
__version__ = None
__all__ = ['__version__']
from ._version import __version__ # noqa
# -*- coding: utf-8 -*-
# This file is part of 'miniver': https://github.com/jbweston/miniver
#
# This file will be overwritten by setup.py when a source or binary
# distribution is made. The magic value "__use_git__" is interpreted by
# version.py.
version = "__use_git__"
# These values are only set if the distribution was created with 'git archive'
refnames = "$Format:%D$"
git_hash = "$Format:%h$"
# -*- coding: utf-8 -*-
# This file is part of 'miniver': https://github.com/jbweston/miniver
# pylint: skip-file
from collections import namedtuple
import os
import subprocess
from setuptools.command.build_py import build_py as build_py_orig
from setuptools.command.sdist import sdist as sdist_orig
Version = namedtuple("Version", ("release", "dev", "labels"))
# No public API
__all__ = []
package_root = os.path.dirname(os.path.realpath(__file__))
package_name = os.path.basename(package_root)
distr_root = os.path.dirname(package_root)
_package_root_inside_src = False
STATIC_VERSION_FILE = "_static_version.py"
def get_version(version_file=STATIC_VERSION_FILE):
version_info = get_static_version_info(version_file)
version = version_info["version"]
if version == "__use_git__":
version = get_version_from_git()
if not version:
version = get_version_from_git_archive(version_info)
if not version:
version = Version("unknown", None, None)
return pep440_format(version)
else:
return version
def get_static_version_info(version_file=STATIC_VERSION_FILE):
version_info = {}
with open(os.path.join(package_root, version_file), "rb") as f:
exec(f.read(), {}, version_info)
return version_info
def version_is_from_git(version_file=STATIC_VERSION_FILE):
return get_static_version_info(version_file)["version"] == "__use_git__"
def pep440_format(version_info):
release, dev, labels = version_info
version_parts = [release]
if dev:
if release.endswith("-dev") or release.endswith(".dev"):
version_parts.append(dev)
else: # prefer PEP440 over strict adhesion to semver
version_parts.append(".dev{}".format(dev))
if labels:
version_parts.append("+")
version_parts.append(".".join(labels))
return "".join(version_parts)
def get_version_from_git():
try:
p = subprocess.Popen(
["git", "rev-parse", "--show-toplevel"],
cwd=distr_root,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except OSError:
return
if p.wait() != 0:
return
if not os.path.samefile(p.communicate()[0].decode().rstrip("\n"),
distr_root):
# The top-level directory of the current Git repository is not the same
# as the root directory of the distribution: do not extract the
# version from Git.
return
# git describe --first-parent does not take into account tags from branches
# that were merged-in. The '--long' flag gets us the 'dev' version and
# git hash, '--always' returns the git hash even if there are no tags.
for opts in [["--first-parent"], []]:
try:
p = subprocess.Popen(
["git", "describe", "--long", "--always"] + opts,
cwd=distr_root,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except OSError:
return
if p.wait() == 0:
break
else:
return
description = (
p.communicate()[0]
.decode()
.strip("v") # Tags can have a leading 'v', but the version should not
.rstrip("\n")
.rsplit("-", 2) # Split the latest tag, commits since tag, and hash
)
try:
release, dev, git = description
except ValueError: # No tags, only the git hash
# prepend 'g' to match with format returned by 'git describe'
git = "g{}".format(*description)
release = "unknown"
dev = None
labels = []
if dev == "0":
dev = None
else:
labels.append(git)
try:
p = subprocess.Popen(["git", "diff", "--quiet"], cwd=distr_root)
except OSError:
labels.append("confused") # This should never happen.
else:
if p.wait() == 1:
labels.append("dirty")
return Version(release, dev, labels)
# TODO: change this logic when there is a git pretty-format
# that gives the same output as 'git describe'.
# Currently we can only tell the tag the current commit is
# pointing to, or its hash (with no version info)
# if it is not tagged.
def get_version_from_git_archive(version_info):
try:
refnames = version_info["refnames"]
git_hash = version_info["git_hash"]
except KeyError:
# These fields are not present if we are running from an sdist.
# Execution should never reach here, though
return None
if git_hash.startswith("$Format") or refnames.startswith("$Format"):
# variables not expanded during 'git archive'
return None
VTAG = "tag: v"
refs = set(r.strip() for r in refnames.split(","))
version_tags = set(r[len(VTAG):] for r in refs if r.startswith(VTAG))
if version_tags:
release, *_ = sorted(version_tags) # prefer e.g. "2.0" over "2.0rc1"
return Version(release, dev=None, labels=None)
else:
return Version("unknown", dev=None, labels=["g{}".format(git_hash)])
__version__ = get_version()
# The following section defines a module global 'cmdclass',
# which can be used from setup.py. The 'package_name' and
# '__version__' module globals are used (but not modified).
def _write_version(fname):
# This could be a hard link, so try to delete it first. Is there any way
# to do this atomically together with opening?
try:
os.remove(fname)
except OSError:
pass
with open(fname, "w") as f:
f.write(
"# This file has been created by setup.py.\n"
"version = '{}'\n".format(__version__)
)
class _build_py(build_py_orig):
def run(self):
super().run()
_write_version(os.path.join(self.build_lib, package_name,
STATIC_VERSION_FILE))
class _sdist(sdist_orig):
def make_release_tree(self, base_dir, files):
super().make_release_tree(base_dir, files)
if _package_root_inside_src:
p = os.path.join("src", package_name)
else:
p = package_name
_write_version(os.path.join(base_dir, p, STATIC_VERSION_FILE))
cmdclass = dict(sdist=_sdist, build_py=_build_py)
......@@ -93,7 +93,8 @@ def run_command(cargs):
modname = 'apkg.commands.%s' % cmd2mod(command)
try:
mod = __import__(modname, fromlist=[''])
except ModuleNotFoundError:
# py35 compat: use ModuleNotFoundError once py3.5 support is dropped
except ImportError:
raise ex.InvalidApkgCommand(command=command)
return mod.run_command(cargs)
......
......@@ -36,8 +36,8 @@ def walk(top, **kwargs):
return os.walk(str(top), **kwargs)
# pylint: disable=redefined-builtin
# this function copies upstream arg names 1:1
# pylint: disable=redefined-builtin
def unpack_archive(filename, extract_dir=None, format=None):
# shutil supports pathlib starting with python 3.7
if extract_dir:
......
......@@ -12,9 +12,9 @@ RUN apt-get update -qqq
RUN apt-get upgrade -y -qqq
# packaging deps
RUN apt-get install -y -qqq dpkg-dev debhelper-compat dh-python python3-all python3-pip
RUN apt-get install -y -qqq dpkg-dev debhelper-compat dh-python python3-all python3-setuptools python3-build python3-pip python3-venv
# basic deps
RUN apt-get install -y -qqq python3-pbr python3-jinja2 python3-distro python3-docopt python3-blessings python3-requests python3-toml python3-cached-property
RUN apt-get install -y -qqq python3-jinja2 python3-distro python3-docopt python3-blessings python3-requests python3-toml python3-cached-property
# non-python deps
RUN apt-get install -y -qqq git
# deps not available in distro
......
......@@ -7,8 +7,7 @@ pkgdesc="cross-distro packaging automation tool"
arch=('any')
url="https://gitlab.nic.cz/packaging/apkg"
license=('GPL3')
depends=('python-pbr'
'python-docopt'
depends=('python-docopt'
'python-distro'
'python-toml'
'python-blessings')
......
......@@ -8,7 +8,6 @@ Build-Depends:
debhelper,
dh-python,
python3-all,
python3-pbr,
python3-setuptools,
Build-Depends-Indep:
python3-blessings
......@@ -17,7 +16,6 @@ Homepage: https://gitlab.nic.cz/packaging/apkg
Package: python3-apkg
Architecture: all
Depends:
python3-pbr,
python3-docopt,
python3-blessings,
python3-jinja2,
......
# apkg installation
`apkg` is going to provide various packages of itself once it's useful, but for now **please use the source**.
`apkg` is going to provide various packages of itself soon,
but for now **please use the source**.
### requirements
## requirements
Required **python modules** are listed in {{ 'requirements.txt' | file_link }}:
You need **Python >= 3.6** and `pip`.
{{ 'requirements.txt' | file_text }}
Python 3.5 is EOL and unofficially supported on best-effort basis in `apkg`
until Debian 9 Stretch LTS EOL June 30, 2022.
Once you have `pbr` (`python3-pbr`) installed, rest of required modules can be
automatically installed by `setup.py`.
Install following downstream packages using your package manager:
Furthermore, `apkg` currently relies on following external tools available
from most distro repos:
* `python3-setuptools`
* `python3-pip`
<<<<<<< HEAD
* `git` to handle `git` repos
=======
Debian/Ubuntu example:
>>>>>>> 1145742 (packaging: refactor and drop pbr)
Python modules needed to build `apkg` docs are listed in {{ 'doc-requirements.txt' | file_link }}:
```
apt install python3-setuptools python3-pip
```
{{ 'doc-requirements.txt' | file_text }}
Other python requirements should be **handled automatically**, they are
listed and briefly explained in {{ 'requirements.txt' | file_link }}.
Python modules needed to build `apkg` docs are listed in
{{ 'doc-requirements.txt' | file_link }}.
### user development install
## install from source
I use this for python projects I develop actively:
Make sure you're in the top `apkg` source dir:
```
git clone https://gitlab.nic.cz/packaging/apkg
cd apkg
```
Then choose one of installation methods below:
### user install
Fastest and recommended way to install from source for CLI usage without affecting the rest of your system is to get
[pipx](https://pipxproject.github.io/pipx/installation/)
and then simply run:
```
pipx install .
```
This installs `apkg` into `virtualenv` without affecting rest of your system
while only exposing `apkg` CLI script.
`pipx install` also features convenient `--editable` mode.
If you're using `apkg` python module or you don't want to use `pipx`, you can
use local user `pip install`:
```
pip install --user .
```
Or the old-fashioned equivalent through `setup.py` but that's using
`easy_install` which is worse than `pip`:
```
python3 setup.py install --user
```
### editable/develop install
For development it's nice when source changes immediately apply and that
can be done with editable/develop install.
Old-fashioned and well tested develop install using `setup.py` has the advantage of making `apkg` module available to other python modules:
```
python3 setup.py develop --user
```
......@@ -38,19 +91,30 @@ and also installs `apkg` script into `~/.local/bin` so make sure you have
`~/.local/bin` in your `$PATH`, possibly before system `bin` paths to override
`apkg` scripts provided by system packages.
When you need CLI only, it's recommended to use `pipx` instead in `--editable` mode:
```
pipx install -e .
```
Upstream python discussions recommend `pip install --editable .` but that has
the fatal flaw of currently not working with local `--user` install. You can
use it inside disposable container or a VM but I'd never taint my system
python installation with a global install like that.
### virtualenv install
If you don't want to taint your system by `develop` method above and/or you
want `apkg` isolated from the rest of your system, you can use `virtualenv`
(`python-virtualenv` package on most distros):
If you don't want `apkg` installation to affect your system but don't want
to/can't use `pipx`, you can use `virtualenv` directly (`python3-venv`
package usually):
```
git clone https://gitlab.nic.cz/packaging/apkg
cd apkg
virtualenv venv
virtualenv3 venv
source venv/bin/activate
python3 setup.py develop
python3 setup.py install
apkg
```
......@@ -59,12 +123,3 @@ You can enter the `venv` later/from other terminals by
```
source apkg/venv/bin/activate
```
You can also do this little trick inside `venv` to avoid conflicts with system `apkg`
packages (assuming `~/bin` is in your `$PATH`):
```
source apkg/venv/bin/activate
ln `which apkg` ~/bin/apkg-dev
apkg-dev
```
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"
......@@ -4,9 +4,9 @@ distro # current distro detection
docopt # nice CLI from docstrings
jinja2 # templating
packaging # version parsing
pbr # packaging/versioning
requests # HTTP for humans™
toml # config files
# OPTIONAL REQUIREMENTS
blessings # terminal colors - recommended!
build # apkg distribution
htmllistparse # upstream version detection
......@@ -5,5 +5,5 @@
DISTDIR=dist
rm -rf "$DISTDIR"
python3 setup.py sdist
python3 -m build --sdist
ls dist/apkg-*.tar.gz
[metadata]
name = apkg
summary = minimalist cross-distro packaging automation tool
description-file = README.md
author = Jakub Ružicka
author-email = jakub.ruzicka@nic.cz
description = minimalist cross-distro packaging automation tool
long_description = file: README.md
long_description_content_type = text/markdown
url = https://gitlab.nic.cz/packaging/apkg
project_urls =
Bug Tracker = https://gitlab.nic.cz/packaging/apkg/-/issues
Docs = https://apkg.readthedocs.io/
classifiers =
Programming Language :: Python :: 3
Intended Audience :: Information Technology
Intended Audience :: System Administrators
Development Status :: 3 - Alpha
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Operating System :: POSIX :: Linux
author = Jakub Ruzicka
author-email = jakub.ruzicka@nic.cz
home-page = https://gitlab.nic.cz/packaging/apkg
keywords = packaging, tool
[global]
setup-hooks = pbr.hooks.setup_hook
Operating System :: OS Independent
Programming Language :: Python :: 3
Topic :: System :: Archiving :: Packaging
Topic :: Utilities
[files]
packages =
apkg
[options]
packages = find:
install_requires =
blessings
cached_property
distro
docopt
htmllistparse
jinja2
packaging
requests
toml
[entry_points]
[options.entry_points]
console_scripts =
apkg = apkg.cli:main
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment