Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 4 additions & 19 deletions .github/workflows/platform-debian-server-build_deb.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
name: Build kolibri-server DEB file
on:
workflow_call:
inputs:
Comment thread
rtibbles marked this conversation as resolved.
tar-file-name:
description: "Name of the Kolibri sdist tar artifact, used to derive the version"
required: true
type: string
outputs:
deb-file-name:
description: "kolibri-server .deb file name"
Expand All @@ -32,22 +27,12 @@ jobs:
- name: Install Debian build tooling
working-directory: platforms/debian-server
run: make install-build-deps
- name: Derive the Kolibri version from the tar artifact
id: version
env:
TAR_FILE_NAME: ${{ inputs.tar-file-name }}
run: |
VERSION="${TAR_FILE_NAME#kolibri-}"
VERSION="${VERSION%.tar.gz}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Building kolibri-server for Kolibri ${VERSION}"
- name: Build the .deb
working-directory: platforms/debian-server
env:
KOLIBRI_VERSION: ${{ steps.version.outputs.version }}
# Run make deb inside the uv env so the workspace kolibri is on PATH for
# the error-pages step; -d skips the Debian build-dependency check since
# we build against that workspace kolibri, not an apt-installed package.
# Run make deb inside the uv env so the workspace kolibri is what the
# error-pages step runs and what the changelog generator imports for the
# version; -d skips the Debian build-dependency check since we build
# against that workspace kolibri, not an apt-installed package.
run: uv run make deb DPKG_BUILDPACKAGE_FLAGS=-d
- name: Identify the built .deb
id: get-deb-filename
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/pr_build_kolibri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ jobs:
ref: v0.16.1
server_deb:
name: Build kolibri-server DEB file
needs: whl
uses: ./.github/workflows/platform-debian-server-build_deb.yml
with:
tar-file-name: ${{ needs.whl.outputs.tar-file-name }}
exe:
name: Build EXE file
needs: whl
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/release_kolibri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,7 @@ jobs:
release_id: ${{ github.event.release.id }}
server_deb:
name: Build kolibri-server DEB file
needs: whl
uses: ./.github/workflows/platform-debian-server-build_deb.yml
with:
tar-file-name: ${{ needs.whl.outputs.tar-file-name }}
exe:
name: Build EXE file
needs: whl
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include CONTRIBUTING.md
include CHANGELOG.md
include LICENSE
include README.md
# Read out of the sdist by an external installer repo — see `writeversion` in the Makefile.
include kolibri/VERSION
recursive-include kolibri/locale *.mo
recursive-include kolibri/locale *.json
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ set-no-uv-python-version-for-tests:
echo "__version__ = version = \"$$VERSION\"" >> kolibri/_version.py; \
echo "Set version to $$VERSION"

# Plaintext copy read out of our sdist by kolibri-installer-debian (pinned
# v0.16.1) — deleting it reddens `deb`. Nothing in this repo reads it.
writeversion:
uv run python -c "import kolibri; print(kolibri.__version__)" > kolibri/VERSION
@echo ""
Expand All @@ -224,7 +226,7 @@ dist: setrequirements writeversion staticdeps staticdeps-cext strip-staticdeps b
ls -l dist

