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 let build_result = builder::build_programs(&included_programs);
52
53 let code = codegen::generate(&build_result)?;
55 codegen::save(&cargo_manifest_dir, &code)?;
56
57 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
68fn 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
76fn 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 seen.entry(program.manifest_path.clone()).or_insert(program);
86 }
87
88 let mut deduplicated: Vec<SolanaProgram> = seen.into_values().collect();
89
90 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 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"), };
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 let input_programs = vec![escrow_program.clone(), program1, program2];
124 assert_eq!(input_programs.len(), 3);
125
126 let deduplicated = deduplicate_programs(input_programs);
128 assert_eq!(
129 deduplicated.len(),
130 2,
131 "Should deduplicate to 2 unique programs"
132 );
133
134 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 let input_programs = vec![program1, program2];
167 let deduplicated = deduplicate_programs(input_programs);
168
169 assert_eq!(deduplicated.len(), 2, "Should preserve all unique programs");
171 }
172}