├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── dist-js └── index.iife.js ├── examples └── plain-javascript │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── package.json │ ├── src-tauri │ ├── .cargo │ │ └── config.toml │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities │ │ └── default.json │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 32x32.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square30x30Logo.png │ │ ├── Square310x310Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── StoreLogo.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── icon.png │ ├── src-python │ │ └── main.py │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── tauri.conf.json │ └── src │ ├── assets │ ├── javascript.svg │ └── tauri.svg │ ├── index.html │ ├── main.js │ └── styles.css ├── guest-js └── index.ts ├── index.d.ts ├── ios ├── .gitignore ├── Package.swift ├── README.md ├── Sources │ └── ExamplePlugin.swift └── Tests │ └── PluginTests │ └── PluginTests.swift ├── package.json ├── permissions ├── autogenerated │ ├── commands │ │ ├── call_function.toml │ │ ├── read_variable.toml │ │ ├── register_function.toml │ │ └── run_python.toml │ └── reference.md ├── default.toml └── schemas │ └── schema.json ├── rollup.config.js ├── src ├── commands.rs ├── desktop.rs ├── error.rs ├── lib.rs ├── mobile.rs ├── models.rs ├── py_lib.rs └── py_lib_pyo3.rs └── tsconfig.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'build' 2 | 3 | # You can also just use this file to build your tauri app via github action 4 | # Just remove the `working-directory` line below and put it in your root folder 5 | 6 | on: workflow_dispatch 7 | 8 | # This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release. 9 | 10 | jobs: 11 | build-tauri: 12 | permissions: 13 | contents: write 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - platform: 'macos-latest' # for Arm based macs (M1 and above). 19 | args: '--target aarch64-apple-darwin' 20 | - platform: 'macos-latest' # for Intel based macs. 21 | args: '--target x86_64-apple-darwin' 22 | - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04. 23 | args: '' 24 | - platform: 'windows-latest' 25 | args: '' 26 | 27 | runs-on: ${{ matrix.platform }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: setup node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: lts/* 35 | 36 | - name: install Rust stable 37 | uses: dtolnay/rust-toolchain@stable 38 | with: 39 | # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. 40 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 41 | 42 | - name: install dependencies (ubuntu only) 43 | if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. 44 | run: | 45 | sudo apt-get update 46 | sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 47 | # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. 48 | # You can remove the one that doesn't apply to your app to speed up the workflow a bit. 49 | 50 | - name: install frontend dependencies 51 | ### !!!! ---------- 52 | # Remove this here, if you want to build a local tauri app 53 | working-directory: ./examples/plain-javascript 54 | ### !!!! ---------- 55 | run: yarn install # change this to npm, pnpm or bun depending on which one you use. 56 | 57 | - uses: tauri-apps/tauri-action@v0 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | with: 61 | tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version. 62 | releaseName: 'App v__VERSION__' 63 | releaseBody: 'See the assets to download this version and install.' 64 | releaseDraft: true 65 | prerelease: false 66 | args: ${{ matrix.args }} 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | .DS_Store 3 | .Thumbs.db 4 | *.sublime* 5 | .idea/ 6 | debug.log 7 | package-lock.json 8 | .vscode/settings.json 9 | yarn.lock 10 | 11 | /.tauri 12 | /target 13 | Cargo.lock 14 | node_modules/ 15 | 16 | dist 17 | dist-js 18 | !dist-js/*.iife.js -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-plugin-python" 3 | version = "0.3.6" 4 | authors = [ "Marco Mengelkoch" ] 5 | description = "A tauri 2 plugin to use python code in the backend." 6 | keywords = ["rust", "python", "tauri", "gui"] 7 | edition = "2021" 8 | rust-version = "1.77.2" 9 | exclude = ["/examples", "/webview-dist", "/webview-src", "/node_modules"] 10 | links = "tauri-plugin-python" 11 | license = "MIT" 12 | homepage = "https://github.com/marcomq/tauri-plugin-python" 13 | repository = "https://github.com/marcomq/tauri-plugin-python" 14 | 15 | [dependencies] 16 | tauri = { version = "2" } 17 | serde = { version = "1", features = ["derive"] } 18 | thiserror = "2" 19 | lazy_static = "1.5.0" 20 | pyo3 = { version = "0.23.3", features=["auto-initialize", "generate-import-lib"], optional = true } 21 | rustpython-pylib = { version = "0.4.0" } 22 | rustpython-stdlib = { version = "0.4.0", features = ["threading"] } 23 | rustpython-vm = { version = "0.4.0", features = [ 24 | "importlib", 25 | "serde", 26 | "threading", 27 | ] } 28 | serde_json = "1.0.136" 29 | dunce = "1.0.5" 30 | 31 | [build-dependencies] 32 | tauri-plugin = { version = "2", features = ["build"] } 33 | 34 | [features] 35 | venv = [] 36 | default = ["venv"] # auto load src-python/.venv 37 | # default = ["venv", "pyo3"] # enable to use pyo3 instead of rustpython 38 | pyo3 = ["dep:pyo3"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marco Mengelkoch / marcomq 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tauri Plugin Python 2 | 3 | This [tauri](https://v2.tauri.app/) v2 plugin is supposed to make it easy to use Python as backend code. 4 | It uses [RustPython](https://github.com/RustPython/RustPython) or alternatively [PyO3](https://pyo3.rs) as interpreter to call python from rust. 5 | 6 | RustPython doesn't require python to be installed on the target platform and makes it 7 | therefore easy to deploy your production binary. Unfortunately, it doesn't even support 8 | some usually built-int python libraries and is slower than PyO3/CPython. 9 | PyO3 is supported as optional Cargo feature for desktop applications. 10 | PyO3 uses the usual CPython as interpreter and therefore has a wide compatibility for available python libraries. 11 | It isn't used as default as it requires to make libpython available for the target platform, 12 | which can be complicated, especially for mobile targets. 13 | 14 | The plugin reads by default the file `src-tauri/src-python/main.py` during 15 | startup and runs it immediately. Make sure to add all your python source as tauri resource, 16 | so it is shipped together with your production binaries. Python functions are all registered during plugin initialization 17 | and can get called during application workflow. 18 | 19 | | Platform | Supported | 20 | | -------- | --------- | 21 | | Linux | ✓ | 22 | | Windows | ✓ | 23 | | MacOS | ✓ | 24 | | Android | x* | 25 | | iOS | ✓* | 26 | 27 | `x*` There is currently a known issue on tauri+android that prevents reading files. 28 | https://github.com/tauri-apps/tauri/issues/11823 29 | 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. 30 | 31 | `✓*` Linux, Windows and MacOS support PyO3 and RustPython as interpreter. Android and iOS 32 | currently only support RustPython. 33 | Android and iOS might also be able to run with PyO3 in theory but would require to have CPython 34 | to be compiled for the target platform. I still need to figure out how to 35 | cross compile python and PyO3 for iOS and Android. Ping me if you know how to do that. 36 | 37 | You can use this plugin for fast prototypes or for (early) production code. 38 | It might be possible that you want to use some python library or code that 39 | is not available for rust yet. 40 | In case that you want to ship production software packages, you need 41 | to make sure to also ship all your python code. If you use PyO3, you also need to ship libpython too. 42 | 43 | ### Switch from RustPython to PyO3 44 | Using [PyO3](https://github.com/PyO3/pyo3) will support much more python libraries than RustPython as it is using CPython. 45 | ```toml 46 | # src-tauri/Cargo.toml 47 | tauri-plugin-python = { version="0.3", features = ["pyo3"] } 48 | ``` 49 | Unfortunately, using PyO3 will use a shared libpython by default, which makes 50 | local development easy but makes 51 | deployment of releases more complicated. 52 | Therefore, it may be recommended to either use [pyoxidizer](https://github.com/indygreg/PyOxidizer) to embed libpython statically 53 | or try to ship the dynamic libpython together with your application, for example as part 54 | of the .venv. Check out the [PyO3 documentation](https://pyo3.rs/v0.24.2/building-and-distribution.html) for additional support. 55 | 56 | Example of how to embed libpython statically using PyOxidizer: 57 | 58 | This has just been tested locally on MacOS. It may be possible that this is more complicated and requires additional steps on your environment. 59 | 60 | Install pyoxidizer `pip install pyoxidizer` in a venv and run it on bash: 61 | ```bash 62 | pyoxidizer generate-python-embedding-artifacts src-tauri/target/pyembed 63 | ``` 64 | Then, add it to your cargo config: 65 | ```toml 66 | # src-tauri/.cargo/config.toml 67 | PYO3_CONFIG_FILE = { value = "target/pyembed/pyo3-build-config-file.txt", relative = true } 68 | ``` 69 | You can check if the release binary has some shared libpython references by running `otool -L tauri_app` on MacOs or `ldd tauri_app` on linux. 70 | 71 | ## Example app 72 | 73 | There is a sample Desktop application for Windows/Linux/MacOS using this plugin and vanilla 74 | Javascript in [examples/plain-javascript](https://github.com/marcomq/tauri-plugin-python/tree/main/examples/plain-javascript). 75 | 76 | ## Add the plugin to an existing tauri application 77 | 78 | These steps assume that you already have a basic tauri application available. Alternatively, you can immediately start with the application in "example" directory. 79 | 80 | - run `npm run tauri add python` 81 | - add `src-tauri/src-python/main.py` and modify it according to your needs, for example add 82 | ```python 83 | # src-tauri/src-python/main.py 84 | _tauri_plugin_functions = ["greet_python"] # make "greet_python" callable from UI 85 | def greet_python(rust_var): 86 | return str(rust_var) + " from python" 87 | ``` 88 | - add `"bundle": {"resources": [ "src-python/"],` to `tauri.conf.json` so that python files are bundled with your application 89 | - add the plugin in your js, so 90 | - add `import { callFunction } from 'tauri-plugin-python-api'` 91 | - add `outputEl.textContent = await callFunction("greet_python", [value])` to get the output of the python function `greet_python` with parameter of js variable `value` 92 | 93 | Check the examples for alternative function calls and code sugar. 94 | 95 | Tauri events and calling js from python is currently not supported yet. You would need to use rust for that. 96 | 97 | ## Alternative manual plugin installation 98 | 99 | - `$ cargo add tauri-plugin-python` 100 | - `$ npm install tauri-plugin-python-api` 101 | - modify `permissions:[]` in src-tauri/capabilities/default.json and add "python:default" 102 | - add file `src-tauri/src-python/main.py` and add python code, for example: 103 | ```python 104 | # src-tauri/src-python/main.py 105 | def greet_python(rust_var): 106 | return str(rust_var) + " from python" 107 | ``` 108 | - 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. 109 | - 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: 110 | ```javascript 111 | import { callFunction } from 'tauri-plugin-python-api' 112 | console.log(await callFunction("greet_python", ["input value"])) 113 | ``` 114 | → 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. 115 | 116 | Alternatively, to have more readable code: 117 | ```javascript 118 | import { call, registerJs } from 'tauri-plugin-python-api' 119 | registerJs("greet_python"); 120 | console.log(await call.greet_python("input value")); 121 | ``` 122 | ## Using a venv 123 | 124 | Using a python venv is highly recommended when using pip dependencies. 125 | It will be loaded automatically, if the folder is called `.venv`. 126 | It would be recommended to create it in the project root: 127 | ```sh 128 | python3 -m venv .venv 129 | source .venv/bin/activate # or run the .venv/bin/activate.bat script 130 | pip install 131 | ``` 132 | 133 | You need to make sure that the relevant venv folders `include` and `lib` are 134 | copied next to the `src-python` tauri resource folder: 135 | 136 | `tauri.conf.json` 137 | ```json 138 | "resources": { 139 | "src-python/": "src-python/", 140 | "../.venv/include/": "src-python/.venv/include/", 141 | "../.venv/lib/": "src-python/.venv/lib/" 142 | } 143 | ``` 144 | 145 | ## Deployment 146 | 147 | The file `src-python/main.py` is always required for the plugin to work correctly. 148 | The included resources can be configured in the `tauri.conf.json` file. 149 | You need to make sure that all python files are included in the tauri resource files and that 150 | your resource file structure is similar to the local python file structure. 151 | 152 | There are no other extra steps required for **RustPython** as it will be linked statically. 153 | For **PyO3**, you either need to have python installed on the target machine or ship the shared 154 | python library with your package. You also may link the python library statically, for example by using PyOxidizer. In addition, you need 155 | to copy all additional python files so that python files are next to the binary and should also export the .venv folder, if you are using a venv. 156 | 157 | The included resources can be configurable in the `tauri.conf.json` file. 158 | 159 | Check the tauri and PyO3 documentation for additional info. 160 | 161 | ## Security considerations 162 | 163 | By default, this plugin cannot call arbitrary python code. Python functions can only be called if registered from rust during plugin initialization. 164 | It may still be possible to read values from python. This can be prevented via additional tauri permissions. 165 | 166 | Keep in mind that this plugin could make it possible to run arbitrary python code when using all allow permissions. 167 | It is therefore highly recommended to **make sure the user interface is not accessible by a network URL** in production. 168 | 169 | The "runPython" command is disabled by default via permissions. If enabled, it is possible to 170 | inject python code directly via javascript. 171 | Also, the function "register" is disabled by default. If enabled, it can 172 | add control from javascript which functions can be called. This avoids to modify rust code when changing or adding python code. 173 | Both functions can be enabled during development for rapid prototyping. 174 | 175 | ## Alternatives 176 | 177 | If you already know that you just want to develop completely in python, you might want to take a look at [pytauri](https://github.com/WSH032/pytauri). 178 | It is a different approach to have all tauri functionality completely in python. 179 | 180 | This approach here with tauri-plugin-python is more lightweight and it is for you, if you 181 | - still want to write rust code 182 | - already have a tauri application and just need a specific python library 183 | - just want to simply support rare tauri plugins 184 | - want to embed python code directly in your javascript 185 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | const COMMANDS: &[&str] = &[ 7 | "run_python", 8 | "register_function", 9 | "call_function", 10 | "read_variable", 11 | ]; 12 | 13 | fn main() { 14 | tauri_plugin::Builder::new(COMMANDS) 15 | .global_api_script_path("./dist-js/index.iife.js") 16 | .android_path("android") 17 | .ios_path("ios") 18 | .build(); 19 | } 20 | -------------------------------------------------------------------------------- /dist-js/index.iife.js: -------------------------------------------------------------------------------- 1 | if ('__TAURI__' in window) { 2 | var __TAURI_PLUGIN_PYTHON_API__ = (function (exports) { 3 | 'use strict'; 4 | 5 | /****************************************************************************** 6 | Copyright (c) Microsoft Corporation. 7 | 8 | Permission to use, copy, modify, and/or distribute this software for any 9 | purpose with or without fee is hereby granted. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 12 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 13 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 14 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 15 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 16 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 17 | PERFORMANCE OF THIS SOFTWARE. 18 | ***************************************************************************** */ 19 | /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ 20 | 21 | 22 | typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { 23 | var e = new Error(message); 24 | return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; 25 | }; 26 | 27 | /** 28 | * Sends a message to the backend. 29 | * @example 30 | * ```typescript 31 | * import { invoke } from '@tauri-apps/api/core'; 32 | * await invoke('login', { user: 'tauri', password: 'poiwe3h4r5ip3yrhtew9ty' }); 33 | * ``` 34 | * 35 | * @param cmd The command name. 36 | * @param args The optional arguments to pass to the command. 37 | * @param options The request options. 38 | * @return A promise resolving or rejecting to the backend response. 39 | * 40 | * @since 1.0.0 41 | */ 42 | async function invoke(cmd, args = {}, options) { 43 | return window.__TAURI_INTERNALS__.invoke(cmd, args, options); 44 | } 45 | 46 | /** Tauri Python Plugin 47 | * © Copyright 2024, by Marco Mengelkoch 48 | * Licensed under MIT License, see License file for more details 49 | * git clone https://github.com/marcomq/tauri-plugin-python 50 | **/ 51 | let call = {}; // array of functions 52 | async function runPython(code) { 53 | return await invoke('plugin:python|run_python', { 54 | payload: { 55 | value: code, 56 | }, 57 | }).then((r) => { 58 | return r.value; 59 | }); 60 | } 61 | /** 62 | * Registers function on server and makes it available via `call.{jsFunctionName}` 63 | * @param {string} pythonFunctionCall - The python function call, can contain one dot 64 | * @param {number} [numberOfArgs] - Number of arguments, used for validation in python, use -1 to ignore this value 65 | * @param {string} [jsFunctionName] - Name that is used in javascript: "call.jsFunctionName". Must not contain dots. 66 | */ 67 | async function registerFunction(pythonFunctionCall, numberOfArgs, jsFunctionName) { 68 | if (numberOfArgs !== undefined && numberOfArgs < 0) { 69 | numberOfArgs = undefined; 70 | } 71 | return await invoke('plugin:python|register_function', { 72 | payload: { 73 | pythonFunctionCall, 74 | numberOfArgs 75 | }, 76 | }).then((r) => { 77 | registerJs(pythonFunctionCall, jsFunctionName); 78 | return r.value; 79 | }); 80 | } 81 | /** 82 | * No server invokation - assumes that function has already been registered server-side 83 | * Makes function available as `call.{jsFunctionName}` 84 | * @param {string} pythonFunctionCall - The python function call, can contain one dot 85 | * @param {string} [jsFunctionName] - Name that is used in javascript: "call.jsFunctionName". Must not contain dots. 86 | */ 87 | async function registerJs(pythonFunctionCall, jsFunctionName) { 88 | if (jsFunctionName === undefined) { 89 | jsFunctionName = pythonFunctionCall.replaceAll(".", "_"); 90 | } 91 | call[jsFunctionName] = function (...args) { return callFunction(pythonFunctionCall, args); }; 92 | } 93 | /** 94 | * calling previously registered function 95 | */ 96 | async function callFunction(functionName, args) { 97 | return invoke('plugin:python|call_function', { 98 | payload: { 99 | functionName, 100 | args, 101 | }, 102 | }).then((r) => { 103 | return r.value; 104 | }); 105 | } 106 | /** 107 | * read variable name directly from python 108 | */ 109 | async function readVariable(value) { 110 | return invoke('plugin:python|read_variable', { 111 | payload: { 112 | value, 113 | }, 114 | }).then((r) => { 115 | return r.value; 116 | }); 117 | } 118 | 119 | exports.call = call; 120 | exports.callFunction = callFunction; 121 | exports.readVariable = readVariable; 122 | exports.registerFunction = registerFunction; 123 | exports.registerJs = registerJs; 124 | exports.runPython = runPython; 125 | 126 | return exports; 127 | 128 | })({}); 129 | Object.defineProperty(window.__TAURI__, 'python', { value: __TAURI_PLUGIN_PYTHON_API__ }) } 130 | -------------------------------------------------------------------------------- /examples/plain-javascript/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | src/tauri-plugin-python-api/index.iife.js 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /examples/plain-javascript/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/plain-javascript/README.md: -------------------------------------------------------------------------------- 1 | # Tauri + Python Plugin + Vanilla 2 | 3 | This template should help get you started developing with Tauri and Python Plugin 4 | and Vanilla Javascript 5 | 6 | ## Quick Start 7 | - install rust, python and nodeJs 8 | - run `npm install` in the tauri-plugin-python base path (`cd ../../`) 9 | - run `npm run build` in the tauri-plugin-python base path 10 | - run `npm install` in the example path (`cd examples/plain-javascript`) 11 | - run `npm run tauri dev` to start the application 12 | 13 | To run this sample app on iOS: 14 | - run `npx @tauri-apps/cli plugin ios init` to init ios project files 15 | - run `npm run tauri ios dev` to start the application on iOS in develop mode 16 | 17 | ## Manual modifications on default template to add plugin: 18 | - add `tauri-plugin-python` to Cargo.toml 19 | - add `tauri-plugin-python-api` to package.json 20 | - modify `permissions:[]` in src-tauri/capabilities/default.json and add "python:default" 21 | - modify `src-tauri/src-python/main.py` and add python code, for example `def greet_python(..` 22 | - add `.plugin(tauri_plugin_python::init(["greet_python"]))` to `src-tauri/src/lib.rs` 23 | - include javascript for python plugin in the index.html file for example by adding `` 24 | - register python functions in javascript by calling `registerJs("greet_python");` 25 | - calling python function by calling `call.greet_python(...)` 26 | 27 | ## Recommended IDE Setup 28 | 29 | - [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 30 | -------------------------------------------------------------------------------- /examples/plain-javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plain_javascript", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "tauri": "tauri" 8 | }, 9 | "dependencies": { 10 | "@tauri-apps/api": "^2.1.1", 11 | "tauri-plugin-python-api": "file:../../" 12 | }, 13 | "devDependencies": { 14 | "@tauri-apps/cli": "^2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | 2 | [env] 3 | PYO3_CONFIG_FILE = { value = "target/pyembed/pyo3-build-config-file.txt", relative = true } -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plain_javascript" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["Marco Mengelkoch"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "plain_javascript_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2.0.2", features = [] } 19 | 20 | [dependencies] 21 | tauri = { version = "2", features = [] } 22 | tauri-plugin-python = { path = "../../../" } 23 | serde = { version = "1", features = ["derive"] } 24 | serde_json = "1" 25 | 26 | -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Enables the default permissions", 5 | "windows": ["main"], 6 | "permissions": [ 7 | "core:default", 8 | "python:default" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcomq/tauri-plugin-python/e71c134fc075d8c69719e98685f6a63a97cfb54c/examples/plain-javascript/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/src-python/main.py: -------------------------------------------------------------------------------- 1 | # File auto loaded on startup 2 | _tauri_plugin_functions = ["greet_python"] # make these functions callable from UI 3 | 4 | counter = 0 5 | 6 | def greet_python(input): 7 | global counter 8 | counter = counter + 1 9 | print("received: " + str(input)) 10 | s = "" if counter < 2 else "s" 11 | return f'Hello {input}! You\'ve been greeted {counter} time{s} from Python.' -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 2 | #[tauri::command] 3 | fn greet_rust(name: &str) -> String { 4 | format!("Hello, {}! You've been greeted from Rust!", name) 5 | } 6 | 7 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 8 | pub fn run() { 9 | tauri::Builder::default() 10 | .invoke_handler(tauri::generate_handler![greet_rust]) 11 | .plugin(tauri_plugin_python::init()) 12 | .run(tauri::generate_context!()) 13 | .expect("error while running tauri application"); 14 | } 15 | -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | plain_javascript_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /examples/plain-javascript/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "plain-javascript", 4 | "version": "0.1.0", 5 | "identifier": "com.plain-javascript.app", 6 | "build": { 7 | "frontendDist": "../src" 8 | }, 9 | "app": { 10 | "withGlobalTauri": true, 11 | "windows": [ 12 | { 13 | "title": "plain-javascript-example", 14 | "width": 800, 15 | "height": 600 16 | } 17 | ], 18 | "security": { 19 | "csp": null 20 | } 21 | }, 22 | "bundle": { 23 | "active": true, 24 | "targets": "all", 25 | "resources": [ 26 | "src-python/" 27 | ], 28 | "icon": [ 29 | "icons/32x32.png", 30 | "icons/128x128.png", 31 | "icons/128x128@2x.png", 32 | "icons/icon.icns", 33 | "icons/icon.ico" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/plain-javascript/src/assets/javascript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/plain-javascript/src/assets/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/plain-javascript/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri App 8 | 9 | 10 | 11 | 12 |
13 |

