Updating Certgrinder

by Tykling


03. oct 2023 04:37 UTC


As you may know I maintain Certgrinder, a kind of LetsEncrypt SSH proxy thing written in Python. There is a Certgrinder client and a Certgrinderd server and together they make it possible to get LetsEncrypt certificates in places and configurations where it would otherwise be tricky, such as tightly isolated environments.

You can read more about the Certgrinder client and server in the documentation on ReadTheDocs.

The Certgrinder client and server both have a few dependencies, one of the primary ones being Cryptography which is used to parse and validate certificates and keys and such. As with most things Cryptography is a moving target and occationally they change something which means Certgrinder breaks. I also have to switch from building with distutils to building with PEP517. This is all part of being a maintainer and it is to be expected when you maintain software over time. Nothing ever stays the same for long.

The following sections describe the changes I made to certgrinder and certgrinderd before releasing v0.18.0.

Switching to PEP517

The certgrinder and certgrinderd Python packages have both been at version 0.17.2 for a while. Until yesterday they were based on the old distutils build system. Most of the Python world has moved to pep517 for building Python packages, and distutils is deprecated and scheduled to be removed in Python 3.12 (which is being released today). Time to switch Certgrinder to pep517!

The main change when switching to PEP517 is the switch from setup.py to pyproject.toml. The pyproject.toml file defines the build system settings but it can also contain settings for other tools like linters and such. While toml is not a very familiar format to me I still really like the idea of putting everything in one file.

Runtime dependencies are now in the dependencies list which looks like this:

dependencies = [
    "dnspython == 2.4.2",
    "PyYAML == 6.0.1",
    "cryptography == 41.0.4",
    "pid == 3.0.4",
]

It is also still possible to specify extras which makes it possible to install the package with extra dependencies if needed. I define three extras in pyproject.toml:

[project.optional-dependencies]
dev = ["twine == 4.0.2", "pre-commit == 3.4.0", "setuptools-scm == 8.0.3"]
test = ["pytest == 7.4.2", "pytest-cov==4.1.0", "tox == 4.11.3"]
docs = ["Sphinx==7.2.6", "sphinx-rtd-theme==1.3.0", "sphinx-argparse==0.4.0"]

To use extras I instruct pip to install them like so if I have the repo checked out:

(venv) user@privat-dev:~/devel/certgrinder/client$ pip install .[dev]
Processing /home/user/devel/certgrinder/client
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Installing backend dependencies ... done
  Preparing metadata (pyproject.toml) ... done
.....output snipped.....

If I want to install from PyPi I just use the name of the package instead:

(venv) user@privat-dev:~/devel/certgrinder/client$ pip install certgrinder[dev]
.....output snipped.....

Building the packages with PEP517 is as easy as it was with distutils. Using the build module it is straightforward:

Using setuptools-scm for Version

All software projects have versions. Typically the version number lives in Git tags as well as multiple different files and it can be a bit of a faff to keep them all in sync. Single-sourcing the version has been the dream for a while and having tried a few of the methods described here I absolutely prefer the last suggestion on that page, using setuptools-scm. setuptools-scm extracts Python package versions from Git (or hg) metadata instead of declaring them as the version argument or in a Git managed file. This means the version of my Python package is the latest Git tag plus some extra if there a commits and changes after the latest tag.

Introducing setuptools-scm made my RELEASE.md file is now considerably shorter because I don't have to change version numbers in a bunch of files when doing a release. Yay! Highly recommended.

Updating Dependencies

I have an extensive testsuite for Certgrinder so updating dependencies is usually trivial. I just update to the latest and run my tests and if they all work then it is probably safe to update that dependency. Writing the tests for 100% coverage was a lot of work, but it is really worth it down the line with all the time saved when updating!

To actually update the dependencies I do pip install --upgrade foo and then note the version and update it in the build files. Before PEP517 it was in setup.py and now after PEP517 it is in pyproject.toml. Githubs Dependabot helps a lot by creating the PRs so I just have to merge them and cut a release if something needs updating.

