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
19pub 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 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 let included_programs: Vec<SolanaProgram> = discovered_programs
47 .iter()
48 .flat_map(|w| w.included.iter().cloned())
49 .collect();
50
51 let included_programs = deduplicate_programs(included_programs);
53
54 let build_result = builder::build_programs(&included_programs);
55
56 let code = codegen::generate(&build_result)?;
58 codegen::save(&cargo_manifest_dir, &code)?;
59
60 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
72fn 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
84fn 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 seen.entry(program.manifest_path.clone()).or_insert(program);
94 }
95
96 let mut deduplicated: Vec<SolanaProgram> = seen.into_values().collect();
97
98 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 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"), };
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 let input_programs = vec![escrow_program.clone(), program1, program2];
132 assert_eq!(input_programs.len(), 3);
133
134 let deduplicated = deduplicate_programs(input_programs);
136 assert_eq!(
137 deduplicated.len(),
138 2,
139 "Should deduplicate to 2 unique programs"
140 );
141
142 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 let input_programs = vec![program1, program2];
175 let deduplicated = deduplicate_programs(input_programs);
176
177 assert_eq!(deduplicated.len(), 2, "Should preserve all unique programs");
179 }
180}