Welcome to Tauri

14 | 15 | 30 |

Click on the Tauri logo to learn more about the framework

31 | 32 |
33 | 34 | 35 | 36 |
37 |

38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/plain-javascript/src/main.js: -------------------------------------------------------------------------------- 1 | const tauri = window.__TAURI__ 2 | 3 | let inputField; 4 | let outputEl; 5 | 6 | async function greet_rust() { 7 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 8 | outputEl.textContent = await tauri.core.invoke("greet_rust", { name: inputField.value }); 9 | } 10 | async function greet_python() { 11 | outputEl.textContent = await tauri.python.call.greet_python(inputField.value); 12 | // Alternatively: 13 | // outputEl.textContent = await tauri.python.callFunction("greet_python", [inputField.value]) 14 | } 15 | 16 | window.addEventListener("DOMContentLoaded", () => { 17 | tauri.python.registerJs("greet_python"); // Optional, makes it possible to use "tauri.python.call.greet_python" 18 | inputField = document.querySelector("#input-field"); 19 | outputEl = document.querySelector("#output-element"); 20 | document.querySelector("#callback-form").addEventListener("submit", (e) => { 21 | e.preventDefault(); 22 | switch (e.submitter.value) { 23 | case "submit_rust": 24 | greet_rust(); 25 | break; 26 | case "submit_python": 27 | greet_python(); 28 | break; 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/plain-javascript/src/styles.css: -------------------------------------------------------------------------------- 1 | .logo.vanilla:hover { 2 | filter: drop-shadow(0 0 2em #ffe21c); 3 | } 4 | :root { 5 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 6 | font-size: 16px; 7 | line-height: 24px; 8 | font-weight: 400; 9 | 10 | color: #0f0f0f; 11 | background-color: #f6f6f6; 12 | 13 | font-synthesis: none; 14 | text-rendering: optimizeLegibility; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | -webkit-text-size-adjust: 100%; 18 | } 19 | 20 | .container { 21 | margin: 0; 22 | padding-top: 10vh; 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: center; 26 | text-align: center; 27 | } 28 | 29 | .logo { 30 | height: 6em; 31 | padding: 1.5em; 32 | will-change: filter; 33 | transition: 0.75s; 34 | } 35 | 36 | .logo.tauri:hover { 37 | filter: drop-shadow(0 0 2em #24c8db); 38 | } 39 | 40 | .row { 41 | display: flex; 42 | justify-content: center; 43 | } 44 | 45 | a { 46 | font-weight: 500; 47 | color: #646cff; 48 | text-decoration: inherit; 49 | } 50 | 51 | a:hover { 52 | color: #535bf2; 53 | } 54 | 55 | h1 { 56 | text-align: center; 57 | } 58 | 59 | input, 60 | button { 61 | border-radius: 8px; 62 | border: 1px solid transparent; 63 | padding: 0.6em 1.2em; 64 | font-size: 1em; 65 | font-weight: 500; 66 | font-family: inherit; 67 | color: #0f0f0f; 68 | background-color: #ffffff; 69 | transition: border-color 0.25s; 70 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 71 | } 72 | 73 | button { 74 | cursor: pointer; 75 | } 76 | 77 | button:hover { 78 | border-color: #396cd8; 79 | } 80 | button:active { 81 | border-color: #396cd8; 82 | background-color: #e8e8e8; 83 | } 84 | 85 | input, 86 | button { 87 | outline: none; 88 | } 89 | 90 | #input-field { 91 | margin-right: 5px; 92 | } 93 | 94 | @media (prefers-color-scheme: dark) { 95 | :root { 96 | color: #f6f6f6; 97 | background-color: #2f2f2f; 98 | } 99 | 100 | a:hover { 101 | color: #24c8db; 102 | } 103 | 104 | input, 105 | button { 106 | color: #ffffff; 107 | background-color: #0f0f0f98; 108 | } 109 | button:active { 110 | background-color: #0f0f0f69; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /guest-js/index.ts: -------------------------------------------------------------------------------- 1 | /** Tauri Python Plugin 2 | * © Copyright 2024, by Marco Mengelkoch 3 | * Licensed under MIT License, see License file for more details 4 | * git clone https://github.com/marcomq/tauri-plugin-python 5 | **/ 6 | 7 | import { invoke } from '@tauri-apps/api/core' 8 | 9 | export let call: { [index: string]: Function } = {}; // array of functions 10 | 11 | export async function runPython(code: string): Promise { 12 | return await invoke<{ value: string }>('plugin:python|run_python', { 13 | payload: { 14 | value: code, 15 | }, 16 | }).then((r: any) => { 17 | return r.value; 18 | }); 19 | } 20 | 21 | /** 22 | * Registers function on server and makes it available via `call.{jsFunctionName}` 23 | * @param {string} pythonFunctionCall - The python function call, can contain one dot 24 | * @param {number} [numberOfArgs] - Number of arguments, used for validation in python, use -1 to ignore this value 25 | * @param {string} [jsFunctionName] - Name that is used in javascript: "call.jsFunctionName". Must not contain dots. 26 | */ 27 | export async function registerFunction( 28 | pythonFunctionCall: string, 29 | numberOfArgs?: number, 30 | jsFunctionName?: string): Promise { 31 | if (numberOfArgs !== undefined && numberOfArgs < 0) { 32 | numberOfArgs = undefined; 33 | } 34 | return await invoke<{ value: string }>('plugin:python|register_function', { 35 | payload: { 36 | pythonFunctionCall, 37 | numberOfArgs 38 | }, 39 | }).then((r: any) => { 40 | registerJs(pythonFunctionCall, jsFunctionName); 41 | return r.value; 42 | }); 43 | } 44 | 45 | /** 46 | * No server invokation - assumes that function has already been registered server-side 47 | * Makes function available as `call.{jsFunctionName}` 48 | * @param {string} pythonFunctionCall - The python function call, can contain one dot 49 | * @param {string} [jsFunctionName] - Name that is used in javascript: "call.jsFunctionName". Must not contain dots. 50 | */ 51 | export async function registerJs(pythonFunctionCall: string, jsFunctionName?: string) { 52 | if (jsFunctionName === undefined) { 53 | jsFunctionName = pythonFunctionCall.replaceAll(".", "_"); 54 | } 55 | call[jsFunctionName] = function (...args: any[]) { return callFunction(pythonFunctionCall, args) }; 56 | } 57 | 58 | /** 59 | * calling previously registered function 60 | */ 61 | export async function callFunction(functionName: string, args: any[]): Promise { 62 | return invoke<{ value: string }>('plugin:python|call_function', { 63 | payload: { 64 | functionName, 65 | args, 66 | }, 67 | }).then((r: any) => { 68 | return r.value; 69 | }); 70 | } 71 | 72 | /** 73 | * read variable name directly from python 74 | */ 75 | export async function readVariable(value: string): Promise { 76 | return invoke<{ value: string }>('plugin:python|read_variable', { 77 | payload: { 78 | value, 79 | }, 80 | }).then((r: any) => { 81 | return r.value; 82 | }); 83 | } -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** Tauri Python Plugin 2 | * © Copyright 2024, by Marco Mengelkoch 3 | * Licensed under MIT License, see License file for more details 4 | * git clonehttps://github.com/marcomq/tauri-plugin-python 5 | **/ 6 | export declare let py: { 7 | [index: string]: Function; 8 | }; 9 | export declare function runPython(code: string): Promise; 10 | export declare function registerFunction(functionName: string, numberOfArgs?: number): Promise; 11 | export declare function callFunction(functionName: string, args: any[]): Promise; 12 | export declare function readVariable(value: string): Promise; 13 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | Package.resolved 11 | -------------------------------------------------------------------------------- /ios/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "tauri-plugin-python", 8 | platforms: [ 9 | .macOS(.v10_13), 10 | .iOS(.v13), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "tauri-plugin-python", 16 | type: .static, 17 | targets: ["tauri-plugin-python"]), 18 | ], 19 | dependencies: [ 20 | .package(name: "Tauri", path: "../.tauri/tauri-api") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "tauri-plugin-python", 27 | dependencies: [ 28 | .byName(name: "Tauri") 29 | ], 30 | path: "Sources") 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /ios/README.md: -------------------------------------------------------------------------------- 1 | # Tauri Plugin python 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /ios/Sources/ExamplePlugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftRs 2 | import Tauri 3 | import UIKit 4 | import WebKit 5 | 6 | class PingArgs: Decodable { 7 | let value: String? 8 | } 9 | 10 | class ExamplePlugin: Plugin { 11 | } 12 | 13 | @_cdecl("init_plugin_python") 14 | func initPlugin() -> Plugin { 15 | return ExamplePlugin() 16 | } 17 | -------------------------------------------------------------------------------- /ios/Tests/PluginTests/PluginTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ExamplePlugin 3 | 4 | final class ExamplePluginTests: XCTestCase { 5 | func testExample() throws { 6 | let plugin = ExamplePlugin() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tauri-plugin-python-api", 3 | "version": "0.3.5", 4 | "author": "Marco Mengelkoch", 5 | "description": "Javascript package for tauri 2 python plugin.", 6 | "type": "module", 7 | "types": "./dist-js/index.d.ts", 8 | "main": "./dist-js/index.cjs", 9 | "module": "./dist-js/index.js", 10 | "exports": { 11 | "types": "./dist-js/index.d.ts", 12 | "import": "./dist-js/index.js", 13 | "require": "./dist-js/index.cjs", 14 | "html": "./dist-js/index.iife.js" 15 | }, 16 | "files": [ 17 | "dist-js", 18 | "README.md" 19 | ], 20 | "scripts": { 21 | "build": "rollup -c", 22 | "prepublishOnly": "pnpm build", 23 | "pretest": "pnpm build" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/marcomq/tauri-plugin-python.git" 28 | }, 29 | "license": "MIT", 30 | "homepage": "https://github.com/marcomq/tauri-plugin-python#readme", 31 | "bugs": { 32 | "url": "https://github.com/marcomq/tauri-plugin-python/issues" 33 | }, 34 | "dependencies": { 35 | "@tauri-apps/api": ">=2.0.0-beta.6" 36 | }, 37 | "devDependencies": { 38 | "@rollup/plugin-node-resolve": "^15.3.0", 39 | "@rollup/plugin-typescript": "^11.1.6", 40 | "rollup": "^4.9.6", 41 | "tslib": "^2.6.2", 42 | "typescript": "^5.3.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /permissions/autogenerated/commands/call_function.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-call-function" 7 | description = "Enables the call_function command without any pre-configured scope." 8 | commands.allow = ["call_function"] 9 | 10 | [[permission]] 11 | identifier = "deny-call-function" 12 | description = "Denies the call_function command without any pre-configured scope." 13 | commands.deny = ["call_function"] 14 | -------------------------------------------------------------------------------- /permissions/autogenerated/commands/read_variable.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-read-variable" 7 | description = "Enables the read_variable command without any pre-configured scope." 8 | commands.allow = ["read_variable"] 9 | 10 | [[permission]] 11 | identifier = "deny-read-variable" 12 | description = "Denies the read_variable command without any pre-configured scope." 13 | commands.deny = ["read_variable"] 14 | -------------------------------------------------------------------------------- /permissions/autogenerated/commands/register_function.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-register-function" 7 | description = "Enables the register_function command without any pre-configured scope." 8 | commands.allow = ["register_function"] 9 | 10 | [[permission]] 11 | identifier = "deny-register-function" 12 | description = "Denies the register_function command without any pre-configured scope." 13 | commands.deny = ["register_function"] 14 | -------------------------------------------------------------------------------- /permissions/autogenerated/commands/run_python.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-run-python" 7 | description = "Enables the run_python command without any pre-configured scope." 8 | commands.allow = ["run_python"] 9 | 10 | [[permission]] 11 | identifier = "deny-run-python" 12 | description = "Denies the run_python command without any pre-configured scope." 13 | commands.deny = ["run_python"] 14 | -------------------------------------------------------------------------------- /permissions/autogenerated/reference.md: -------------------------------------------------------------------------------- 1 | ## Default Permission 2 | 3 | Default permissions for the plugin 4 | 5 | - `allow-call-function` 6 | - `allow-read-variable` 7 | 8 | ## Permission Table 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 28 | 29 | 30 | 31 | 36 | 41 | 42 | 43 | 44 | 49 | 54 | 55 | 56 | 57 | 62 | 67 | 68 | 69 | 70 | 75 | 80 | 81 | 82 | 83 | 88 | 93 | 94 | 95 | 96 | 101 | 106 | 107 | 108 | 109 | 114 | 119 | 120 |
IdentifierDescription
19 | 20 | `python:allow-call-function` 21 | 22 | 24 | 25 | Enables the call_function command without any pre-configured scope. 26 | 27 |
32 | 33 | `python:deny-call-function` 34 | 35 | 37 | 38 | Denies the call_function command without any pre-configured scope. 39 | 40 |
45 | 46 | `python:allow-read-variable` 47 | 48 | 50 | 51 | Enables the read_variable command without any pre-configured scope. 52 | 53 |
58 | 59 | `python:deny-read-variable` 60 | 61 | 63 | 64 | Denies the read_variable command without any pre-configured scope. 65 | 66 |
71 | 72 | `python:allow-register-function` 73 | 74 | 76 | 77 | Enables the register_function command without any pre-configured scope. 78 | 79 |
84 | 85 | `python:deny-register-function` 86 | 87 | 89 | 90 | Denies the register_function command without any pre-configured scope. 91 | 92 |
97 | 98 | `python:allow-run-python` 99 | 100 | 102 | 103 | Enables the run_python command without any pre-configured scope. 104 | 105 |
110 | 111 | `python:deny-run-python` 112 | 113 | 115 | 116 | Denies the run_python command without any pre-configured scope. 117 | 118 |
121 | -------------------------------------------------------------------------------- /permissions/default.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | description = "Default permissions for the plugin" 3 | permissions = [ 4 | "allow-call-function", 5 | "allow-read-variable" 6 | ] 7 | # "allow-register-function" is disabled due to the "secure by default" concept. It can be enabled if the UI isn't exposed via network and secured against XSS sufficiently. 8 | # "allow-run-python" is also disabled as it allows to run random python code. It must not be enabled if the UI is exposed via network. 9 | -------------------------------------------------------------------------------- /permissions/schemas/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "PermissionFile", 4 | "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", 5 | "type": "object", 6 | "properties": { 7 | "default": { 8 | "description": "The default permission set for the plugin", 9 | "anyOf": [ 10 | { 11 | "$ref": "#/definitions/DefaultPermission" 12 | }, 13 | { 14 | "type": "null" 15 | } 16 | ] 17 | }, 18 | "set": { 19 | "description": "A list of permissions sets defined", 20 | "type": "array", 21 | "items": { 22 | "$ref": "#/definitions/PermissionSet" 23 | } 24 | }, 25 | "permission": { 26 | "description": "A list of inlined permissions", 27 | "default": [], 28 | "type": "array", 29 | "items": { 30 | "$ref": "#/definitions/Permission" 31 | } 32 | } 33 | }, 34 | "definitions": { 35 | "DefaultPermission": { 36 | "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", 37 | "type": "object", 38 | "required": [ 39 | "permissions" 40 | ], 41 | "properties": { 42 | "version": { 43 | "description": "The version of the permission.", 44 | "type": [ 45 | "integer", 46 | "null" 47 | ], 48 | "format": "uint64", 49 | "minimum": 1.0 50 | }, 51 | "description": { 52 | "description": "Human-readable description of what the permission does. Tauri convention is to use

headings in markdown content for Tauri documentation generation purposes.", 53 | "type": [ 54 | "string", 55 | "null" 56 | ] 57 | }, 58 | "permissions": { 59 | "description": "All permissions this set contains.", 60 | "type": "array", 61 | "items": { 62 | "type": "string" 63 | } 64 | } 65 | } 66 | }, 67 | "PermissionSet": { 68 | "description": "A set of direct permissions grouped together under a new name.", 69 | "type": "object", 70 | "required": [ 71 | "description", 72 | "identifier", 73 | "permissions" 74 | ], 75 | "properties": { 76 | "identifier": { 77 | "description": "A unique identifier for the permission.", 78 | "type": "string" 79 | }, 80 | "description": { 81 | "description": "Human-readable description of what the permission does.", 82 | "type": "string" 83 | }, 84 | "permissions": { 85 | "description": "All permissions this set contains.", 86 | "type": "array", 87 | "items": { 88 | "$ref": "#/definitions/PermissionKind" 89 | } 90 | } 91 | } 92 | }, 93 | "Permission": { 94 | "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", 95 | "type": "object", 96 | "required": [ 97 | "identifier" 98 | ], 99 | "properties": { 100 | "version": { 101 | "description": "The version of the permission.", 102 | "type": [ 103 | "integer", 104 | "null" 105 | ], 106 | "format": "uint64", 107 | "minimum": 1.0 108 | }, 109 | "identifier": { 110 | "description": "A unique identifier for the permission.", 111 | "type": "string" 112 | }, 113 | "description": { 114 | "description": "Human-readable description of what the permission does. Tauri internal convention is to use

headings in markdown content for Tauri documentation generation purposes.", 115 | "type": [ 116 | "string", 117 | "null" 118 | ] 119 | }, 120 | "commands": { 121 | "description": "Allowed or denied commands when using this permission.", 122 | "default": { 123 | "allow": [], 124 | "deny": [] 125 | }, 126 | "allOf": [ 127 | { 128 | "$ref": "#/definitions/Commands" 129 | } 130 | ] 131 | }, 132 | "scope": { 133 | "description": "Allowed or denied scoped when using this permission.", 134 | "allOf": [ 135 | { 136 | "$ref": "#/definitions/Scopes" 137 | } 138 | ] 139 | }, 140 | "platforms": { 141 | "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", 142 | "type": [ 143 | "array", 144 | "null" 145 | ], 146 | "items": { 147 | "$ref": "#/definitions/Target" 148 | } 149 | } 150 | } 151 | }, 152 | "Commands": { 153 | "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", 154 | "type": "object", 155 | "properties": { 156 | "allow": { 157 | "description": "Allowed command.", 158 | "default": [], 159 | "type": "array", 160 | "items": { 161 | "type": "string" 162 | } 163 | }, 164 | "deny": { 165 | "description": "Denied command, which takes priority.", 166 | "default": [], 167 | "type": "array", 168 | "items": { 169 | "type": "string" 170 | } 171 | } 172 | } 173 | }, 174 | "Scopes": { 175 | "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", 176 | "type": "object", 177 | "properties": { 178 | "allow": { 179 | "description": "Data that defines what is allowed by the scope.", 180 | "type": [ 181 | "array", 182 | "null" 183 | ], 184 | "items": { 185 | "$ref": "#/definitions/Value" 186 | } 187 | }, 188 | "deny": { 189 | "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", 190 | "type": [ 191 | "array", 192 | "null" 193 | ], 194 | "items": { 195 | "$ref": "#/definitions/Value" 196 | } 197 | } 198 | } 199 | }, 200 | "Value": { 201 | "description": "All supported ACL values.", 202 | "anyOf": [ 203 | { 204 | "description": "Represents a null JSON value.", 205 | "type": "null" 206 | }, 207 | { 208 | "description": "Represents a [`bool`].", 209 | "type": "boolean" 210 | }, 211 | { 212 | "description": "Represents a valid ACL [`Number`].", 213 | "allOf": [ 214 | { 215 | "$ref": "#/definitions/Number" 216 | } 217 | ] 218 | }, 219 | { 220 | "description": "Represents a [`String`].", 221 | "type": "string" 222 | }, 223 | { 224 | "description": "Represents a list of other [`Value`]s.", 225 | "type": "array", 226 | "items": { 227 | "$ref": "#/definitions/Value" 228 | } 229 | }, 230 | { 231 | "description": "Represents a map of [`String`] keys to [`Value`]s.", 232 | "type": "object", 233 | "additionalProperties": { 234 | "$ref": "#/definitions/Value" 235 | } 236 | } 237 | ] 238 | }, 239 | "Number": { 240 | "description": "A valid ACL number.", 241 | "anyOf": [ 242 | { 243 | "description": "Represents an [`i64`].", 244 | "type": "integer", 245 | "format": "int64" 246 | }, 247 | { 248 | "description": "Represents a [`f64`].", 249 | "type": "number", 250 | "format": "double" 251 | } 252 | ] 253 | }, 254 | "Target": { 255 | "description": "Platform target.", 256 | "oneOf": [ 257 | { 258 | "description": "MacOS.", 259 | "type": "string", 260 | "enum": [ 261 | "macOS" 262 | ] 263 | }, 264 | { 265 | "description": "Windows.", 266 | "type": "string", 267 | "enum": [ 268 | "windows" 269 | ] 270 | }, 271 | { 272 | "description": "Linux.", 273 | "type": "string", 274 | "enum": [ 275 | "linux" 276 | ] 277 | }, 278 | { 279 | "description": "Android.", 280 | "type": "string", 281 | "enum": [ 282 | "android" 283 | ] 284 | }, 285 | { 286 | "description": "iOS.", 287 | "type": "string", 288 | "enum": [ 289 | "iOS" 290 | ] 291 | } 292 | ] 293 | }, 294 | "PermissionKind": { 295 | "type": "string", 296 | "oneOf": [ 297 | { 298 | "description": "Enables the call_function command without any pre-configured scope.", 299 | "type": "string", 300 | "const": "allow-call-function" 301 | }, 302 | { 303 | "description": "Denies the call_function command without any pre-configured scope.", 304 | "type": "string", 305 | "const": "deny-call-function" 306 | }, 307 | { 308 | "description": "Enables the read_variable command without any pre-configured scope.", 309 | "type": "string", 310 | "const": "allow-read-variable" 311 | }, 312 | { 313 | "description": "Denies the read_variable command without any pre-configured scope.", 314 | "type": "string", 315 | "const": "deny-read-variable" 316 | }, 317 | { 318 | "description": "Enables the register_function command without any pre-configured scope.", 319 | "type": "string", 320 | "const": "allow-register-function" 321 | }, 322 | { 323 | "description": "Denies the register_function command without any pre-configured scope.", 324 | "type": "string", 325 | "const": "deny-register-function" 326 | }, 327 | { 328 | "description": "Enables the run_python command without any pre-configured scope.", 329 | "type": "string", 330 | "const": "allow-run-python" 331 | }, 332 | { 333 | "description": "Denies the run_python command without any pre-configured scope.", 334 | "type": "string", 335 | "const": "deny-run-python" 336 | }, 337 | { 338 | "description": "Default permissions for the plugin", 339 | "type": "string", 340 | "const": "default" 341 | } 342 | ] 343 | } 344 | } 345 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { join } from 'path' 3 | import { cwd } from 'process' 4 | import { nodeResolve } from '@rollup/plugin-node-resolve' 5 | import typescript from '@rollup/plugin-typescript' 6 | 7 | const pkg = JSON.parse(readFileSync(join(cwd(), 'package.json'), 'utf8')) 8 | 9 | const pluginJsName = 'python' // window.__TAURI__.python 10 | const iifeVarName = '__TAURI_PLUGIN_PYTHON_API__' 11 | 12 | export default [{ 13 | input: 'guest-js/index.ts', 14 | output: [ 15 | { 16 | file: pkg.exports.import, 17 | format: 'esm' 18 | }, 19 | { 20 | file: pkg.exports.require, 21 | format: 'cjs' 22 | } 23 | ], 24 | plugins: [ 25 | typescript({ 26 | declaration: true, 27 | declarationDir: `./${pkg.exports.import.split('/')[0]}` 28 | }) 29 | ], 30 | external: [ 31 | /^@tauri-apps\/api/, 32 | ...Object.keys(pkg.dependencies || {}), 33 | ...Object.keys(pkg.peerDependencies || {}) 34 | ] 35 | }, 36 | { 37 | input: 'guest-js/index.ts', 38 | output: [ 39 | { 40 | name: iifeVarName, 41 | file: pkg.exports.html, 42 | format: 'iife', 43 | banner: "if ('__TAURI__' in window) {", 44 | // the last `}` closes the if in the banner 45 | footer: `Object.defineProperty(window.__TAURI__, '${pluginJsName}', { value: ${iifeVarName} }) }` 46 | 47 | } 48 | ], 49 | plugins: [typescript(), nodeResolve()], 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | use tauri::{command, AppHandle, Runtime}; 7 | 8 | use crate::models::*; 9 | use crate::PythonExt; 10 | use crate::Result; 11 | 12 | #[command] 13 | pub(crate) async fn run_python( 14 | app: AppHandle, 15 | payload: StringRequest, 16 | ) -> Result { 17 | app.run_python(payload) 18 | } 19 | #[command] 20 | pub(crate) async fn register_function( 21 | app: AppHandle, 22 | payload: RegisterRequest, 23 | ) -> Result { 24 | app.register_function(payload) 25 | } 26 | #[command] 27 | pub(crate) async fn call_function( 28 | app: AppHandle, 29 | payload: RunRequest, 30 | ) -> Result { 31 | app.call_function(payload) 32 | } 33 | #[command] 34 | pub(crate) async fn read_variable( 35 | app: AppHandle, 36 | payload: StringRequest, 37 | ) -> Result { 38 | app.read_variable(payload) 39 | } 40 | -------------------------------------------------------------------------------- /src/desktop.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | use serde::de::DeserializeOwned; 7 | use tauri::{plugin::PluginApi, AppHandle, Runtime}; 8 | 9 | /// Access to the python plugin APIs. 10 | pub struct Python(AppHandle); 11 | 12 | pub fn init( 13 | app: &AppHandle, 14 | _api: PluginApi, 15 | ) -> crate::Result> { 16 | Ok(Python(app.clone())) 17 | } 18 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | #[cfg(feature = "pyo3")] 7 | use pyo3::{prelude::*, PyErr}; 8 | use serde::{ser::Serializer, Serialize}; 9 | 10 | pub type Result = std::result::Result; 11 | 12 | #[derive(Debug, thiserror::Error)] 13 | pub enum Error { 14 | #[error("Error: {0}")] 15 | String(String), 16 | #[error(transparent)] 17 | Io(#[from] std::io::Error), 18 | #[cfg(mobile)] 19 | #[error(transparent)] 20 | PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), 21 | } 22 | 23 | impl Serialize for Error { 24 | fn serialize(&self, serializer: S) -> std::result::Result 25 | where 26 | S: Serializer, 27 | { 28 | serializer.serialize_str(self.to_string().as_ref()) 29 | } 30 | } 31 | 32 | impl From<&str> for Error { 33 | fn from(error: &str) -> Self { 34 | Error::String(error.into()) 35 | } 36 | } 37 | 38 | #[cfg(not(feature = "pyo3"))] 39 | impl From> for Error { 40 | fn from(error: rustpython_vm::PyRef) -> Self { 41 | let msg = format!("{:?}", &error); 42 | println!("error: {}", &msg); 43 | if let Some(tb) = error.traceback() { 44 | println!("Traceback (most recent call last):"); 45 | for trace in tb.iter() { 46 | let file = trace.frame.code.source_path.as_str(); 47 | let original_line = trace.lineno.to_usize(); 48 | let line = if file == "main.py" { 49 | original_line - 2 // sys.path import has 2 additional lines 50 | } else { 51 | original_line 52 | }; 53 | println!( 54 | " File \"{file}\", line {line}, in {}", 55 | trace.frame.code.obj_name 56 | ); 57 | } 58 | } 59 | Error::String(msg) 60 | } 61 | } 62 | 63 | #[cfg(feature = "pyo3")] 64 | impl From for Error { 65 | fn from(error: PyErr) -> Self { 66 | let error_msg = match pyo3::Python::with_gil(|py| -> Result> { 67 | let traceback_module = py.import("traceback")?; 68 | let traceback_object = error 69 | .traceback(py) 70 | .ok_or(pyo3::exceptions::PyWarning::new_err("No traceback found."))?; 71 | let extract_traceback = traceback_module.getattr("extract_tb")?; 72 | 73 | // Get the formatted traceback lines 74 | let result = extract_traceback.call1((traceback_object,)).and_then(|r| { 75 | match r.extract::>() { 76 | Ok(v) => { 77 | let mut formatted_lines = Vec::new(); 78 | for arg in v.iter() { 79 | let frame = arg.bind(py); 80 | 81 | // Extract filename 82 | let filename = match frame.getattr("filename") { 83 | Ok(f) => match f.extract::() { 84 | Ok(s) if s == "".to_string() => { 85 | // Special handling for 86 | frame.setattr("filename", "main.py")?; 87 | let lineno = frame.getattr("lineno")?.extract::()?; 88 | frame.setattr("lineno", lineno - 2)?; 89 | "main.py".to_string() 90 | } 91 | Ok(s) => s, 92 | Err(_) => "".to_string(), 93 | }, 94 | Err(_) => "".to_string(), 95 | }; 96 | 97 | // Extract line number 98 | let lineno = match frame.getattr("lineno") { 99 | Ok(l) => match l.extract::() { 100 | Ok(n) => n, 101 | Err(_) => 0, 102 | }, 103 | Err(_) => 0, 104 | }; 105 | 106 | // Extract function name 107 | let name = match frame.getattr("name") { 108 | Ok(n) => match n.extract::() { 109 | Ok(s) => s, 110 | Err(_) => "".to_string(), 111 | }, 112 | Err(_) => "".to_string(), 113 | }; 114 | 115 | // Extract line content (if available) 116 | let line = match frame.getattr("line") { 117 | Ok(l) => match l.extract::>() { 118 | Ok(Some(s)) => format!("\t{}", s), 119 | _ => "".to_string(), 120 | }, 121 | Err(_) => "".to_string(), 122 | }; 123 | 124 | // Format the line like requested 125 | let formatted_line = format!( 126 | "File \"{}\", line {}, in {}\n{}", 127 | filename, lineno, name, line 128 | ); 129 | 130 | formatted_lines.push(formatted_line); 131 | } 132 | 133 | Ok(formatted_lines) 134 | } 135 | Err(_) => Err(PyErr::new::( 136 | "Failed to extract traceback", 137 | )), 138 | } 139 | })?; 140 | 141 | // Add traceback header 142 | let mut full_traceback = vec!["Traceback (most recent call last):".to_string()]; 143 | full_traceback.extend(result); 144 | 145 | // Add error type and message 146 | full_traceback.push(error.to_string()); 147 | 148 | Ok(full_traceback) 149 | }) { 150 | Ok(formatted) => formatted.join("\n"), 151 | Err(_) => error.to_string(), // Fall back to simple error message 152 | }; 153 | 154 | Error::String(error_msg) 155 | } 156 | } 157 | 158 | impl From for Error { 159 | fn from(error: tauri::Error) -> Self { 160 | Error::String(error.to_string()) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | use tauri::{ 7 | path::BaseDirectory, 8 | plugin::{Builder, TauriPlugin}, 9 | AppHandle, Manager, Runtime, 10 | }; 11 | 12 | #[cfg(desktop)] 13 | mod desktop; 14 | #[cfg(mobile)] 15 | mod mobile; 16 | 17 | mod commands; 18 | mod error; 19 | mod models; 20 | #[cfg(not(feature = "pyo3"))] 21 | mod py_lib; 22 | #[cfg(feature = "pyo3")] 23 | mod py_lib_pyo3; 24 | #[cfg(feature = "pyo3")] 25 | use py_lib_pyo3 as py_lib; 26 | 27 | pub use error::{Error, Result}; 28 | use models::*; 29 | use std::path::{Path, PathBuf}; 30 | 31 | #[cfg(desktop)] 32 | use desktop::Python; 33 | #[cfg(mobile)] 34 | use mobile::Python; 35 | 36 | /// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the python APIs. 37 | pub trait PythonExt { 38 | fn python(&self) -> &Python; 39 | fn run_python(&self, payload: StringRequest) -> crate::Result; 40 | fn register_function(&self, payload: RegisterRequest) -> crate::Result; 41 | fn call_function(&self, payload: RunRequest) -> crate::Result; 42 | fn read_variable(&self, payload: StringRequest) -> crate::Result; 43 | } 44 | 45 | impl> crate::PythonExt for T { 46 | fn python(&self) -> &Python { 47 | self.state::>().inner() 48 | } 49 | fn run_python(&self, payload: StringRequest) -> crate::Result { 50 | py_lib::run_python(payload)?; 51 | Ok(StringResponse { value: "Ok".into() }) 52 | } 53 | fn register_function(&self, payload: RegisterRequest) -> crate::Result { 54 | py_lib::register_function(payload)?; 55 | Ok(StringResponse { value: "Ok".into() }) 56 | } 57 | fn call_function(&self, payload: RunRequest) -> crate::Result { 58 | let py_res: String = py_lib::call_function(payload)?; 59 | Ok(StringResponse { value: py_res }) 60 | } 61 | fn read_variable(&self, payload: StringRequest) -> crate::Result { 62 | let py_res = py_lib::read_variable(payload)?; 63 | Ok(StringResponse { value: py_res }) 64 | } 65 | } 66 | 67 | fn get_resource_dir(app: &AppHandle) -> PathBuf { 68 | app.path() 69 | .resolve("src-python", BaseDirectory::Resource) 70 | .unwrap_or_default() 71 | } 72 | 73 | fn get_src_python_dir() -> PathBuf { 74 | std::env::current_dir().unwrap().join("src-python") 75 | } 76 | 77 | /// Initializes the plugin with functions 78 | pub fn init() -> TauriPlugin { 79 | init_and_register(vec![]) 80 | } 81 | 82 | fn cleanup_path_for_python(path: &PathBuf) -> String { 83 | dunce::canonicalize(path) 84 | .unwrap() 85 | .to_string_lossy() 86 | .replace("\\", "/") 87 | } 88 | 89 | fn print_path_for_python(path: &PathBuf) -> String { 90 | #[cfg(not(target_os = "windows"))] { 91 | format!("\"{}\"", cleanup_path_for_python(path)) 92 | } 93 | #[cfg(target_os = "windows")] { 94 | format!("r\"{}\"", cleanup_path_for_python(path)) 95 | } 96 | } 97 | 98 | fn init_python(code: String, dir: PathBuf) { 99 | #[allow(unused_mut)] 100 | let mut sys_pyth_dir = vec![print_path_for_python(&dir)]; 101 | #[cfg(feature = "venv")] 102 | { 103 | let venv_dir = dir.join(".venv").join("lib"); 104 | if Path::exists(venv_dir.as_path()) { 105 | if let Ok(py_dir) = venv_dir.read_dir() { 106 | for entry in py_dir.flatten() { 107 | let site_packages = entry.path().join("site-packages"); 108 | // use first folder with site-packages for venv, ignore venv version 109 | if Path::exists(site_packages.as_path()) { 110 | sys_pyth_dir 111 | .push(print_path_for_python(&site_packages)); 112 | break; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | let path_import = format!( 119 | r#"import sys 120 | sys.path = sys.path + [{}] 121 | {} 122 | "#, 123 | sys_pyth_dir.join(", "), 124 | code 125 | ); 126 | py_lib::run_python_internal(path_import, "main.py".into()) 127 | .unwrap_or_else(|e| panic!("Error initializing main.py:\n\n{e}\n")); 128 | } 129 | 130 | /// Initializes the plugin. 131 | pub fn init_and_register(python_functions: Vec<&'static str>) -> TauriPlugin { 132 | Builder::new("python") 133 | .invoke_handler(tauri::generate_handler![ 134 | commands::run_python, 135 | commands::register_function, 136 | commands::call_function, 137 | commands::read_variable 138 | ]) 139 | .setup(|app, api| { 140 | #[cfg(mobile)] 141 | let python = mobile::init(app, api)?; 142 | #[cfg(desktop)] 143 | let python = desktop::init(app, api)?; 144 | app.manage(python); 145 | 146 | let mut dir = get_resource_dir(app); 147 | let mut code = std::fs::read_to_string(dir.join("main.py")).unwrap_or_default(); 148 | if code.is_empty() { 149 | println!( 150 | "Warning: 'src-tauri/main.py' seems not to be registered in 'tauri.conf.json'" 151 | ); 152 | dir = get_src_python_dir(); 153 | code = std::fs::read_to_string(dir.join("main.py")).unwrap_or_default(); 154 | } 155 | if code.is_empty() { 156 | println!("ERROR: Error reading 'src-tauri/main.py'"); 157 | } 158 | init_python(code, dir); 159 | for function_name in python_functions { 160 | py_lib::register_function_str(function_name.into(), None).unwrap(); 161 | } 162 | let functions = py_lib::read_variable(StringRequest { 163 | value: "_tauri_plugin_functions".into(), 164 | }) 165 | .unwrap_or_default() 166 | .replace("'", "\""); // python arrays are serialized usings ' instead of " 167 | 168 | if let Ok(python_functions) = serde_json::from_str::>(&functions) { 169 | for function_name in python_functions { 170 | py_lib::register_function_str(function_name, None).unwrap(); 171 | } 172 | } 173 | Ok(()) 174 | }) 175 | .build() 176 | } 177 | -------------------------------------------------------------------------------- /src/mobile.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | use serde::de::DeserializeOwned; 7 | use tauri::{ 8 | plugin::{PluginApi, PluginHandle}, 9 | AppHandle, Runtime, 10 | }; 11 | 12 | #[cfg(target_os = "ios")] 13 | tauri::ios_plugin_binding!(init_plugin_python); 14 | 15 | // initializes the Kotlin or Swift plugin classes 16 | pub fn init( 17 | _app: &AppHandle, 18 | api: PluginApi, 19 | ) -> crate::Result> { 20 | #[cfg(target_os = "android")] 21 | let handle = api.register_android_plugin("com.plugin.python", "ExamplePlugin")?; 22 | #[cfg(target_os = "ios")] 23 | let handle = api.register_ios_plugin(init_plugin_python)?; 24 | Ok(Python(handle)) 25 | } 26 | 27 | pub struct Python(PluginHandle); 28 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Debug, Deserialize, Serialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct StringRequest { 11 | pub value: String, 12 | } 13 | 14 | #[cfg(feature = "pyo3")] 15 | #[derive(Debug, Serialize, Deserialize, pyo3::IntoPyObject)] 16 | #[serde(untagged)] 17 | pub enum JsMany { 18 | Bool(bool), 19 | Number(u64), 20 | Float(f64), 21 | String(String), 22 | StringVec(Vec), 23 | FloatVec(Vec), 24 | } 25 | 26 | #[cfg(not(feature = "pyo3"))] 27 | use serde_json::Value as JsMany; 28 | 29 | #[derive(Debug, Deserialize, Serialize)] 30 | #[serde(rename_all = "camelCase")] 31 | pub struct RegisterRequest { 32 | pub python_function_call: String, 33 | pub number_of_args: Option, 34 | } 35 | 36 | #[derive(Debug, Deserialize, Serialize)] 37 | #[serde(rename_all = "camelCase")] 38 | pub struct RunRequest { 39 | pub function_name: String, 40 | pub args: Vec, 41 | } 42 | 43 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct StringResponse { 46 | pub value: String, 47 | } 48 | -------------------------------------------------------------------------------- /src/py_lib.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | use std::sync::atomic::AtomicBool; 7 | use std::{collections::HashSet, sync::Mutex}; 8 | 9 | use rustpython_vm::py_serde; 10 | 11 | use lazy_static::lazy_static; 12 | 13 | use crate::{models::*, Error}; 14 | 15 | fn create_globals() -> rustpython_vm::scope::Scope { 16 | rustpython_vm::Interpreter::without_stdlib(Default::default()) 17 | .enter(|vm| vm.new_scope_with_builtins()) 18 | } 19 | 20 | lazy_static! { 21 | static ref INIT_BLOCKED: AtomicBool = false.into(); 22 | static ref FUNCTION_MAP: Mutex> = Mutex::new(HashSet::new()); 23 | static ref GLOBALS: rustpython_vm::scope::Scope = create_globals(); 24 | } 25 | 26 | pub fn run_python(payload: StringRequest) -> crate::Result<()> { 27 | run_python_internal(payload.value, "".into()) 28 | } 29 | 30 | pub fn run_python_internal(code: String, filename: String) -> crate::Result<()> { 31 | rustpython_vm::Interpreter::without_stdlib(Default::default()).enter(|vm| { 32 | let code_obj = vm 33 | .compile(&code, rustpython_vm::compiler::Mode::Exec, filename) 34 | .map_err(|err| vm.new_syntax_error(&err, Some(&code)))?; 35 | vm.run_code_obj(code_obj, GLOBALS.clone()) 36 | })?; 37 | Ok(()) 38 | } 39 | 40 | pub fn register_function(payload: RegisterRequest) -> crate::Result<()> { 41 | register_function_str(payload.python_function_call, payload.number_of_args) 42 | } 43 | 44 | pub fn register_function_str( 45 | function_name: String, 46 | number_of_args: Option, 47 | ) -> crate::Result<()> { 48 | if INIT_BLOCKED.load(std::sync::atomic::Ordering::Relaxed) { 49 | return Err("Cannot register after function called".into()); 50 | } 51 | rustpython_vm::Interpreter::without_stdlib(Default::default()).enter(|vm| { 52 | let var_dot_split: Vec<&str> = function_name.split(".").collect(); 53 | let func = GLOBALS 54 | .globals 55 | .get_item(var_dot_split[0], vm) 56 | .unwrap_or_else(|_| { 57 | panic!("Cannot find '{}' in globals", var_dot_split[0]); 58 | }); 59 | if var_dot_split.len() > 2 { 60 | func.get_attr(&vm.ctx.new_str(var_dot_split[1]), vm) 61 | .unwrap() 62 | .get_attr(&vm.ctx.new_str(var_dot_split[2]), vm) 63 | .unwrap(); 64 | } else if var_dot_split.len() > 1 { 65 | func.get_attr(&vm.ctx.new_str(var_dot_split[1]), vm) 66 | .unwrap_or_else(|_| { 67 | panic!( 68 | "Cannot find sub function '{}' in '{}'", 69 | var_dot_split[1], var_dot_split[0] 70 | ); 71 | }); 72 | } 73 | 74 | if let Some(num_args) = number_of_args { 75 | let py_analyze_sig = format!( 76 | r#" 77 | from inspect import signature 78 | if len(signature({}).parameters) != {}: 79 | raise Exception("Function parameters don't match in 'registerFunction'") 80 | "#, 81 | function_name, num_args 82 | ); 83 | 84 | let code_obj = vm 85 | .compile( 86 | &py_analyze_sig, 87 | rustpython_vm::compiler::Mode::Exec, 88 | "".to_owned(), 89 | ) 90 | .map_err(|err| vm.new_syntax_error(&err, Some(&py_analyze_sig)))?; 91 | vm.run_code_obj(code_obj, GLOBALS.clone()) 92 | .unwrap_or_else(|_| { 93 | panic!("Number of args doesn't match signature of {function_name}.") 94 | }); 95 | } 96 | // dbg!(format!("Added '{function_name}'")); 97 | FUNCTION_MAP.lock().unwrap().insert(function_name); 98 | Ok(()) 99 | }) 100 | } 101 | pub fn call_function(payload: RunRequest) -> crate::Result { 102 | INIT_BLOCKED.store(true, std::sync::atomic::Ordering::Relaxed); 103 | let function_name = payload.function_name; 104 | if FUNCTION_MAP.lock().unwrap().get(&function_name).is_none() { 105 | return Err(Error::String(format!( 106 | "Function {function_name} has not been registered yet" 107 | ))); 108 | } 109 | rustpython_vm::Interpreter::without_stdlib(Default::default()).enter(|vm| { 110 | let posargs: Vec<_> = payload 111 | .args 112 | .into_iter() 113 | .map(|value| py_serde::deserialize(vm, value).unwrap()) 114 | .collect(); 115 | let var_dot_split: Vec<&str> = function_name.split(".").collect(); 116 | let func = GLOBALS.globals.get_item(var_dot_split[0], vm)?; 117 | Ok(if var_dot_split.len() > 2 { 118 | func.get_attr(&vm.ctx.new_str(var_dot_split[1]), vm)? 119 | .get_attr(&vm.ctx.new_str(var_dot_split[2]), vm)? 120 | } else if var_dot_split.len() > 1 { 121 | func.get_attr(&vm.ctx.new_str(var_dot_split[1]), vm)? 122 | } else { 123 | func 124 | } 125 | .call(posargs, vm)? 126 | .str(vm)? 127 | .to_string()) 128 | }) 129 | } 130 | 131 | pub fn read_variable(payload: StringRequest) -> crate::Result { 132 | rustpython_vm::Interpreter::without_stdlib(Default::default()).enter(|vm| { 133 | let var_dot_split: Vec<&str> = payload.value.split(".").collect(); 134 | let var = GLOBALS.globals.get_item(var_dot_split[0], vm)?; 135 | Ok(if var_dot_split.len() > 2 { 136 | var.get_attr(&vm.ctx.new_str(var_dot_split[1]), vm)? 137 | .get_attr(&vm.ctx.new_str(var_dot_split[2]), vm)? 138 | } else if var_dot_split.len() > 1 { 139 | var.get_attr(&vm.ctx.new_str(var_dot_split[1]), vm)? 140 | } else { 141 | var 142 | } 143 | .str(vm)? 144 | .to_string()) 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /src/py_lib_pyo3.rs: -------------------------------------------------------------------------------- 1 | // Tauri Python Plugin 2 | // © Copyright 2024, by Marco Mengelkoch 3 | // Licensed under MIT License, see License file for more details 4 | // git clone https://github.com/marcomq/tauri-plugin-python 5 | 6 | use std::sync::atomic::AtomicBool; 7 | use std::{collections::HashMap, ffi::CString, sync::Mutex}; 8 | 9 | use lazy_static::lazy_static; 10 | use pyo3::exceptions::PyBaseException; 11 | use pyo3::types::{PyAnyMethods, PyDictMethods}; 12 | use pyo3::PyErr; 13 | use pyo3::{marker, types::PyDict, Py, PyAny}; 14 | 15 | use crate::{models::*, Error}; 16 | 17 | lazy_static! { 18 | static ref INIT_BLOCKED: AtomicBool = false.into(); 19 | static ref FUNCTION_MAP: Mutex>> = Mutex::new(HashMap::new()); 20 | static ref GLOBALS: Mutex> = 21 | Mutex::new(marker::Python::with_gil(|py| { PyDict::new(py).into() })); 22 | } 23 | 24 | pub fn run_python(payload: StringRequest) -> crate::Result<()> { 25 | run_python_internal(payload.value, "".into()) 26 | } 27 | 28 | pub fn run_python_internal(code: String, _filename: String) -> crate::Result<()> { 29 | marker::Python::with_gil(|py| -> crate::Result<()> { 30 | let globals = GLOBALS.lock().unwrap().clone_ref(py).into_bound(py); 31 | let c_code = CString::new(code).expect("CString::new failed"); 32 | Ok(py.run(&c_code, Some(&globals), None)?) 33 | }) 34 | } 35 | pub fn register_function(payload: RegisterRequest) -> crate::Result<()> { 36 | register_function_str(payload.python_function_call, payload.number_of_args) 37 | } 38 | 39 | pub fn register_function_str(fn_name: String, number_of_args: Option) -> crate::Result<()> { 40 | // TODO, check actual function signature 41 | if INIT_BLOCKED.load(std::sync::atomic::Ordering::Relaxed) { 42 | return Err("Cannot register after function called".into()); 43 | } 44 | marker::Python::with_gil(|py| -> crate::Result<()> { 45 | let globals = GLOBALS.lock().unwrap().clone_ref(py).into_bound(py); 46 | 47 | let fn_dot_split: Vec<&str> = fn_name.split(".").collect(); 48 | let app = globals.get_item(fn_dot_split[0])?; 49 | if app.is_none() { 50 | return Err(Error::String(format!("{} not found", &fn_name))); 51 | } 52 | let app = if fn_dot_split.len() > 2 { 53 | app.unwrap() 54 | .getattr(fn_dot_split.get(1).unwrap())? 55 | .getattr(fn_dot_split.get(2).unwrap())? 56 | } else if fn_dot_split.len() > 1 { 57 | app.unwrap().getattr(fn_dot_split.get(1).unwrap())? 58 | } else { 59 | app.unwrap() 60 | }; 61 | if !app.is_callable() { 62 | return Err(Error::String(format!( 63 | "{} not a callable function", 64 | &fn_name 65 | ))); 66 | } 67 | if let Some(num_args) = number_of_args { 68 | let py_analyze_sig = format!( 69 | r#" 70 | from inspect import signature 71 | if len(signature({}).parameters) != {}: 72 | raise Exception("Function parameters don't match in 'registerFunction'") 73 | "#, 74 | fn_name, num_args 75 | ); 76 | let code_c = CString::new(py_analyze_sig).expect("CString::new failed"); 77 | py.run(&code_c, Some(&globals), None) 78 | .unwrap_or_else(|_| panic!("Could not register '{}'. ", &fn_name)); 79 | } 80 | // dbg!("{} was inserted", &fn_name); 81 | FUNCTION_MAP.lock().unwrap().insert(fn_name, app.into()); 82 | Ok(()) 83 | }) 84 | } 85 | pub fn call_function(payload: RunRequest) -> crate::Result { 86 | INIT_BLOCKED.store(true, std::sync::atomic::Ordering::Relaxed); 87 | marker::Python::with_gil(|py| -> crate::Result { 88 | let arg = pyo3::types::PyTuple::new(py, payload.args)?; 89 | let map = FUNCTION_MAP 90 | .lock() 91 | .map_err(|msg| PyErr::new::(msg.to_string()))?; 92 | match map.get(&payload.function_name) { 93 | Some(app) => { 94 | // dbg!(&arg); 95 | let res = app.call1(py, arg)?; 96 | // dbg!(&res); 97 | Ok(res.to_string()) 98 | } 99 | _ => Err(Error::String(format!( 100 | "{} not found", 101 | payload.function_name 102 | ))), 103 | } 104 | }) 105 | } 106 | 107 | pub fn read_variable(payload: StringRequest) -> crate::Result { 108 | marker::Python::with_gil(|py| -> crate::Result { 109 | let globals = GLOBALS.lock().unwrap().clone_ref(py).into_bound(py); 110 | 111 | let var_dot_split: Vec<&str> = payload.value.split(".").collect(); 112 | let var = globals.get_item(var_dot_split[0])?; 113 | if let Some(var) = var { 114 | Ok(if var_dot_split.len() > 2 { 115 | var.getattr(var_dot_split.get(1).unwrap())? 116 | .getattr(var_dot_split.get(2).unwrap())? 117 | } else if var_dot_split.len() > 1 { 118 | var.getattr(var_dot_split.get(1).unwrap())? 119 | } else { 120 | var 121 | } 122 | .to_string()) 123 | } else { 124 | Err(Error::String(format!("{} not set", &payload.value))) 125 | } 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noImplicitAny": true, 10 | "noEmit": true 11 | }, 12 | "include": ["guest-js/*.ts"], 13 | "exclude": ["dist-js", "node_modules"] 14 | } 15 | --------------------------------------------------------------------------------