Skip to content

Commit

Permalink
feat: single binary compilation
Browse files Browse the repository at this point in the history
  • Loading branch information
azmeuk committed Jan 25, 2025
1 parent 192c02c commit 3966bf0
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 31 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: release
on:
push:
tags:
- '*.*.*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Install apt dependencies
run: |
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt --yes --quiet install libsasl2-dev python3-dev libldap2-dev libssl-dev slapd ldap-utils
- run: |
export TZ=UTC
uv sync --group release --all-extras --no-dev
uv run pyinstaller canaille.spec
./dist/canaille --version
uv cache prune --ci
- uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
with:
files: |
dist/canaille
18 changes: 18 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ jobs:
uv sync --all-extras
uv run pre-commit run --all-files
bundle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Install apt dependencies
run: |
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt --yes --quiet install libsasl2-dev python3-dev libldap2-dev libssl-dev slapd ldap-utils
- run: |
export TZ=UTC
uv sync --group release --all-extras --no-dev
uv run pyinstaller canaille.spec
./dist/canaille --version
uv cache prune --ci
doc:
runs-on: ubuntu-22.04
steps:
Expand Down
15 changes: 13 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ style:

coverage:
variables:
PYTHON_VERSION: "3.12"
PYTHON_VERSION: "3.13"
image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
stage: test
script:
Expand All @@ -45,8 +45,19 @@ coverage:
- uv run coveralls
- uv cache prune --ci

binary:
variables:
PYTHON_VERSION: "3.13"
image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
stage: test
script:
- uv sync --all-extras --group release
- uv run pyinstaller canaille.spec
- ./dist/canaille --version
- uv cache prune --ci

tests:
needs: ["coverage", "style"]
needs: ["coverage", "style", "binary"]
parallel:
matrix:
- PYTHON_VERSION: ['3.10', '3.11', '3.12']
Expand Down
28 changes: 25 additions & 3 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,28 @@ Documentation translation

.. include:: ../locales/readme.rst

Build a release
---------------

Python package
~~~~~~~~~~~~~~

The Python packaging step is took care of by uv:

.. code-block:: bash
uv build
Binary file
~~~~~~~~~~~

To build a single binary of Canaille, you can use pyinstaller by installing the `release` dependency group:

.. code-block:: bash
uv sync --group release
uv run pyinstaller --name canaille --onefile canaille/commands.py
Publish a new release
---------------------

Expand All @@ -244,9 +266,9 @@ Publish a new release
4. Check that the :ref:`development/changelog:Release notes` section is correctly filled up;
5. Increase the version number in ``pyproject.toml``;
6. Commit with ``git commit``;
7. Build with ``uv build``;
8. Publish on test PyPI with ``uv publish --publish-url https://test.pypi.org/legacy/``;
7. :ref:`Build the packages <development/contributing:Build a release>`;
8. Publish the Python package on test PyPI with ``uv publish --publish-url https://test.pypi.org/legacy/``;
9. Install the test package somewhere with ``pip install --extra-index-url https://test.pypi.org/simple --upgrade canaille``. Check that everything looks fine;
10. Publish on production PyPI ``uv publish``;
10. Publish the Python package on production PyPI ``uv publish``;
11. Tag the commit with ``git tag XX.YY.ZZ``;
12. Push the release commit and the new tag on the repository with ``git push --tags``.
112 changes: 112 additions & 0 deletions canaille.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# -*- mode: python ; coding: utf-8 -*-

import os
import re
from pathlib import Path
import importlib.resources
from canaille.app.i18n import available_language_codes
from canaille import create_app

with create_app({"SECRET_KEY": "foo"}).app_context():
codes = available_language_codes()

with importlib.resources.path('wtforms', 'locale') as locale_path:
wtforms_locale = str(locale_path)

def filter_wtforms_catalogs(item):
dest, _, _ = item
if not dest.startswith("wtforms/locale"):
return True

if Path(dest).suffix != ".mo":
return False

code = dest.split("/")[2][:2]
return code in codes


def filter_babel_catalogs(item):
dest, _, _ = item
if not re.match(r"babel/locale-data/\w+\.dat", dest):
return True

code = Path(dest).stem[:2]
return code in codes


def filter_pycountry_catalogs(item):
dest, _, _ = item
if not re.match(r"pycountry/locales/\w+/LC_MESSAGES/.+\.mo", dest):
return True

code = dest.split("/")[2][:2]
return code in codes


def filter_map_files(item):
dest, _, _ = item
return not dest.endswith(".map")


def filter_faker_providers(item):
dest, _, _ = item
if not re.match(r"faker/providers/\w+/\w+", dest):
return True

code = dest.split("/")[3][:2]
return code in codes

