1mod builder;
2mod codegen;
3mod 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::Magic => "magic".to_string(),
65 Config::Pedantic { .. } => "pedantic".to_string(),
66 };
67
68 Ok(GenerationResult::new(mode, discovered_programs))
69}
70
71fn enable_incremental_builds(manifest_dir: &Path, programs: &[SolanaProgram]) -> Result<(), Error> {
73 let output_path = manifest_dir.join("src").join("lib.rs");
74 println!("cargo:rerun-if-changed={}", output_path.display());
75
76 for program in programs {
77 let program_root = program.manifest_path.parent().unwrap();
78 println!("cargo:rerun-if-changed={}", program_root.display());
79 }
80 Ok(())
81}
82
83fn deduplicate_programs(programs: Vec<SolanaProgram>) -> Vec<SolanaProgram> {
86 use std::collections::HashMap;
87
88 let mut seen: HashMap<PathBuf, SolanaProgram> = HashMap::new();
89
90 for program in programs {
91 seen.entry(program.manifest_path.clone()).or_insert(program);
93 }
94
95 let mut deduplicated: Vec<SolanaProgram> = seen.into_values().collect();
96
97 deduplicated.sort_by(|a, b| a.target_name.cmp(&b.target_name));
99
100 deduplicated
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::path::PathBuf;
107
108 #[test]
109 fn test_deduplicate_programs_removes_duplicates_by_manifest_path() {
110 let program1 = SolanaProgram {
112 package_name: "apl-token".to_string(),
113 target_name: "apl_token".to_string(),
114 manifest_path: PathBuf::from("/repo/token/Cargo.toml"),
115 };
116
117 let program2 = SolanaProgram {
118 package_name: "apl-token".to_string(),
119 target_name: "apl_token".to_string(),
120 manifest_path: PathBuf::from("/repo/token/Cargo.toml"), };
122
123 let escrow_program = SolanaProgram {
124 package_name: "escrow_program".to_string(),
125 target_name: "escrow_program".to_string(),
126 manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"),
127 };
128
129 let input_programs = vec![escrow_program.clone(), program1, program2];
131 assert_eq!(input_programs.len(), 3);
132
133 let deduplicated = deduplicate_programs(input_programs);
135 assert_eq!(
136 deduplicated.len(),
137 2,
138 "Should deduplicate to 2 unique programs"
139 );
140
141 let apl_token_count = deduplicated
143 .iter()
144 .filter(|p| p.target_name == "apl_token")
145 .count();
146 assert_eq!(
147 apl_token_count, 1,
148 "Should have exactly 1 apl_token after deduplication"
149 );
150
151 let escrow_count = deduplicated
152 .iter()
153 .filter(|p| p.target_name == "escrow_program")
154 .count();
155 assert_eq!(escrow_count, 1, "Should have exactly 1 escrow_program");
156 }
157
158 #[test]
159 fn test_deduplicate_programs_preserves_unique_programs() {
160 let program1 = SolanaProgram {
161 package_name: "counter".to_string(),
162 target_name: "counter_program".to_string(),
163 manifest_path: PathBuf::from("/repo/examples/counter/program/Cargo.toml"),
164 };
165
166 let program2 = SolanaProgram {
167 package_name: "escrow".to_string(),
168 target_name: "escrow_program".to_string(),
169 manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"),
170 };
171
172 let input_programs = vec![program1, program2];
174 let deduplicated = deduplicate_programs(input_programs);
175
176 assert_eq!(deduplicated.len(), 2, "Should preserve all unique programs");
178 }
179}