elf_magic/
lib.rs

1mod builder;
2mod codegen;
3mod config;
4mod error;
5mod programs;
6mod workspace;
7
8use std::{env, path::PathBuf};
9
10use crate::{
11    config::Config,
12    error::Error,
13    programs::{GenerationResult, SolanaProgram},
14};
15
16/// Generate Rust bindings for Solana programs
17pub fn generate() -> Result<GenerationResult, Error> {
18    let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR")
19        .map(PathBuf::from)
20        .map_err(|e| {
21            let message = format!("CARGO_MANIFEST_DIR not set: {}", e);
22            Error::WorkspaceDiscovery(message)
23        })?;
24
25    // Core pipeline:
26    // 1. load config
27    // 2. load workspaces
28    // 3. discover programs across all workspaces
29    // 4. build included programs across all workspaces (collect successes and failures)
30    // 5. generate code - one lib.rs from successfully built programs, with build status
31    // 6. enable incremental builds for all included programs
32
33    let config = Config::load(&cargo_manifest_dir)?;
34
35    let workspaces = workspace::load_workspaces(&config)?;
36
37    let discovered_programs = workspaces
38        .iter()
39        .map(|w| w.discover_programs())
40        .collect::<Result<Vec<_>, _>>()?;
41
42    // Extract all programs for building and code generation
43    let included_programs: Vec<SolanaProgram> = discovered_programs
44        .iter()
45        .flat_map(|w| w.included.iter().cloned())
46        .collect();
47
48    // Deduplicate programs by manifest_path to avoid duplicate constants
49    let included_programs = deduplicate_programs(included_programs);
50
51    let build_result = builder::build_programs(&included_programs);
52
53    // Generate code from successfully built programs, including build status
54    let code = codegen::generate(&build_result)?;
55    codegen::save(&cargo_manifest_dir, &code)?;
56
57    // Enable incremental builds for all programs (successful and failed)
58    enable_incremental_builds(&included_programs)?;
59
60    let mode = match &config {
61        Config::Magic => "magic".to_string(),
62        Config::Pedantic { .. } => "pedantic".to_string(),
63    };
64
65    Ok(GenerationResult::new(mode, discovered_programs))
66}
67
68/// Enable incremental builds for each program
69fn enable_incremental_builds(programs: &[SolanaProgram]) -> Result<(), Error> {
70    for program in programs {
71        println!("cargo:rerun-if-changed={}", program.manifest_path.display());
72    }
73    Ok(())
74}
75
76/// Deduplicate programs by manifest_path to handle cases where multiple workspaces
77/// discover the same program (e.g., shared dependencies)
78fn deduplicate_programs(programs: Vec<SolanaProgram>) -> Vec<SolanaProgram> {
79    use std::collections::HashMap;
80
81    let mut seen: HashMap<PathBuf, SolanaProgram> = HashMap::new();
82
83    for program in programs {
84        // Use manifest_path as the key, keeping the first occurrence
85        seen.entry(program.manifest_path.clone()).or_insert(program);
86    }
87
88    let mut deduplicated: Vec<SolanaProgram> = seen.into_values().collect();
89
90    // Sort by target_name to ensure consistent alphabetical ordering
91    deduplicated.sort_by(|a, b| a.target_name.cmp(&b.target_name));
92
93    deduplicated
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::path::PathBuf;
100
101    #[test]
102    fn test_deduplicate_programs_removes_duplicates_by_manifest_path() {
103        // Same program appearing twice (realistic scenario from multiple workspaces)
104        let program1 = SolanaProgram {
105            package_name: "apl-token".to_string(),
106            target_name: "apl_token".to_string(),
107            manifest_path: PathBuf::from("/repo/token/Cargo.toml"),
108        };
109
110        let program2 = SolanaProgram {
111            package_name: "apl-token".to_string(),
112            target_name: "apl_token".to_string(),
113            manifest_path: PathBuf::from("/repo/token/Cargo.toml"), // Same path!
114        };
115
116        let escrow_program = SolanaProgram {
117            package_name: "escrow_program".to_string(),
118            target_name: "escrow_program".to_string(),
119            manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"),
120        };
121
122        // Input: 3 programs (with 1 duplicate)
123        let input_programs = vec![escrow_program.clone(), program1, program2];
124        assert_eq!(input_programs.len(), 3);
125
126        // After deduplication: should have 2 unique programs
127        let deduplicated = deduplicate_programs(input_programs);
128        assert_eq!(
129            deduplicated.len(),
130            2,
131            "Should deduplicate to 2 unique programs"
132        );
133
134        // Verify we have one of each program type
135        let apl_token_count = deduplicated
136            .iter()
137            .filter(|p| p.target_name == "apl_token")
138            .count();
139        assert_eq!(
140            apl_token_count, 1,
141            "Should have exactly 1 apl_token after deduplication"
142        );
143
144        let escrow_count = deduplicated
145            .iter()
146            .filter(|p| p.target_name == "escrow_program")
147            .count();
148        assert_eq!(escrow_count, 1, "Should have exactly 1 escrow_program");
149    }
150
151    #[test]
152    fn test_deduplicate_programs_preserves_unique_programs() {
153        let program1 = SolanaProgram {
154            package_name: "counter".to_string(),
155            target_name: "counter_program".to_string(),
156            manifest_path: PathBuf::from("/repo/examples/counter/program/Cargo.toml"),
157        };
158
159        let program2 = SolanaProgram {
160            package_name: "escrow".to_string(),
161            target_name: "escrow_program".to_string(),
162            manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"),
163        };
164
165        // Input: 2 unique programs
166        let input_programs = vec![program1, program2];
167        let deduplicated = deduplicate_programs(input_programs);
168
169        // Should preserve both programs
170        assert_eq!(deduplicated.len(), 2, "Should preserve all unique programs");
171    }
172}