urun-cli 0.6.3__tar.gz → 0.7.0__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.
Files changed (34) hide show
  1. {urun_cli-0.6.3 → urun_cli-0.7.0}/PKG-INFO +110 -2
  2. {urun_cli-0.6.3 → urun_cli-0.7.0}/README.md +106 -1
  3. {urun_cli-0.6.3 → urun_cli-0.7.0}/pyproject.toml +13 -3
  4. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/catalog.py +60 -0
  5. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/cli.py +137 -30
  6. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/deps.py +394 -10
  7. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/dev.py +174 -20
  8. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/discovery.py +450 -2
  9. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/manifest.py +63 -0
  10. urun_cli-0.7.0/src/urun/rust.py +1497 -0
  11. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/scratch.py +389 -22
  12. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/serve.py +127 -5
  13. {urun_cli-0.6.3 → urun_cli-0.7.0}/.gitignore +0 -0
  14. {urun_cli-0.6.3 → urun_cli-0.7.0}/CHANGELOG.md +0 -0
  15. {urun_cli-0.6.3 → urun_cli-0.7.0}/LICENSE +0 -0
  16. {urun_cli-0.6.3 → urun_cli-0.7.0}/SECURITY.md +0 -0
  17. {urun_cli-0.6.3 → urun_cli-0.7.0}/hatch_build.py +0 -0
  18. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/__init__.py +0 -0
  19. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/_bin/README.md +0 -0
  20. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/api.py +0 -0
  21. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/buildlogs.py +0 -0
  22. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/client_tokens.py +0 -0
  23. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/config.py +0 -0
  24. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/devconsole.py +0 -0
  25. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/devstream.py +0 -0
  26. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/errors.py +0 -0
  27. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/fleet.py +0 -0
  28. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/frontend_launch.py +0 -0
  29. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/launch_auth.py +0 -0
  30. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/nodes.py +0 -0
  31. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/openai_proxy_bin.py +0 -0
  32. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/repo_deps.py +0 -0
  33. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/retry.py +0 -0
  34. {urun_cli-0.6.3 → urun_cli-0.7.0}/src/urun/runtime_health.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urun-cli
3
- Version: 0.6.3
3
+ Version: 0.7.0
4
4
  Summary: End-user CLI for deploying apps to urun
5
5
  Project-URL: Homepage, https://urun.sh
6
6
  Project-URL: Repository, https://github.com/urun-sh/urun-cli
@@ -18,11 +18,14 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Software Development :: Build Tools
20
20
  Requires-Python: >=3.11
21
+ Provides-Extra: rust
22
+ Requires-Dist: tree-sitter-rust==0.24.0; extra == 'rust'
23
+ Requires-Dist: tree-sitter==0.25.2; extra == 'rust'
21
24
  Description-Content-Type: text/markdown
22
25
 
23
26
  # urun CLI
24
27
 
25
- Deploy Python apps to urun from your terminal.
28
+ Deploy Python and Rust (native SDK) apps to urun from your terminal.
26
29
 