One of the primary dependencies of both certgrinder and certgrinderd is Cryptography which is used for parsing, reading and converting keys and certificates. It is a great project offering a nice level of abstraction for working with crypto stuff in Python. As time goes by stuff inevitably changes in a project with the size and complexity of Cryptography and sometimes that means consumers have to change too. In this case the ed25519.Ed25519PrivateKey stuff moved from cryptography.hazmat.backends.openssl to cryptography.hazmat.primitives so I change the imports accordingly.

Letting pre-commit Handle Linters

The pre-commit package is a very popular framework for running linters as pre-commit hooks in Git. I use it in all my projects and lately I have been leaning towards letting pre-commit handle updating the versions of linters too. This means I can keep them out of pyproject.toml and it also means I get just 1 PR from the pre-commit bot instead of many (one per linter which needs updating) from Dependabot.

I already used pre-commit in this project so this change was as simple as removing all the linters from my dev dependencies. I did this while converting to pyproject.toml so there is no seperate commit to show. After the change my dev deps look nice and clean - just twine for uploading packages to PyPi, pre-commit it self, and setuptools-scm now:

[project.optional-dependencies]
dev = ["twine == 4.0.2", "pre-commit == 3.4.0", "setuptools-scm == 8.0.3"]

My pre-commit config looks like this:

repos:
  - repo: "https://github.com/asottile/pyupgrade"
    rev: "v3.13.0"
    hooks:
    - id: "pyupgrade"
      args: ["--py38-plus"]
  - repo: "https://github.com/ambv/black"
    rev: "23.9.1"
    hooks:
    - id: "black"
      language_version: "python3.9"
  - repo: "https://github.com/pycqa/flake8"
    rev: "6.1.0"
    hooks:
    - id: "flake8"
  - repo: "https://github.com/pre-commit/mirrors-mypy"
    rev: 'v1.5.1'
    hooks:
    - id: "mypy"
      additional_dependencies: ["types-cryptography", "types-requests", "types-PyYAML"]
      name: "mypy (client/certgrinder)"
      args: ["--strict"]
      files: ^client/
    - id: "mypy"
      additional_dependencies: ["types-cryptography", "types-requests", "types-PyYAML"]
      name: "mypy (server/certgrinderd)"
      args: ["--strict"]
      files: ^server/
  - repo: "https://github.com/pre-commit/mirrors-isort"
    rev: "v5.10.1"
    hooks:
    - id: "isort"
  - repo: "https://github.com/pycqa/pydocstyle"
    rev: "6.3.0"
    hooks:
    - id: "pydocstyle"
  - repo: "local"
    hooks:
      - id: "sphinx-build-manpages"
        name: "sphinx manpage build"
        entry: "make --directory docs/ man"
        language: "system"
        pass_filenames: False
      - id: "sphinx-copy-certgrinder-manpage"
        name: "sphinx certgrinder.8 manpage copy"
        entry: "cp docs/_build/man/certgrinder.8 client/man/"
        language: "system"
        pass_filenames: False
      - id: "sphinx-copy-certgrinderd-manpage"
        name: "sphinx certgrinderd.8 manpage copy"
        entry: "cp docs/_build/man/certgrinderd.8 server/man/"
        language: "system"
        pass_filenames: False
      - id: "sphinx-git-add-manpages"
        name: "sphinx manpage git add"
        entry: "git add client/man server/man"
        language: "system"
        pass_filenames: False

Note: The last bit under repo: local is for building manpages and has nothing to do with linting.

Building Under PEP517, Finding a Bug

Now that everything is updated I am ready to tag the new v0.18.0 release. After tagging I need to build and upload the new packages to PyPi.

Under PEP517 I use the build package to build packages. Building is as simple as running python -m build in the folder containing pyproject.toml. The output when I was building v0.18.0 showed a problem though:

