caspian-native 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- caspian_native-0.0.1/LICENSE +22 -0
- caspian_native-0.0.1/PKG-INFO +158 -0
- caspian_native-0.0.1/README.md +134 -0
- caspian_native-0.0.1/pyproject.toml +49 -0
- caspian_native-0.0.1/setup.cfg +4 -0
- caspian_native-0.0.1/src/caspian_native/__init__.py +8 -0
- caspian_native-0.0.1/src/caspian_native/__main__.py +5 -0
- caspian_native-0.0.1/src/caspian_native/backend.py +118 -0
- caspian_native-0.0.1/src/caspian_native/cli.py +119 -0
- caspian_native-0.0.1/src/caspian_native/config.py +165 -0
- caspian_native-0.0.1/src/caspian_native/doctor.py +57 -0
- caspian_native-0.0.1/src/caspian_native/errors.py +3 -0
- caspian_native-0.0.1/src/caspian_native/project.py +48 -0
- caspian_native-0.0.1/src/caspian_native/runner.py +168 -0
- caspian_native-0.0.1/src/caspian_native/scaffold.py +471 -0
- caspian_native-0.0.1/src/caspian_native/schema.py +54 -0
- caspian_native-0.0.1/src/caspian_native/security.py +81 -0
- caspian_native-0.0.1/src/caspian_native.egg-info/PKG-INFO +158 -0
- caspian_native-0.0.1/src/caspian_native.egg-info/SOURCES.txt +26 -0
- caspian_native-0.0.1/src/caspian_native.egg-info/dependency_links.txt +1 -0
- caspian_native-0.0.1/src/caspian_native.egg-info/entry_points.txt +2 -0
- caspian_native-0.0.1/src/caspian_native.egg-info/requires.txt +8 -0
- caspian_native-0.0.1/src/caspian_native.egg-info/top_level.txt +1 -0
- caspian_native-0.0.1/tests/test_config.py +41 -0
- caspian_native-0.0.1/tests/test_project.py +30 -0
- caspian_native-0.0.1/tests/test_runner.py +50 -0
- caspian_native-0.0.1/tests/test_scaffold.py +82 -0
- caspian_native-0.0.1/tests/test_security.py +46 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Caspian Native contributors
|
|
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
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: caspian-native
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Optional Tauri packaging for Caspian applications
|
|
5
|
+
Author: Caspian Native contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: caspian,pulsepoint,tauri,desktop,webview
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
10
|
+
Classifier: Framework :: FastAPI
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: build
|
|
18
|
+
Requires-Dist: pyinstaller>=6; extra == "build"
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
21
|
+
Requires-Dist: ruff>=0.8; extra == "test"
|
|
22
|
+
Requires-Dist: pyright>=1.1; extra == "test"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# caspian-native
|
|
26
|
+
|
|
27
|
+
Optional Tauri packaging for applications built with the Caspian framework and
|
|
28
|
+
PulsePoint. Version `0.0.1` packages the existing application in a system
|
|
29
|
+
WebView; it does not translate HTML into operating-system widgets.
|
|
30
|
+
|
|
31
|
+
## Support in 0.0.1
|
|
32
|
+
|
|
33
|
+
| Target | Backend | Status |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| Windows | Embedded Python sidecar | Initial support |
|
|
36
|
+
| Windows | Remote Caspian server | Initial support |
|
|
37
|
+
| Android | Remote Caspian server | Initial support |
|
|
38
|
+
| Android | Embedded Python | Not supported yet |
|
|
39
|
+
|
|
40
|
+
Android embedded Python is deliberately refused. A Caspian application may
|
|
41
|
+
depend on native Python wheels such as `asyncpg`, `cryptography`, or Pillow;
|
|
42
|
+
shipping an APK without proving every wheel for every ABI would produce a
|
|
43
|
+
package that builds unreliably or fails on launch.
|
|
44
|
+
|
|
45
|
+
## Install for development
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
python -m pip install -e ".[test,build]"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Tauri's CLI is installed separately so it can match the version selected by
|
|
52
|
+
the generated application's `native/Cargo.toml`:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
cargo install tauri-cli --version "^2" --locked
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Add native packaging to a Caspian project
|
|
59
|
+
|
|
60
|
+
From a directory containing `caspian.config.json`:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
caspian-native init --identifier com.example.myapp --windows
|
|
64
|
+
caspian-native doctor --target windows
|
|
65
|
+
caspian-native backend build
|
|
66
|
+
caspian-native dev --target windows
|
|
67
|
+
caspian-native build --target windows
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For an Android thin client backed by a deployed Caspian server:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
caspian-native init \
|
|
74
|
+
--identifier com.example.myapp \
|
|
75
|
+
--windows --android \
|
|
76
|
+
--remote-url https://app.example.com
|
|
77
|
+
caspian-native doctor --target android
|
|
78
|
+
caspian-native build --target android
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The generated files live under `native/`. Re-running `init` never overwrites a
|
|
82
|
+
changed generated file unless `--force` is supplied.
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
`caspian.native.json` is the source of truth:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"$schema": "./caspian.native.schema.json",
|
|
91
|
+
"schema": 1,
|
|
92
|
+
"productName": "My App",
|
|
93
|
+
"identifier": "com.example.myapp",
|
|
94
|
+
"version": "0.0.1",
|
|
95
|
+
"targets": ["windows"],
|
|
96
|
+
"window": {
|
|
97
|
+
"title": "My App",
|
|
98
|
+
"width": 1200,
|
|
99
|
+
"height": 800,
|
|
100
|
+
"resizable": true
|
|
101
|
+
},
|
|
102
|
+
"backend": {
|
|
103
|
+
"desktopMode": "embedded",
|
|
104
|
+
"remoteUrl": null
|
|
105
|
+
},
|
|
106
|
+
"security": {
|
|
107
|
+
"loopbackToken": true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Runtime model
|
|
113
|
+
|
|
114
|
+
Embedded desktop mode builds `main.py`, the Caspian runtime, project modules,
|
|
115
|
+
route index, and `public/` into a one-file sidecar. The sidecar:
|
|
116
|
+
|
|
117
|
+
1. restores a per-installation `AUTH_SECRET` from application data;
|
|
118
|
+
2. imports the project's `main.app` in production mode;
|
|
119
|
+
3. binds `127.0.0.1` on an operating-system-assigned port;
|
|
120
|
+
4. prints the protected launch URL to the Tauri host;
|
|
121
|
+
5. serves the normal Caspian ASGI application with Uvicorn.
|
|
122
|
+
|
|
123
|
+
The WebView receives a per-launch token once. The native middleware exchanges
|
|
124
|
+
it for an `HttpOnly; SameSite=Strict` cookie and refuses documents, assets,
|
|
125
|
+
RPCs, uploads, streams, and WebSocket handshakes without that cookie.
|
|
126
|
+
|
|
127
|
+
Caspian's production session cookie normally has the `Secure` flag. A packaged
|
|
128
|
+
loopback server intentionally uses `http://127.0.0.1`, so the adapter changes
|
|
129
|
+
only that middleware option before Starlette constructs the stack. The app
|
|
130
|
+
otherwise remains in production mode.
|
|
131
|
+
|
|
132
|
+
## Native bridge
|
|
133
|
+
|
|
134
|
+
Pages may capability-check `window.caspianNative`:
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const native = window.caspianNative;
|
|
138
|
+
if (native?.has("open-external")) {
|
|
139
|
+
await native.invoke("open_external", { url: "https://example.com" });
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The bridge allowlist is intentionally short: platform, application version,
|
|
144
|
+
application data directory, external HTTP/HTTPS/mail links, and a user-driven
|
|
145
|
+
file picker. An XSS can reach every bridge command, so commands must remain
|
|
146
|
+
safe even when the calling page is compromised.
|
|
147
|
+
|
|
148
|
+
## Current limitations
|
|
149
|
+
|
|
150
|
+
- The first embedded-backend implementation uses PyInstaller and has only been
|
|
151
|
+
designed for Windows packaging.
|
|
152
|
+
- Dynamic route discovery requires project Python sources and
|
|
153
|
+
`settings/files-list.json` to be bundled.
|
|
154
|
+
- Secrets from `.env` are never embedded. Remote database credentials and
|
|
155
|
+
third-party credentials must be provisioned outside the package.
|
|
156
|
+
- Android remote mode requires connectivity and a deployed Caspian server.
|
|
157
|
+
- Installer signing and Android store signing are not automated in `0.0.1`.
|
|
158
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# caspian-native
|
|
2
|
+
|
|
3
|
+
Optional Tauri packaging for applications built with the Caspian framework and
|
|
4
|
+
PulsePoint. Version `0.0.1` packages the existing application in a system
|
|
5
|
+
WebView; it does not translate HTML into operating-system widgets.
|
|
6
|
+
|
|
7
|
+
## Support in 0.0.1
|
|
8
|
+
|
|
9
|
+
| Target | Backend | Status |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Windows | Embedded Python sidecar | Initial support |
|
|
12
|
+
| Windows | Remote Caspian server | Initial support |
|
|
13
|
+
| Android | Remote Caspian server | Initial support |
|
|
14
|
+
| Android | Embedded Python | Not supported yet |
|
|
15
|
+
|
|
16
|
+
Android embedded Python is deliberately refused. A Caspian application may
|
|
17
|
+
depend on native Python wheels such as `asyncpg`, `cryptography`, or Pillow;
|
|
18
|
+
shipping an APK without proving every wheel for every ABI would produce a
|
|
19
|
+
package that builds unreliably or fails on launch.
|
|
20
|
+
|
|
21
|
+
## Install for development
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
python -m pip install -e ".[test,build]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Tauri's CLI is installed separately so it can match the version selected by
|
|
28
|
+
the generated application's `native/Cargo.toml`:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
cargo install tauri-cli --version "^2" --locked
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Add native packaging to a Caspian project
|
|
35
|
+
|
|
36
|
+
From a directory containing `caspian.config.json`:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
caspian-native init --identifier com.example.myapp --windows
|
|
40
|
+
caspian-native doctor --target windows
|
|
41
|
+
caspian-native backend build
|
|
42
|
+
caspian-native dev --target windows
|
|
43
|
+
caspian-native build --target windows
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For an Android thin client backed by a deployed Caspian server:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
caspian-native init \
|
|
50
|
+
--identifier com.example.myapp \
|
|
51
|
+
--windows --android \
|
|
52
|
+
--remote-url https://app.example.com
|
|
53
|
+
caspian-native doctor --target android
|
|
54
|
+
caspian-native build --target android
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The generated files live under `native/`. Re-running `init` never overwrites a
|
|
58
|
+
changed generated file unless `--force` is supplied.
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
`caspian.native.json` is the source of truth:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"$schema": "./caspian.native.schema.json",
|
|
67
|
+
"schema": 1,
|
|
68
|
+
"productName": "My App",
|
|
69
|
+
"identifier": "com.example.myapp",
|
|
70
|
+
"version": "0.0.1",
|
|
71
|
+
"targets": ["windows"],
|
|
72
|
+
"window": {
|
|
73
|
+
"title": "My App",
|
|
74
|
+
"width": 1200,
|
|
75
|
+
"height": 800,
|
|
76
|
+
"resizable": true
|
|
77
|
+
},
|
|
78
|
+
"backend": {
|
|
79
|
+
"desktopMode": "embedded",
|
|
80
|
+
"remoteUrl": null
|
|
81
|
+
},
|
|
82
|
+
"security": {
|
|
83
|
+
"loopbackToken": true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Runtime model
|
|
89
|
+
|
|
90
|
+
Embedded desktop mode builds `main.py`, the Caspian runtime, project modules,
|
|
91
|
+
route index, and `public/` into a one-file sidecar. The sidecar:
|
|
92
|
+
|
|
93
|
+
1. restores a per-installation `AUTH_SECRET` from application data;
|
|
94
|
+
2. imports the project's `main.app` in production mode;
|
|
95
|
+
3. binds `127.0.0.1` on an operating-system-assigned port;
|
|
96
|
+
4. prints the protected launch URL to the Tauri host;
|
|
97
|
+
5. serves the normal Caspian ASGI application with Uvicorn.
|
|
98
|
+
|
|
99
|
+
The WebView receives a per-launch token once. The native middleware exchanges
|
|
100
|
+
it for an `HttpOnly; SameSite=Strict` cookie and refuses documents, assets,
|
|
101
|
+
RPCs, uploads, streams, and WebSocket handshakes without that cookie.
|
|
102
|
+
|
|
103
|
+
Caspian's production session cookie normally has the `Secure` flag. A packaged
|
|
104
|
+
loopback server intentionally uses `http://127.0.0.1`, so the adapter changes
|
|
105
|
+
only that middleware option before Starlette constructs the stack. The app
|
|
106
|
+
otherwise remains in production mode.
|
|
107
|
+
|
|
108
|
+
## Native bridge
|
|
109
|
+
|
|
110
|
+
Pages may capability-check `window.caspianNative`:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
const native = window.caspianNative;
|
|
114
|
+
if (native?.has("open-external")) {
|
|
115
|
+
await native.invoke("open_external", { url: "https://example.com" });
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The bridge allowlist is intentionally short: platform, application version,
|
|
120
|
+
application data directory, external HTTP/HTTPS/mail links, and a user-driven
|
|
121
|
+
file picker. An XSS can reach every bridge command, so commands must remain
|
|
122
|
+
safe even when the calling page is compromised.
|
|
123
|
+
|
|
124
|
+
## Current limitations
|
|
125
|
+
|
|
126
|
+
- The first embedded-backend implementation uses PyInstaller and has only been
|
|
127
|
+
designed for Windows packaging.
|
|
128
|
+
- Dynamic route discovery requires project Python sources and
|
|
129
|
+
`settings/files-list.json` to be bundled.
|
|
130
|
+
- Secrets from `.env` are never embedded. Remote database credentials and
|
|
131
|
+
third-party credentials must be provisioned outside the package.
|
|
132
|
+
- Android remote mode requires connectivity and a deployed Caspian server.
|
|
133
|
+
- Installer signing and Android store signing are not automated in `0.0.1`.
|
|
134
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=75"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "caspian-native"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Optional Tauri packaging for Caspian applications"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Caspian Native contributors" }]
|
|
13
|
+
keywords = ["caspian", "pulsepoint", "tauri", "desktop", "webview"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
16
|
+
"Environment :: Win32 (MS Windows)",
|
|
17
|
+
"Framework :: FastAPI",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Software Development :: Build Tools",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
build = ["pyinstaller>=6"]
|
|
26
|
+
test = ["pytest>=8", "ruff>=0.8", "pyright>=1.1"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
caspian-native = "caspian_native.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
addopts = "-q"
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 100
|
|
40
|
+
target-version = "py311"
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
44
|
+
ignore = ["E501"]
|
|
45
|
+
|
|
46
|
+
[tool.pyright]
|
|
47
|
+
include = ["src", "tests"]
|
|
48
|
+
typeCheckingMode = "basic"
|
|
49
|
+
pythonVersion = "3.11"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .errors import NativeError
|
|
10
|
+
from .project import CaspianProject
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def rust_host_triple() -> str:
|
|
14
|
+
result = subprocess.run(
|
|
15
|
+
["rustc", "-vV"], capture_output=True, text=True, check=False, encoding="utf-8"
|
|
16
|
+
)
|
|
17
|
+
if result.returncode:
|
|
18
|
+
raise NativeError("rustc is required to name the Tauri sidecar target")
|
|
19
|
+
for line in result.stdout.splitlines():
|
|
20
|
+
if line.startswith("host: "):
|
|
21
|
+
return line.removeprefix("host: ").strip()
|
|
22
|
+
raise NativeError("rustc did not report a host target triple")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_backend(project: CaspianProject) -> Path:
|
|
26
|
+
uv = shutil.which("uv")
|
|
27
|
+
if uv is None and shutil.which("pyinstaller") is None:
|
|
28
|
+
raise NativeError(
|
|
29
|
+
"embedded backend builds require uv (the normal Caspian environment manager) "
|
|
30
|
+
"or PyInstaller in the current Python environment"
|
|
31
|
+
)
|
|
32
|
+
public = project.root / "public"
|
|
33
|
+
files_list = project.root / "settings" / "files-list.json"
|
|
34
|
+
if not public.is_dir():
|
|
35
|
+
raise NativeError("the Caspian project has no public directory; run its build first")
|
|
36
|
+
if not files_list.is_file():
|
|
37
|
+
raise NativeError("settings/files-list.json is missing; run the Caspian project build first")
|
|
38
|
+
|
|
39
|
+
work = project.native_dir / ".backend-build"
|
|
40
|
+
dist = work / "dist"
|
|
41
|
+
spec = work / "spec"
|
|
42
|
+
binaries = project.native_dir / "binaries"
|
|
43
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
binaries.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
launcher = work / "launcher.py"
|
|
46
|
+
launcher.write_text(
|
|
47
|
+
"from caspian_native.runner import main\n\nif __name__ == '__main__':\n main()\n",
|
|
48
|
+
encoding="utf-8",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
add_data = [
|
|
52
|
+
(project.root / "main.py", "app"),
|
|
53
|
+
(project.root / "src", "app/src"),
|
|
54
|
+
(public, "app/public"),
|
|
55
|
+
(project.root / "caspian.config.json", "app"),
|
|
56
|
+
(files_list, "app/settings"),
|
|
57
|
+
]
|
|
58
|
+
component_map = project.root / "settings" / "component-map.json"
|
|
59
|
+
if component_map.is_file():
|
|
60
|
+
add_data.append((component_map, "app/settings"))
|
|
61
|
+
|
|
62
|
+
separator = ";" if os.name == "nt" else ":"
|
|
63
|
+
if uv:
|
|
64
|
+
command = [
|
|
65
|
+
uv,
|
|
66
|
+
"run",
|
|
67
|
+
"--project",
|
|
68
|
+
str(project.root),
|
|
69
|
+
"--with",
|
|
70
|
+
"pyinstaller>=6",
|
|
71
|
+
"python",
|
|
72
|
+
"-m",
|
|
73
|
+
"PyInstaller",
|
|
74
|
+
]
|
|
75
|
+
else:
|
|
76
|
+
command = [sys.executable, "-m", "PyInstaller"]
|
|
77
|
+
command.extend(
|
|
78
|
+
[
|
|
79
|
+
"--noconfirm",
|
|
80
|
+
"--clean",
|
|
81
|
+
"--onefile",
|
|
82
|
+
"--name",
|
|
83
|
+
"caspian-server",
|
|
84
|
+
"--distpath",
|
|
85
|
+
str(dist),
|
|
86
|
+
"--workpath",
|
|
87
|
+
str(work / "work"),
|
|
88
|
+
"--specpath",
|
|
89
|
+
str(spec),
|
|
90
|
+
"--paths",
|
|
91
|
+
str(project.root),
|
|
92
|
+
"--paths",
|
|
93
|
+
str(Path(__file__).resolve().parents[1]),
|
|
94
|
+
"--hidden-import",
|
|
95
|
+
"main",
|
|
96
|
+
"--collect-submodules",
|
|
97
|
+
"src",
|
|
98
|
+
"--collect-submodules",
|
|
99
|
+
"caspian_native",
|
|
100
|
+
"--collect-all",
|
|
101
|
+
"casp",
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
for source, destination in add_data:
|
|
105
|
+
command.extend(["--add-data", f"{source}{separator}{destination}"])
|
|
106
|
+
command.append(str(launcher))
|
|
107
|
+
|
|
108
|
+
result = subprocess.run(command, cwd=project.root, check=False)
|
|
109
|
+
if result.returncode:
|
|
110
|
+
raise NativeError("PyInstaller failed to build the Caspian backend")
|
|
111
|
+
|
|
112
|
+
suffix = ".exe" if os.name == "nt" else ""
|
|
113
|
+
built = dist / f"caspian-server{suffix}"
|
|
114
|
+
if not built.is_file():
|
|
115
|
+
raise NativeError(f"PyInstaller completed but did not produce {built}")
|
|
116
|
+
target = binaries / f"caspian-server-{rust_host_triple()}{suffix}"
|
|
117
|
+
shutil.copy2(built, target)
|
|
118
|
+
return target
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .backend import build_backend
|
|
10
|
+
from .config import BackendConfig, NativeConfig, SecurityConfig, WindowConfig
|
|
11
|
+
from .doctor import inspect
|
|
12
|
+
from .errors import NativeError
|
|
13
|
+
from .project import CaspianProject
|
|
14
|
+
from .scaffold import write_generated
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parser() -> argparse.ArgumentParser:
|
|
18
|
+
root = argparse.ArgumentParser(prog="caspian-native")
|
|
19
|
+
root.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
20
|
+
root.add_argument("--project", type=Path, help="Caspian project root")
|
|
21
|
+
commands = root.add_subparsers(dest="command", required=True)
|
|
22
|
+
|
|
23
|
+
init = commands.add_parser("init", help="create or refresh the native Tauri shell")
|
|
24
|
+
init.add_argument("--identifier", required=True)
|
|
25
|
+
init.add_argument("--product-name")
|
|
26
|
+
init.add_argument("--app-version", default="0.0.1")
|
|
27
|
+
init.add_argument("--windows", action="store_true")
|
|
28
|
+
init.add_argument("--android", action="store_true")
|
|
29
|
+
init.add_argument("--remote-url")
|
|
30
|
+
init.add_argument("--desktop-mode", choices=("embedded", "remote"), default="embedded")
|
|
31
|
+
init.add_argument("--force", action="store_true")
|
|
32
|
+
|
|
33
|
+
doctor = commands.add_parser("doctor", help="check native packaging prerequisites")
|
|
34
|
+
doctor.add_argument("--target", choices=("windows", "android"), default="windows")
|
|
35
|
+
|
|
36
|
+
backend = commands.add_parser("backend", help="build the embedded Caspian sidecar")
|
|
37
|
+
backend.add_argument("action", choices=("build",))
|
|
38
|
+
|
|
39
|
+
dev = commands.add_parser("dev", help="run the generated Tauri application")
|
|
40
|
+
dev.add_argument("--target", choices=("windows", "android"), default="windows")
|
|
41
|
+
|
|
42
|
+
build = commands.add_parser("build", help="build a native installer or Android package")
|
|
43
|
+
build.add_argument("--target", choices=("windows", "android"), default="windows")
|
|
44
|
+
build.add_argument("--debug", action="store_true")
|
|
45
|
+
return root
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main(argv: list[str] | None = None) -> None:
|
|
49
|
+
args = parser().parse_args(argv)
|
|
50
|
+
try:
|
|
51
|
+
project = (
|
|
52
|
+
CaspianProject.from_root(args.project)
|
|
53
|
+
if args.project
|
|
54
|
+
else CaspianProject.discover()
|
|
55
|
+
)
|
|
56
|
+
if args.command == "init":
|
|
57
|
+
_init(project, args)
|
|
58
|
+
elif args.command == "doctor":
|
|
59
|
+
_doctor(project, args.target)
|
|
60
|
+
elif args.command == "backend":
|
|
61
|
+
print(build_backend(project))
|
|
62
|
+
elif args.command in {"dev", "build"}:
|
|
63
|
+
_tauri(project, args.command, args.target, getattr(args, "debug", False))
|
|
64
|
+
except NativeError as exc:
|
|
65
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
66
|
+
raise SystemExit(2) from exc
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _init(project: CaspianProject, args: argparse.Namespace) -> None:
|
|
70
|
+
targets = [target for target, enabled in (("windows", args.windows), ("android", args.android)) if enabled]
|
|
71
|
+
if not targets:
|
|
72
|
+
targets = ["windows"]
|
|
73
|
+
product = args.product_name or project.name.replace("-", " ").title()
|
|
74
|
+
config = NativeConfig(
|
|
75
|
+
product_name=product,
|
|
76
|
+
identifier=args.identifier,
|
|
77
|
+
version=args.app_version,
|
|
78
|
+
targets=targets,
|
|
79
|
+
window=WindowConfig(title=product),
|
|
80
|
+
backend=BackendConfig(desktop_mode=args.desktop_mode, remote_url=args.remote_url),
|
|
81
|
+
security=SecurityConfig(loopback_token=True),
|
|
82
|
+
)
|
|
83
|
+
config.validate()
|
|
84
|
+
for path, outcome in write_generated(project, config, force=args.force):
|
|
85
|
+
print(f"{outcome:9} {path}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _doctor(project: CaspianProject, target: str) -> None:
|
|
89
|
+
config = NativeConfig.load(project.native_config)
|
|
90
|
+
checks = inspect(project, config, target)
|
|
91
|
+
for check in checks:
|
|
92
|
+
print(f"{'OK' if check.ok else 'MISSING':7} {check.name}: {check.detail}")
|
|
93
|
+
if not all(check.ok for check in checks):
|
|
94
|
+
raise NativeError(f"{target} prerequisites are incomplete")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _tauri(project: CaspianProject, action: str, target: str, debug: bool) -> None:
|
|
98
|
+
config = NativeConfig.load(project.native_config)
|
|
99
|
+
if target not in config.targets:
|
|
100
|
+
raise NativeError(f"target {target} is not enabled in caspian.native.json")
|
|
101
|
+
if target == "android" and not config.backend.remote_url:
|
|
102
|
+
raise NativeError("Android requires backend.remoteUrl in caspian-native 0.0.1")
|
|
103
|
+
if target == "windows" and config.backend.desktop_mode == "embedded":
|
|
104
|
+
build_backend(project)
|
|
105
|
+
|
|
106
|
+
command = ["cargo", "tauri"]
|
|
107
|
+
if target == "android":
|
|
108
|
+
command.extend(["android", action])
|
|
109
|
+
else:
|
|
110
|
+
command.append(action)
|
|
111
|
+
if debug and action == "build":
|
|
112
|
+
command.append("--debug")
|
|
113
|
+
result = subprocess.run(command, cwd=project.native_dir, check=False)
|
|
114
|
+
if result.returncode:
|
|
115
|
+
raise NativeError(f"Tauri {action} failed for {target}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|