pex:
ls dist/*.whl | while read whlfile; do version=$$(uv run --script ./build_tools/read_whl_version.py $$whlfile); uvx --from "pex==2.1.153" pex $$whlfile --disable-cache -o dist/kolibri-`echo $$version | sed 's/+/_/g'`.pex -m kolibri --python-shebang=/usr/bin/python3; done
ls dist/*.whl | while read whlfile; do version=$$(uv run --script ./build_tools/read_kolibri_version.py $$whlfile); uvx --from "pex==2.1.153" pex $$whlfile --disable-cache -o dist/kolibri-`echo $$version | sed 's/+/_/g'`.pex -m kolibri --python-shebang=/usr/bin/python3; done

# Kolibri plugins packaged under python_packages/. Each entry is the inner
# <dist>/<module> dir (the importable package). kolibri_plugin.py marks a real
Expand Down
33 changes: 33 additions & 0 deletions build_tools/read_kolibri_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# /// script
# requires-python = ">=3.6"
# dependencies = []
# ///
"""Print the Kolibri version from ``kolibri/_version.py`` -- the setuptools-scm
source of truth -- given a directory holding the ``kolibri`` package or a ``.whl``.

Stdlib-only and 3.6-compatible: callers run it under a bare ``python3``.
"""

import os
import sys
import zipfile

VERSION_MODULE = "kolibri/_version.py"


def read_version(path):
if path.endswith(".whl"):
with zipfile.ZipFile(path) as wheel:
source = wheel.read(VERSION_MODULE)
else:
with open(os.path.join(path, VERSION_MODULE), "rb") as version_module:
source = version_module.read()
# _version.py imports nothing and only assigns, so running it is the same as
# importing it -- which is not an option for a member of a wheel.
namespace = {}
exec(source, namespace)
return namespace["__version__"]


if __name__ == "__main__":
print(read_version(sys.argv[1])) # noqa: T201
16 changes: 0 additions & 16 deletions build_tools/read_whl_version.py

This file was deleted.

59 changes: 15 additions & 44 deletions platforms/android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,16 @@ workspace member. Open **`platforms/android/`** (not the monorepo root) as the
Android Studio project — Android Studio maps the enclosing monorepo as the VCS
root above it, so history and `git blame` still work.

The build consumes the shared workspace rather than a standalone checkout, so in
the monorepo prefer these over the standalone commands shown below:

- **Python / `buildPython`:** resolves to the shared root `.venv`. Create it from
the monorepo root with `uv sync --group dev --all-packages` (not a standalone
`uv venv`).
- **Kolibri tar:** stage the workspace-built tar with `make stage-workspace-tar`
— it builds `kolibri-*.tar.gz` from the monorepo and copies it into `tar/`,
replacing the `make get-tar tar=<URL>` download.
The build consumes the shared workspace rather than a standalone checkout:

- **Python / `buildPython`:** resolves to the shared root `.venv`. Create it with
`uv sync --group dev --all-packages` from anywhere in the workspace. Nothing
needs activating — the Makefile and `app/build.gradle` resolve it directly.
- **Chaquopy runtime:** the embedded runtime (`requirements.txt`) stays
Chaquopy-resolved and out of the workspace lock — install it into the shared
`.venv` with `uv pip install -r requirements.txt` as before.
`.venv` with `uv pip install -r requirements.txt`.
- **Kolibri tar:** `make stage-workspace-tar` builds `kolibri-*.tar.gz` from the
monorepo and copies it into `tar/`.

The nested `AGENTS.md` / `CLAUDE.md` in this directory remain the
Android-specific developer docs.
Expand All @@ -39,16 +37,15 @@ Android-specific developer docs.
# 1. Ensure JDK 21 is installed
java -version

# 2. Setup Python environment (recommended)
uv sync --extra build
# 2. Set up the shared workspace Python environment
uv sync --group dev --all-packages
uv pip install -r requirements.txt
source .venv/bin/activate

# 3. Setup Android SDK and emulator
make setup

# 4. Download Kolibri tar file
make get-tar tar=https://github.com/learningequality/kolibri/releases/download/v0.17.0/kolibri-0.17.0.tar.gz
# 4. Build & stage the workspace Kolibri tar
make stage-workspace-tar

# 5. Build!
make kolibri.apk.unsigned
Expand All @@ -67,35 +64,8 @@ Output: `dist/kolibri-*.apk`

- **Python 3.10+** - For build scripts and Chaquopy

### Initial Setup

1. **Clone the repository**
```bash
git clone https://github.com/learningequality/kolibri-installer-android.git
cd kolibri-installer-android
```

2. **Set up Python virtual environment**
```bash
uv venv
source .venv/bin/activate
uv pip install ".[build]" -r requirements.txt
```

3. **Set up Android SDK** (automatically downloads SDK, NDK, emulator)
```bash
make setup
```

4. **Get Kolibri tar file**
```bash
make get-tar tar=<URL_TO_KOLIBRI_TAR>
```

5. **Build the APK**
```bash
make kolibri.apk.unsigned
```
For the Python environment and Kolibri tar, see
[Monorepo & Android Studio](#monorepo--android-studio) above.

## Architecture

Expand Down Expand Up @@ -189,6 +159,7 @@ make kolibri.apk
```bash
make help # Show all available targets
make setup # Setup SDK, NDK, and emulator
make stage-workspace-tar # Build & stage the workspace Kolibri tar into tar/ (no download)
make kolibri.apk.unsigned # Build debug APK
make kolibri.apk # Build release APK (requires signing keys)
make kolibri.aab # Build release AAB for Play Store
Expand Down
18 changes: 11 additions & 7 deletions platforms/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ if (venvPython.exists()) {
def extractedKolibriDir = file('../tar/extracted')
def tarFile = file('../tar').listFiles()?.find { it.name.endsWith('.tar.gz') && it.name.startsWith('kolibri') }

if (tarFile != null && tarFile.exists() && !new File(extractedKolibriDir, 'kolibri/VERSION').exists()) {
if (tarFile != null && tarFile.exists() && !new File(extractedKolibriDir, 'kolibri/_version.py').exists()) {
StreamlineKolibriTask.extract(tarFile, extractedKolibriDir, this)
}

Expand All @@ -104,13 +104,17 @@ def calculatedVersionName
def calculatedVersionCode

if (extractedKolibriDir.exists() && extractedKolibriDir.isDirectory()) {
// Read VERSION_NAME from Kolibri's VERSION file
def kolibriVersionFile = new File(extractedKolibriDir, 'kolibri/VERSION')
if (!kolibriVersionFile.exists()) {
throw new GradleException("Kolibri VERSION file not found at: ${kolibriVersionFile}")
// Read the version with the shared monorepo helper, as versionCode below
// shells out to scripts/version.py. Pre-monorepo tars have no _version.py,
// so the helper fails here: stage one with 'make stage-workspace-tar'.
def versionOutput = new ByteArrayOutputStream()
exec {
commandLine buildPythonExecutable,
file('../../../build_tools/read_kolibri_version.py').absolutePath,
extractedKolibriDir.absolutePath
standardOutput = versionOutput
}

def kolibriVersion = kolibriVersionFile.text.trim()
def kolibriVersion = versionOutput.toString().trim()
def buildType = System.getenv("RELEASE_KEYALIAS") == "LE_RELEASE_KEY" ? "official" :
System.getenv("RELEASE_KEYALIAS") == "LE_DEV_KEY" ? "dev" : "debug"
calculatedVersionName = "${kolibriVersion}-${buildType}"
Expand Down
4 changes: 1 addition & 3 deletions platforms/debian-server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ error-pages:
kolibri manage loadingpage --reload --output-dir $(CURDIR)/error_pages --version-text $(shell dpkg-parsechangelog -SVersion)

# Generate debian/changelog with the .deb version bound to the Kolibri version.
# Set KOLIBRI_VERSION=<pep440> (the PR build parses it from the whl artifact);
# make exports it into the recipe env, where the generator reads it. Unset, the
# generator falls back to the installed kolibri (release/local).
# Set KOLIBRI_VERSION=<pep440> to pin it; make exports it into the recipe env.
changelog:
uv run python scripts/generate_changelog.py

Expand Down
6 changes: 2 additions & 4 deletions platforms/debian-server/scripts/generate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
Kolibri's own .deb: a pre-release or dev segment gains a leading ``~`` so it
sorts *before* the corresponding final release.

Version source (two contexts, one authoritative source per context):
* PR build: the caller passes ``KOLIBRI_VERSION`` parsed from the whl job's
``kolibri-<version>.tar.gz`` artifact — the version actually built.
* release / local: no tar exists, so we fall back to ``import kolibri``.
Version source: ``KOLIBRI_VERSION`` from the environment if set, else ``import
kolibri`` — both resolve the same ``kolibri/_version.py``.
"""

import argparse
Expand Down
19 changes: 17 additions & 2 deletions platforms/desktop-app/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: clean get-whl install-whl clean-whl build-mac-app pyinstaller build-dmg compile-mo needs-version write-version
.PHONY: clean get-whl stage-workspace-whl install-whl clean-whl build-mac-app pyinstaller build-dmg compile-mo needs-version write-version

PYTHON_EXEC := uv run python

Expand All @@ -17,8 +17,11 @@ guard-%:
exit 1; \
fi

# kolibrisrc/ is what PyInstaller ships, so its kolibri/_version.py is the app
# version. $(shell) hides a failed read as an empty one, hence the guard.
needs-version:
$(eval KOLIBRI_VERSION ?= $(shell uv run --extra build python -c "import os; import sys; sys.path = [os.path.abspath('kolibrisrc')] + sys.path; from pkginfo import Installed; print(Installed('kolibri').version)"))
$(eval KOLIBRI_VERSION ?= $(shell $(PYTHON_EXEC) ../../build_tools/read_kolibri_version.py kolibrisrc))
@test -n "$(KOLIBRI_VERSION)" || { echo "No Kolibri version in kolibrisrc/: stage a whl with 'make stage-workspace-whl'"; exit 1; }

# Write the app version (= the bundled Kolibri version) to _version.py so
# PyInstaller bundles it and kolibri_app.__init__ can
Expand Down Expand Up @@ -72,6 +75,18 @@ get-whl: clean-whl
# Call the install-whl target with the clean path
$(MAKE) install-whl whl="$(OUTPUT_PATH)"

# Build the Kolibri whl from the workspace and stage it into whl/. The lookup is
# a shell block, not make's parse-time $(eval), so it sees the freshly built whl.
stage-workspace-whl: clean-whl
$(MAKE) -C ../.. dist
@WORKSPACE_WHL="$$(ls -t ../../dist/kolibri-*.whl 2>/dev/null | head -1)"; \
if [ -z "$$WORKSPACE_WHL" ]; then \
echo "Monorepo 'make dist' produced no kolibri-*.whl in ../../dist"; \
exit 1; \
fi; \
cp "$$WORKSPACE_WHL" whl/; \
$(MAKE) install-whl whl="whl/$$(basename "$$WORKSPACE_WHL")"

dependencies:
PYINSTALLER_COMPILE_BOOTLOADER=1 uv sync --extra build --no-binary-package pyinstaller
$(PYTHON_EXEC) -c "import PyInstaller; import os; os.truncate(os.path.join(PyInstaller.__path__[0], 'hooks', 'rthooks', 'pyi_rth_django.py'), 0)"
Expand Down
16 changes: 9 additions & 7 deletions platforms/desktop-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Cross-platform Kolibri app
```

- **Install Build Dependencies:**
This step installs PyInstaller, pkginfo, and other Python packages required by the build process:
This step installs PyInstaller and other Python packages required by the build process:
```
make dependencies
```
Expand All @@ -59,16 +59,18 @@ If you plan to make changes to the kolibri-app wrapper code and want it recogniz


## Build the Application
The general workflow is to fetch a specific Kolibri Python wheel (`.whl`) and then use PyInstaller to package it.
The general workflow is to stage a Kolibri Python wheel (`.whl`) and then use PyInstaller to package it.

- **Fetch and Prepare the Kolibri Wheel:**
You'll need the URL of the Kolibri `.whl` file for the version you intend to package. You can find release URLs on the [Kolibri GitHub Releases page](https://github.com/learningequality/kolibri/releases).
- **Build and Prepare the Workspace Kolibri Wheel:**
Builds Kolibri from the monorepo workspace and stages it into `whl/`, so the app bundles the Kolibri you have checked out:
```
make get-whl whl="<URL_TO_KOLIBRI_WHL_FILE>"
make stage-workspace-whl
```
**Example:**

- **Or Fetch and Prepare a Kolibri Wheel from a URL:**
Takes the URL of a `.whl`. The version read needs `kolibri/_version.py`, so the wheel must be built from a commit that has it — no [published release](https://github.com/learningequality/kolibri/releases) does yet.
```
make get-whl whl="https://github.com/learningequality/kolibri/releases/download/v0.18.0/kolibri-0.18.0-py2.py3-none-any.whl"
make get-whl whl="<URL_TO_KOLIBRI_WHL_FILE>"
```
This command will:
* Download the wheel into the `whl/` directory.
Expand Down
1 change: 0 additions & 1 deletion platforms/desktop-app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ build = [
"pyinstaller==6.20.0; python_version >= '3.10'",
"dmgbuild==1.6.7; sys_platform == 'darwin' and python_version >= '3.10'",
"wheel; python_version >= '3.10'",
"pkginfo; python_version >= '3.10'",
"attrdict==2.0.1; python_version >= '3.10'",
"polib==1.2.0; python_version >= '3.10'",
# PyInstaller's pkg_resources hooks need setuptools at bundle time; the
Expand Down
29 changes: 29 additions & 0 deletions test/test_read_kolibri_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
import zipfile

from build_tools.read_kolibri_version import read_version

VERSION = "0.19.0.dev0+g1234abc"

# What setuptools-scm's version_file_template emits (pyproject.toml).
SCM_VERSION_PY = (
"# file generated by setuptools-scm\n"
"# don't change, don't track in version control\n"
"__version__ = version = '%s'\n" % VERSION
)


def test_reads_version_from_a_tree(tmp_path):
package_dir = os.path.join(str(tmp_path), "kolibri")
os.makedirs(package_dir)
with open(os.path.join(package_dir, "_version.py"), "w") as version_module:
version_module.write(SCM_VERSION_PY)
assert read_version(str(tmp_path)) == VERSION


def test_reads_version_from_a_wheel(tmp_path):
whl = os.path.join(str(tmp_path), "kolibri-0.19.0-py2.py3-none-any.whl")
with zipfile.ZipFile(whl, "w") as archive:
archive.writestr("kolibri/_version.py", SCM_VERSION_PY)
archive.writestr("kolibri-0.19.0.dist-info/METADATA", "Name: kolibri\n")
assert read_version(whl) == VERSION
Loading
Loading