(venv) user@privat-dev:~/devel/certgrinder/client$ python -m build
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (setuptools, setuptools-scm)
* Getting build dependencies for sdist...
running egg_info
writing certgrinder.egg-info/PKG-INFO
writing dependency_links to certgrinder.egg-info/dependency_links.txt
writing entry points to certgrinder.egg-info/entry_points.txt
writing requirements to certgrinder.egg-info/requires.txt
writing top-level names to certgrinder.egg-info/top_level.txt
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'certgrinder.egg-info/SOURCES.txt'
* Building sdist...
running sdist
running egg_info
writing certgrinder.egg-info/PKG-INFO
writing dependency_links to certgrinder.egg-info/dependency_links.txt
writing entry points to certgrinder.egg-info/entry_points.txt
writing requirements to certgrinder.egg-info/requires.txt
writing top-level names to certgrinder.egg-info/top_level.txt
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'certgrinder.egg-info/SOURCES.txt'
running check
creating certgrinder-0.18.0
creating certgrinder-0.18.0/certgrinder
creating certgrinder-0.18.0/certgrinder.egg-info
creating certgrinder-0.18.0/certgrinder/tests
creating certgrinder-0.18.0/man
copying files to certgrinder-0.18.0...
copying LICENSE -> certgrinder-0.18.0
copying MANIFEST.in -> certgrinder-0.18.0
copying README.md -> certgrinder-0.18.0
copying pyproject.toml -> certgrinder-0.18.0
copying certgrinder/__init__.py -> certgrinder-0.18.0/certgrinder
copying certgrinder/_version.py -> certgrinder-0.18.0/certgrinder
copying certgrinder/certgrinder.conf.dist -> certgrinder-0.18.0/certgrinder
copying certgrinder/certgrinder.py -> certgrinder-0.18.0/certgrinder
copying certgrinder.egg-info/PKG-INFO -> certgrinder-0.18.0/certgrinder.egg-info
copying certgrinder.egg-info/SOURCES.txt -> certgrinder-0.18.0/certgrinder.egg-info
copying certgrinder.egg-info/dependency_links.txt -> certgrinder-0.18.0/certgrinder.egg-info
copying certgrinder.egg-info/entry_points.txt -> certgrinder-0.18.0/certgrinder.egg-info
copying certgrinder.egg-info/requires.txt -> certgrinder-0.18.0/certgrinder.egg-info
copying certgrinder.egg-info/top_level.txt -> certgrinder-0.18.0/certgrinder.egg-info
copying certgrinder/tests/__init__.py -> certgrinder-0.18.0/certgrinder/tests
copying certgrinder/tests/test_certgrinder.py -> certgrinder-0.18.0/certgrinder/tests
copying man/certgrinder.8 -> certgrinder-0.18.0/man
Writing certgrinder-0.18.0/setup.cfg
Creating tar archive
removing 'certgrinder-0.18.0' (and everything under it)
* Building wheel from sdist
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (setuptools, setuptools-scm)
* Getting build dependencies for wheel...
running egg_info
writing certgrinder.egg-info/PKG-INFO
writing dependency_links to certgrinder.egg-info/dependency_links.txt
writing entry points to certgrinder.egg-info/entry_points.txt
writing requirements to certgrinder.egg-info/requires.txt
writing top-level names to certgrinder.egg-info/top_level.txt
ERROR setuptools_scm._file_finders.git listing git files failed - pretending there aren't any
reading manifest file 'certgrinder.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'certgrinder.egg-info/SOURCES.txt'
* Installing packages in isolated environment... (wheel)
* Building wheel...
running bdist_wheel
running build
running build_py
creating build
creating build/lib
creating build/lib/certgrinder
copying certgrinder/_version.py -> build/lib/certgrinder
copying certgrinder/certgrinder.py -> build/lib/certgrinder
copying certgrinder/__init__.py -> build/lib/certgrinder
running egg_info
writing certgrinder.egg-info/PKG-INFO
writing dependency_links to certgrinder.egg-info/dependency_links.txt
writing entry points to certgrinder.egg-info/entry_points.txt
writing requirements to certgrinder.egg-info/requires.txt
writing top-level names to certgrinder.egg-info/top_level.txt
ERROR setuptools_scm._file_finders.git listing git files failed - pretending there aren't any
reading manifest file 'certgrinder.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'certgrinder.egg-info/SOURCES.txt'
/tmp/build-env-lkrf4mfl/lib/python3.9/site-packages/setuptools/command/build_py.py:204: _Warning: Package 'certgrinder.tests' is absent from the `packages` configuration.
!!

        ********************************************************************************
        ############################
        # Package would be ignored #
        ############################
        Python recognizes 'certgrinder.tests' as an importable package[^1],
        but it is absent from setuptools' `packages` configuration.

        This leads to an ambiguous overall configuration. If you want to distribute this
        package, please make sure that 'certgrinder.tests' is explicitly added
        to the `packages` configuration field.

        Alternatively, you can also rely on setuptools' discovery methods
        (for example by using `find_namespace_packages(...)`/`find_namespace:`
        instead of `find_packages(...)`/`find:`).

        You can read more about "package discovery" on setuptools documentation page:

        - https://setuptools.pypa.io/en/latest/userguide/package_discovery.html

        If you don't want 'certgrinder.tests' to be distributed and are
        already explicitly excluding 'certgrinder.tests' via
        `find_namespace_packages(...)/find_namespace` or `find_packages(...)/find`,
        you can try to use `exclude_package_data`, or `include-package-data=False` in
        combination with a more fine grained `package-data` configuration.

        You can read more about "package data files" on setuptools documentation page:

        - https://setuptools.pypa.io/en/latest/userguide/datafiles.html


        [^1]: For Python, any directory (with suitable naming) can be imported,
              even if it does not contain any `.py` files.
              On the other hand, currently there is no concept of package data
              directory, all directories are treated like packages.
        ********************************************************************************

