aardvark_core/persistent/
inline.rs1use std::collections::HashSet;
2use std::io::{Cursor, Write};
3
4use serde_json::Value;
5use zip::write::FileOptions;
6use zip::CompressionMethod;
7
8use crate::bundle::Bundle;
9use crate::bundle_manifest::{
10 BundleManifest, ManifestResources, ManifestRuntime, MANIFEST_BASENAME, MANIFEST_SCHEMA_VERSION,
11};
12use crate::error::{PyRunnerError, Result};
13use crate::runtime_language::RuntimeLanguage;
14
15#[derive(Debug, Clone, Default)]
17pub struct InlinePythonOptions {
18 pub entrypoint: Option<String>,
20 pub packages: Vec<String>,
22 pub runtime: Option<ManifestRuntime>,
24 pub resources: Option<ManifestResources>,
26}
27
28impl InlinePythonOptions {
29 pub fn entrypoint(&self) -> &str {
31 self.entrypoint.as_deref().unwrap_or("main:handler")
32 }
33
34 pub(crate) fn build_bundle(&self, code: &str) -> Result<(Bundle, String)> {
36 let (manifest, manifest_bytes) = self.build_manifest()?;
37 let entrypoint = manifest.entrypoint().to_owned();
38 let (module_path, init_modules) = module_path_for_entrypoint(&entrypoint)?;
39 let bundle = assemble_inline_bundle(code, &module_path, &init_modules, &manifest_bytes)?;
40 Ok((bundle, entrypoint))
41 }
42
43 fn build_manifest(&self) -> Result<(BundleManifest, Vec<u8>)> {
44 if let Some(runtime) = &self.runtime {
45 if matches!(runtime.language, Some(RuntimeLanguage::JavaScript)) {
46 return Err(PyRunnerError::Manifest(
47 "inline python options must target the python runtime".into(),
48 ));
49 }
50 }
51
52 let mut map = serde_json::Map::new();
53 map.insert(
54 "schemaVersion".to_string(),
55 Value::String(MANIFEST_SCHEMA_VERSION.to_string()),
56 );
57 map.insert(
58 "entrypoint".to_string(),
59 Value::String(self.entrypoint().to_string()),
60 );
61 if !self.packages.is_empty() {
62 map.insert(
63 "packages".to_string(),
64 serde_json::to_value(&self.packages).map_err(|err| {
65 PyRunnerError::Manifest(format!("failed to encode inline package list: {err}"))
66 })?,
67 );
68 }
69 if let Some(runtime) = &self.runtime {
70 map.insert(
71 "runtime".to_string(),
72 serde_json::to_value(runtime).map_err(|err| {
73 PyRunnerError::Manifest(format!("failed to encode inline runtime block: {err}"))
74 })?,
75 );
76 }
77 if let Some(resources) = &self.resources {
78 map.insert(
79 "resources".to_string(),
80 serde_json::to_value(resources).map_err(|err| {
81 PyRunnerError::Manifest(format!(
82 "failed to encode inline resources block: {err}"
83 ))
84 })?,
85 );
86 }
87
88 let manifest_value = Value::Object(map);
89 let manifest_bytes = serde_json::to_vec(&manifest_value).map_err(|err| {
90 PyRunnerError::Manifest(format!("failed to serialise inline manifest: {err}"))
91 })?;
92 let manifest = BundleManifest::from_bytes(&manifest_bytes)?;
93 let manifest_bytes = serde_json::to_vec(&manifest).map_err(|err| {
94 PyRunnerError::Manifest(format!(
95 "failed to serialise normalised inline manifest: {err}"
96 ))
97 })?;
98 Ok((manifest, manifest_bytes))
99 }
100}
101
102fn module_path_for_entrypoint(entrypoint: &str) -> Result<(String, Vec<String>)> {
103 let (module, _) = entrypoint
104 .split_once(':')
105 .ok_or_else(|| PyRunnerError::Manifest("entrypoint must include module:function".into()))?;
106 let module = module.trim();
107 if module.is_empty() {
108 return Err(PyRunnerError::Manifest(
109 "entrypoint must specify a module".into(),
110 ));
111 }
112
113 let mut components = Vec::new();
114 for token in module.split('.') {
115 let trimmed = token.trim();
116 if trimmed.is_empty() {
117 return Err(PyRunnerError::Manifest(
118 "entrypoint module cannot contain empty components".into(),
119 ));
120 }
121 components.push(trimmed);
122 }
123 let mut init_modules = Vec::new();
124 if components.len() > 1 {
125 let mut prefix = String::new();
126 for component in &components[..components.len() - 1] {
127 if !prefix.is_empty() {
128 prefix.push('/');
129 }
130 prefix.push_str(component);
131 init_modules.push(format!("{prefix}/__init__.py"));
132 }
133 }
134 let module_path = format!(
135 "{}{}.py",
136 if components.len() > 1 {
137 components[..components.len() - 1].join("/") + "/"
138 } else {
139 String::new()
140 },
141 components.last().unwrap()
142 );
143 Ok((module_path, init_modules))
144}
145
146fn assemble_inline_bundle(
147 code: &str,
148 module_path: &str,
149 init_modules: &[String],
150 manifest_bytes: &[u8],
151) -> Result<Bundle> {
152 let cursor = Cursor::new(Vec::new());
153 let mut writer = zip::ZipWriter::new(cursor);
154 let options = FileOptions::default().compression_method(CompressionMethod::Stored);
155
156 let mut seen = HashSet::new();
157 for init in init_modules {
158 if seen.insert(init.clone()) {
159 writer
160 .start_file(init, options)
161 .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
162 writer
163 .write_all(b"")
164 .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
165 }
166 }
167
168 writer
169 .start_file(module_path, options)
170 .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
171 writer
172 .write_all(code.as_bytes())
173 .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
174
175 writer
176 .start_file(MANIFEST_BASENAME, options)
177 .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
178 writer
179 .write_all(manifest_bytes)
180 .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
181
182 let cursor = writer
183 .finish()
184 .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
185 Bundle::from_zip_bytes(cursor.into_inner())
186}