1use serde::Deserialize;
2use std::fs;
3use std::path::Path;
4use std::process::{Command, Stdio};
5use tempfile::NamedTempFile;
6
7use crate::{
8 codegen,
9 domain::{ElfMagicError, ManifestConfig, Package, SolanaProgram, Workspace},
10};
11
12#[derive(Debug, Deserialize)]
14struct CargoMetadata {
15 packages: Vec<Package>,
17}
18
19#[derive(Debug, Deserialize)]
21struct CargoToml {
22 package: Option<PackageSection>,
23}
24
25#[derive(Debug, Deserialize)]
26struct PackageSection {
27 metadata: Option<Metadata>,
28}
29
30#[derive(Debug, Deserialize)]
31struct Metadata {
32 #[serde(rename = "elf-magic")]
33 elf_magic: Option<ManifestConfig>,
34}
35
36pub fn parse_manifest_config(cargo_manifest_dir: &Path) -> Result<ManifestConfig, ElfMagicError> {
41 let manifest_path = cargo_manifest_dir.join("Cargo.toml");
42
43 let content = fs::read_to_string(&manifest_path).map_err(|e| {
45 ElfMagicError::Metadata(format!(
46 "Failed to read Cargo.toml at {}: {}",
47 manifest_path.display(),
48 e
49 ))
50 })?;
51
52 let cargo_toml: CargoToml = toml::from_str(&content)
54 .map_err(|e| ElfMagicError::Metadata(format!("Failed to parse Cargo.toml: {}", e)))?;
55
56 let config = cargo_toml
58 .package
59 .and_then(|p| p.metadata)
60 .and_then(|m| m.elf_magic)
61 .unwrap_or_else(ManifestConfig::allow_all);
62
63 Ok(config)
64}
65
66pub fn discover_workspace(cargo_manifest_dir: &Path) -> Result<Workspace, ElfMagicError> {
71 let manifest_path = cargo_manifest_dir.join("Cargo.toml");
72
73 let output = Command::new("cargo")
74 .args([
75 "metadata",
76 "--format-version",
77 "1",
78 "--no-deps",
79 "--manifest-path",
80 manifest_path.to_str().unwrap(),
81 ])
82 .stderr(Stdio::inherit())
83 .output()
84 .map_err(|e| {
85 ElfMagicError::WorkspaceDiscovery(format!("Failed to execute cargo metadata: {}", e))
86 })?;
87
88 let mut metadata: CargoMetadata = serde_json::from_slice(&output.stdout).map_err(|e| {
90 ElfMagicError::WorkspaceDiscovery(format!("Failed to parse cargo metadata JSON: {}", e))
91 })?;
92
93 metadata.packages.sort_by(|a, b| a.name.cmp(&b.name));
95
96 Ok(Workspace {
97 packages: metadata.packages,
98 })
99}
100
101pub fn write_lib_file(
106 cargo_manifest_dir: &Path,
107 programs: &[SolanaProgram],
108) -> Result<(), ElfMagicError> {
109 let rendered_content = codegen::render_lib_file(programs)?;
111
112 let target_path = cargo_manifest_dir.join("src/lib.rs");
114 let target_dir = target_path.parent().unwrap_or(cargo_manifest_dir);
115
116 let temp_file = NamedTempFile::new_in(target_dir).map_err(ElfMagicError::Io)?;
117
118 fs::write(temp_file.path(), &rendered_content).map_err(ElfMagicError::Io)?;
120
121 let temp_path = temp_file.path().to_path_buf();
124 let fmt_result = Command::new("cargo")
125 .arg("fmt")
126 .arg("--")
127 .arg(&temp_path)
128 .output();
129
130 match fmt_result {
131 Ok(output) if !output.status.success() => {
132 eprintln!(
133 "Warning: cargo fmt failed: {}",
134 String::from_utf8_lossy(&output.stderr)
135 );
136 }
137 Err(e) => {
138 eprintln!("Warning: failed to run cargo fmt: {}", e);
139 }
140 _ => {} }
142
143 temp_file
145 .persist(target_path)
146 .map_err(|e| ElfMagicError::Io(e.error))?;
147
148 Ok(())
149}
150
151pub fn setup_incremental_builds(programs: &[SolanaProgram]) -> Result<(), ElfMagicError> {
156 for program in programs {
157 let src_path = program.path.join("src");
159 if src_path.exists() {
160 println!("cargo:rerun-if-changed={}", src_path.display());
161 }
162
163 println!("cargo:rerun-if-changed={}", program.manifest_path.display());
165 }
166
167 Ok(())
168}
169
170#[cfg(test)]
171mod tests {
172 use std::path::PathBuf;
173
174 use crate::domain::ProgramFilter;
175
176 use super::*;
177
178 fn create_test_cargo_toml(content: &str) -> tempfile::TempDir {
179 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
180 let cargo_toml_path = temp_dir.path().join("Cargo.toml");
181 std::fs::write(&cargo_toml_path, content).expect("Failed to write test Cargo.toml");
182 temp_dir
183 }
184
185 fn create_test_workspace() -> tempfile::TempDir {
186 let temp_dir = tempfile::tempdir().expect("Failed to create temp workspace");
187 let workspace_root = temp_dir.path();
188
189 let root_cargo_toml = r#"
191[workspace]
192members = ["program-a", "program-b", "lib-crate"]
193"#;
194 std::fs::write(workspace_root.join("Cargo.toml"), root_cargo_toml)
195 .expect("Failed to write root Cargo.toml");
196
197 let program_a_dir = workspace_root.join("program-a");
199 std::fs::create_dir_all(program_a_dir.join("src")).expect("Failed to create program-a dir");
200 let program_a_cargo_toml = r#"
201[package]
202name = "program-a"
203version = "0.1.0"
204edition = "2021"
205
206[lib]
207crate-type = ["cdylib"]
208name = "program_a"
209"#;
210 std::fs::write(program_a_dir.join("Cargo.toml"), program_a_cargo_toml)
211 .expect("Failed to write program-a Cargo.toml");
212 std::fs::write(program_a_dir.join("src/lib.rs"), "// Test Solana program A")
213 .expect("Failed to write program-a lib.rs");
214
215 let program_b_dir = workspace_root.join("program-b");
217 std::fs::create_dir_all(program_b_dir.join("src")).expect("Failed to create program-b dir");
218 let program_b_cargo_toml = r#"
219[package]
220name = "program-b"
221version = "0.1.0"
222edition = "2021"
223
224[lib]
225crate-type = ["cdylib"]
226"#;
227 std::fs::write(program_b_dir.join("Cargo.toml"), program_b_cargo_toml)
228 .expect("Failed to write program-b Cargo.toml");
229 std::fs::write(program_b_dir.join("src/lib.rs"), "// Test Solana program B")
230 .expect("Failed to write program-b lib.rs");
231
232 let lib_crate_dir = workspace_root.join("lib-crate");
234 std::fs::create_dir_all(lib_crate_dir.join("src")).expect("Failed to create lib-crate dir");
235 let lib_crate_cargo_toml = r#"
236[package]
237name = "lib-crate"
238version = "0.1.0"
239edition = "2021"
240
241[lib]
242crate-type = ["lib"]
243"#;
244 std::fs::write(lib_crate_dir.join("Cargo.toml"), lib_crate_cargo_toml)
245 .expect("Failed to write lib-crate Cargo.toml");
246 std::fs::write(lib_crate_dir.join("src/lib.rs"), "// Test regular library")
247 .expect("Failed to write lib-crate lib.rs");
248
249 temp_dir
250 }
251
252 #[test]
253 fn test_parse_manifest_config_with_full_config() {
254 let cargo_toml_content = r#"
255[package]
256name = "test-package"
257version = "0.1.0"
258edition = "2021"
259
260[package.metadata.elf-magic]
261include = ["programs/*", "contracts/*"]
262exclude = ["programs/deprecated-*", "contracts/old-*"]
263"#;
264
265 let temp_dir = create_test_cargo_toml(cargo_toml_content);
266 let result = parse_manifest_config(temp_dir.path()).unwrap();
267
268 assert_eq!(result.include, vec!["programs/*", "contracts/*"]);
269 assert_eq!(
270 result.exclude,
271 vec!["programs/deprecated-*", "contracts/old-*"]
272 );
273 }
274
275 #[test]
276 fn test_parse_manifest_config_with_partial_config() {
277 let cargo_toml_content = r#"
278[package]
279name = "test-package"
280version = "0.1.0"
281edition = "2021"
282
283[package.metadata.elf-magic]
284include = ["programs/*"]
285"#;
286
287 let temp_dir = create_test_cargo_toml(cargo_toml_content);
288 let result = parse_manifest_config(temp_dir.path()).unwrap();
289
290 assert_eq!(result.include, vec!["programs/*"]);
291 assert_eq!(result.exclude, Vec::<String>::new());
292 }
293
294 #[test]
295 fn test_parse_manifest_config_missing_elf_magic_section() {
296 let cargo_toml_content = r#"
297[package]
298name = "test-package"
299version = "0.1.0"
300edition = "2021"
301"#;
302
303 let temp_dir = create_test_cargo_toml(cargo_toml_content);
304 let result = parse_manifest_config(temp_dir.path()).unwrap();
305
306 assert_eq!(result.include, vec!["**/*"]);
308 assert_eq!(result.exclude, Vec::<String>::new());
309 }
310
311 #[test]
312 fn test_parse_manifest_config_missing_metadata_section() {
313 let cargo_toml_content = r#"
314[package]
315name = "test-package"
316version = "0.1.0"
317edition = "2021"
318"#;
319
320 let temp_dir = create_test_cargo_toml(cargo_toml_content);
321 let result = parse_manifest_config(temp_dir.path()).unwrap();
322
323 assert_eq!(result.include, vec!["**/*"]);
325 assert_eq!(result.exclude, Vec::<String>::new());
326 }
327
328 #[test]
329 fn test_parse_manifest_config_missing_package_section() {
330 let cargo_toml_content = r#"
331[workspace]
332members = ["programs/*"]
333"#;
334
335 let temp_dir = create_test_cargo_toml(cargo_toml_content);
336 let result = parse_manifest_config(temp_dir.path()).unwrap();
337
338 assert_eq!(result.include, vec!["**/*"]);
340 assert_eq!(result.exclude, Vec::<String>::new());
341 }
342
343 #[test]
344 fn test_parse_manifest_config_file_not_found() {
345 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
346 let result = parse_manifest_config(temp_dir.path());
349
350 assert!(result.is_err());
351 let error_message = result.unwrap_err().to_string();
352 assert!(error_message.contains("Failed to read Cargo.toml"));
353 }
354
355 #[test]
356 fn test_parse_manifest_config_invalid_toml() {
357 let invalid_cargo_toml_content = r#"
358[package
359name = "test-package" # Missing closing bracket
360"#;
361
362 let temp_dir = create_test_cargo_toml(invalid_cargo_toml_content);
363 let result = parse_manifest_config(temp_dir.path());
364
365 assert!(result.is_err());
366 let error_message = result.unwrap_err().to_string();
367 assert!(error_message.contains("Failed to parse Cargo.toml"));
368 }
369
370 #[test]
371 fn test_parse_manifest_config_empty_arrays() {
372 let cargo_toml_content = r#"
373[package]
374name = "test-package"
375version = "0.1.0"
376edition = "2021"
377
378[package.metadata.elf-magic]
379include = []
380exclude = []
381"#;
382
383 let temp_dir = create_test_cargo_toml(cargo_toml_content);
384 let result = parse_manifest_config(temp_dir.path()).unwrap();
385
386 assert_eq!(result.include, Vec::<String>::new());
387 assert_eq!(result.exclude, Vec::<String>::new());
388 }
389
390 #[test]
391 fn test_discover_workspace_integration() {
392 let temp_workspace = create_test_workspace();
393
394 let result = discover_workspace(temp_workspace.path());
395
396 let workspace = result.expect("Should successfully discover test workspace");
398
399 assert_eq!(
401 workspace
402 .packages
403 .iter()
404 .map(|p| p.manifest_path.parent().unwrap().canonicalize().unwrap())
405 .collect::<Vec<_>>(),
406 vec![
407 temp_workspace
408 .path()
409 .canonicalize()
410 .unwrap()
411 .join("lib-crate"),
412 temp_workspace
413 .path()
414 .canonicalize()
415 .unwrap()
416 .join("program-a"),
417 temp_workspace
418 .path()
419 .canonicalize()
420 .unwrap()
421 .join("program-b"),
422 ]
423 );
424
425 assert_eq!(workspace.packages.len(), 3);
427
428 let package_names: Vec<&String> = workspace.packages.iter().map(|m| &m.name).collect();
430 assert_eq!(package_names, vec!["lib-crate", "program-a", "program-b"]);
431
432 for package in &workspace.packages {
434 match package.name.as_str() {
435 "lib-crate" => {
436 assert_eq!(
437 package
438 .targets
439 .iter()
440 .map(|t| t.crate_types.clone())
441 .collect::<Vec<_>>(),
442 vec![vec!["lib"]]
443 );
444 }
445 "program-a" => {
446 assert_eq!(
447 package
448 .targets
449 .iter()
450 .map(|t| t.crate_types.clone())
451 .collect::<Vec<_>>(),
452 vec![vec!["cdylib"]]
453 );
454 }
455 "program-b" => {
456 assert_eq!(
457 package
458 .targets
459 .iter()
460 .map(|t| t.crate_types.clone())
461 .collect::<Vec<_>>(),
462 vec![vec!["cdylib"]]
463 );
464 }
465 _ => {
466 panic!("Unexpected package name: {}", package.name);
467 }
468 }
469 }
470 }
471
472 #[test]
473 fn test_discover_workspace_with_filtering() {
474 let temp_workspace = create_test_workspace();
475
476 let config = ManifestConfig {
478 include: vec!["**/*".to_string()],
479 exclude: vec!["program-a".to_string()],
480 };
481
482 let result = discover_workspace(temp_workspace.path());
483
484 let workspace = result.expect("Should successfully discover test workspace");
485
486 let filter = ProgramFilter::from(&config);
488 let solana_programs = workspace.find_solana_programs(&filter);
489
490 assert_eq!(solana_programs.len(), 1);
492 assert_eq!(solana_programs[0].name, "program_b");
493 }
494
495 #[test]
496 fn test_discover_workspace_find_solana_programs() {
497 let temp_workspace = create_test_workspace();
498
499 let config = ManifestConfig::allow_all();
500 let result = discover_workspace(temp_workspace.path());
501
502 let workspace = result.expect("Should successfully discover test workspace");
503
504 let filter = ProgramFilter::from(&config);
506 let solana_programs = workspace.find_solana_programs(&filter);
507
508 assert_eq!(solana_programs.len(), 2);
510
511 assert_eq!(solana_programs[0].name, "program_a");
513 assert_eq!(solana_programs[1].name, "program_b");
514
515 for program in &solana_programs {
517 assert!(program.name.starts_with("program_"));
518 assert_eq!(
519 program.env_var_name(),
520 format!("PROGRAM_{}_ELF_MAGIC_PATH", program.name.to_uppercase())
521 );
522 assert_eq!(
523 program.constant_name(),
524 format!("{}_ELF", program.name.to_uppercase())
525 );
526 }
527 }
528
529 #[test]
530 fn test_discover_workspace() {
531 let current_dir = std::env::current_dir().unwrap();
533 match discover_workspace(¤t_dir) {
534 Ok(workspace) => {
535 assert!(!workspace
537 .packages
538 .iter()
539 .map(|p| p.manifest_path.parent().unwrap().as_os_str())
540 .collect::<Vec<_>>()
541 .is_empty());
542
543 let member_names: Vec<&String> =
545 workspace.packages.iter().map(|m| &m.name).collect();
546 let mut sorted_names = member_names.clone();
547 sorted_names.sort();
548 assert_eq!(member_names, sorted_names);
549 }
550 Err(e) => {
551 println!("Expected error in test environment: {}", e);
553 assert!(
554 e.to_string().contains("cargo metadata") || e.to_string().contains("workspace")
555 );
556 }
557 }
558 }
559
560 #[test]
561 fn test_discover_workspace_sorted() {
562 let current_dir = std::env::current_dir().unwrap();
564 match discover_workspace(¤t_dir) {
565 Ok(workspace) => {
566 for i in 1..workspace.packages.len() {
568 assert!(workspace.packages[i - 1].name <= workspace.packages[i].name);
569 }
570 }
571 Err(_) => {
572 }
575 }
576 }
577
578 #[test]
579 fn test_write_lib_file() {
580 use tempfile::TempDir;
581
582 let programs = vec![SolanaProgram {
584 name: "test_program".to_string(),
585 path: PathBuf::from("programs/test-program"),
586 manifest_path: PathBuf::from("programs/test-program/Cargo.toml"),
587 }];
588
589 let temp_workspace = TempDir::new().expect("Failed to create temp workspace");
591 let temp_src_dir = temp_workspace.path().join("src");
592 std::fs::create_dir_all(&temp_src_dir).expect("Failed to create temp src dir");
593
594 let original_dir = std::env::current_dir().expect("Failed to get current dir");
596
597 std::env::set_current_dir(temp_workspace.path())
599 .expect("Failed to change to temp workspace");
600
601 let result = write_lib_file(temp_workspace.path(), &programs);
603
604 std::env::set_current_dir(original_dir).expect("Failed to restore original dir");
606
607 assert!(result.is_ok(), "write_lib_file should succeed");
609
610 let lib_path = temp_src_dir.join("lib.rs");
612 assert!(lib_path.exists(), "lib.rs should be created");
613
614 let content = std::fs::read_to_string(&lib_path).expect("Failed to read generated file");
615
616 assert!(content.contains("auto-generated by elf-magic"));
618 assert!(content.contains("TEST_PROGRAM_ELF"));
619 assert!(content.contains("pub fn elves()"));
620
621 let temp_files: Vec<_> = std::fs::read_dir(&temp_src_dir)
623 .expect("Failed to read temp src dir")
624 .filter_map(|entry| entry.ok())
625 .filter(|entry| {
626 let file_name = entry.file_name();
627 let name = file_name.to_string_lossy();
628 name.starts_with(".tmp") || name.ends_with(".tmp")
629 })
630 .collect();
631
632 assert!(
633 temp_files.is_empty(),
634 "Temporary files should be cleaned up: found {:?}",
635 temp_files
636 );
637 }
638
639 #[test]
640 fn test_setup_incremental_builds() {
641 use std::path::PathBuf;
642
643 let programs = vec![
644 SolanaProgram {
645 name: "token-manager".to_string(),
646 path: PathBuf::from("programs/token-manager"),
647 manifest_path: PathBuf::from("programs/token-manager/Cargo.toml"),
648 },
649 SolanaProgram {
650 name: "governance".to_string(),
651 path: PathBuf::from("programs/governance"),
652 manifest_path: PathBuf::from("programs/governance/Cargo.toml"),
653 },
654 ];
655
656 let result = setup_incremental_builds(&programs);
659 assert!(result.is_ok());
660
661 }
667}