!!
  check.warn(importable)
copying certgrinder/certgrinder.conf.dist -> build/lib/certgrinder
creating build/lib/certgrinder/tests
copying certgrinder/tests/__init__.py -> build/lib/certgrinder/tests
copying certgrinder/tests/test_certgrinder.py -> build/lib/certgrinder/tests
installing to build/bdist.linux-x86_64/wheel
running install
running install_lib
creating build/bdist.linux-x86_64
creating build/bdist.linux-x86_64/wheel
creating build/bdist.linux-x86_64/wheel/certgrinder
copying build/lib/certgrinder/_version.py -> build/bdist.linux-x86_64/wheel/certgrinder
copying build/lib/certgrinder/certgrinder.py -> build/bdist.linux-x86_64/wheel/certgrinder
creating build/bdist.linux-x86_64/wheel/certgrinder/tests
copying build/lib/certgrinder/tests/__init__.py -> build/bdist.linux-x86_64/wheel/certgrinder/tests
copying build/lib/certgrinder/tests/test_certgrinder.py -> build/bdist.linux-x86_64/wheel/certgrinder/tests
copying build/lib/certgrinder/certgrinder.conf.dist -> build/bdist.linux-x86_64/wheel/certgrinder
copying build/lib/certgrinder/__init__.py -> build/bdist.linux-x86_64/wheel/certgrinder
running install_egg_info
Copying certgrinder.egg-info to build/bdist.linux-x86_64/wheel/certgrinder-0.18.0-py3.9.egg-info
running install_scripts
creating build/bdist.linux-x86_64/wheel/certgrinder-0.18.0.dist-info/WHEEL
creating '/home/user/devel/certgrinder/client/dist/.tmp-saz0mezc/certgrinder-0.18.0-py3-none-any.whl' and adding 'build/bdist.linux-x86_64/wheel' to it
adding 'certgrinder/__init__.py'
adding 'certgrinder/_version.py'
adding 'certgrinder/certgrinder.conf.dist'
adding 'certgrinder/certgrinder.py'
adding 'certgrinder/tests/__init__.py'
adding 'certgrinder/tests/test_certgrinder.py'
adding 'certgrinder-0.18.0.dist-info/LICENSE'
adding 'certgrinder-0.18.0.dist-info/METADATA'
adding 'certgrinder-0.18.0.dist-info/WHEEL'
adding 'certgrinder-0.18.0.dist-info/entry_points.txt'
adding 'certgrinder-0.18.0.dist-info/top_level.txt'
adding 'certgrinder-0.18.0.dist-info/RECORD'
removing build/bdist.linux-x86_64/wheel
Successfully built certgrinder-0.18.0.tar.gz and certgrinder-0.18.0-py3-none-any.whl
(venv) user@privat-dev:~/devel/certgrinder/client$ 

Including unit tests in packages is not recommended, so rather than including certgrinder.tests I want to exclude it.

Package discovery can be a bit fiddly. Setuptools has both auto-discovery and its related options, as well as different ways to specify packages manually. The issue in this case was the certgrinder.tests package being included as a sub-package when building the certgrinder package.

