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
16pub 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 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 let included_programs: Vec<SolanaProgram> = discovered_programs
44 .iter()
45 .flat_map(|w| w.included.iter().cloned())
46 .collect();
47
48 let included_programs = deduplicate_programs(included_programs);
50
51 builder::build_programs(&included_programs)?;
52
53 let code = codegen::generate(&included_programs)?;
54 codegen::save(&cargo_manifest_dir, &code)?;
55
56 enable_incremental_builds(&included_programs)?;
57
58 let mode = match &config {
59 Config::Magic => "magic".to_string(),
60 Config::Pedantic { .. } => "pedantic".to_string(),
61 };
62
63 Ok(GenerationResult::new(mode, discovered_programs))
64}
65
66fn enable_incremental_builds(programs: &[SolanaProgram]) -> Result<(), Error> {
68 for program in programs {
69 println!("cargo:rerun-if-changed={}", program.manifest_path.display());
70 }
71 Ok(())
72}
73
74fn deduplicate_programs(programs: Vec<SolanaProgram>) -> Vec<SolanaProgram> {
77 use std::collections::HashMap;
78
79 let mut seen: HashMap<PathBuf, SolanaProgram> = HashMap::new();
80
81 for program in programs {
82 seen.entry(program.manifest_path.clone()).or_insert(program);
84 }
85
86 seen.into_values().collect()
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::path::PathBuf;
93
94 #[test]
95 fn test_deduplicate_programs_removes_duplicates_by_manifest_path() {
96 let program1 = SolanaProgram {
98 package_name: "apl-token".to_string(),
99 target_name: "apl_token".to_string(),
100 manifest_path: PathBuf::from("/repo/token/Cargo.toml"),
101 };
102
103 let program2 = SolanaProgram {
104 package_name: "apl-token".to_string(),
105 target_name: "apl_token".to_string(),
106 manifest_path: PathBuf::from("/repo/token/Cargo.toml"), };
108
109 let escrow_program = SolanaProgram {
110 package_name: "escrow_program".to_string(),
111 target_name: "escrow_program".to_string(),
112 manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"),
113 };
114
115 let input_programs = vec![escrow_program.clone(), program1, program2];
117 assert_eq!(input_programs.len(), 3);
118
119 let deduplicated = deduplicate_programs(input_programs);
121 assert_eq!(
122 deduplicated.len(),
123 2,
124 "Should deduplicate to 2 unique programs"
125 );
126
127 let apl_token_count = deduplicated
129 .iter()
130 .filter(|p| p.target_name == "apl_token")
131 .count();
132 assert_eq!(
133 apl_token_count, 1,
134 "Should have exactly 1 apl_token after deduplication"
135 );
136
137 let escrow_count = deduplicated
138 .iter()
139 .filter(|p| p.target_name == "escrow_program")
140 .count();
141 assert_eq!(escrow_count, 1, "Should have exactly 1 escrow_program");
142 }
143
144 #[test]
145 fn test_deduplicate_programs_preserves_unique_programs() {
146 let program1 = SolanaProgram {
147 package_name: "counter".to_string(),
148 target_name: "counter_program".to_string(),
149 manifest_path: PathBuf::from("/repo/examples/counter/program/Cargo.toml"),
150 };
151
152 let program2 = SolanaProgram {
153 package_name: "escrow".to_string(),
154 target_name: "escrow_program".to_string(),
155 manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"),
156 };
157
158 let input_programs = vec![program1, program2];
160 let deduplicated = deduplicate_programs(input_programs);
161
162 assert_eq!(deduplicated.len(), 2, "Should preserve all unique programs");
164 }
165}