Skip to main content

aptos_sdk/codegen/
build_helper.rs

1//! Build script helper for code generation.
2//!
3//! This module provides utilities for generating code at compile time via `build.rs`.
4//!
5//! # Example
6//!
7//! Add to your `build.rs`:
8//!
9//! ```rust,ignore
10//! use aptos_sdk::codegen::build_helper;
11//!
12//! fn main() {
13//!     // Generate from a local ABI file
14//!     build_helper::generate_from_abi(
15//!         "abi/my_module.json",
16//!         "src/generated/",
17//!     ).expect("code generation failed");
18//!
19//!     // Generate from multiple modules
20//!     build_helper::generate_from_abis(&[
21//!         "abi/coin.json",
22//!         "abi/token.json",
23//!     ], "src/generated/").expect("code generation failed");
24//!
25//!     // Rerun if ABI files change
26//!     println!("cargo:rerun-if-changed=abi/");
27//! }
28//! ```
29//!
30//! # Directory Structure
31//!
32//! ```text
33//! my_project/
34//! ├── build.rs
35//! ├── abi/
36//! │   ├── my_module.json
37//! │   └── another_module.json
38//! └── src/
39//!     └── generated/
40//!         ├── mod.rs          (auto-generated)
41//!         ├── my_module.rs
42//!         └── another_module.rs
43//! ```
44
45use crate::api::response::MoveModuleABI;
46use crate::codegen::{GeneratorConfig, ModuleGenerator, MoveSourceParser};
47use crate::error::{AptosError, AptosResult};
48use std::fs;
49use std::path::Path;
50
51/// Configuration for build-time code generation.
52#[derive(Debug, Clone)]
53pub struct BuildConfig {
54    /// Generator configuration.
55    pub generator_config: GeneratorConfig,
56    /// Whether to generate a `mod.rs` file.
57    pub generate_mod_file: bool,
58    /// Whether to print build instructions to cargo.
59    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    /// Creates a new build configuration.
74    #[must_use]
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Sets whether to generate a mod.rs file.
80    #[must_use]
81    pub fn with_mod_file(mut self, enabled: bool) -> Self {
82        self.generate_mod_file = enabled;
83        self
84    }
85
86    /// Sets the generator configuration.
87    #[must_use]
88    pub fn with_generator_config(mut self, config: GeneratorConfig) -> Self {
89        self.generator_config = config;
90        self
91    }
92
93    /// Sets whether to print cargo instructions.
94    #[must_use]
95    pub fn with_cargo_instructions(mut self, enabled: bool) -> Self {
96        self.print_cargo_instructions = enabled;
97        self
98    }
99}
100
101/// Generates Rust code from a single ABI file.
102///
103/// # Arguments
104///
105/// * `abi_path` - Path to the ABI JSON file
106/// * `output_dir` - Directory where generated code will be written
107///
108/// # Errors
109///
110/// Returns an error if:
111/// * The ABI file cannot be read
112/// * The ABI JSON cannot be parsed
113/// * Code generation fails
114/// * The output directory cannot be created
115/// * The output file cannot be written
116///
117/// # Example
118///
119/// ```rust,ignore
120/// build_helper::generate_from_abi("abi/coin.json", "src/generated/")?;
121/// ```
122pub 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
129/// Generates Rust code from a single ABI file with custom configuration.
130///
131/// # Errors
132///
133/// Returns an error if:
134/// * The ABI file cannot be read
135/// * The ABI JSON cannot be parsed
136/// * Code generation fails
137/// * The output directory cannot be created
138/// * The output file cannot be written
139pub 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    // Read and parse ABI
148    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    // Generate code
160    let generator = ModuleGenerator::new(&abi, config.generator_config);
161    let code = generator.generate()?;
162
163    // Create output directory
164    fs::create_dir_all(output_dir)
165        .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
166
167    // Write output file
168    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
181/// Generates Rust code from multiple ABI files.
182///
183/// Also generates a `mod.rs` file that re-exports all generated modules.
184///
185/// # Arguments
186///
187/// * `abi_paths` - Paths to ABI JSON files
188/// * `output_dir` - Directory where generated code will be written
189///
190/// # Errors
191///
192/// Returns an error if:
193/// * Any ABI file cannot be read
194/// * Any ABI JSON cannot be parsed
195/// * Code generation fails for any module
196/// * The output directory cannot be created
197/// * Any output file cannot be written
198/// * The `mod.rs` file cannot be written
199///
200/// # Example
201///
202/// ```rust,ignore
203/// build_helper::generate_from_abis(&[
204///     "abi/coin.json",
205///     "abi/token.json",
206/// ], "src/generated/")?;
207/// ```
208pub 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
215/// Generates Rust code from multiple ABI files with custom configuration.
216///
217/// # Errors
218///
219/// Returns an error if:
220/// * Any ABI file cannot be read
221/// * Any ABI JSON cannot be parsed
222/// * Code generation fails for any module
223/// * The output directory cannot be created
224/// * Any output file cannot be written
225/// * The `mod.rs` file cannot be written (if enabled)
226pub 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    // Generate code for each ABI
235    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        // Create output directory
258        fs::create_dir_all(output_dir)
259            .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
260
261        // Write output file
262        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    // Generate mod.rs
276    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
287/// Generates Rust code from an ABI file with Move source for better names.
288///
289/// # Arguments
290///
291/// * `abi_path` - Path to the ABI JSON file
292/// * `source_path` - Path to the Move source file
293/// * `output_dir` - Directory where generated code will be written
294///
295/// # Errors
296///
297/// Returns an error if:
298/// * The ABI file cannot be read
299/// * The ABI JSON cannot be parsed
300/// * The Move source file cannot be read
301/// * Code generation fails
302/// * The output directory cannot be created
303/// * The output file cannot be written
304pub 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    // Read and parse ABI
314    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    // Read and parse Move source
321    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    // Generate code
327    let generator =
328        ModuleGenerator::new(&abi, GeneratorConfig::default()).with_source_info(source_info);
329    let code = generator.generate()?;
330
331    // Create output directory
332    fs::create_dir_all(output_dir)
333        .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
334
335    // Write output file
336    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
348/// Generates a mod.rs file for the given module names.
349fn 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    // Re-export all modules
367    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
375/// Scans a directory for ABI files and generates code for all of them.
376///
377/// # Arguments
378///
379/// * `abi_dir` - Directory containing ABI JSON files
380/// * `output_dir` - Directory where generated code will be written
381///
382/// # Errors
383///
384/// Returns an error if:
385/// * The directory cannot be read
386/// * No JSON files are found in the directory
387/// * Any ABI file cannot be read or parsed
388/// * Code generation fails for any module
389/// * The output directory cannot be created
390/// * Any output file cannot be written
391///
392/// # Example
393///
394/// ```rust,ignore
395/// build_helper::generate_from_directory("abi/", "src/generated/")?;
396/// ```
397pub 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    // Convert PathBuf to Path references for the function
420    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        // Write sample ABI
456        let mut file = fs::File::create(&abi_path).unwrap();
457        file.write_all(sample_abi_json().as_bytes()).unwrap();
458
459        // Generate
460        let config = BuildConfig::new().with_cargo_instructions(false);
461        generate_from_abi_with_config(&abi_path, &output_dir, config).unwrap();
462
463        // Verify output exists
464        let output_path = output_dir.join("coin.rs");
465        assert!(output_path.exists());
466
467        // Verify content
468        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}