Both the certgrinder and the certgrinderd packages use what setuptools calls a flat layout (where the package directory is in the same directory as pyproject.toml) as opposed to an src layout (where the package directory is inside an src directory in the same directory as pyproject.toml). Both these layouts are supported by setuptools autodiscovery, which comes preloaded with an exclude list which includes tests and much more. This means that by simply switching to setuptools autodiscovery I get my unit tests excluded automatically. I did have to exclude the man directory where the man-pages live, but that was simple enough:

[tool.setuptools.packages.find]
exclude = ["man*"]

After fixing this the build is less complain-y:

(venv) user@privat-dev:~/devel/certgrinder/client$ python -m build
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (setuptools, setuptools-scm)
* Getting build dependencies for sdist...
running egg_info
writing certgrinder.egg-info/PKG-INFO
writing dependency_links to certgrinder.egg-info/dependency_links.txt
writing entry points to certgrinder.egg-info/entry_points.txt
writing requirements to certgrinder.egg-info/requires.txt
writing top-level names to certgrinder.egg-info/top_level.txt
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'certgrinder.egg-info/SOURCES.txt'
* Building sdist...
running sdist
running egg_info
writing certgrinder.egg-info/PKG-INFO
writing dependency_links to certgrinder.egg-info/dependency_links.txt
writing entry points to certgrinder.egg-info/entry_points.txt
writing requirements to certgrinder.egg-info/requires.txt
writing top-level names to certgrinder.egg-info/top_level.txt
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'certgrinder.egg-info/SOURCES.txt'
running check
creating certgrinder-0.18.1.dev7+g822fbd5
creating certgrinder-0.18.1.dev7+g822fbd5/certgrinder
creating certgrinder-0.18.1.dev7+g822fbd5/certgrinder.egg-info
creating certgrinder-0.18.1.dev7+g822fbd5/certgrinder/tests
creating certgrinder-0.18.1.dev7+g822fbd5/man
copying files to certgrinder-0.18.1.dev7+g822fbd5...
copying LICENSE -> certgrinder-0.18.1.dev7+g822fbd5
copying MANIFEST.in -> certgrinder-0.18.1.dev7+g822fbd5
copying README.md -> certgrinder-0.18.1.dev7+g822fbd5
copying pyproject.toml -> certgrinder-0.18.1.dev7+g822fbd5
copying certgrinder/__init__.py -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder
copying certgrinder/_version.py -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder
copying certgrinder/certgrinder.conf.dist -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder
copying certgrinder/certgrinder.py -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder
copying certgrinder.egg-info/PKG-INFO -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder.egg-info
copying certgrinder.egg-info/SOURCES.txt -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder.egg-info
copying certgrinder.egg-info/dependency_links.txt -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder.egg-info
copying certgrinder.egg-info/entry_points.txt -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder.egg-info
copying certgrinder.egg-info/requires.txt -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder.egg-info
copying certgrinder.egg-info/top_level.txt -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder.egg-info
copying certgrinder/tests/__init__.py -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder/tests
copying certgrinder/tests/test_certgrinder.py -> certgrinder-0.18.1.dev7+g822fbd5/certgrinder/tests
copying man/certgrinder.8 -> certgrinder-0.18.1.dev7+g822fbd5/man
Writing certgrinder-0.18.1.dev7+g822fbd5/setup.cfg
Creating tar archive
removing 'certgrinder-0.18.1.dev7+g822fbd5' (and everything under it)
* Building wheel from sdist
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (setuptools, setuptools-scm)
* Getting build dependencies for wheel...
running egg_info
writing certgrinder.egg-info/PKG-INFO
writing dependency_links to certgrinder.egg-info/dependency_links.txt
writing entry points to certgrinder.egg-info/entry_points.txt
writing requirements to certgrinder.egg-info/requires.txt
writing top-level names to certgrinder.egg-info/top_level.txt
ERROR setuptools_scm._file_finders.git listing git files failed - pretending there aren't any
reading manifest file 'certgrinder.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'certgrinder.egg-info/SOURCES.txt'
* Installing packages in isolated environment... (wheel)
* Building wheel...
running bdist_wheel
running build
running build_py
creating build
creating build/lib
creating build/lib/certgrinder
copying certgrinder/_version.py -> build/lib/certgrinder
copying certgrinder/certgrinder.py -> build/lib/certgrinder
copying certgrinder/__init__.py -> build/lib/certgrinder
creating build/lib/certgrinder/tests
copying certgrinder/tests/__init__.py -> build/lib/certgrinder/tests
copying certgrinder/tests/test_certgrinder.py -> build/lib/certgrinder/tests
running egg_info
writing certgrinder.egg-info/PKG-INFO
writing dependency_links to certgrinder.egg-info/dependency_links.txt
writing entry points to certgrinder.egg-info/entry_points.txt
writing requirements to certgrinder.egg-info/requires.txt
writing top-level names to certgrinder.egg-info/top_level.txt
ERROR setuptools_scm._file_finders.git listing git files failed - pretending there aren't any
reading manifest file 'certgrinder.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'certgrinder.egg-info/SOURCES.txt'
copying certgrinder/certgrinder.conf.dist -> build/lib/certgrinder
installing to build/bdist.linux-x86_64/wheel
running install
running install_lib
creating build/bdist.linux-x86_64
creating build/bdist.linux-x86_64/wheel
creating build/bdist.linux-x86_64/wheel/certgrinder
copying build/lib/certgrinder/_version.py -> build/bdist.linux-x86_64/wheel/certgrinder
copying build/lib/certgrinder/certgrinder.py -> build/bdist.linux-x86_64/wheel/certgrinder
creating build/bdist.linux-x86_64/wheel/certgrinder/tests
copying build/lib/certgrinder/tests/__init__.py -> build/bdist.linux-x86_64/wheel/certgrinder/tests
copying build/lib/certgrinder/tests/test_certgrinder.py -> build/bdist.linux-x86_64/wheel/certgrinder/tests
copying build/lib/certgrinder/certgrinder.conf.dist -> build/bdist.linux-x86_64/wheel/certgrinder
copying build/lib/certgrinder/__init__.py -> build/bdist.linux-x86_64/wheel/certgrinder
running install_egg_info
Copying certgrinder.egg-info to build/bdist.linux-x86_64/wheel/certgrinder-0.18.1.dev7+g822fbd5-py3.9.egg-info
running install_scripts
creating build/bdist.linux-x86_64/wheel/certgrinder-0.18.1.dev7+g822fbd5.dist-info/WHEEL
creating '/home/user/devel/certgrinder/client/dist/.tmp-9w4umatk/certgrinder-0.18.1.dev7+g822fbd5-py3-none-any.whl' and adding 'build/bdist.linux-x86_64/wheel' to it
adding 'certgrinder/__init__.py'
adding 'certgrinder/_version.py'
adding 'certgrinder/certgrinder.conf.dist'
adding 'certgrinder/certgrinder.py'
adding 'certgrinder/tests/__init__.py'
adding 'certgrinder/tests/test_certgrinder.py'
adding 'certgrinder-0.18.1.dev7+g822fbd5.dist-info/LICENSE'
adding 'certgrinder-0.18.1.dev7+g822fbd5.dist-info/METADATA'
adding 'certgrinder-0.18.1.dev7+g822fbd5.dist-info/WHEEL'
adding 'certgrinder-0.18.1.dev7+g822fbd5.dist-info/entry_points.txt'
adding 'certgrinder-0.18.1.dev7+g822fbd5.dist-info/top_level.txt'
adding 'certgrinder-0.18.1.dev7+g822fbd5.dist-info/RECORD'
removing build/bdist.linux-x86_64/wheel
Successfully built certgrinder-0.18.1.dev7+g822fbd5.tar.gz and certgrinder-0.18.1.dev7+g822fbd5-py3-none-any.whl
(venv) user@privat-dev:~/devel/certgrinder/client$ 

