Skip to content

Gradle/KSP plugin to compile Kotlin/Native to Python C API

License

Notifications You must be signed in to change notification settings

Martmists-GH/kpy-plugin

Folders and files

NameName
Last commit message
Last commit date

Latest commit

bd2ede0 · Jun 2, 2024
Jun 1, 2024
Jun 2, 2024
Jun 1, 2024
Jun 2, 2024
Jun 2, 2024
Jun 2, 2024
Jun 2, 2024
Jun 1, 2024
Jun 1, 2024
May 29, 2022
Jun 2, 2024
Jun 2, 2024
Jun 1, 2024
Apr 30, 2022
Apr 30, 2022
Sep 16, 2022

Repository files navigation

KPy Plugin

The KPy gradle plugin allows you to write Kotlin/Native code and use it from python.

Note: Modules built with KPy still require XCode when building on macOS, this is a Kotlin/Native limitation.

A huge thank you to the indygreg/python-build-standalone project for providing prebuilt python binaries to build against. This project would be impossible to maintain without it.

Features

Implemented

  • Export Kotlin/Native functions and classes without having to touch the Python API directly
  • Convert between Kotlin and Python types with .toPython() and .toKotlin()
  • Conversions handled mostly automatically
  • Class inheritance mapped to python
  • Generate Python stubs
  • Catch Kotlin exceptions and raise them as Python exceptions

Setup

Change your gradle version to 7.5 (nightly builds only as of writing) Enable the plugin in your build.gradle.kts file:

plugins {
    kotlin("multiplatform") version "2.0.0"
    id("com.martmists.kpy.kpy-plugin") version "1.0.1"
}

kotlin {
    val hostOs = System.getProperty("os.name")
    val isMingwX64 = hostOs.startsWith("Windows")
    val isArm64 = System.getProperty("os.arch") == "aarch64"
    // You can rename the target from `native` to something else, 
    // but make sure to also change setup.py to match this change!
    val nativeTarget = when {
        hostOs == "Mac OS X" && !isArm64 -> macosX64("native")
        hostOs == "Linux" && !isArm64 -> linuxX64("native")
        hostOs == "Mac OS X" && isArm64 -> macosArm64("native")
        hostOs == "Linux" && isArm64 -> linuxArm64("native")
        isMingwX64 -> mingwX64("native")
        else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
    }
}

Use the following setup.py template (note: may be outdated, see kpy-sample for an up-to-date example):

from os.path import dirname, abspath
from platform import system
from setuptools import setup, Extension, find_packages
from subprocess import Popen, PIPE

osname = system()
debug = False  # Debug currently has some issues
dir_name = dirname(abspath(__file__))

if osname == "Linux" or osname == "Darwin":
    gradle_bin = "./gradlew"
else:
    gradle_bin = ".\\gradlew.bat"

# Build the project
proc = Popen([gradle_bin, "build"])
if proc.wait() != 0:
    raise Exception("Build failed")

# Fetch configuration from gradle task
proc = Popen([gradle_bin, "setupMetadata"], stdout=PIPE)
if proc.wait() != 0:
    raise Exception("Failed to fetch metadata")
output = proc.stdout.read().decode()
real_output = output.split("===METADATA START===")[1].split("===METADATA END===")[0]

exec(real_output, globals(), locals())
# Types of variables from gradle metadata
has_stubs: bool
project_name: str
module_name: str
project_version: str
build_dir: str
root_dir: str
target: str

print("name: " + project_name)
print("version: " + project_version)


def snake_case(name):
    return name.replace("-", "_").lower()


def extensions():
    folder = "debugStatic" if debug else "releaseStatic"
    prefix = "_" if has_stubs else ""
    native = Extension(prefix + module_name,
                       sources=[f'{build_dir}/generated/ksp/{target}/{target}Main/resources/entrypoint.cpp'],
                       include_dirs=[f"{build_dir}/bin/{target}/{folder}/"],
                       library_dirs=[f"{build_dir}/bin/{target}/{folder}/"],
                       libraries=[project_name])

    return [native]


with open("README.md", "r") as fh:
    long_description = fh.read()


attrs = {}

if has_stubs:
    stub_root = f'{build_dir}/generated/ksp/{target}/{target}Main/resources'
    attrs["packages"] = find_packages(where=stub_root)
    attrs["package_dir"] = {"": stub_root}
else:
    attrs["packages"] = []

setup(
    name=module_name,
    version=project_version,
    description=long_description,
    ext_modules=extensions(),
    **attrs
)

Configuration

To configure the plugin, you can use the kpy configuration.

kpy {
    // Pass properties to setup.py, the exec() command will pass them to the context
    // Note: the second parameter is an expression, and must be valid python.
    metadata("my_key", "'my' + 'value'")  // in setup.py you can now use my_key and it evaluates to 'myvalue'

    // Specify the python version to build against.
    // Currently supported: [3.9, 3.10]
    pyVersion.set(PythonVersion.Py310)

    // Generate python stubs for the native sources
    // These are stored to `build/generated/ksp/<target>/<target>Main/resources/`
    // Note: these will be overwritten every time you build the project
    generateStubs.set(true)
}