This document contains important information about Nicotine+ design decisions and development procedures for maintainers, developers and code contributors alike.
Nicotine+ is written in Python, and is originally based on backend code from the PySoulSeek project created in 2001. Ever since, Nicotine+ has remained a pure Python application. While one could argue that Python is a bad choice due to reason x and y, and that parts of the code could be rewritten in a more performant language, sticking with Python makes it easy to distribute and run Nicotine+ virtually anywhere. This allows us to devote more time towards actual development.
We aim to support Python 3 versions used by the oldest distributions still actively maintained. Once a Python version is no longer used and supported by any distribution, the minimum supported Python version in Nicotine+ should be changed. Currently, the minimum version Nicotine+ supports is 3.5. This version should be dropped once Ubuntu 16.04 reaches EOL in 2021.
Nicotine+ and its predecessors were originally developed with GNU/Linux in mind, at a time when the official Soulseek client only supported Windows. The Nicotine project (from which Nicotine+ was later forked) opted to use GTK as the GUI toolkit, as opposed to wxPython used by PySoulSeek. This decision was made due to various issues in wxPython at the time, such as large memory overhead and long compile/build times. We’re happy with this choice today, and have no plans of switching to another toolkit.
Nicotine+ aims to be as portable as possible, providing access to the Soulseek network for people who can’t use the official Soulseek client. Nicotine+ runs on almost any architecture and system available, and has active users on a plethora of different systems. This also means that the introduction of an external software depencency can cause issues for both packagers and users.
Dependencies preinstalled on a majority of systems, as well as modules included in the Python Standard Library, should be used as much as possible. Avoid introducing “convenient” and “new hotness” dependencies, if the standard library already includes the required functionality to some degree. If a new dependency needs to be introduced, think about the following points:
Prefer pure-Python dependencies, as these can be used on any system and architecture.
Try to find small, maintainable dependencies that can be bundled with the Nicotine+ source code, and give proper attribution. External dependencies can behave surprisingly different on some systems, and be quite outdated on some older systems. Use common sense though; don’t bundle security-critical dependencies, rapidly changing APIs etc.
Profiling code changes from time to time is important, to ensure that Nicotine+ performs well and doesn’t use unnecessary resources. Our goal is to develop a lightweight client than runs well on older hardware and servers, and these can be quite constrained at times.
Addressing performance in Python can be a challenge at times, and there are no straightforward ways of solving all performance issues. These points generally help:
Use better data structures and algorithms for the intended purpose.
Use functions in the Python Standard Library when possible, instead of reimplementing algorithms yourself.
Look for alterative ways of accomplishing a task, search engines help a lot here. Certain modules in the standard library are written in C, and tend to perform better than pure-Python counterparts.
py-spy is an excellent tool for profiling Python applications in real time, and will save you a lot of time.
It is important that all patches pass unit testing. Unfortunately developers make all kinds of changes to their local development environment that can have unintended consequences. This means sometimes tests on the developer’s computer pass when they should not, and other times failing when they should not have.
To properly validate that things are working, continuous integration (CI) is required. This means compiling, performing local in-tree unit tests, installing through the system package manager, and finally testing the actually installed build artifacts to ensure they do what the user expects them to do.
The key thing to remember is that in order to do this properly, this all needs to be done within a realistic end user system that hasn’t been unintentionally modified by a developer. This might mean a chroot container with the help of QEMU and KVM to verify that everything is working as expected. The hermetically sealed test environment validates that the developer’s expected steps for, as an example in the case of a library, compilation, linking, unit testing, and post installation testing are actually replicable.
On Debian based distributions,
autopkgtest implements the DEP-8 standard. To create and use a build image environment for Ubuntu, follow these steps. First install the autopkgtest(1) tools:
sudo apt install autopkgtest
Next create the test image, substituting
amd64 for other releases or architectures:
autopkgtest-buildvm-ubuntu-cloud -r groovy -a amd64
Test your changes on the host architecture in QEMU with KVM support and 8GB of RAM and four CPUs:
autopkgtest --shell-fail --apt-upgrade . -- \ qemu --ram-size=8192 --cpus=4 --show-boot path_to_build_image.img \ --qemu-options='-enable-kvm'
Tests are defined in the test/ folder, and should be expanded to cover larger parts of the client when possible.
As strings change in the Nicotine+ source code, the translation template file should also be updated regularly.
To update the template (.pot) file, run the following command:
python3 setup.py update_pot
Translations should be imported to the GitHub repository regularly, at the very latest before a new Nicotine+ release is tagged.
To do this:
Export all translations in PO format
Add the updated files to the
Commit the files
Also remember to add new translators to the list of translators, located in TRANSLATORS.md and
Nicotine+ tries to follow Semantic Versioning when possible. As cited in the specification:
Given a version number MAJOR.MINOR.PATCH, increment the:
MAJOR version when you make incompatible API changes,
MINOR version when you add functionality in a backwards compatible manner, and
PATCH version when you make backwards compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
Release dates are not set in stone, as Nicotine+ development is done by volunteers in their spare time. However, keep the following points in mind:
Taking too long to release a new Nicotine+ version (e.g. years) will likely end up in Nicotine+ no longer working due to technological advancements, or being dropped from distributions. This already happened when support for Python 2 ended in 2019.
We have no way of delivering updates to all users at the same time. Packagers for various distributions need to package and test new Nicotine+ versions before users receive them. It would be preferable to avoid creating too many releases in a very short period of time.
Releasing large updates can make it harder to pinpoint eventual issues that have been introduced since the previous release.
The following is a step-by-step guide on what a Nicotine+ maintainer should do when releasing a new version of Nicotine+.
Ensure that Nicotine+ works on at least these four systems: Windows, macOS, the oldest Ubuntu version we still support (16.04), as well as the newest Ubuntu version available.
python3 setup.py sdist
Add a new release note entry in NEWS.md. The release notes should contain a user-readable list of noteworthy changes since the last release (not a list of commits), as well as a list of closed bugs on GitHub.
Ensure that the Windows and macOS packages generated by GitHub Actions still work. They can be found here.
Generate a stable PPA release for Ubuntu / Debian. First, ensure that the repository mirror on Launchpad is up to date. Once it is, update the contents of the stable build recipe, replacing the previous commit hash with the one used for the release you tagged on GitHub. Then, generate stable builds by pressing Request build(s).