Note setuptools-scm doing good work here, latest tag on this branch is v0.18.0 so setuptools-scm bumps the patch version and also includes the information that there has been 7 commits since the v0.18.0 tag, and the latest has the hash g822fbd5. Good stuff!

After fixing this I tagged v0.18.1 with the fixes and uploaded it to PyPi.

Uploading To PyPi

After building the packages need to be made available on PyPi. I use twine to upload, it usually works great. Since I used it last they made a change so accounts with 2fa activated need to use an API token for uploads. Or maybe I just activated 2fa on my account since last time. Anyway, I went and logged into PyPi and was able to create two new API tokens for certgrinder and certgrinderd.

Uploading to PyPi looks like this:

(venv) user@privat-dev:~/devel/certgrinder/client$ twine upload dist/certgrinder-0.18.1.tar.gz dist/certgrinder-0.18.1-py3-none-any.whl 
Uploading distributions to https://upload.pypi.org/legacy/
Enter your username: __token__
Enter your password: 
Uploading certgrinder-0.18.1-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.0/41.0 kB • 00:00 • 158.0 MB/s
Uploading certgrinder-0.18.1.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 53.8/53.8 kB • 00:00 • 185.4 MB/s

View at:
https://pypi.org/project/certgrinder/0.18.1/
(venv) user@privat-dev:~/devel/certgrinder/client$ 