a = Analysis(
['canaille/commands.py'],
pathex=[],
binaries=[],
datas = [
('canaille/backends/sql/migrations', 'canaille/backends/sql/migrations'),
('canaille/templates', 'canaille/templates'),
('canaille/static', 'canaille/static'),
(wtforms_locale, 'wtforms/locale'),
],
hiddenimports=[
"canaille.app.server",
"canaille.backends.memory.backend",
"canaille.backends.sql.backend",
"canaille.backends.ldap.backend",
# TODO: import all passlib handlers?
"passlib.hash",
"passlib.handlers.pbkdf2",
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=1,
)
pyz = PYZ(a.pure)

a.datas = list(filter(filter_wtforms_catalogs, a.datas))
a.datas = list(filter(filter_babel_catalogs, a.datas))
a.datas = list(filter(filter_pycountry_catalogs, a.datas))
a.datas = list(filter(filter_faker_providers, a.datas))
a.datas = list(filter(filter_map_files, a.datas))

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='canaille',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
7 changes: 1 addition & 6 deletions canaille/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime
import importlib
import json
import os
import typing
from contextlib import contextmanager
from math import ceil
Expand Down Expand Up @@ -228,11 +227,7 @@ def setup_backend(app, backend=None, init_backend=None):


def available_backends():
return {
elt.name
for elt in os.scandir(os.path.dirname(__file__))
if elt.is_dir() and os.path.exists(os.path.join(elt, "backend.py"))
}
return {"sql", "memory", "ldap"}


def get_lockout_delay_message(current_lockout_delay):
Expand Down
4 changes: 3 additions & 1 deletion canaille/backends/sql/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ def __init__(self, config):
SQLBackend.engine = create_engine(
self.config["CANAILLE_SQL"]["DATABASE_URI"], echo=False, future=True
)
SQLBackend.alembic = Alembic(metadatas=Base.metadata, engines=SQLBackend.engine)
SQLBackend.alembic = Alembic(
metadatas=Base.metadata, engines=SQLBackend.engine, run_mkdir=False
)

@classmethod
def install(cls, app): # pragma: no cover
Expand Down
7 changes: 7 additions & 0 deletions canaille/commands.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import importlib.metadata
import multiprocessing
import sys

import click
from flask.cli import FlaskGroup
Expand Down Expand Up @@ -35,4 +37,9 @@ def cli():


if __name__ == "__main__": # pragma: no cover
# Needed by pyinstaller (not just on Windows)
# https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html#multi-processing
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
multiprocessing.freeze_support()

cli()
71 changes: 54 additions & 17 deletions doc/tutorial/install.rst
Original file line number Diff line number Diff line change
@@ -1,30 +1,64 @@
Installation
############

.. warning ::
Canaille is under heavy development and may not fit a production environment yet.
The installation of canaille consist in several steps, some of which you can do manually or with command line tool:
The installation of canaille consist in several steps, some of which you can do manually or with command line tools:

Get the code
============

As the moment there is no distribution package for canaille.
However, it can be installed with Python package managers such as ``pip``.
Let us choose a place for the canaille environment, like ``/opt/canaille/env``.
Binaries
--------

Canaille provides a ready-to-use single file executable for Linux.
The binary installation is the easiest way to get a production-ready Canaille release, though this is not the most customizable.
This is generally the recommended method to use Canaille in production.

.. parsed-literal::
wget https://github.com/yaal-coop/canaille/releases/download/\ |version|\ /canaille -o canaille
chmod +x canaille
.. note::

Canaille binaries comes with lesser performances than other installation methods on startup.
This is generally not an issue, since Canaille is used as a long-running service,
but if this is important for you, you might want to choose another installation method.

Linux packages
--------------

At the moment, only NixOS provides a `Canaille package <Canaille_NixOS>`_.
For other distros, you must use a different way to install Canaille.

.. _Canaille_NixOS: https://mynixos.com/nixpkgs/package/canaille

Python package
--------------

Canaille provides a `Python package <Canaille_PyPI>`_ that you can install with package managers like ``uv`` or ``pip``.
This is the recommended method if you want fast CLI performances, if you need to customize the dependencies, or if you want to use Canaille in a development environment.

In the following example, we use a custom virtualenv to install Canaille.
Note that you should customize the ``EXTRAS`` packages, depending on your needs.

.. code-block:: bash
:caption: Canaille installation using a Python virtualenv
export CANAILLE_INSTALL_DIR=/opt/canaille
sudo mkdir --parents "$CANAILLE_INSTALL_DIR"
sudo virtualenv --python=python3 "$CANAILLE_INSTALL_DIR/env"
sudo mkdir --parents /opt/canaille
virtualenv /opt/canaille/env
. /opt/canaille/env/bin/activate
pip install "canaille[EXTRAS]"
canaille --version
# Adapt the package extras at your will:
sudo "$CANAILLE_INSTALL_DIR/env/bin/pip" install "canaille[EXTRAS]"
.. _Canaille_PyPI: https://pypi.org/project/Canaille

.. note::

In the rest of the documentation, we consider that your virtualenv is activated,
and that the ``canaille`` command is available.

Extras
------
~~~~~~

Canaille provides different package options:

Expand Down Expand Up @@ -61,6 +95,10 @@ A configuration file with default values can be initialized with the :ref:`expor
You can then edit your configuration file and tune its values.
Have a look at the :ref:`reference <references/configuration:Parameters>` to know the exhaustive list of available parameters.

.. note::

In the rest of the documentation, we consider that your Canaille instance is configured by one of the available methods (either with a :envvar:`CONFIG` environment var, either with ``.env`` files etc.).

Install
=======

Expand All @@ -69,8 +107,7 @@ Depending on the configured :doc:`database <databases>` it will create the SQL t

.. code-block:: bash
export CONFIG="$CANAILLE_CONF_DIR/config.toml"
"$CANAILLE_INSTALL_DIR/env/bin/canaille" install
canaille install
Check
=====
Expand All @@ -79,4 +116,4 @@ After a manual installation, you can check your configuration file using the :re

.. code-block:: bash
"$CANAILLE_INSTALL_DIR/env/bin/canaille" check
canaille check
Loading

0 comments on commit 3966bf0

Please sign in to comment.