glassvm 0.1.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.
@@ -0,0 +1,382 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
10
+
11
+ [[package]]
12
+ name = "block-buffer"
13
+ version = "0.10.4"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
16
+ dependencies = [
17
+ "generic-array",
18
+ ]
19
+
20
+ [[package]]
21
+ name = "cfg-if"
22
+ version = "1.0.4"
23
+ source = "registry+https://github.com/rust-lang/crates.io-index"
24
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
25
+
26
+ [[package]]
27
+ name = "cpufeatures"
28
+ version = "0.2.17"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
31
+ dependencies = [
32
+ "libc",
33
+ ]
34
+
35
+ [[package]]
36
+ name = "crypto-common"
37
+ version = "0.1.7"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
40
+ dependencies = [
41
+ "generic-array",
42
+ "typenum",
43
+ ]
44
+
45
+ [[package]]
46
+ name = "digest"
47
+ version = "0.10.7"
48
+ source = "registry+https://github.com/rust-lang/crates.io-index"
49
+ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
50
+ dependencies = [
51
+ "block-buffer",
52
+ "crypto-common",
53
+ ]
54
+
55
+ [[package]]
56
+ name = "generic-array"
57
+ version = "0.14.7"
58
+ source = "registry+https://github.com/rust-lang/crates.io-index"
59
+ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
60
+ dependencies = [
61
+ "typenum",
62
+ "version_check",
63
+ ]
64
+
65
+ [[package]]
66
+ name = "glassvm"
67
+ version = "0.1.1"
68
+ dependencies = [
69
+ "pyo3",
70
+ "serde_json",
71
+ ]
72
+
73
+ [[package]]
74
+ name = "glassvm_core"
75
+ version = "0.1.1"
76
+ dependencies = [
77
+ "glassvm_normalizer_contract",
78
+ "serde",
79
+ "serde_json",
80
+ "sha2",
81
+ ]
82
+
83
+ [[package]]
84
+ name = "glassvm_normalizer_contract"
85
+ version = "0.1.1"
86
+ dependencies = [
87
+ "serde",
88
+ "serde_json",
89
+ ]
90
+
91
+ [[package]]
92
+ name = "glassvm_query"
93
+ version = "0.1.1"
94
+ dependencies = [
95
+ "glassvm_core",
96
+ "serde",
97
+ "serde_json",
98
+ ]
99
+
100
+ [[package]]
101
+ name = "glassvm_recorder"
102
+ version = "0.1.1"
103
+ dependencies = [
104
+ "glassvm_core",
105
+ "serde",
106
+ "serde_cbor",
107
+ "serde_json",
108
+ ]
109
+
110
+ [[package]]
111
+ name = "glassvm_registry"
112
+ version = "0.1.1"
113
+ dependencies = [
114
+ "glassvm_core",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "half"
119
+ version = "1.8.3"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
122
+
123
+ [[package]]
124
+ name = "heck"
125
+ version = "0.5.0"
126
+ source = "registry+https://github.com/rust-lang/crates.io-index"
127
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
128
+
129
+ [[package]]
130
+ name = "indoc"
131
+ version = "2.0.7"
132
+ source = "registry+https://github.com/rust-lang/crates.io-index"
133
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
134
+ dependencies = [
135
+ "rustversion",
136
+ ]
137
+
138
+ [[package]]
139
+ name = "itoa"
140
+ version = "1.0.18"
141
+ source = "registry+https://github.com/rust-lang/crates.io-index"
142
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
143
+
144
+ [[package]]
145
+ name = "libc"
146
+ version = "0.2.189"
147
+ source = "registry+https://github.com/rust-lang/crates.io-index"
148
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
149
+
150
+ [[package]]
151
+ name = "memchr"
152
+ version = "2.8.3"
153
+ source = "registry+https://github.com/rust-lang/crates.io-index"
154
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
155
+
156
+ [[package]]
157
+ name = "memoffset"
158
+ version = "0.9.1"
159
+ source = "registry+https://github.com/rust-lang/crates.io-index"
160
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
161
+ dependencies = [
162
+ "autocfg",
163
+ ]
164
+
165
+ [[package]]
166
+ name = "once_cell"
167
+ version = "1.21.4"
168
+ source = "registry+https://github.com/rust-lang/crates.io-index"
169
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
170
+
171
+ [[package]]
172
+ name = "portable-atomic"
173
+ version = "1.14.0"
174
+ source = "registry+https://github.com/rust-lang/crates.io-index"
175
+ checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
176
+
177
+ [[package]]
178
+ name = "proc-macro2"
179
+ version = "1.0.107"
180
+ source = "registry+https://github.com/rust-lang/crates.io-index"
181
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
182
+ dependencies = [
183
+ "unicode-ident",
184
+ ]
185
+
186
+ [[package]]
187
+ name = "pyo3"
188
+ version = "0.27.2"
189
+ source = "registry+https://github.com/rust-lang/crates.io-index"
190
+ checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
191
+ dependencies = [
192
+ "indoc",
193
+ "libc",
194
+ "memoffset",
195
+ "once_cell",
196
+ "portable-atomic",
197
+ "pyo3-build-config",
198
+ "pyo3-ffi",
199
+ "pyo3-macros",
200
+ "unindent",
201
+ ]
202
+
203
+ [[package]]
204
+ name = "pyo3-build-config"
205
+ version = "0.27.2"
206
+ source = "registry+https://github.com/rust-lang/crates.io-index"
207
+ checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
208
+ dependencies = [
209
+ "target-lexicon",
210
+ ]
211
+
212
+ [[package]]
213
+ name = "pyo3-ffi"
214
+ version = "0.27.2"
215
+ source = "registry+https://github.com/rust-lang/crates.io-index"
216
+ checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
217
+ dependencies = [
218
+ "libc",
219
+ "pyo3-build-config",
220
+ ]
221
+
222
+ [[package]]
223
+ name = "pyo3-macros"
224
+ version = "0.27.2"
225
+ source = "registry+https://github.com/rust-lang/crates.io-index"
226
+ checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
227
+ dependencies = [
228
+ "proc-macro2",
229
+ "pyo3-macros-backend",
230
+ "quote",
231
+ "syn 2.0.119",
232
+ ]
233
+
234
+ [[package]]
235
+ name = "pyo3-macros-backend"
236
+ version = "0.27.2"
237
+ source = "registry+https://github.com/rust-lang/crates.io-index"
238
+ checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
239
+ dependencies = [
240
+ "heck",
241
+ "proc-macro2",
242
+ "pyo3-build-config",
243
+ "quote",
244
+ "syn 2.0.119",
245
+ ]
246
+
247
+ [[package]]
248
+ name = "quote"
249
+ version = "1.0.47"
250
+ source = "registry+https://github.com/rust-lang/crates.io-index"
251
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
252
+ dependencies = [
253
+ "proc-macro2",
254
+ ]
255
+
256
+ [[package]]
257
+ name = "rustversion"
258
+ version = "1.0.23"
259
+ source = "registry+https://github.com/rust-lang/crates.io-index"
260
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
261
+
262
+ [[package]]
263
+ name = "serde"
264
+ version = "1.0.229"
265
+ source = "registry+https://github.com/rust-lang/crates.io-index"
266
+ checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
267
+ dependencies = [
268
+ "serde_core",
269
+ "serde_derive",
270
+ ]
271
+
272
+ [[package]]
273
+ name = "serde_cbor"
274
+ version = "0.11.2"
275
+ source = "registry+https://github.com/rust-lang/crates.io-index"
276
+ checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
277
+ dependencies = [
278
+ "half",
279
+ "serde",
280
+ ]
281
+
282
+ [[package]]
283
+ name = "serde_core"
284
+ version = "1.0.229"
285
+ source = "registry+https://github.com/rust-lang/crates.io-index"
286
+ checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
287
+ dependencies = [
288
+ "serde_derive",
289
+ ]
290
+
291
+ [[package]]
292
+ name = "serde_derive"
293
+ version = "1.0.229"
294
+ source = "registry+https://github.com/rust-lang/crates.io-index"
295
+ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
296
+ dependencies = [
297
+ "proc-macro2",
298
+ "quote",
299
+ "syn 3.0.3",
300
+ ]
301
+
302
+ [[package]]
303
+ name = "serde_json"
304
+ version = "1.0.151"
305
+ source = "registry+https://github.com/rust-lang/crates.io-index"
306
+ checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
307
+ dependencies = [
308
+ "itoa",
309
+ "memchr",
310
+ "serde",
311
+ "serde_core",
312
+ "zmij",
313
+ ]
314
+
315
+ [[package]]
316
+ name = "sha2"
317
+ version = "0.10.9"
318
+ source = "registry+https://github.com/rust-lang/crates.io-index"
319
+ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
320
+ dependencies = [
321
+ "cfg-if",
322
+ "cpufeatures",
323
+ "digest",
324
+ ]
325
+
326
+ [[package]]
327
+ name = "syn"
328
+ version = "2.0.119"
329
+ source = "registry+https://github.com/rust-lang/crates.io-index"
330
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
331
+ dependencies = [
332
+ "proc-macro2",
333
+ "quote",
334
+ "unicode-ident",
335
+ ]
336
+
337
+ [[package]]
338
+ name = "syn"
339
+ version = "3.0.3"
340
+ source = "registry+https://github.com/rust-lang/crates.io-index"
341
+ checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
342
+ dependencies = [
343
+ "proc-macro2",
344
+ "quote",
345
+ "unicode-ident",
346
+ ]
347
+
348
+ [[package]]
349
+ name = "target-lexicon"
350
+ version = "0.13.5"
351
+ source = "registry+https://github.com/rust-lang/crates.io-index"
352
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
353
+
354
+ [[package]]
355
+ name = "typenum"
356
+ version = "1.20.1"
357
+ source = "registry+https://github.com/rust-lang/crates.io-index"
358
+ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
359
+
360
+ [[package]]
361
+ name = "unicode-ident"
362
+ version = "1.0.24"
363
+ source = "registry+https://github.com/rust-lang/crates.io-index"
364
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
365
+
366
+ [[package]]
367
+ name = "unindent"
368
+ version = "0.2.4"
369
+ source = "registry+https://github.com/rust-lang/crates.io-index"
370
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
371
+
372
+ [[package]]
373
+ name = "version_check"
374
+ version = "0.9.5"
375
+ source = "registry+https://github.com/rust-lang/crates.io-index"
376
+ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
377
+
378
+ [[package]]
379
+ name = "zmij"
380
+ version = "1.0.23"
381
+ source = "registry+https://github.com/rust-lang/crates.io-index"
382
+ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -0,0 +1,7 @@
1
+ [workspace]
2
+ members = ["python/facade"]
3
+ resolver = "2"
4
+
5
+ [workspace.package]
6
+ version = "0.1.1"
7
+ edition = "2024"
glassvm-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: glassvm
3
+ Version: 0.1.1
4
+ Requires-Dist: glassvm-chip8==0.1.1 ; extra == 'all'
5
+ Requires-Dist: glassvm-pico8==0.1.1 ; extra == 'all'
6
+ Requires-Dist: glassvm-tic80==0.1.1 ; extra == 'all'
7
+ Requires-Dist: glassvm-chip8==0.1.1 ; extra == 'chip8'
8
+ Requires-Dist: glassvm-pico8==0.1.1 ; extra == 'pico8'
9
+ Requires-Dist: glassvm-tic80==0.1.1 ; extra == 'tic80'
10
+ Provides-Extra: all
11
+ Provides-Extra: chip8
12
+ Provides-Extra: pico8
13
+ Provides-Extra: tic80
14
+ Summary: Generic Python facade for entry-point-discovered GlassVM machine bundles
15
+ License-Expression: BSD-3-Clause
16
+ Requires-Python: >=3.12
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["maturin[zig]>=1.0,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "glassvm"
7
+ version = "0.1.1"
8
+ requires-python = ">=3.12"
9
+ description = "Generic Python facade for entry-point-discovered GlassVM machine bundles"
10
+ license = "BSD-3-Clause"
11
+
12
+ [project.optional-dependencies]
13
+ chip8 = ["glassvm-chip8==0.1.1"]
14
+ pico8 = ["glassvm-pico8==0.1.1"]
15
+ tic80 = ["glassvm-tic80==0.1.1"]
16
+ all = [
17
+ "glassvm-chip8==0.1.1",
18
+ "glassvm-pico8==0.1.1",
19
+ "glassvm-tic80==0.1.1",
20
+ ]
21
+
22
+ [tool.maturin]
23
+ module-name = "glassvm"
24
+ manifest-path = "python/facade/Cargo.toml"
@@ -0,0 +1,14 @@
1
+ [package]
2
+ name = "glassvm"
3
+ version = "0.1.1"
4
+ edition = "2024"
5
+ license = "BSD-3-Clause"
6
+ publish = false
7
+
8
+ [lib]
9
+ name = "glassvm"
10
+ crate-type = ["cdylib", "rlib"]
11
+
12
+ [dependencies]
13
+ pyo3 = { version = "0.27", features = ["extension-module"] }
14
+ serde_json = "1"
@@ -0,0 +1,460 @@
1
+ //! Generic Python facade for entry-point-discovered GlassVM machine bundles.
2
+ //!
3
+ //! The base wheel contains no machine implementation. Bundle wheels register
4
+ //! provider entry points in the `glassvm.machine_bundles` group and own the
5
+ //! machine-specific preparation and execution implementation.
6
+
7
+ use std::sync::atomic::{AtomicU64, Ordering};
8
+
9
+ use pyo3::prelude::*;
10
+ use pyo3::types::{PyBytes, PyDict};
11
+ use serde_json::Value;
12
+
13
+ const PYTHON_BUNDLE_ENTRY_POINT_GROUP: &str = "glassvm.machine_bundles";
14
+ const PYTHON_BUNDLE_PROVIDER_CONTRACT: &str = "glassvm.python_bundle";
15
+ const PYTHON_BUNDLE_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion {
16
+ major: 1,
17
+ minor: 0,
18
+ patch: 0,
19
+ };
20
+
21
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
22
+ struct ProtocolVersion {
23
+ major: u16,
24
+ minor: u16,
25
+ patch: u16,
26
+ }
27
+
28
+ fn parse_protocol_version(value: &Value, entry_point: &str) -> Result<ProtocolVersion, String> {
29
+ let object = value.as_object().ok_or_else(|| {
30
+ format!(
31
+ "entry point {entry_point:?} provider has invalid protocol_version: expected an object"
32
+ )
33
+ })?;
34
+ for field in object.keys() {
35
+ if !matches!(field.as_str(), "major" | "minor" | "patch") {
36
+ return Err(format!(
37
+ "entry point {entry_point:?} provider has invalid protocol_version: unsupported field {field:?}"
38
+ ));
39
+ }
40
+ }
41
+ let component = |field: &str| -> Result<u16, String> {
42
+ let value = object.get(field).and_then(Value::as_u64).ok_or_else(|| {
43
+ format!(
44
+ "entry point {entry_point:?} provider has invalid protocol_version: {field:?} must be an unsigned integer"
45
+ )
46
+ })?;
47
+ u16::try_from(value).map_err(|_| {
48
+ format!(
49
+ "entry point {entry_point:?} provider has invalid protocol_version: {field:?} is out of range"
50
+ )
51
+ })
52
+ };
53
+ Ok(ProtocolVersion {
54
+ major: component("major")?,
55
+ minor: component("minor")?,
56
+ patch: component("patch")?,
57
+ })
58
+ }
59
+
60
+ fn validate_bundle_provider(
61
+ mut provider: Value,
62
+ entry_point: &str,
63
+ ) -> Result<(String, Value), String> {
64
+ let object = provider
65
+ .as_object_mut()
66
+ .ok_or_else(|| "bundle provider must return a JSON object".to_owned())?;
67
+ let protocol = object
68
+ .get("protocol")
69
+ .and_then(Value::as_str)
70
+ .ok_or_else(|| {
71
+ format!("entry point {entry_point:?} provider is missing string field \"protocol\"")
72
+ })?;
73
+ if protocol != PYTHON_BUNDLE_PROVIDER_CONTRACT {
74
+ return Err(format!(
75
+ "entry point {entry_point:?} returned unsupported provider protocol {protocol:?}"
76
+ ));
77
+ }
78
+ let protocol_version = object.get("protocol_version").ok_or_else(|| {
79
+ format!("entry point {entry_point:?} provider is missing object field \"protocol_version\"")
80
+ })?;
81
+ let protocol_version = parse_protocol_version(protocol_version, entry_point)?;
82
+ if protocol_version != PYTHON_BUNDLE_PROTOCOL_VERSION {
83
+ return Err(format!(
84
+ "entry point {entry_point:?} returned unsupported provider protocol version {}.{}.{}",
85
+ protocol_version.major, protocol_version.minor, protocol_version.patch
86
+ ));
87
+ }
88
+
89
+ const REQUIRED_STRING_FIELDS: &[&str] = &[
90
+ "machine_id",
91
+ "distribution",
92
+ "module",
93
+ "prepare_function",
94
+ "execute_function",
95
+ "bundle_version",
96
+ ];
97
+ for field in REQUIRED_STRING_FIELDS {
98
+ if object
99
+ .get(*field)
100
+ .and_then(Value::as_str)
101
+ .is_none_or(str::is_empty)
102
+ {
103
+ return Err(format!(
104
+ "entry point {entry_point:?} provider is missing string field {field:?}"
105
+ ));
106
+ }
107
+ }
108
+
109
+ for field in object.keys() {
110
+ if !matches!(
111
+ field.as_str(),
112
+ "protocol"
113
+ | "protocol_version"
114
+ | "machine_id"
115
+ | "distribution"
116
+ | "module"
117
+ | "prepare_function"
118
+ | "execute_function"
119
+ | "bundle_version"
120
+ ) {
121
+ return Err(format!(
122
+ "entry point {entry_point:?} provider contains unsupported metadata field {field:?}"
123
+ ));
124
+ }
125
+ }
126
+ let machine_id = object
127
+ .get("machine_id")
128
+ .and_then(Value::as_str)
129
+ .expect("machine_id was validated")
130
+ .to_owned();
131
+ object.insert(
132
+ "entry_point".to_owned(),
133
+ Value::String(entry_point.to_owned()),
134
+ );
135
+ Ok((machine_id, provider))
136
+ }
137
+
138
+ fn provider_string(value: &Value, field: &str) -> Result<String, String> {
139
+ value
140
+ .get(field)
141
+ .and_then(Value::as_str)
142
+ .map(str::to_owned)
143
+ .ok_or_else(|| format!("bundle provider field {field:?} is not a string"))
144
+ }
145
+
146
+ fn discover_bundle_values(py: Python<'_>) -> PyResult<Vec<Value>> {
147
+ let metadata = py.import("importlib.metadata")?;
148
+ let all_entries = metadata.call_method0("entry_points")?;
149
+ let kwargs = PyDict::new(py);
150
+ kwargs.set_item("group", PYTHON_BUNDLE_ENTRY_POINT_GROUP)?;
151
+ let entries = if all_entries.hasattr("select")? {
152
+ all_entries.call_method("select", (), Some(&kwargs))?
153
+ } else {
154
+ all_entries
155
+ };
156
+
157
+ let mut providers = Vec::new();
158
+ let mut machine_ids = std::collections::BTreeSet::new();
159
+ for item in entries.try_iter()? {
160
+ let entry = item?;
161
+ let entry_point = entry.getattr("name")?.extract::<String>()?;
162
+ let provider = entry.call_method0("load")?;
163
+ let encoded = provider.call0()?.extract::<String>()?;
164
+ let value: Value = serde_json::from_str(&encoded).map_err(|error| {
165
+ pyo3::exceptions::PyRuntimeError::new_err(format!(
166
+ "bundle entry point {entry_point:?} returned invalid JSON: {error}"
167
+ ))
168
+ })?;
169
+ let (machine_id, value) = validate_bundle_provider(value, &entry_point)
170
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
171
+ if !machine_ids.insert(machine_id.clone()) {
172
+ return Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
173
+ "multiple bundle entry points advertise machine_id {machine_id:?}"
174
+ )));
175
+ }
176
+ providers.push((machine_id, value));
177
+ }
178
+ providers.sort_by(|left, right| left.0.cmp(&right.0));
179
+ Ok(providers
180
+ .into_iter()
181
+ .map(|(_, provider)| provider)
182
+ .collect())
183
+ }
184
+
185
+ /// Discover separately installed machine-bundle wheels through Python entry
186
+ /// points. Providers return metadata only; this boundary does not exchange
187
+ /// Rust trait objects or invoke Python during machine execution.
188
+ #[pyfunction]
189
+ fn discover_bundles(py: Python<'_>) -> PyResult<String> {
190
+ serde_json::to_string(&discover_bundle_values(py)?).map_err(|error| {
191
+ pyo3::exceptions::PyRuntimeError::new_err(format!("cannot encode bundle metadata: {error}"))
192
+ })
193
+ }
194
+
195
+ #[derive(Debug, Clone)]
196
+ struct PythonBundleProvider {
197
+ protocol: String,
198
+ protocol_version: ProtocolVersion,
199
+ entry_point: String,
200
+ machine_id: String,
201
+ distribution: String,
202
+ module: String,
203
+ prepare_function: String,
204
+ execute_function: String,
205
+ bundle_version: String,
206
+ }
207
+
208
+ fn parse_bundle_providers(values: Vec<Value>) -> Result<Vec<PythonBundleProvider>, String> {
209
+ values
210
+ .into_iter()
211
+ .map(|value| {
212
+ Ok(PythonBundleProvider {
213
+ protocol: provider_string(&value, "protocol")?,
214
+ protocol_version: parse_protocol_version(
215
+ value
216
+ .get("protocol_version")
217
+ .ok_or_else(|| "bundle provider is missing protocol_version".to_owned())?,
218
+ "parsed provider",
219
+ )?,
220
+ entry_point: provider_string(&value, "entry_point")?,
221
+ machine_id: provider_string(&value, "machine_id")?,
222
+ distribution: provider_string(&value, "distribution")?,
223
+ module: provider_string(&value, "module")?,
224
+ prepare_function: provider_string(&value, "prepare_function")?,
225
+ execute_function: provider_string(&value, "execute_function")?,
226
+ bundle_version: provider_string(&value, "bundle_version")?,
227
+ })
228
+ })
229
+ .collect()
230
+ }
231
+
232
+ fn json_argument(
233
+ py: Python<'_>,
234
+ value: Option<&Bound<'_, PyAny>>,
235
+ field: &str,
236
+ ) -> PyResult<String> {
237
+ let Some(value) = value else {
238
+ return Ok("null".to_owned());
239
+ };
240
+ py.import("json")?
241
+ .call_method1("dumps", (value,))?
242
+ .extract::<String>()
243
+ .map_err(|error| {
244
+ pyo3::exceptions::PyValueError::new_err(format!(
245
+ "{field} must be JSON-compatible: {error}"
246
+ ))
247
+ })
248
+ }
249
+
250
+ #[pyclass]
251
+ struct Runtime {
252
+ providers: Vec<PythonBundleProvider>,
253
+ }
254
+
255
+ #[pyclass]
256
+ struct PreparedRun {
257
+ provider: PythonBundleProvider,
258
+ /// Provider-owned preparation state. This is deliberately an in-process
259
+ /// Python object, not a serialization, persistence, or replay format.
260
+ prepared: Py<PyAny>,
261
+ }
262
+
263
+ static NEXT_PYTHON_RUN_ID: AtomicU64 = AtomicU64::new(1);
264
+
265
+ #[pymethods]
266
+ impl Runtime {
267
+ #[staticmethod]
268
+ fn discover(py: Python<'_>) -> PyResult<Self> {
269
+ let providers = parse_bundle_providers(discover_bundle_values(py)?).map_err(|error| {
270
+ pyo3::exceptions::PyRuntimeError::new_err(format!("invalid bundle provider: {error}"))
271
+ })?;
272
+ Ok(Self { providers })
273
+ }
274
+
275
+ fn bundles(&self) -> PyResult<String> {
276
+ let values = self
277
+ .providers
278
+ .iter()
279
+ .map(|provider| {
280
+ serde_json::json!({
281
+ "protocol": provider.protocol,
282
+ "protocol_version": {
283
+ "major": provider.protocol_version.major,
284
+ "minor": provider.protocol_version.minor,
285
+ "patch": provider.protocol_version.patch,
286
+ },
287
+ "entry_point": provider.entry_point,
288
+ "machine_id": provider.machine_id,
289
+ "distribution": provider.distribution,
290
+ "module": provider.module,
291
+ "prepare_function": provider.prepare_function,
292
+ "execute_function": provider.execute_function,
293
+ "bundle_version": provider.bundle_version,
294
+ })
295
+ })
296
+ .collect::<Vec<_>>();
297
+ serde_json::to_string(&values)
298
+ .map_err(|error| pyo3::exceptions::PyRuntimeError::new_err(error.to_string()))
299
+ }
300
+
301
+ #[pyo3(signature = (machine_id, artifact, configuration = None, input_schedule = None, execution_controls = None, observation = None))]
302
+ fn prepare(
303
+ &self,
304
+ py: Python<'_>,
305
+ machine_id: &str,
306
+ artifact: &[u8],
307
+ configuration: Option<&Bound<'_, PyAny>>,
308
+ input_schedule: Option<&Bound<'_, PyAny>>,
309
+ execution_controls: Option<&Bound<'_, PyAny>>,
310
+ observation: Option<&Bound<'_, PyAny>>,
311
+ ) -> PyResult<PreparedRun> {
312
+ let provider = self
313
+ .providers
314
+ .iter()
315
+ .find(|provider| provider.machine_id == machine_id)
316
+ .cloned()
317
+ .ok_or_else(|| {
318
+ pyo3::exceptions::PyLookupError::new_err(format!(
319
+ "machine bundle {machine_id:?} is not installed; install with `uv add \"glassvm[{machine_id}]\"`"
320
+ ))
321
+ })?;
322
+ let module = py.import(&provider.module)?;
323
+ let prepare = module.getattr(&provider.prepare_function)?;
324
+ let run_id = format!(
325
+ "python-{}-{}",
326
+ provider.machine_id,
327
+ NEXT_PYTHON_RUN_ID.fetch_add(1, Ordering::Relaxed)
328
+ );
329
+ let prepared = prepare.call1((
330
+ PyBytes::new(py, artifact),
331
+ run_id,
332
+ json_argument(py, configuration, "configuration")?,
333
+ json_argument(py, input_schedule, "input_schedule")?,
334
+ json_argument(py, execution_controls, "execution_controls")?,
335
+ json_argument(py, observation, "observation")?,
336
+ ))?;
337
+ Ok(PreparedRun {
338
+ provider,
339
+ prepared: prepared.unbind(),
340
+ })
341
+ }
342
+ }
343
+
344
+ #[pymethods]
345
+ impl PreparedRun {
346
+ #[pyo3(signature = (output_path))]
347
+ fn execute(&self, py: Python<'_>, output_path: &str) -> PyResult<Py<PyAny>> {
348
+ let module = py.import(&self.provider.module)?;
349
+ let execute = module.getattr(&self.provider.execute_function)?;
350
+ let result = execute.call1((self.prepared.bind(py), output_path))?;
351
+ if !result.is_instance_of::<PyDict>() {
352
+ return Err(pyo3::exceptions::PyTypeError::new_err(
353
+ "bundle execute_prepared must return a structured mapping",
354
+ ));
355
+ }
356
+ Ok(result.unbind())
357
+ }
358
+ }
359
+
360
+ #[pymodule]
361
+ fn glassvm(m: &Bound<'_, PyModule>) -> PyResult<()> {
362
+ m.add_class::<Runtime>()?;
363
+ m.add_class::<PreparedRun>()?;
364
+ m.add_function(wrap_pyfunction!(discover_bundles, m)?)?;
365
+ Ok(())
366
+ }
367
+
368
+ #[cfg(test)]
369
+ mod tests {
370
+ use serde_json::json;
371
+
372
+ use super::{
373
+ PYTHON_BUNDLE_PROTOCOL_VERSION, PYTHON_BUNDLE_PROVIDER_CONTRACT, parse_bundle_providers,
374
+ validate_bundle_provider,
375
+ };
376
+
377
+ #[test]
378
+ fn canonical_provider_requires_both_canonical_operations() {
379
+ let (_, provider) = validate_bundle_provider(
380
+ json!({
381
+ "protocol": "glassvm.python_bundle",
382
+ "protocol_version": {"major": 1, "minor": 0, "patch": 0},
383
+ "machine_id": "chip8",
384
+ "distribution": "glassvm-chip8",
385
+ "module": "glassvm_chip8",
386
+ "prepare_function": "prepare_run",
387
+ "execute_function": "execute_prepared",
388
+ "bundle_version": "0.1.0"
389
+ }),
390
+ "chip8",
391
+ )
392
+ .expect("valid canonical provider");
393
+ let parsed = parse_bundle_providers(vec![provider]).expect("parse provider");
394
+ assert_eq!(parsed[0].protocol, PYTHON_BUNDLE_PROVIDER_CONTRACT);
395
+ assert_eq!(parsed[0].protocol_version, PYTHON_BUNDLE_PROTOCOL_VERSION);
396
+ assert_eq!(parsed[0].prepare_function, "prepare_run");
397
+ assert_eq!(parsed[0].execute_function, "execute_prepared");
398
+ }
399
+
400
+ #[test]
401
+ fn zero_providers_is_valid() {
402
+ assert!(parse_bundle_providers(Vec::new()).unwrap().is_empty());
403
+ }
404
+
405
+ #[test]
406
+ fn wrong_protocol_version_is_rejected() {
407
+ let error = validate_bundle_provider(
408
+ json!({
409
+ "protocol": "glassvm.python_bundle",
410
+ "protocol_version": {"major": 2, "minor": 0, "patch": 0},
411
+ "machine_id": "chip8",
412
+ "distribution": "glassvm-chip8",
413
+ "module": "glassvm_chip8",
414
+ "prepare_function": "prepare_run",
415
+ "execute_function": "execute_prepared",
416
+ "bundle_version": "0.1.0"
417
+ }),
418
+ "chip8",
419
+ )
420
+ .expect_err("unsupported protocol version must fail closed");
421
+ assert!(error.contains("unsupported provider protocol version"));
422
+ }
423
+
424
+ #[test]
425
+ fn legacy_protocol_name_is_rejected_without_fallback() {
426
+ let error = validate_bundle_provider(
427
+ json!({
428
+ "protocol": format!("{PYTHON_BUNDLE_PROVIDER_CONTRACT}.v1"),
429
+ "protocol_version": {"major": 1, "minor": 0, "patch": 0},
430
+ "machine_id": "chip8",
431
+ "distribution": "glassvm-chip8",
432
+ "module": "glassvm_chip8",
433
+ "prepare_function": "prepare_run",
434
+ "execute_function": "execute_prepared",
435
+ "bundle_version": "0.1.0"
436
+ }),
437
+ "chip8",
438
+ )
439
+ .expect_err("legacy protocol names must not dispatch");
440
+ assert!(error.contains("unsupported provider protocol"));
441
+ }
442
+
443
+ #[test]
444
+ fn canonical_provider_rejects_missing_execute_operation() {
445
+ let error = validate_bundle_provider(
446
+ json!({
447
+ "protocol": "glassvm.python_bundle",
448
+ "protocol_version": {"major": 1, "minor": 0, "patch": 0},
449
+ "machine_id": "chip8",
450
+ "distribution": "glassvm-chip8",
451
+ "module": "glassvm_chip8",
452
+ "prepare_function": "prepare_run",
453
+ "bundle_version": "0.1.0"
454
+ }),
455
+ "chip8",
456
+ )
457
+ .expect_err("missing execute operation must fail");
458
+ assert!(error.contains("execute_function"));
459
+ }
460
+ }
@@ -0,0 +1,34 @@
1
+ """Base-wheel smoke coverage for the generic GlassVM provider facade."""
2
+
3
+ import json
4
+ import unittest
5
+
6
+ import glassvm
7
+
8
+ class ProviderDiscoveryTests(unittest.TestCase):
9
+ def setUp(self):
10
+ self.runtime = glassvm.Runtime.discover()
11
+
12
+ def test_base_runtime_discovers_entry_points_without_static_bundle_assumptions(self):
13
+ metadata = json.loads(self.runtime.bundles())
14
+ self.assertIsInstance(metadata, list)
15
+ machine_ids = [entry["machine_id"] for entry in metadata]
16
+ self.assertEqual(machine_ids, sorted(machine_ids))
17
+
18
+ for entry in metadata:
19
+ with self.subTest(machine_id=entry["machine_id"]):
20
+ self.assertEqual(entry["protocol"], "glassvm.python_bundle")
21
+ self.assertEqual(
22
+ entry["protocol_version"],
23
+ {"major": 1, "minor": 0, "patch": 0},
24
+ )
25
+ self.assertEqual(entry["prepare_function"], "prepare_run")
26
+ self.assertEqual(entry["execute_function"], "execute_prepared")
27
+
28
+ def test_unavailable_machine_fails_at_the_facade_boundary(self):
29
+ with self.assertRaises(LookupError):
30
+ self.runtime.prepare("glassvm.machine.not-installed", b"")
31
+
32
+
33
+ if __name__ == "__main__":
34
+ unittest.main()