I repeat the same for the certgrinderd package and then the Python part of the job is done.

Updating the FreeBSD Port

The FreeBSD ports for certgrinder and certgrinderd are maintained outside the official ports tree. I would like to get them into the official tree but for now it hasn't happened.

FreeBSD porting is centered around Makefiles, and the ports collection is so vast that examples for almost anything are readily available. In case of questions I consult the FreeBSD Porters Handbook which has plenty of answers. For Python specific questions there is also #freebsd-python on Libera (IRC) which is always helpful.

The changes needed to switch a FreeBSD port from building with distutils to PEP517 are pretty trivial. I changed the USE_PYTHON line to include pep517 instead of distutils and of course update the actual versions of the package and dependencies. I also update the distinfo file which contains size and checksums for the source distribution files.

The complete diff looks like this:

(venv) user@privat-dev:~/devel/tykports$ PAGER= git diff 0f9b41f4ac4041104cedf8025c81225eb82c5d0c..324fae9c4a0cfa6cc673333b2e3eca69e4bfda17
diff --git a/sysutils/py-certgrinder/Makefile b/sysutils/py-certgrinder/Makefile
index 95b5a39..01f24e8 100644
--- a/sysutils/py-certgrinder/Makefile
+++ b/sysutils/py-certgrinder/Makefile
@@ -2,10 +2,9 @@
 # $FreeBSD$
 
 PORTNAME=	certgrinder
-PORTVERSION=	0.17.2
-PORTREVISION=	16
+PORTVERSION=	0.18.1
 CATEGORIES=	sysutils python
-MASTER_SITES=	CHEESESHOP
+MASTER_SITES=	PYPI
 PKGNAMEPREFIX=	${PYTHON_PKGNAMEPREFIX}
 
 MAINTAINER=	thomas@gibfest.dk
@@ -14,14 +13,17 @@ COMMENT=	Client for getting LetsEncrypt certificates from a Certgrinderd server
 LICENSE=	BSD3CLAUSE
 LICENSE_FILE=	${WRKSRC}/LICENSE
 
-RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}cryptography>=3.3.2:security/py-cryptography@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}dnspython>=1.16.0:dns/py-dnspython@${PY_FLAVOR} \
+BUILD_DEPENDS=	${PYTHON_PKGNAMEPREFIX}setuptools>0:devel/py-setuptools@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}wheel>0:devel/py-wheel@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}setuptools_scm>0:devel/py-setuptools_scm@${PY_FLAVOR}
+
+RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}cryptography>=41.0.4:security/py-cryptography@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}dnspython>=2.4.2:dns/py-dnspython@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}pid>=3.0.4:devel/py-pid@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}yaml>=5.4.1:devel/py-yaml@${PY_FLAVOR}
+		${PYTHON_PKGNAMEPREFIX}yaml>=6.0:devel/py-yaml@${PY_FLAVOR}
 
-# 3.7, 3.8, 3.9, 3.10
-USES=		python:3.7+
-USE_PYTHON=	autoplist distutils
+USES=		python:3.8+
+USE_PYTHON=	autoplist pep517
 
 USERS=		certgrinder
 GROUPS=		certgrinder