27
30
  [![PyPI](https://img.shields.io/pypi/v/urun-cli.svg)](https://pypi.org/project/urun-cli/)
28
31
  [![Python](https://img.shields.io/pypi/pyversions/urun-cli.svg)](https://pypi.org/project/urun-cli/)
@@ -35,6 +38,15 @@ uv tool install urun-cli
35
38
  pip install urun-cli
36
39
  ```
37
40
 
41
+ For Rust (native SDK) deploys, install with the `[rust]` extra — see
42
+ [Deploy a Rust app (native SDK)](#deploy-a-rust-app-native-sdk):
43
+
44
+ ```bash
45
+ uv tool install "urun-cli[rust]"
46
+ # or
47
+ pip install "urun-cli[rust]"
48
+ ```
49
+
38
50
  The package installs the `urun` command:
39
51
 
40
52
  ```bash
@@ -154,6 +166,100 @@ Override the examples checkout with `--examples-root` or `URUN_EXAMPLES_ROOT`;
154
166
  override the session API with `--session-url` or `URUN_SESSION_BASE_URL`; pin
155
167
  the function with `--function` and the environment with `--environment`.
156
168
 
169
+ ## Deploy a Rust app (native SDK)
170
+
171
+ Rust apps use the native SDK crate `urun` and are declared by an inline module
172
+ owning `#[urun::app(name = "...")]` with nested `#[urun::function]` handlers.
173
+
174
+ The executable calls `urun::main!(app)` from `fn main()`. Each handler crosses
175
+ the `ctx.self_test(...)` barrier before accessing session state. The runnable
176
+ `native-echo` example below includes the complete lifecycle.
177
+
178
+ Install the CLI with the Rust parser extra (a plain install stays
179
+ dependency-free; the extra pulls the pinned `tree-sitter`/`tree-sitter-rust`
180
+ wheels used for static declaration extraction — no Rust toolchain needed):
181
+
182
+ ```bash
183
+ uv tool install "urun-cli[rust]"
184
+ # or
185
+ pip install "urun-cli[rust]"
186
+ ```
187
+
188
+ Deploy by pointing at the project's `Cargo.toml` (a `src/main.rs` or the
189
+ project directory also work):
190
+
191
+ ```bash
192
+ urun deploy path/to/project/Cargo.toml
193
+ ```
194
+
195
+ - **Vendor the SDK in-root.** The `urun` SDK crate is not published to
196
+ crates.io and the build environment has no private git credentials, so the
197
+ crate must ship inside your deploy root: reference it by a relative path
198
+ dependency (`urun = { path = "vendor/urun" }`) or as a member of the Cargo
199
+ workspace rooted there. Credential-free registry/git dependencies are fine.
200
+ - **`Cargo.lock` is mandatory.** The builder builds with `--locked`; a missing
201
+ lock file fails the deploy.
202
+ - The bundle is the full in-root Cargo workspace / path-dependency / resource
203
+ closure, minus `target/`, `.git`, and `.cargo/credentials{,.toml}`.
204
+
205
+ A runnable starting point is the SDK checkout's `examples/native-echo` app,
206
+ which vendors the SDK via a path dependency and deploys as-is:
207
+
208
+ ```bash
209
+ git clone git@github.com:urun-sh/urun-rs.git
210
+ urun deploy urun-rs/examples/native-echo
211
+ ```
212
+
213
+ ## Deploy a mixed Python + Rust app
214
+
215
+ ONE app can hold functions implemented in BOTH languages: one identity, one
216
+ release, the full function set replaced atomically on every deploy. Declare
217
+ the sources explicitly in the project's `pyproject.toml` — nothing is scanned
218
+ or inferred, so vendored SDK examples are never pulled into your app:
219
+
220
+ ```toml
221
+ # pyproject.toml
222
+ [tool.urun]
223
+ entrypoints = ["app.py", "native/src/main.rs"]
224
+ ```
225
+
226
+ Python functions register on one `App("mixed-app")`. Rust functions use
227
+ `#[urun::function(...)]` inside `#[urun::app(name = "mixed-app")]`.
228
+ The builder assembles both into one function registry: the language belongs
229
+ to the function, not to a separate app. See the runnable
230
+ [`examples/mixed-app`](https://github.com/urun-sh/urun-rs/tree/main/examples/mixed-app)
231
+ for complete Python and Rust implementations, including self-tests and calls
232
+ in both directions.
233
+
234
+ Deploy the whole app with any of these — each resolves the ENTIRE declared
235
+ app, so one component invocation never accidentally retires its sibling
236
+ functions:
237
+
238
+ ```bash
239
+ urun deploy . # the config directory
240
+ urun deploy pyproject.toml # the declaration itself
241
+ urun deploy app.py # a declared component
242
+ urun deploy native/src/main.rs
243
+ ```
244
+
245
+ - **One identity across all sources.** Python helpers may import the same
246
+ `App` object; Rust declarations use the same app-name literal. A different
247
+ App object, mismatched name, or duplicate function name is a hard error.
248
+ - **Declared paths stay in the config directory** (root-relative, no `..`
249
+ escapes, every declared file present). The bundle root may widen through
250
+ ordinary Cargo workspace resolution — e.g. a crate that is a member of a
251
+ wider workspace ships that whole workspace closure — and every entrypoint
252
+ and `manifest_path` rebases onto it.
253
+ - **Python dependencies stay Python.** The manifest's `deps` block is
254
+ resolved from the Python source exactly like a pure Python deploy
255
+ (`Dependencies(python=...)`, `requirements.txt`, `python_version`); the
256
+ Rust sources' `apt`/`build_deps` function options fold into the same lists.
257
+ - **Resources ride with their code** in both halves: the Python import/data
258
+ closure plus each Rust crate's full workspace/path closure and every
259
+ referenced `Cargo.lock`.
260
+
261
+ A runnable starting point is the SDK checkout's `examples/mixed-app` app.
262
+
157
263
  ## Serve a model from the catalog
158
264
 
159
265
  `urun serve` deploys a model straight from the urun model catalog — no app code
@@ -449,6 +555,8 @@ would fire kubectl at a cluster you are not looking at.
449
555
  | Entrypoint | Included source |
450
556
  | --- | --- |
451
557
  | `urun deploy app.py` | `app.py` and local Python files it imports |
558
+ | `urun deploy Cargo.toml` | The in-root Cargo workspace / path-dependency closure (see [Deploy a Rust app (native SDK)](#deploy-a-rust-app-native-sdk)). |
559
+ | `urun deploy .` / `pyproject.toml` | The whole explicitly-declared `[tool.urun]` app — Python + Rust sources, both closures (see [Deploy a mixed Python + Rust app](#deploy-a-mixed-python--rust-app)). A declared component (`app.py`, `native/src/main.rs`) resolves the same entire app. |
452
560
 
453
561
  Dependencies are declared in your urun app code. Project-level files such as
454
562
  `pyproject.toml` and `requirements.txt` are not uploaded as dependency
@@ -1,6 +1,6 @@
1
1
  # urun CLI
2
2
 
3
- Deploy Python apps to urun from your terminal.
3
+ Deploy Python and Rust (native SDK) apps to urun from your terminal.
4
4
 
5
5
  [![PyPI](https://img.shields.io/pypi/v/urun-cli.svg)](https://pypi.org/project/urun-cli/)
6
6
  [![Python](https://img.shields.io/pypi/pyversions/urun-cli.svg)](https://pypi.org/project/urun-cli/)
@@ -13,6 +13,15 @@ uv tool install urun-cli
13
13
  pip install urun-cli
14
14
  ```
15
15
 
16
+ For Rust (native SDK) deploys, install with the `[rust]` extra — see
17
+ [Deploy a Rust app (native SDK)](#deploy-a-rust-app-native-sdk):
18
+
19
+ ```bash
20
+ uv tool install "urun-cli[rust]"
21
+ # or
22
+ pip install "urun-cli[rust]"
23
+ ```
24
+
16
25
  The package installs the `urun` command:
17
26
 
18
27
  ```bash
@@ -132,6 +141,100 @@ Override the examples checkout with `--examples-root` or `URUN_EXAMPLES_ROOT`;
132
141
  override the session API with `--session-url` or `URUN_SESSION_BASE_URL`; pin
133
142
  the function with `--function` and the environment with `--environment`.
134
143
 
144
+ ## Deploy a Rust app (native SDK)
145
+
146
+ Rust apps use the native SDK crate `urun` and are declared by an inline module
147
+ owning `#[urun::app(name = "...")]` with nested `#[urun::function]` handlers.
148
+
149
+ The executable calls `urun::main!(app)` from `fn main()`. Each handler crosses
150
+ the `ctx.self_test(...)` barrier before accessing session state. The runnable
151
+ `native-echo` example below includes the complete lifecycle.
152
+
153
+ Install the CLI with the Rust parser extra (a plain install stays
154
+ dependency-free; the extra pulls the pinned `tree-sitter`/`tree-sitter-rust`
155
+ wheels used for static declaration extraction — no Rust toolchain needed):
156
+
157
+ ```bash
158
+ uv tool install "urun-cli[rust]"
159
+ # or
160
+ pip install "urun-cli[rust]"
161
+ ```
162
+
163
+ Deploy by pointing at the project's `Cargo.toml` (a `src/main.rs` or the
164
+ project directory also work):
165
+
166
+ ```bash
167
+ urun deploy path/to/project/Cargo.toml
168
+ ```
169
+
170
+ - **Vendor the SDK in-root.** The `urun` SDK crate is not published to
171
+ crates.io and the build environment has no private git credentials, so the
172
+ crate must ship inside your deploy root: reference it by a relative path
173
+ dependency (`urun = { path = "vendor/urun" }`) or as a member of the Cargo
174
+ workspace rooted there. Credential-free registry/git dependencies are fine.
175
+ - **`Cargo.lock` is mandatory.** The builder builds with `--locked`; a missing
176
+ lock file fails the deploy.
177
+ - The bundle is the full in-root Cargo workspace / path-dependency / resource
178
+ closure, minus `target/`, `.git`, and `.cargo/credentials{,.toml}`.
179
+
180
+ A runnable starting point is the SDK checkout's `examples/native-echo` app,
181
+ which vendors the SDK via a path dependency and deploys as-is:
182
+
183
+ ```bash
184
+ git clone git@github.com:urun-sh/urun-rs.git
185
+ urun deploy urun-rs/examples/native-echo
186
+ ```
187
+
188
+ ## Deploy a mixed Python + Rust app
189
+
190
+ ONE app can hold functions implemented in BOTH languages: one identity, one
191
+ release, the full function set replaced atomically on every deploy. Declare
192
+ the sources explicitly in the project's `pyproject.toml` — nothing is scanned
193
+ or inferred, so vendored SDK examples are never pulled into your app:
194
+
195
+ ```toml
196
+ # pyproject.toml
197
+ [tool.urun]
198
+ entrypoints = ["app.py", "native/src/main.rs"]
199
+ ```
200
+
201
+ Python functions register on one `App("mixed-app")`. Rust functions use
202
+ `#[urun::function(...)]` inside `#[urun::app(name = "mixed-app")]`.
203
+ The builder assembles both into one function registry: the language belongs
204
+ to the function, not to a separate app. See the runnable
205
+ [`examples/mixed-app`](https://github.com/urun-sh/urun-rs/tree/main/examples/mixed-app)
206
+ for complete Python and Rust implementations, including self-tests and calls
207
+ in both directions.
208
+
209
+ Deploy the whole app with any of these — each resolves the ENTIRE declared
210
+ app, so one component invocation never accidentally retires its sibling
211
+ functions:
212
+
213
+ ```bash
214
+ urun deploy . # the config directory
215
+ urun deploy pyproject.toml # the declaration itself
216
+ urun deploy app.py # a declared component
217
+ urun deploy native/src/main.rs
218
+ ```
219
+
220
+ - **One identity across all sources.** Python helpers may import the same
221
+ `App` object; Rust declarations use the same app-name literal. A different
222
+ App object, mismatched name, or duplicate function name is a hard error.
223
+ - **Declared paths stay in the config directory** (root-relative, no `..`
224
+ escapes, every declared file present). The bundle root may widen through
225
+ ordinary Cargo workspace resolution — e.g. a crate that is a member of a
226
+ wider workspace ships that whole workspace closure — and every entrypoint
227
+ and `manifest_path` rebases onto it.
228
+ - **Python dependencies stay Python.** The manifest's `deps` block is
229
+ resolved from the Python source exactly like a pure Python deploy
230
+ (`Dependencies(python=...)`, `requirements.txt`, `python_version`); the
231
+ Rust sources' `apt`/`build_deps` function options fold into the same lists.
232
+ - **Resources ride with their code** in both halves: the Python import/data
233
+ closure plus each Rust crate's full workspace/path closure and every
234
+ referenced `Cargo.lock`.
235
+
236
+ A runnable starting point is the SDK checkout's `examples/mixed-app` app.
237
+
135
238
  ## Serve a model from the catalog
136
239
 
137
240
  `urun serve` deploys a model straight from the urun model catalog — no app code
@@ -427,6 +530,8 @@ would fire kubectl at a cluster you are not looking at.
427
530
  | Entrypoint | Included source |
428
531
  | --- | --- |
429
532
  | `urun deploy app.py` | `app.py` and local Python files it imports |
533
+ | `urun deploy Cargo.toml` | The in-root Cargo workspace / path-dependency closure (see [Deploy a Rust app (native SDK)](#deploy-a-rust-app-native-sdk)). |
534
+ | `urun deploy .` / `pyproject.toml` | The whole explicitly-declared `[tool.urun]` app — Python + Rust sources, both closures (see [Deploy a mixed Python + Rust app](#deploy-a-mixed-python--rust-app)). A declared component (`app.py`, `native/src/main.rs`) resolves the same entire app. |
430
535
 
431
536
  Dependencies are declared in your urun app code. Project-level files such as
432
537
  `pyproject.toml` and `requirements.txt` are not uploaded as dependency
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
10
10
 
11
11
  [project]
12
12
  name = "urun-cli"
13
- # Cut the paused-function recovery fixes merged after v0.6.0.
13
+ # Cut the native Rust SDK deploy path (feature minor on 0.6.x).
14
14
  #
15
15
  # WHY THIS IS EDITED BY HAND. [tool.semantic_release] below is fully
16
16
  # configured -- conventional commit parser, version_toml pointing here, tag
@@ -19,8 +19,8 @@ name = "urun-cli"
19
19
  # READS this value and skips publishing when the matching tag already exists,
20
20
  # so every merge remains on the already-published version until this value is
21
21
  # advanced. Wiring semantic-release into CI remains separate work; this patch
22
- # bump is the minimal release unblock for the three post-v0.6.0 fixes.
23
- version = "0.6.3"
22
+ # bump is the minimal release unblock (post-v0.6.3 fixes + the Rust feature minor).
23
+ version = "0.7.0"
24
24
  description = "End-user CLI for deploying apps to urun"
25
25
  readme = "README.md"
26
26
  requires-python = ">=3.11"
@@ -49,6 +49,16 @@ Issues = "https://github.com/urun-sh/urun-cli/issues"
49
49
  urun = "urun.cli:main"
50
50
  urun-cli = "urun.cli:main"
51
51
 
52
+ [project.optional-dependencies]
53
+ # Rust app discovery (urun/rust.py) parses with the pinned native
54
+ # tree-sitter wheels; lazy-imported only for Rust deploys, so the ordinary
55
+ # wheel stays dependency-free. Pin advance = contract native_crate_baselines
56
+ # (tree-sitter 0.25.2 / tree-sitter-rust 0.24.0).
57
+ rust = [
58
+ "tree-sitter==0.25.2",
59
+ "tree-sitter-rust==0.24.0",
60
+ ]
61
+
52
62
  [dependency-groups]
53
63
  dev = [
54
64
  # The build backend, present in the dev env so tests can import hatch_build.py
@@ -104,6 +104,7 @@ ENGINE_DEPS: dict[str, list[str]] = {
104
104
  "echo-speak": [],
105
105
  "echo-video": [],
106
106
  "echo-video-diffusion": [],
107
+ "echo-t2av": [],
107
108
  "vllm": [
108
109
  "vllm==0.24.0",
109
110
  "arctic-inference==0.1.1",
@@ -386,6 +387,65 @@ ENGINE_DEPS: dict[str, list[str]] = {
386
387
  "huggingface_hub[hf_transfer]>=0.30.0,<1.0.0",
387
388
  "sentencepiece",
388
389
  ],
390
+ "t2av": [
391
+ "remote_pdb>=1.4",
392
+ "scipy>=1.14.1",
393
+ "six>=1.16.0",
394
+ "h5py>=3.12.1",
395
+ "requests>=2.32.2",
396
+ "numpy>=1.26,<2",
397
+ "transformers>=5.0.0",
398
+ "tokenizers>=0.20.1,<0.23",
399
+ "sentencepiece>=0.2.0",
400
+ "timm>=1.0.11",
401
+ "peft>=0.15.0",
402
+ "diffusers>=0.38.0",
403
+ "torch==2.12.0",
404
+ "torchvision",
405
+ "torchaudio",
406
+ "accelerate==1.0.1",
407
+ "opencv-python>=4.10,<5",
408
+ "pillow>=10.3.0",
409
+ "imageio>=2.36.0",
410
+ "imageio-ffmpeg>=0.5.1",
411
+ "einops",
412
+ "loguru",
413
+ "tqdm",
414
+ "PyYAML>=6.0.1",
415
+ "protobuf>=5.28.3",
416
+ "aiohttp",
417
+ "huggingface_hub",
418
+ "cloudpickle",
419
+ "omegaconf",
420
+ "gpustat",
421
+ "ninja",
422
+ "fastvideo-kernel==0.3.2; sys_platform == 'linux'",
423
+ ],
424
+ "ltx-realtime": [
425
+ "torch~=2.7",
426
+ "torchaudio",
427
+ "einops",
428
+ "numpy",
429
+ "av",
430
+ "transformers>=5.8.0,<5.15",
431
+ "safetensors",
432
+ "accelerate",
433
+ "scipy>=1.14",
434
+ "tqdm",
435
+ "pillow>=10.0.0",
436
+ "openimageio",
437
+ "cloudpickle>=3.1",
438
+ "huggingface_hub[hf_transfer]>=0.30.0,<1.0.0",
439
+ "sentencepiece",
440
+ "nvidia-cuda-nvcc==13.0.88",
441
+ "nvidia-cuda-crt==13.0.88",
442
+ "nvidia-cuda-cccl==13.0.85",
443
+ "nvidia-nvvm==13.0.88",
444
+ "nvidia-cuda-runtime==13.0.96",
445
+ "ninja",
446
+ "pip",
447
+ "nvidia-cutlass-dsl>=4.6.0.dev0,<5",
448
+ ],
389
449
  "media-transform": [
390
450
  "torch==2.6.0",
391
451
  "transformers==4.51.3",
@@ -50,10 +50,15 @@ from .config import (
50
50
  secret_consent_scope,
51
51
  )
52
52
  from .deps import (
53
+ collect_python_sources_metadata,
53
54
  confirm_local_env_capture,
54
55
  extract_app_credentials_env,
55
56
  extract_app_gpu_type,
56
57
  extract_app_python_deps,
58
+ fold_rust_options,
59
+ mixed_gpu_type,
60
+ native_gpu_type,
61
+ native_runtime_credentials,
57
62
  resolve_declared_credentials,
58
63
  resolve_deps,
59
64
  running_in_ci,
@@ -360,7 +365,7 @@ def build_parser() -> argparse.ArgumentParser:
360
365
  )
361
366
  add_login_args(login_parser)
362
367
 
363
- deploy = sub.add_parser("deploy", help="Deploy a Python app to urun")
368
+ deploy = sub.add_parser("deploy", help="Deploy a Python or Rust (native SDK) app to urun")
364
369
  add_deploy_args(deploy)
365
370
 
366
371
  env_parser = sub.add_parser(
@@ -444,11 +449,13 @@ def build_parser() -> argparse.ArgumentParser:
444
449
  "dev",
445
450
  help="Watch a local app and hot-reload it on every save, or --attach to a deployed one",
446
451
  description=(
447
- "Deploy a Python app, then watch its directory and continuously "
448
- "redeploy on every save. Code-only edits take the control plane's "
449
- "hot-reload fast lane (the running pod adopts the new code in place, "
450
- "no dependency reinstall or image rebuild), so iteration is fast. "
451
- "Changing declared dependencies, adding/removing files, or moving the "
452
+ "Deploy a Python or Rust (native SDK) app, then watch its directory "
453
+ "and continuously redeploy on every save. Code-only Python edits "
454
+ "take the control plane's hot-reload fast lane (the running pod "
455
+ "adopts the new code in place, no dependency reinstall or image "
456
+ "rebuild), so iteration is fast; native Rust source changes always "
457
+ "rebuild through Cargo — they are never hot-patched. Changing "
458
+ "declared dependencies, adding/removing files, or moving the "
452
459
  "entrypoint falls back to a full rebuild (and the CLI says so). "
453
460
  "Dev deploys are ephemeral: while `urun dev` runs the app keeps one "
454
461
  "replica warm with capacity capped at 1 (your app's own warm= / "
@@ -467,10 +474,11 @@ def build_parser() -> argparse.ArgumentParser:
467
474
 
468
475
  run = sub.add_parser(
469
476
  "run",
470
- help="Run a Python app on urun",
477
+ help="Run a Python or Rust (native SDK) app on urun",
471
478
  description=(
472
- "Run a Python app on urun. In this release, run uses the deploy pipeline "
473
- "while the full deploy/run/monitor workflow is still being built."
479
+ "Run a Python or Rust (native SDK) app on urun. In this release, run "
480
+ "uses the deploy pipeline while the full deploy/run/monitor workflow "
481
+ "is still being built."
474
482
  ),
475
483
  )
476
484
  add_deploy_args(run)
@@ -1728,7 +1736,14 @@ def make_retry_logger(verbose: bool) -> RetryReporter:
1728
1736
 
1729
1737
 
1730
1738
  def add_deploy_args(parser: argparse.ArgumentParser) -> None:
1731
- parser.add_argument("entrypoint", nargs="?", help="Python app file, e.g. app.py")
1739
+ parser.add_argument(
1740
+ "entrypoint",
1741
+ nargs="?",
1742
+ help=(
1743
+ "App entrypoint: a Python file (app.py), or a Rust project's "
1744
+ "Cargo.toml / src/main.rs for a native SDK deploy"
1745
+ ),
1746
+ )
1732
1747
  parser.add_argument("--name", help="App name (defaults from entrypoint file)")
1733
1748
  parser.add_argument(
1734
1749
  "--environment",
@@ -2122,13 +2137,47 @@ def deploy_once(args: argparse.Namespace) -> DeployOutcome:
2122
2137
  layout = discover_deploy_layout(args.entrypoint, invocation_root)
2123
2138
  entrypoint, files = layout.entrypoint, layout.files
2124
2139
  entrypoint_path = layout.entrypoint_path
2125
- # Fail LOUD before registering anything: an import that would silently not
2126
- # ship (or not import at runtime) is a deploy-time error, never a pod
2127
- # crashloop (the dreamx 2026-06-26 silent-drop hop).
2128
- assert_collected_imports_resolve(
2129
- layout, declared_deps=extract_app_python_deps(entrypoint_path).get("python", [])
2130
- )
2131
- deps, deps_blob, python_version = resolve_deps(layout.root, entrypoint_path)
2140
+ runtime = layout.runtime
2141
+ if runtime is None:
2142
+ # Python deploy (unchanged behavior). Fail LOUD before registering
2143
+ # anything: an import that would silently not ship (or not import at
2144
+ # runtime) is a deploy-time error, never a pod crashloop (the dreamx
2145
+ # 2026-06-26 silent-drop hop).
2146
+ assert_collected_imports_resolve(
2147
+ layout, declared_deps=extract_app_python_deps(entrypoint_path).get("python", [])
2148
+ )
2149
+ deps, deps_blob, python_version = resolve_deps(layout.root, entrypoint_path)
2150
+ elif runtime.get("kind") == "mixed":
2151
+ # MIXED deploy: ONE app (Python + Rust sources), ONE release. EVERY
2152
+ # declared Python source's import closure is first-class metadata:
2153
+ # credentials, deps and GPU declarations are extracted across the
2154
+ # WHOLE union (deduped by file identity), imports are verified against
2155
+ # the collected closure, deps resolve through the normal Python path
2156
+ # (app/requirements kinds; NEVER the Rust-only supervisor spec) with
2157
+ # per-function declarations preserved for the builder, and the Rust
2158
+ # sources' apt/build_deps options fold into the SAME deps block.
2159
+ # Discovery already rejected identity mismatches and statically-visible
2160
+ # duplicate function names.
2161
+ python_entrypoint_path = layout.python_entrypoint_path
2162
+ # Computed ONCE here and reused by the credentials and GPU-class
2163
+ # blocks below — never re-parsed per metadata consumer.
2164
+ mixed_metadata = collect_python_sources_metadata(layout.collected)
2165
+ assert_collected_imports_resolve(
2166
+ layout._replace(entrypoint_path=python_entrypoint_path),
2167
+ declared_deps=mixed_metadata["python_deps"],
2168
+ )
2169
+ deps, deps_blob, python_version = resolve_deps(
2170
+ layout.root, python_entrypoint_path, python_sources=list(layout.collected)
2171
+ )
2172
+ deps = fold_rust_options(deps, runtime)
2173
+ else:
2174
+ # Native Rust deploy: there is no Python entrypoint to parse, so the
2175
+ # Python import/deps AST scans are skipped entirely. resolve_deps
2176
+ # consumes the discovery-produced runtime object directly — the Rust
2177
+ # source is never reparsed here.
2178
+ deps, deps_blob, python_version = resolve_deps(
2179
+ layout.root, entrypoint_path, runtime=runtime
2180
+ )
2132
2181
 
2133
2182
  # PER-DEPLOY torch backend. `resolve_deps` already threads the app-declared
2134
2183
  # ``Dependencies(torch_backend=...)`` into ``deps`` for a plain `urun deploy`.
@@ -2137,7 +2186,7 @@ def deploy_once(args: argparse.Namespace) -> DeployOutcome:
2137
2186
  # build's app-deps install (urun-python deps_install) installs the engine's
2138
2187
  # pinned torch from the GPU-correct CUDA index.
2139
2188
  torch_backend = getattr(args, "torch_backend", None)
2140
- if torch_backend:
2189
+ if torch_backend and (runtime is None or runtime.get("kind") == "mixed"):
2141
2190
  deps = {**deps, "torch_backend": torch_backend}
2142
2191
 
2143
2192
  # Derive the fallback name from the entrypoint's boundary-relative path —
@@ -2145,22 +2194,45 @@ def deploy_once(args: argparse.Namespace) -> DeployOutcome:
2145
2194
  # app used to derive "app" from inside backend/ and "backend" from the
2146
2195
  # repo root).
2147
2196
  entrypoint_rel = entrypoint_path.relative_to(layout.boundary).as_posix()
2148
- # The app's OWN declared App("...") name is the SINGLE source of truth
2149
- # (OWNER ruling: one identity per app, no divergence). --name stays for
2150
- # compat, but when the entrypoint declares a static App("<literal>") name a
2151
- # divergent --name is a HARD ERROR — that mismatch is exactly how one app
2197
+ # The app's OWN declared identity is the SINGLE source of truth (OWNER
2198
+ # ruling: one identity per app, no divergence). Python reads a static
2199
+ # App("<literal>") name; a native deploy reads the #[urun::app(name =
2200
+ # "...")] literal carried on the layout. A divergent --name is a HARD
2201
+ # ERROR either way — that mismatch is exactly how one app
2152
2202
  # (App("magic-mirror")) got deployed under a second control-plane identity
2153
2203
  # (--name magic-mirror-sana) and forked into a duplicate row.
2154
- declared = declared_app_name(entrypoint_path)
2204
+ declared = declared_app_name(entrypoint_path) if runtime is None else layout.app_name
2155
2205
  if args.name is not None and declared is not None and args.name != declared:
2206
+ if runtime is None:
2207
+ declared_how = 'App("...")'
2208
+ elif runtime.get("kind") == "mixed":
2209
+ declared_how = (
2210
+ 'the mixed app\'s declared identity (App("...") / #[urun::app(name = "...")])'
2211
+ )
2212
+ else:
2213
+ declared_how = '#[urun::app(name = "...")]'
2156
2214
  raise UrunError(
2157
2215
  f"--name {args.name!r} disagrees with the app's own declared identity "
2158
- f'App("{declared}") in {entrypoint_rel}. The App("...") name is the '
2159
- "single source of truth for an app's identity; drop --name (it is "
2160
- f'inferred from App("...")) or make it match. To rename the app, '
2161
- f'change the App(...) literal to "{args.name}".'
2216
+ f'{declared_how} "{declared}" in {entrypoint_rel}. The declared name is '
2217
+ "the single source of truth for an app's identity; drop --name (it is "
2218
+ "inferred from the declaration) or make it match. To rename the app, "
2219
+ f'change the declared name to "{args.name}".'
2162
2220
  )
2163
- app_name = args.name or derive_app_name(entrypoint_rel, entrypoint_path)
2221
+ if runtime is None:
2222
+ app_name = args.name or derive_app_name(entrypoint_rel, entrypoint_path)
2223
+ elif declared is None:
2224
+ # Fail-closed, and unreachable through real discovery (urun.rust
2225
+ # refuses any Rust app whose #[urun::app] lacks `name = "<literal>"`),
2226
+ # so --name can never substitute for a missing declared identity —
2227
+ # a --name-only identity is exactly the duplicate-app fork the
2228
+ # declared-identity ruling exists to prevent. No fallback is invented.
2229
+ raise UrunError(
2230
+ f'native app {entrypoint_rel} carries no declared #[urun::app(name = "...")] '
2231
+ "identity. The declared name is the single source of truth for a "
2232
+ "native deploy — declare it in the source; --name cannot substitute."
2233
+ )
2234
+ else:
2235
+ app_name = args.name or declared
2164
2236
 
2165
2237
  # Segregated dev identity (OWNER ruling 2026-07-18): a dev deploy and a
2166
2238
  # standard deploy of the same app share a release but NOT the app
@@ -2204,7 +2276,30 @@ def deploy_once(args: argparse.Namespace) -> DeployOutcome:
2204
2276
  # AT PRIMITIVE USE — surfaced through the boot self-test's ``MissingCredentialError``
2205
2277
  # (rendered by :func:`print_build_failure`), not by a blanket declaration check.
2206
2278
  # Secret values are never printed.
2207
- required_env, optional_env = extract_app_credentials_env(entrypoint_path)
2279
+ if runtime is None:
2280
+ required_env, optional_env = extract_app_credentials_env(entrypoint_path)
2281
+ elif runtime.get("kind") == "mixed":
2282
+ # Mixed runtime: EVERY declared Python source's closure contributes
2283
+ # its Credentials declarations (deduped by file identity); the Rust
2284
+ # sources' declared function-credential NAMES union in on top.
2285
+ # Everything rides the same org-store resolution + local-env capture
2286
+ # consent as the pure Python extraction.
2287
+ rust_required, rust_optional = native_runtime_credentials(runtime)
2288
+ required_env = mixed_metadata["required_env"] + [
2289
+ n for n in rust_required if n not in mixed_metadata["required_env"]
2290
+ ]
2291
+ # REQUIRED wins over OPTIONAL: a name any Python source or Rust
2292
+ # function REQUIRES never also rides the optional lane — the optional
2293
+ # consent path (a declined name just deploys without it) must never
2294
+ # weaken a required secret into a skippable one.
2295
+ optional_env = [n for n in mixed_metadata["optional_env"] if n not in required_env] + [
2296
+ n for n in rust_optional if n not in required_env
2297
+ ]
2298
+ else:
2299
+ # Native runtime: credentials come from the declared function options
2300
+ # (env-var NAMES only) and ride the exact same org-store resolution +
2301
+ # local-env capture consent as the Python extraction.
2302
+ required_env, optional_env = native_runtime_credentials(runtime)
2208
2303
  creds_to_push: dict[str, str] = {}
2209
2304
  creds_from_store: list[str] = []
2210
2305
  declared_credential_names: list[str] = []
@@ -2234,6 +2329,11 @@ def deploy_once(args: argparse.Namespace) -> DeployOutcome:
2234
2329
  "client_version": f"urun-cli/{__version__}",
2235
2330
  "environment": environment,
2236
2331
  }
2332
+ if runtime is not None:
2333
+ # runtime IS content identity (contract): emitted VERBATIM from
2334
+ # discovery and folded into the canonical manifest hash — never
2335
+ # stripped. Python manifests carry no runtime key at all.
2336
+ manifest["runtime"] = runtime
2237
2337
  # DEPLOY-DECLARED GPU CLASS (content-bearing — the class IS content: it
2238
2338
  # decides the venv's torch backend). The builder's only other source of the
2239
2339
  # class is importing the app, which legitimately fails pre-deps-install
@@ -2242,7 +2342,14 @@ def deploy_once(args: argparse.Namespace) -> DeployOutcome:
2242
2342
  # an sm_120 (rtx6000) release (prod-use2 2026-07-17 rolling-sink poison).
2243
2343
  # Stamped only when statically certain (see extract_app_gpu_type); absent =>
2244
2344
  # byte-identical manifest and legacy builder behavior.
2245
- gpu_type = extract_app_gpu_type(entrypoint_path)
2345
+ if runtime is None:
2346
+ gpu_type = extract_app_gpu_type(entrypoint_path)
2347
+ elif runtime.get("kind") == "mixed":
2348
+ # EVERY declared Python source's closure contributes its GPU class
2349
+ # (order-free conflict check against the Rust classes downstream).
2350
+ gpu_type = mixed_gpu_type(mixed_metadata["gpu_classes"], runtime)
2351
+ else:
2352
+ gpu_type = native_gpu_type(runtime)
2246
2353
  if gpu_type:
2247
2354
  manifest["gpu_type"] = gpu_type
2248
2355
  # Declare the credential env-var NAMES (never values) on the manifest so the