Skip to content

adding pyembed #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ tauri = { version = "2" }
serde = { version = "1", features = ["derive"] }
thiserror = "2"
lazy_static = "1.5.0"
pyo3 = { version = "0.23.3", features=["auto-initialize", "abi3-py39", "generate-import-lib"], optional = true }
pyo3 = { version = "0.24.1", features=["generate-import-lib"], optional = true }
# requires: cargo install --git https://github.com/ntamas/PyOxidizer.git pyoxidizer
pyembed = { git = "https://github.com/ntamas/PyOxidizer.git", optional = true }
rustpython-pylib = { version = "0.4.0" }
rustpython-stdlib = { version = "0.4.0", features = ["threading"] }
rustpython-vm = { version = "0.4.0", features = [
Expand All @@ -32,7 +34,8 @@ dunce = "1.0.5"
tauri-plugin = { version = "2", features = ["build"] }

[features]
venv = []
default = ["venv"] # auto load src-python/.venv
# default = ["venv", "pyo3"] # enable to use pyo3 instead of rustpython
pyo3 = ["dep:pyo3"]
venv = []
pyembed = ["dep:pyembed", "dep:pyo3"] # automatically includes pyo3
pyo3 = ["dep:pyo3"]
89 changes: 74 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ and can get called during application workflow.
https://github.com/tauri-apps/tauri/issues/11823 \
So python code cannot be read on android right now. Android is going to be supported as soon as reading resource files will be fixed.

`✓*` Linux, Windows and MacOS support PyO3 and RustPython as interpreter. Android and IOS
`✓*` Linux, Windows and MacOS support PyO3 and RustPython as interpreter. Android and iOS
currently only support RustPython.
Android and iOS might also be able to run with PyO3 in theory but would require to have CPython
to be compiled for the target platform. I still need to figure out how to
Expand All @@ -44,12 +44,38 @@ to make sure to also ship all your python code. If you use PyO3, you also need t

### Switch from RustPython to PyO3

Using PyO3 will support much more python libraries than RustPython as it is using CPython.
Unfortunately, using PyO3 will use a shared libpython by default, which makes
local development easy but makes
deployment of releases more complicated.

```toml
# src-tauri/Cargo.toml
tauri-plugin-python = { version="0.3", , features = ["pyo3"] }

```

### Switch to PyEmbed

Using PyEmbed will internally also use PyO3. It will perform static linking
of libpython, so deployment of a release binary is much easier.
It may support less libraries than PyO3, but much more than RustPython.
Unfortunately, development is more complicated as rust-analyzer may create issues
and the application may crash during startup if environment variables are not set correctly.
To use it, you need to set `PYO3_CONFIG_FILE`, for example:
```sh
PYO3_CONFIG_FILE=${PWD}/src-tauri/target/pyembed/pyo3-build-config-file.txt npm run tauri dev
```

```toml
# src-tauri/Cargo.toml
tauri-plugin-python = { version="0.3", features = ["pyembed"] }
```

You also need to install `pyoxidizer` first, either by cargo
`cargo install pyoxidizer` or pip `pip install pyoxidizer`.


## Example app

There is a sample Desktop application for Windows/Linux/MacOS using this plugin and vanilla
Expand All @@ -65,10 +91,10 @@ These steps assume that you already have a basic tauri application available. Al
```python
# src-tauri/src-python/main.py
_tauri_plugin_functions = ["greet_python"] # make "greet_python" callable from UI
def greet_python(rust_var)
def greet_python(rust_var):
return str(rust_var) + " from python"
```
- add `"bundle": {"resources": [ "src-python/**/*"],` to `tauri.conf.json` so that python files are bundled with your application
- add `"bundle": {"resources": [ "src-python/"],` to `tauri.conf.json` so that python files are bundled with your application
- add the plugin in your js, so
- add `import { callFunction } from 'tauri-plugin-python-api'`
- add `outputEl.textContent = await callFunction("greet_python", [value])` to get the output of the python function `greet_python` with parameter of js variable `value`
Expand All @@ -85,16 +111,16 @@ Tauri events and calling js from python is currently not supported yet. You woul
- add file `src-tauri/src-python/main.py` and add python code, for example:
```python
# src-tauri/src-python/main.py
def greet_python(rust_var)
def greet_python(rust_var):
return str(rust_var) + " from python"
```
- add `.plugin(tauri_plugin_python::init_and_register(vec!["greet_python"))` to `tauri::Builder::default()`, usually in `src-tauri/src/lib.rs`. This will initialize the plugin and make the python function "greet_python" available from javascript.
- add javascript for python plugin in the index.html file directly or in your somewhere in your javascript application. For vanilla javascript / iife, the modules can be found in `window.__TAURI__.python`. For modern javascript:
- add javascript for python plugin in the index.html file directly or somewhere in your javascript application. For vanilla javascript / iife, the modules can be found in `window.__TAURI__.python`. For modern javascript:
```javascript
import { callFunction } from 'tauri-plugin-python-api'
console.log(await callFunction("greet_python", ["input value"]))
```
-> this will call the python function "greet_python" with parameter "input value". Of course, you can just pass in any available javascript value. This should work with "boolean", "integer", "double", "string", "string[]", "double[]" parameter types.
this will call the python function "greet_python" with parameter "input value". Of course, you can just pass in any available javascript value. This should work with "boolean", "integer", "double", "string", "string[]", "double[]" parameter types.

Alternatively, to have more readable code:
```javascript
Expand All @@ -103,18 +129,51 @@ registerJs("greet_python");
console.log(await call.greet_python("input value"));
```

## Deployment
## Using a venv

Using a python venv is highly recommended when using pip dependencies.
It will be loaded automatically, if the folder is called `.venv`.
It would be recommended to create it in the project root:
```sh
python3 -m venv .venv
source .venv/bin/activate
pip install <your_lib>
```

You need to make sure that the relevant venv folders `include` and `lib` are
copied next to the `src-python` tauri resource folder:

`tauri.conf.json`
```json
"resources": {
"src-python/": "src-python/",
"../.venv/include/": "src-python/.venv/include/",
"../.venv/lib/": "src-python/.venv/lib/"
}
```

You either need to have python installed on the target machine or ship the shared
python library with your package. You also may link the python library statically - PyO3
may do this by default if it finds a static python library. In addition, you need
to copy the python files so that python files are next to the binary.
## Deployment

The file `src-python/main.py` is required for the plugin to work correctly.
The file `src-python/main.py` is always required for the plugin to work correctly.
You may also add additional python files or use a venv environment.
The included resources can be configurable in the `tauri.conf.json` file.
The included resources can be configured in the `tauri.conf.json` file.
You need to make sure that all python files are included in the tauri resource files and that
your resource file structure is similar to the local python file structure.

There are no other extra steps required for **RustPython** as it will be linked statically.

For **PyEmbed**, python will also be linked statically. Internally PyEmbed is using PyO3 too.
PyEmbed will be harder to build, but easier to deploy.
You need to make sure to set all PyO3 variables correctly. This is typically
the environment variable `PYO3_CONFIG_FILE`.
Otherwise, the application will typically crash during startup with an error
`during initializing Python main: Failed to import encodings module`.

For **PyO3**, python will be linked dynamically by default. You either need to
have python installed on the target machine with the same version or ship the shared
python library with your package.
Check the PyO3 documentation for additional info.

Check the tauri and PyO3 documentation for additional info.

## Security considerations
By default, this plugin cannot call arbitrary python code. Python functions can only be called if registered from rust during plugin initialization.
Expand All @@ -136,5 +195,5 @@ It is a different approach to have all tauri functionality completely in python.
This approach here with tauri-plugin-python is more lightweight and it is for you, if you
- still want to write rust code
- already have a tauri application and just need a specific python library
- just want to simply support rare custom plugins
- just want to simply support rare custom tauri plugins
- if you want to embed python code directly in your javascript
39 changes: 39 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,53 @@
// Licensed under MIT License, see License file for more details
// git clone https://github.com/marcomq/tauri-plugin-python

// python3 -m venv .venv
// .venv/bin/activate
// pip install pyoxidizer

const COMMANDS: &[&str] = &[
"run_python",
"register_function",
"call_function",
"read_variable",
];

#[cfg(feature = "pyembed")]
fn init_pyembed() {
let pyoxidizer_exe = if let Ok(path) = std::env::var("PYOXIDIZER_EXE") {
path
} else {
"pyoxidizer".to_string()
};
let out_dir = std::env::var("OUT_DIR").unwrap();
let pyembed_dir = format!("{}/../../../../pyembed", out_dir);
let args = vec!["generate-python-embedding-artifacts", &pyembed_dir];
// Setting PYO3_CONFIG_FILE here wouldn't set it while compiling pyo3
// Therefore, we need to set it manually:
// `PYO3_CONFIG_FILE=${PWD}/src-tauri/target/pyembed/pyo3-build-config-file.txt npm run tauri dev`
// println!("cargo::rustc-env=PYO3_CONFIG_FILE={}/{}", pyembed_dir, "pyo3-build-config-file.txt");
match std::process::Command::new(pyoxidizer_exe)
.args(args)
.status()
{
Ok(status) => {
if !status.success() {
panic!("`pyoxidizer run-build-script` failed");
}
}
Err(e) => panic!(
"`pyoxidizer run-build-script` failed, please install pyoxidizer first: {}/n
cargo install pyoxidizer",
e.to_string()
),
}
}

#[cfg(not(feature = "pyembed"))]
fn init_pyembed() {}

fn main() {
init_pyembed();
tauri_plugin::Builder::new(COMMANDS)
.global_api_script_path("./dist-js/index.iife.js")
.android_path("android")
Expand Down
2 changes: 1 addition & 1 deletion examples/plain-javascript/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ tauri-build = { version = "2.0.2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-python = { path = "../../../" }
tauri-plugin-python = { path = "../../../", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

6 changes: 3 additions & 3 deletions examples/plain-javascript/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
"bundle": {
"active": true,
"targets": "all",
"resources": [
"src-python/**/*"
],
"resources": {
"src-python/": "src-python/"
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
Expand Down
6 changes: 3 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Licensed under MIT License, see License file for more details
// git clone https://github.com/marcomq/tauri-plugin-python

#[cfg(feature = "pyo3")]
#[cfg(any(feature = "pyo3", feature = "pyembed"))]
use pyo3::{prelude::*, PyErr};
use serde::{ser::Serializer, Serialize};

Expand Down Expand Up @@ -35,7 +35,7 @@ impl From<&str> for Error {
}
}

#[cfg(not(feature = "pyo3"))]
#[cfg(all(not(feature = "pyo3"), not(feature = "pyembed")))]
impl From<rustpython_vm::PyRef<rustpython_vm::builtins::PyBaseException>> for Error {
fn from(error: rustpython_vm::PyRef<rustpython_vm::builtins::PyBaseException>) -> Self {
let msg = format!("{:?}", &error);
Expand All @@ -60,7 +60,7 @@ impl From<rustpython_vm::PyRef<rustpython_vm::builtins::PyBaseException>> for Er
}
}

#[cfg(feature = "pyo3")]
#[cfg(any(feature = "pyo3", feature = "pyembed"))]
impl From<PyErr> for Error {
fn from(error: PyErr) -> Self {
let error_msg = match pyo3::Python::with_gil(|py| -> Result<Vec<String>> {
Expand Down
18 changes: 10 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ mod mobile;
mod commands;
mod error;
mod models;
#[cfg(not(feature = "pyo3"))]
#[cfg(all(not(feature = "pyo3"), not(feature = "pyembed")))]
mod py_lib;
#[cfg(feature = "pyo3")]
#[cfg(any(feature = "pyo3", feature = "pyembed"))]
mod py_lib_pyo3;
#[cfg(feature = "pyo3")]
#[cfg(any(feature = "pyo3", feature = "pyembed"))]
use py_lib_pyo3 as py_lib;

pub use error::{Error, Result};
Expand Down Expand Up @@ -87,11 +87,13 @@ fn cleanup_path_for_python(path: &PathBuf) -> String {
}

fn print_path_for_python(path: &PathBuf) -> String {
#[cfg(not(target_os = "windows"))] {
#[cfg(not(target_os = "windows"))]
{
format!("\"{}\"", cleanup_path_for_python(path))
}
#[cfg(target_os = "windows")] {
format!("r\"{}\"", cleanup_path_for_python(path))
#[cfg(target_os = "windows")]
{
format!("r\"{}\"", cleanup_path_for_python(path))
}
}

Expand All @@ -107,8 +109,7 @@ fn init_python(code: String, dir: PathBuf) {
let site_packages = entry.path().join("site-packages");
// use first folder with site-packages for venv, ignore venv version
if Path::exists(site_packages.as_path()) {
sys_pyth_dir
.push(print_path_for_python(&site_packages));
sys_pyth_dir.push(print_path_for_python(&site_packages));
break;
}
}
Expand All @@ -123,6 +124,7 @@ sys.path = sys.path + [{}]
sys_pyth_dir.join(", "),
code
);
py_lib::init();
py_lib::run_python_internal(path_import, "main.py".into())
.unwrap_or_else(|e| panic!("Error initializing main.py:\n\n{e}\n"));
}
Expand Down
4 changes: 2 additions & 2 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct StringRequest {
pub value: String,
}

#[cfg(feature = "pyo3")]
#[cfg(any(feature = "pyo3", feature = "pyembed"))]
#[derive(Debug, Serialize, Deserialize, pyo3::IntoPyObject)]
#[serde(untagged)]
pub enum JsMany {
Expand All @@ -23,7 +23,7 @@ pub enum JsMany {
FloatVec(Vec<f64>),
}

#[cfg(not(feature = "pyo3"))]
#[cfg(all(not(feature = "pyo3"), not(feature = "pyembed")))]
use serde_json::Value as JsMany;

#[derive(Debug, Deserialize, Serialize)]
Expand Down
2 changes: 2 additions & 0 deletions src/py_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ lazy_static! {
static ref GLOBALS: rustpython_vm::scope::Scope = create_globals();
}

pub fn init() {}

pub fn run_python(payload: StringRequest) -> crate::Result<()> {
run_python_internal(payload.value, "<embedded>".into())
}
Expand Down
Loading