elf_magic/
lib.rs

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