diff --git a/sysutils/py-certgrinder/distinfo b/sysutils/py-certgrinder/distinfo
index 953bca6..f3af624 100644
--- a/sysutils/py-certgrinder/distinfo
+++ b/sysutils/py-certgrinder/distinfo
@@ -1,3 +1,3 @@
-TIMESTAMP = 1638043424
-SHA256 (certgrinder-0.17.2.tar.gz) = b96590e7e9c5909e6ef35fdc8a16e8be4bee6dc8c12852947172ebedde9cfa33
-SIZE (certgrinder-0.17.2.tar.gz) = 35267
+TIMESTAMP = 1697010638
+SHA256 (certgrinder-0.18.1.tar.gz) = 9df6315f6f19ee40d06325244183769ba947bbdc4a323b43ad35206c8cd26654
+SIZE (certgrinder-0.18.1.tar.gz) = 47602
diff --git a/sysutils/py-certgrinderd/Makefile b/sysutils/py-certgrinderd/Makefile
index 990811a..4de5b14 100644
--- a/sysutils/py-certgrinderd/Makefile
+++ b/sysutils/py-certgrinderd/Makefile
@@ -2,10 +2,9 @@
 # $FreeBSD$
 
 PORTNAME=	certgrinderd
-PORTVERSION=	0.17.2
-PORTREVISION=	8
+PORTVERSION=	0.18.1
 CATEGORIES=	sysutils python
-MASTER_SITES=	CHEESESHOP
+MASTER_SITES=	PYPI
 PKGNAMEPREFIX=	${PYTHON_PKGNAMEPREFIX}
 
 MAINTAINER=	thomas@gibfest.dk
@@ -14,12 +13,15 @@ COMMENT=	Server for getting LetsEncrypt certificates for Certgrinder clients
 LICENSE=	BSD3CLAUSE
 LICENSE_FILE=	${WRKSRC}/LICENSE
 
+BUILD_DEPENDS=	${PYTHON_PKGNAMEPREFIX}setuptools>0:devel/py-setuptools@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}wheel>0:devel/py-wheel@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}setuptools_scm>0:devel/py-setuptools_scm@${PY_FLAVOR}
+
 RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}certbot>=${ACME_VERSION},1:security/py-certbot@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}yaml>=5.4.1:devel/py-yaml@${PY_FLAVOR}
+		${PYTHON_PKGNAMEPREFIX}yaml>=6.0:devel/py-yaml@${PY_FLAVOR}
 
-# 3.7, 3.8, 3.9, 3.10
-USES=		python:3.7+
-USE_PYTHON=	autoplist distutils
+USES=		python:3.8+
+USE_PYTHON=	autoplist pep517
 
 USERS=		certgrinderd
 GROUPS=		certgrinderd
diff --git a/sysutils/py-certgrinderd/distinfo b/sysutils/py-certgrinderd/distinfo
index ffe9c6e..839f924 100644
--- a/sysutils/py-certgrinderd/distinfo
+++ b/sysutils/py-certgrinderd/distinfo
@@ -1,3 +1,3 @@
-TIMESTAMP = 1638043512
-SHA256 (certgrinderd-0.17.2.tar.gz) = 5a4254ace06dbda5b19ab4d2eb9de2006b2bb6c3b72d294c4376aa9215508062
-SIZE (certgrinderd-0.17.2.tar.gz) = 22499
+TIMESTAMP = 1697010646
+SHA256 (certgrinderd-0.18.1.tar.gz) = 7838f144d37db5c8d7b520e0160cc7bda69aa9cf3b3093c0c8e62b45785d899b
+SIZE (certgrinderd-0.18.1.tar.gz) = 27939
(venv) user@privat-dev:~/devel/tykports$ 

After a test build I committed the changes and started the build on my Poudriere box. I have since updated my own infrastructure and both the client and the server port work as intended.

Final Thoughts

I really like the way the Python ecosystem has been maturing over the last few years. PEP517 and getting the build and packaging stuff streamlined was way overdue. Using setuptools-scm is lovely. New linters are also coming out, I am looking at switching to Ruff which can replace the linters I am currently using, do more stuff, and is way faster. Win/win.

That is it. I think I've covered everything. In some ways this was a trivial change, it was basically just an import which moved. But it was also time to get the PEP517 conversion done. All in all it was a bit of work but not too bad. Hopefully it will be a while before I have to update again.

Donating

I've recently signed up for Github Sponsors meaning it is now easy to sponsor me and my work. If this post or some of my other writing, software or services have helped you then you can consider becoming a sponsor.

Search this blog