Build and publish Nim extensions for Python with zero configuration.
Project description
Nuwa Build
Build Python extensions with Nim using zero-configuration tooling.
Status
๐ง Work In Progress: This library is currently under active development. APIs may change, and things might break. Use at your own risk!
Features
- Zero Configuration: Works out of the box with sensible defaults
- Multi-file Projects: Compile multiple Nim files into a single Python extension
- Flexible Configuration: Configure via
pyproject.tomlor CLI arguments - PEP 517/660 Compatible: Build wheels and source distributions
- Editable Installs:
pip install -e .support for development - Watch Mode: Auto-recompile on file changes with
nuwa watch - Auto Dependencies: Automatically install Nimble packages before build
- Testing Support: Includes pytest tests in project template
- Validation: Validates configuration and provides helpful error messages
- Proper Platform Tags: Generates correct wheel tags for your platform
Installation
# Install Nuwa
pip install nuwa-build
# Install nimpy (Nim-Python bridge)
nimble install nimpy
Requirements:
- Python 3.9+
- Nim compiler (must be installed and available in your PATH)
- nimpy library (install via
nimble install nimpy)
Quick Start
Create a New Project
nuwa new my_project
cd my_project
This creates:
my_project/
โโโ pyproject.toml # Python project config
โโโ nim/ # Nim source files
โ โโโ my_project_lib.nim # Main entry point (filename = module name)
โ โโโ helpers.nim # Additional modules
โโโ my_project/ # Python package
โ โโโ __init__.py # Package wrapper
โ โโโ my_project_lib.so # Compiled extension (generated)
โโโ tests/ # Test files
โ โโโ test_my_project.py # Pytest tests
โโโ example.py # Example/test file
โโโ README.md
Build and Test
# Compile debug build
nuwa develop
# Compile release build
nuwa develop --release
# Run example
python example.py
# Run tests (requires pytest)
pip install pytest
pytest
Note: No pip install -e . needed due to flat project layout. You can run python example.py and pytest directly after compiling the default template project.
๐ค AI-Assisted Development
This project includes a skill.md file designed to teach AI coding agents (Cursor, Antigravity, Copilot, etc.) how to work with Nuwa Build.
Why use it?
Standard LLMs often assume Python extensions require setup.py or pip install -e .. The skill.md file provides your agent with the correct context to:
- Understand the flat layout: Knows that
.sofiles are generated directly in the package directory. - Use correct commands: Enforces
nuwa developandnuwa watchinstead of standard pip commands. - Write correct Nim: Reminds the agent to use
includefor shared libraries and{.nuwa_export.}for bindings (which automatically generates type stubs).
How to use:
- Claude Code: Copy the
skill.mdfile into~/.claude/skills/nuwa-build/for global use or<workspace-root>/.claude/skills/nuwa-build/for project-specific use. - Antigravity: Copy the
skill.mdfile into~/.gemini/antigravity/skills/nuwa-build/for global use or<workspace-root>/.agent/skills/nuwa-build/for project-specific use. - Cursor: Copy the
skill.mdfile into~/.cursor/skills/nuwa-build/for global use or<workspace-root>/.cursor/skills/nuwa-build/for project-specific use. - General Chat: Upload or paste
skill.mdinto your context window when starting a new session.
Watch Mode
For development, use watch mode to automatically recompile when you change Nim files:
# Watch for changes and auto-recompile
nuwa watch
# Watch with tests after each compile
nuwa watch --run-tests
# Watch in release mode
nuwa watch --release
Install and Distribute
# Build a wheel
pip install . --no-build-isolation
# Build source distribution
python -m build
Project Structure
Nuwa uses a simple flat layout for easy development:
project/
โโโ pyproject.toml # Configuration
โโโ nim/ # Nim source files
โ โโโ my_package_lib.nim # Main entry point (determines module name)
โ โโโ helpers.nim # Additional modules
โโโ my_package/ # Python package
โ โโโ __init__.py # Package wrapper (can add Python code)
โ โโโ my_package_lib.so # Compiled Nim extension (generated)
โโโ tests/
โโโ test_my_package.py # Pytest tests
The compiled extension is named {module_name}_lib.so to avoid conflicts with the Python package. Your __init__.py imports from it and can add Python wrappers.
Configuration
pyproject.toml
Configure your project in the [tool.nuwa] section:
[build-system]
requires = ["nuwa-build"]
build-backend = "nuwa_build"
[project]
name = "my-package"
version = "0.1.0"
[tool.nuwa]
# Nim source directory (default: "nim")
nim-source = "nim"
# Python module name (default: derived from project name)
module-name = "my_package"
# Internal library name (default: "{module_name}_lib")
lib-name = "my_package_lib"
# Entry point file (default: "{lib_name}.nim")
entry-point = "my_package_lib.nim"
# Output location: "auto", "src", or explicit path
output-location = "auto"
# Additional Nim compiler flags (optional)
nim-flags = []
# Nimble dependencies (auto-installed before build)
nimble-deps = ["nimpy", "cligen >= 1.0.0"]
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
nim-source |
string | "nim" |
Directory containing Nim source files |
module-name |
string | Derived from project name | Python package name |
lib-name |
string | {module_name}_lib |
Internal compiled extension name |
entry-point |
string | {lib_name}.nim |
Main entry point file (relative to nim-source) |
output-location |
string | "auto" |
Where to place compiled extension ("auto", "src", or path) |
nim-flags |
list | [] |
Additional compiler flags |
nimble-deps |
list | [] |
Nimble packages to auto-install before build |
Note: The entry point filename determines the Python module name of the compiled extension. If your entry point is my_package_lib.nim, the module will be importable as my_package_lib.
CLI Commands
nuwa new <path>
Create a new project scaffold:
nuwa new my_project
nuwa new my_project --name custom-name
nuwa develop
Compile the project in-place:
# Debug build
nuwa develop
# Release build
nuwa develop --release
# Override configuration
nuwa develop --module-name my_module
nuwa develop --nim-source my_nim_dir
nuwa develop --entry-point main.nim
nuwa develop --output-dir build/
nuwa develop --nim-flag="-d:danger" --nim-flag="--opt:size"
Note: After running nuwa develop, the compiled extension will be in {module_name}/. You can then run python example.py or pytest directly without any installation step.
nuwa watch
Watch for file changes and automatically recompile:
# Watch for changes and auto-recompile
nuwa watch
# Watch with tests after each compile
nuwa watch --run-tests
# Watch in release mode
nuwa watch --release
# Override configuration
nuwa watch --module-name my_module
nuwa watch --nim-source my_nim_dir
nuwa clean
Clean build artifacts and dependencies:
# Clean everything (dependencies + artifacts)
nuwa clean
# Clean only dependencies (.nimble/ directory)
nuwa clean --deps
# Clean only build artifacts and cache
nuwa clean --artifacts
What gets cleaned:
--deps: Removes the.nimble/directory (local Nimble packages)--artifacts: Removesnimcache/,build/,dist/directories and compiled.so/.pydfiles- (no flags): Cleans both dependencies and artifacts
Entry Point Discovery
If entry-point is not specified, Nuwa will automatically discover the main entry point using this priority:
- Explicit
[tool.nuwa] entry-pointconfiguration {module_name}_lib.nim(matches the lib-name)lib.nim(fallback convention)- First (and only)
.nimfile if only one exists - Error if multiple files found and no clear entry point
Mixing Python and Nim
Your __init__.py can import from the compiled Nim extension and add Python wrappers:
# In my_package/__init__.py
from .my_package_lib import *
__version__ = "0.1.0"
# Example: Wrap Nim functions with Python code
def validate_dataframe(df, column_name):
"""Extract pandas data and pass to Nim for zero-copy validation"""
import numpy as np
from ctypes import c_void_p
# Extract data as numpy array (zero-copy view)
data = df[column_name].to_numpy()
# Get pointer and pass to Nim for validation
result = validate_array_raw(
data.ctypes.data_as(c_void_p),
len(data)
)
return result
This allows you to:
- Use Python to extract/prepare data (e.g., from pandas DataFrames)
- Pass pointers/arrays to Nim for zero-copy processing
- Return results back to Python for formatting
Multi-File Projects
Nim's module system handles dependencies automatically. Use include to add code from other Nim files:
nim/my_package_lib.nim:
import nuwa_sdk # Provides nuwa_export for automatic type stub generation
include helpers # Include helpers.nim from same directory
proc greet(name: string): string {.nuwa_export.} =
return make_greeting(name)
proc add(a: int, b: int): int {.nuwa_export.} =
return a + b
nim/helpers.nim:
proc make_greeting(name: string): string =
return "Hello, " & name & "!"
Compile my_package_lib.nim and both modules are included in the final .so/.pyd file.
Important: Use include (not import) when building shared libraries. The include directive literally includes the code at compile time, while import creates a separate module namespace.
Exporting Functions to Python
You must add the {.nuwa_export.} pragma to any Nim procedure you want to access from Python.
- โ
Exported:
proc add(a: int, b: int): int {.nuwa_export.}- Accessible from Python - โ Not exported:
proc add(a: int, b: int): int- Not accessible from Python
The {.nuwa_export.} pragma does two things:
- Makes the function callable from Python (via the underlying nimpy library)
- Generates type stub information for IDE autocomplete and type checking
Common mistake: Forgetting the pragma means your function won't be available in Python, even though it compiles successfully.
Output Location
The output-location setting controls where the compiled extension is placed:
-
"auto"(default): Uses flat layout - places extension in{module_name}/ -
"src": Explicitly usessrc/{module_name}/(for compatibility with old projects) -
Explicit path: Use a custom output directory
Python Usage
Once compiled and installed, use your Nim extension like any Python module:
import my_package
# Call Nim-compiled functions (imported via __init__.py)
result = my_package.greet("World")
print(result) # "Hello, World!"
sum_result = my_package.add(5, 10)
print(sum_result) # 15
How It Works
- Validation: Checks Nim compiler is installed and config is valid
- Source Discovery: Finds Nim files in the configured directory
- Entry Point Detection: Identifies the main entry point file
- Compilation: Invokes
nim c --app:libwith appropriate flags - Module Path: Adds
--path:{nim_dir}so includes work between files - Output: Generates proper
{lib_name}.so(Linux/Mac) or{lib_name}.pyd(Windows) in the module directory - Ready to use: Module is immediately importable from the project root
Contributing
Contributions are welcome! The codebase is well-organized with:
- Full type hints
- Comprehensive error handling
- Proper logging support
- Context managers for resource management
Troubleshooting
"Nim compiler not found"
Make sure Nim is installed and in your PATH:
nim --version
Install from https://nim-lang.org/install.html if needed.
"cannot open file: nimpy"
You need to install the nimpy library. You can either:
Option 1: Auto-install via configuration (Recommended)
[tool.nuwa]
nimble-deps = ["nimpy"]
Option 2: Manual installation
nimble install nimpy
"nimble package manager not found"
Nimble is installed with Nim. Make sure Nim is properly installed and in your PATH:
nim --version
nimble --version
If nimble is not found, reinstall Nim from https://nim-lang.org/install.html.
"ModuleNotFoundError: No module named 'my_package'"
The module needs to be compiled first. Run:
nuwa develop
Then you can import it directly from the project root. No pip install needed!
For pytest: Make sure you've compiled the extension with nuwa develop first. The flat layout allows pytest to discover the module automatically.
"ValueError: Module name '...' is not a valid Python identifier"
Your project name contains invalid characters for Python modules. Module names can only contain letters, numbers, and underscores, and cannot start with a number. Use the --name option:
nuwa new my-project --name my_valid_name
"Multiple .nim files found in nim/"
Nuwa found multiple .nim files but can't determine which is the entry point. Specify it in pyproject.toml:
[tool.nuwa]
entry-point = "my_entry_file.nim"
Or ensure there's only one .nim file, or name your entry point {module_name}_lib.nim.
License
MIT
Acknowledgments
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nuwa_build-0.2.2.tar.gz.
File metadata
- Download URL: nuwa_build-0.2.2.tar.gz
- Upload date:
- Size: 39.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4bea88c81b128f1baa47dab66c292fa09c098b163abce20cb272e770dba4171e
|
|
| MD5 |
91a10088e20209e1b1341a9f7c7af865
|
|
| BLAKE2b-256 |
ca1f8405c2db4c216fb970b85269c04978540ac3921696c3b7163cbc0ded5224
|
Provenance
The following attestation bundles were made for nuwa_build-0.2.2.tar.gz:
Publisher:
publish.yml on martineastwood/nuwa-build
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nuwa_build-0.2.2.tar.gz -
Subject digest:
4bea88c81b128f1baa47dab66c292fa09c098b163abce20cb272e770dba4171e - Sigstore transparency entry: 832791128
- Sigstore integration time:
-
Permalink:
martineastwood/nuwa-build@998127dfdeb4874ac9ae0946a66b6ec21294ce5c -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/martineastwood
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@998127dfdeb4874ac9ae0946a66b6ec21294ce5c -
Trigger Event:
push
-
Statement type:
File details
Details for the file nuwa_build-0.2.2-py3-none-any.whl.
File metadata
- Download URL: nuwa_build-0.2.2-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34c342abb973e0f8cea3660f9df0e4744117adfe6a007c1858b5066dfdaae070
|
|
| MD5 |
3cfe96279e3bda5f1f7fc07a40b91d44
|
|
| BLAKE2b-256 |
d3ea9d5b4d8f3339abfeefc462d09340c555c00dc0ee694a58ffb323262d226f
|
Provenance
The following attestation bundles were made for nuwa_build-0.2.2-py3-none-any.whl:
Publisher:
publish.yml on martineastwood/nuwa-build
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nuwa_build-0.2.2-py3-none-any.whl -
Subject digest:
34c342abb973e0f8cea3660f9df0e4744117adfe6a007c1858b5066dfdaae070 - Sigstore transparency entry: 832791129
- Sigstore integration time:
-
Permalink:
martineastwood/nuwa-build@998127dfdeb4874ac9ae0946a66b6ec21294ce5c -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/martineastwood
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@998127dfdeb4874ac9ae0946a66b6ec21294ce5c -
Trigger Event:
push
-
Statement type: