aptos_sdk/codegen/
build_helper.rs1use crate::api::response::MoveModuleABI;
46use crate::codegen::{GeneratorConfig, ModuleGenerator, MoveSourceParser};
47use crate::error::{AptosError, AptosResult};
48use std::fs;
49use std::path::Path;
50
51#[derive(Debug, Clone)]
53pub struct BuildConfig {
54 pub generator_config: GeneratorConfig,
56 pub generate_mod_file: bool,
58 pub print_cargo_instructions: bool,
60}
61
62impl Default for BuildConfig {
63 fn default() -> Self {
64 Self {
65 generator_config: GeneratorConfig::default(),
66 generate_mod_file: true,
67 print_cargo_instructions: true,
68 }
69 }
70}
71
72impl BuildConfig {
73 #[must_use]
75 pub fn new() -> Self {
76 Self::default()
77 }
78
79 #[must_use]
81 pub fn with_mod_file(mut self, enabled: bool) -> Self {
82 self.generate_mod_file = enabled;
83 self
84 }
85
86 #[must_use]
88 pub fn with_generator_config(mut self, config: GeneratorConfig) -> Self {
89 self.generator_config = config;
90 self
91 }
92
93 #[must_use]
95 pub fn with_cargo_instructions(mut self, enabled: bool) -> Self {
96 self.print_cargo_instructions = enabled;
97 self
98 }
99}
100
101pub fn generate_from_abi(
123 abi_path: impl AsRef<Path>,
124 output_dir: impl AsRef<Path>,
125) -> AptosResult<()> {
126 generate_from_abi_with_config(abi_path, output_dir, BuildConfig::default())
127}
128
129pub fn generate_from_abi_with_config(
140 abi_path: impl AsRef<Path>,
141 output_dir: impl AsRef<Path>,
142 config: BuildConfig,
143) -> AptosResult<()> {
144 let abi_path = abi_path.as_ref();
145 let output_dir = output_dir.as_ref();
146
147 let abi_content = fs::read_to_string(abi_path).map_err(|e| {
149 AptosError::Config(format!(
150 "Failed to read ABI file {}: {}",
151 abi_path.display(),
152 e
153 ))
154 })?;
155
156 let abi: MoveModuleABI = serde_json::from_str(&abi_content)
157 .map_err(|e| AptosError::Config(format!("Failed to parse ABI JSON: {e}")))?;
158
159 let generator = ModuleGenerator::new(&abi, config.generator_config);
161 let code = generator.generate()?;
162
163 fs::create_dir_all(output_dir)
165 .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
166
167 let output_filename = format!("{}.rs", abi.name);
169 let output_path = output_dir.join(&output_filename);
170
171 fs::write(&output_path, &code)
172 .map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
173
174 if config.print_cargo_instructions {
175 println!("cargo:rerun-if-changed={}", abi_path.display());
176 }
177
178 Ok(())
179}
180
181pub fn generate_from_abis(
209 abi_paths: &[impl AsRef<Path>],
210 output_dir: impl AsRef<Path>,
211) -> AptosResult<()> {
212 generate_from_abis_with_config(abi_paths, output_dir, &BuildConfig::default())
213}
214
215pub fn generate_from_abis_with_config(
227 abi_paths: &[impl AsRef<Path>],
228 output_dir: impl AsRef<Path>,
229 config: &BuildConfig,
230) -> AptosResult<()> {
231 let output_dir = output_dir.as_ref();
232 let mut module_names = Vec::new();
233
234 for abi_path in abi_paths {
236 let abi_path = abi_path.as_ref();
237
238 let abi_content = fs::read_to_string(abi_path).map_err(|e| {
239 AptosError::Config(format!(
240 "Failed to read ABI file {}: {}",
241 abi_path.display(),
242 e
243 ))
244 })?;
245
246 let abi: MoveModuleABI = serde_json::from_str(&abi_content).map_err(|e| {
247 AptosError::Config(format!(
248 "Failed to parse ABI JSON from {}: {}",
249 abi_path.display(),
250 e
251 ))
252 })?;
253
254 let generator = ModuleGenerator::new(&abi, config.generator_config.clone());
255 let code = generator.generate()?;
256
257 fs::create_dir_all(output_dir)
259 .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
260
261 let output_filename = format!("{}.rs", abi.name);
263 let output_path = output_dir.join(&output_filename);
264
265 fs::write(&output_path, &code)
266 .map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
267
268 module_names.push(abi.name);
269
270 if config.print_cargo_instructions {
271 println!("cargo:rerun-if-changed={}", abi_path.display());
272 }
273 }
274
275 if config.generate_mod_file && !module_names.is_empty() {
277 let mod_content = generate_mod_file(&module_names);
278 let mod_path = output_dir.join("mod.rs");
279
280 fs::write(&mod_path, mod_content)
281 .map_err(|e| AptosError::Config(format!("Failed to write mod.rs: {e}")))?;
282 }
283
284 Ok(())
285}
286
287pub fn generate_from_abi_with_source(
305 abi_path: impl AsRef<Path>,
306 source_path: impl AsRef<Path>,
307 output_dir: impl AsRef<Path>,
308) -> AptosResult<()> {
309 let abi_path = abi_path.as_ref();
310 let source_path = source_path.as_ref();
311 let output_dir = output_dir.as_ref();
312
313 let abi_content = fs::read_to_string(abi_path)
315 .map_err(|e| AptosError::Config(format!("Failed to read ABI file: {e}")))?;
316
317 let abi: MoveModuleABI = serde_json::from_str(&abi_content)
318 .map_err(|e| AptosError::Config(format!("Failed to parse ABI JSON: {e}")))?;
319
320 let source_content = fs::read_to_string(source_path)
322 .map_err(|e| AptosError::Config(format!("Failed to read Move source: {e}")))?;
323
324 let source_info = MoveSourceParser::parse(&source_content);
325
326 let generator =
328 ModuleGenerator::new(&abi, GeneratorConfig::default()).with_source_info(source_info);
329 let code = generator.generate()?;
330
331 fs::create_dir_all(output_dir)
333 .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
334
335 let output_filename = format!("{}.rs", abi.name);
337 let output_path = output_dir.join(&output_filename);
338
339 fs::write(&output_path, &code)
340 .map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
341
342 println!("cargo:rerun-if-changed={}", abi_path.display());
343 println!("cargo:rerun-if-changed={}", source_path.display());
344
345 Ok(())
346}
347
348fn generate_mod_file(module_names: &[String]) -> String {
350 use std::fmt::Write as _;
351 let mut content = String::new();
352 let _ = writeln!(&mut content, "//! Auto-generated module exports.");
353 let _ = writeln!(&mut content, "//!");
354 let _ = writeln!(
355 &mut content,
356 "//! This file was auto-generated by aptos-sdk codegen."
357 );
358 let _ = writeln!(&mut content, "//! Do not edit manually.");
359 let _ = writeln!(&mut content);
360
361 for name in module_names {
362 let _ = writeln!(&mut content, "pub mod {name};");
363 }
364 let _ = writeln!(&mut content);
365
366 let _ = writeln!(&mut content, "// Re-exports for convenience");
368 for name in module_names {
369 let _ = writeln!(&mut content, "pub use {name}::*;");
370 }
371
372 content
373}
374
375pub fn generate_from_directory(
398 abi_dir: impl AsRef<Path>,
399 output_dir: impl AsRef<Path>,
400) -> AptosResult<()> {
401 let abi_dir = abi_dir.as_ref();
402
403 let entries = fs::read_dir(abi_dir)
404 .map_err(|e| AptosError::Config(format!("Failed to read ABI directory: {e}")))?;
405
406 let abi_paths: Vec<_> = entries
407 .filter_map(Result::ok)
408 .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
409 .map(|e| e.path())
410 .collect();
411
412 if abi_paths.is_empty() {
413 return Err(AptosError::Config(format!(
414 "No JSON files found in {}",
415 abi_dir.display()
416 )));
417 }
418
419 let path_refs: Vec<&Path> = abi_paths.iter().map(std::path::PathBuf::as_path).collect();
421 generate_from_abis(&path_refs, output_dir)
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use std::io::Write;
428 use tempfile::TempDir;
429
430 fn sample_abi_json() -> &'static str {
431 r#"{
432 "address": "0x1",
433 "name": "coin",
434 "exposed_functions": [
435 {
436 "name": "transfer",
437 "visibility": "public",
438 "is_entry": true,
439 "is_view": false,
440 "generic_type_params": [{"constraints": []}],
441 "params": ["&signer", "address", "u64"],
442 "return": []
443 }
444 ],
445 "structs": []
446 }"#
447 }
448
449 #[test]
450 fn test_generate_from_abi() {
451 let temp_dir = TempDir::new().unwrap();
452 let abi_path = temp_dir.path().join("coin.json");
453 let output_dir = temp_dir.path().join("generated");
454
455 let mut file = fs::File::create(&abi_path).unwrap();
457 file.write_all(sample_abi_json().as_bytes()).unwrap();
458
459 let config = BuildConfig::new().with_cargo_instructions(false);
461 generate_from_abi_with_config(&abi_path, &output_dir, config).unwrap();
462
463 let output_path = output_dir.join("coin.rs");
465 assert!(output_path.exists());
466
467 let content = fs::read_to_string(&output_path).unwrap();
469 assert!(content.contains("Generated Rust bindings"));
470 assert!(content.contains("pub fn transfer"));
471 }
472
473 #[test]
474 fn test_generate_mod_file() {
475 let modules = vec!["coin".to_string(), "token".to_string()];
476 let mod_content = generate_mod_file(&modules);
477
478 assert!(mod_content.contains("pub mod coin;"));
479 assert!(mod_content.contains("pub mod token;"));
480 assert!(mod_content.contains("pub use coin::*;"));
481 assert!(mod_content.contains("pub use token::*;"));
482 }
483
484 #[test]
485 fn test_build_config() {
486 let config = BuildConfig::new()
487 .with_mod_file(false)
488 .with_cargo_instructions(false);
489
490 assert!(!config.generate_mod_file);
491 assert!(!config.print_cargo_instructions);
492 }
493}