rez_next_package/package/
methods.rs1use rez_next_common::RezCoreError;
4use rez_next_version::Version;
5use std::collections::HashMap;
6
7use super::types::Package;
8
9impl Package {
10 pub fn new(name: String) -> Self {
11 Self {
12 name,
13 version: None,
14 description: None,
15 authors: Vec::new(),
16 requires: Vec::new(),
17 build_requires: Vec::new(),
18 private_build_requires: Vec::new(),
19 variants: Vec::new(),
20 tools: Vec::new(),
21 commands: None,
22 commands_function: None,
23 build_command: None,
24 build_system: None,
25 pre_commands: None,
26 post_commands: None,
27 pre_test_commands: None,
28 pre_build_commands: None,
29 tests: HashMap::new(),
30 requires_rez_version: None,
31 uuid: None,
32 config: HashMap::new(),
33 help: None,
34 relocatable: None,
35 cachable: None,
36 timestamp: None,
37 revision: None,
38 changelog: None,
39 release_message: None,
40 previous_version: None,
41 previous_revision: None,
42 vcs: None,
43 format_version: None,
44 base: None,
45 has_plugins: None,
46 plugin_for: Vec::new(),
47 hashed_variants: None,
48 preprocess: None,
49 is_dev_package: None,
50 filepath: None,
51 includes: None,
52 }
53 }
54
55 pub fn qualified_name(&self) -> String {
57 match &self.version {
58 Some(version) => format!("{}-{}", self.name, version.as_str()),
59 None => self.name.clone(),
60 }
61 }
62
63 pub fn as_exact_requirement(&self) -> String {
65 match &self.version {
66 Some(version) => format!("{}=={}", self.name, version.as_str()),
67 None => self.name.clone(),
68 }
69 }
70
71 pub fn is_package(&self) -> bool {
73 true
74 }
75
76 pub fn is_variant(&self) -> bool {
78 false
79 }
80
81 pub fn num_variants(&self) -> usize {
83 self.variants.len()
84 }
85
86 pub fn set_version(&mut self, version: Version) {
88 self.version = Some(version);
89 }
90
91 pub fn set_description(&mut self, description: String) {
93 self.description = Some(description);
94 }
95
96 pub fn add_author(&mut self, author: String) {
98 self.authors.push(author);
99 }
100
101 pub fn add_requirement(&mut self, requirement: String) {
103 self.requires.push(requirement);
104 }
105
106 pub fn add_build_requirement(&mut self, requirement: String) {
108 self.build_requires.push(requirement);
109 }
110
111 pub fn add_private_build_requirement(&mut self, requirement: String) {
113 self.private_build_requires.push(requirement);
114 }
115
116 pub fn add_variant(&mut self, variant: Vec<String>) {
118 self.variants.push(variant);
119 }
120
121 pub fn add_tool(&mut self, tool: String) {
123 self.tools.push(tool);
124 }
125
126 pub fn set_commands(&mut self, commands: String) {
128 self.commands = Some(commands);
129 }
130
131 pub fn is_valid(&self) -> bool {
133 self.validate().is_ok()
134 }
135
136 pub fn validate(&self) -> Result<(), RezCoreError> {
138 if self.name.is_empty() {
139 return Err(RezCoreError::PackageParse(
140 "Package name cannot be empty".to_string(),
141 ));
142 }
143
144 if !self
145 .name
146 .chars()
147 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
148 {
149 return Err(RezCoreError::PackageParse(format!(
150 "Invalid package name '{}': only alphanumeric, underscore, and hyphen allowed",
151 self.name
152 )));
153 }
154
155 if let Some(ref version) = self.version
156 && version.as_str().is_empty()
157 {
158 return Err(RezCoreError::PackageParse(
159 "Package version cannot be empty".to_string(),
160 ));
161 }
162
163 for req in &self.requires {
164 if req.is_empty() {
165 return Err(RezCoreError::PackageParse(
166 "Requirement cannot be empty".to_string(),
167 ));
168 }
169 }
170
171 for req in &self.build_requires {
172 if req.is_empty() {
173 return Err(RezCoreError::PackageParse(
174 "Build requirement cannot be empty".to_string(),
175 ));
176 }
177 }
178
179 for req in &self.private_build_requires {
180 if req.is_empty() {
181 return Err(RezCoreError::PackageParse(
182 "Private build requirement cannot be empty".to_string(),
183 ));
184 }
185 }
186
187 for variant in &self.variants {
188 for req in variant {
189 if req.is_empty() {
190 return Err(RezCoreError::PackageParse(
191 "Variant requirement cannot be empty".to_string(),
192 ));
193 }
194 }
195 }
196
197 Ok(())
198 }
199
200 pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self, RezCoreError> {
223 let path = path.as_ref();
224
225 let file_path = if path.is_dir() {
227 let package_py = path.join("package.py");
229 let package_yaml = path.join("package.yaml");
230 let package_yml = path.join("package.yml");
231
232 if package_py.exists() {
233 package_py
234 } else if package_yaml.exists() {
235 package_yaml
236 } else if package_yml.exists() {
237 package_yml
238 } else {
239 return Err(RezCoreError::PackageParse(format!(
240 "No package definition file found in {}",
241 path.display()
242 )));
243 }
244 } else {
245 path.to_path_buf()
246 };
247
248 let mut pkg = crate::serialization::PackageSerializer::load_from_file(&file_path)
250 .map_err(|e| RezCoreError::PackageParse(format!("Failed to load package: {}", e)))?;
251
252 pkg.filepath = Some(file_path.to_string_lossy().to_string());
254
255 pkg.is_dev_package = Some(true);
257
258 Ok(pkg)
263 }
264
265 pub fn root(&self) -> Option<String> {
269 self.filepath.as_ref().and_then(|fp| {
270 std::path::Path::new(fp)
271 .parent()
272 .map(|p| p.to_string_lossy().to_string())
273 })
274 }
275
276 pub fn to_package_py(&self) -> String {
279 let mut lines = Vec::new();
280
281 lines.push(format!("name = \"{}\"", self.name));
283
284 if let Some(ref version) = self.version {
286 lines.push(format!("version = \"{}\"", version.as_str()));
287 }
288
289 if let Some(ref desc) = self.description {
291 let desc_escaped = desc.replace("\"", "\\\"");
293 lines.push(format!("description = \"{}\"", desc_escaped));
294 }
295
296 if !self.authors.is_empty() {
298 let authors_str = self
299 .authors
300 .iter()
301 .map(|a| format!("\"{}\"", a.replace("\"", "\\\"")))
302 .collect::<Vec<_>>()
303 .join(", ");
304 lines.push(format!("authors = [{}]", authors_str));
305 }
306
307 if !self.requires.is_empty() {
309 let req_str = self
310 .requires
311 .iter()
312 .map(|r| format!(" \"{}\"", r))
313 .collect::<Vec<_>>()
314 .join(",\n");
315 lines.push(format!("requires = [\n{}\n]", req_str));
316 }
317
318 if !self.build_requires.is_empty() {
320 let req_str = self
321 .build_requires
322 .iter()
323 .map(|r| format!(" \"{}\"", r))
324 .collect::<Vec<_>>()
325 .join(",\n");
326 lines.push(format!("build_requires = [\n{}\n]", req_str));
327 }
328
329 if !self.private_build_requires.is_empty() {
331 let req_str = self
332 .private_build_requires
333 .iter()
334 .map(|r| format!(" \"{}\"", r))
335 .collect::<Vec<_>>()
336 .join(",\n");
337 lines.push(format!("private_build_requires = [\n{}\n]", req_str));
338 }
339
340 if !self.variants.is_empty() {
342 let mut variant_lines = Vec::new();
343 for variant in &self.variants {
344 let var_str = variant
345 .iter()
346 .map(|r| format!("\"{}\"", r))
347 .collect::<Vec<_>>()
348 .join(", ");
349 variant_lines.push(format!(" [{}]", var_str));
350 }
351 lines.push(format!("variants = [\n{}\n]", variant_lines.join(",\n")));
352 }
353
354 if !self.tools.is_empty() {
356 let tools_str = self
357 .tools
358 .iter()
359 .map(|t| format!("\"{}\"", t))
360 .collect::<Vec<_>>()
361 .join(", ");
362 lines.push(format!("tools = [{}]", tools_str));
363 }
364
365 if let Some(ref uuid) = self.uuid {
367 lines.push(format!("uuid = \"{}\"", uuid));
368 }
369
370 if let Some(ref relocatable) = self.relocatable {
372 lines.push(format!("relocatable = {}", relocatable));
373 }
374
375 if let Some(ref cachable) = self.cachable {
377 lines.push(format!("cachable = {}", cachable));
378 }
379
380 if let Some(ref commands) = self.commands {
382 lines.push("\ndef commands():".to_string());
383 for line in commands.lines() {
385 if line.trim().is_empty() {
386 lines.push("".to_string());
387 } else {
388 lines.push(format!(" {}", line));
389 }
390 }
391 }
392
393 if let Some(ref pre_commands) = self.pre_commands {
395 lines.push("\ndef pre_commands():".to_string());
396 for line in pre_commands.lines() {
397 if line.trim().is_empty() {
398 lines.push("".to_string());
399 } else {
400 lines.push(format!(" {}", line));
401 }
402 }
403 }
404
405 if let Some(ref post_commands) = self.post_commands {
407 lines.push("\ndef post_commands():".to_string());
408 for line in post_commands.lines() {
409 if line.trim().is_empty() {
410 lines.push("".to_string());
411 } else {
412 lines.push(format!(" {}", line));
413 }
414 }
415 }
416
417 if !self.tests.is_empty() {
419 lines.push("\ntests = {".to_string());
420 for test_name in self.tests.keys() {
421 lines.push(format!(" \"{}\": {{", test_name));
423 lines.push(" # test definition".to_string());
424 lines.push(" },".to_string());
425 }
426 lines.push("}".to_string());
427 }
428
429 lines.join("\n")
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use rez_next_version::Version;
437
438 #[test]
439 fn test_to_package_py_basic() {
440 let pkg = Package::new("test_pkg".to_string());
441 let result = pkg.to_package_py();
442 assert!(result.contains("name = \"test_pkg\""));
443 assert!(!result.contains("version = "));
444 }
445
446 #[test]
447 fn test_to_package_py_with_version() {
448 let mut pkg = Package::new("test_pkg".to_string());
449 pkg.version = Some(Version::parse("1.0.0").unwrap());
450 let result = pkg.to_package_py();
451 assert!(result.contains("name = \"test_pkg\""));
452 assert!(result.contains("version = \"1.0.0\""));
453 }
454
455 #[test]
456 fn test_to_package_py_with_description() {
457 let mut pkg = Package::new("test_pkg".to_string());
458 pkg.description = Some("A test package".to_string());
459 let result = pkg.to_package_py();
460 assert!(result.contains("description = \"A test package\""));
461 }
462
463 #[test]
464 fn test_to_package_py_with_authors() {
465 let mut pkg = Package::new("test_pkg".to_string());
466 pkg.authors = vec!["Author One".to_string(), "Author Two".to_string()];
467 let result = pkg.to_package_py();
468 assert!(result.contains("authors = ["));
469 assert!(result.contains("\"Author One\""));
470 assert!(result.contains("\"Author Two\""));
471 }
472
473 #[test]
474 fn test_to_package_py_with_requires() {
475 let mut pkg = Package::new("test_pkg".to_string());
476 pkg.requires = vec!["python-3.9".to_string(), "maya-2024".to_string()];
477 let result = pkg.to_package_py();
478 assert!(result.contains("requires = ["));
479 assert!(result.contains("\"python-3.9\""));
480 assert!(result.contains("\"maya-2024\""));
481 }
482
483 #[test]
484 fn test_to_package_py_with_variants() {
485 let mut pkg = Package::new("test_pkg".to_string());
486 pkg.variants = vec![
487 vec!["python-3.9".to_string()],
488 vec!["python-3.10".to_string()],
489 ];
490 let result = pkg.to_package_py();
491 assert!(result.contains("variants = ["));
492 assert!(result.contains("[\"python-3.9\"]"));
493 assert!(result.contains("[\"python-3.10\"]"));
494 }
495
496 #[test]
497 fn test_to_package_py_with_tools() {
498 let mut pkg = Package::new("test_pkg".to_string());
499 pkg.tools = vec!["mytool".to_string(), "another_tool".to_string()];
500 let result = pkg.to_package_py();
501 assert!(result.contains("tools = ["));
502 assert!(result.contains("\"mytool\""));
503 assert!(result.contains("\"another_tool\""));
504 }
505
506 #[test]
507 fn test_to_package_py_with_commands() {
508 let mut pkg = Package::new("test_pkg".to_string());
509 pkg.commands = Some(" env.PATH.prepend(\"{root}/bin\")\n".to_string());
510 let result = pkg.to_package_py();
511 assert!(result.contains("def commands():"));
512 assert!(result.contains("env.PATH.prepend"));
513 }
514
515 #[test]
516 fn test_to_package_py_with_uuid() {
517 let mut pkg = Package::new("test_pkg".to_string());
518 pkg.uuid = Some("12345678-1234-1234-1234-123456789012".to_string());
519 let result = pkg.to_package_py();
520 assert!(result.contains("uuid = \"12345678-1234-1234-1234-123456789012\""));
521 }
522
523 #[test]
524 fn test_to_package_py_with_relocatable() {
525 let mut pkg = Package::new("test_pkg".to_string());
526 pkg.relocatable = Some(true);
527 let result = pkg.to_package_py();
528 assert!(result.contains("relocatable = true"));
529 }
530
531 #[test]
532 fn test_to_package_py_with_cachable() {
533 let mut pkg = Package::new("test_pkg".to_string());
534 pkg.cachable = Some(false);
535 let result = pkg.to_package_py();
536 assert!(result.contains("cachable = false"));
537 }
538
539 #[test]
540 fn test_to_package_py_complete() {
541 let mut pkg = Package::new("complete_pkg".to_string());
542 pkg.version = Some(Version::parse("2.1.0").unwrap());
543 pkg.description = Some("A complete test package".to_string());
544 pkg.authors = vec!["Test Author".to_string()];
545 pkg.requires = vec!["python-3.9".to_string()];
546 pkg.build_requires = vec!["cmake-3.20".to_string()];
547 pkg.variants = vec![
548 vec!["python-3.9".to_string()],
549 vec!["python-3.10".to_string()],
550 ];
551 pkg.tools = vec!["mytool".to_string()];
552 pkg.commands = Some(" env.PATH.prepend(\"{root}/bin\")\n".to_string());
553 pkg.uuid = Some("12345678-1234-1234-1234-123456789012".to_string());
554 pkg.relocatable = Some(true);
555 pkg.cachable = Some(true);
556
557 let result = pkg.to_package_py();
558
559 assert!(result.contains("name = \"complete_pkg\""));
561 assert!(result.contains("version = \"2.1.0\""));
562 assert!(result.contains("description = \"A complete test package\""));
563 assert!(result.contains("authors = ["));
564 assert!(result.contains("requires = ["));
565 assert!(result.contains("build_requires = ["));
566 assert!(result.contains("variants = ["));
567 assert!(result.contains("tools = ["));
568 assert!(result.contains("def commands():"));
569 assert!(result.contains("uuid = "));
570 assert!(result.contains("relocatable = true"));
571 assert!(result.contains("cachable = true"));
572 }
573
574 #[test]
575 fn test_to_package_py_output_format_valid() {
576 let mut pkg = Package::new("format_test".to_string());
577 pkg.version = Some(Version::parse("1.0.0").unwrap());
578 pkg.requires = vec!["python-3.9".to_string()];
579 pkg.commands = Some(" env.PATH.prepend(\"{root}/bin\")\n".to_string());
580
581 let result = pkg.to_package_py();
582
583 assert!(result.starts_with("name = "));
585 assert!(result.contains("def commands():"));
586 assert!(result.contains(" env.PATH.prepend"));
587 }
588
589 #[test]
590 fn test_to_package_py_escapes_description() {
591 let mut pkg = Package::new("escape_test".to_string());
592 pkg.description = Some("Description with \"quotes\"".to_string());
593 let result = pkg.to_package_py();
594 assert!(result.contains("Description with \\\"quotes\\\""));
595 }
596
597 #[test]
600 fn test_from_path_with_package_py() {
601 use std::fs::File;
602 use std::io::Write;
603 use tempfile::TempDir;
604
605 let tmp = TempDir::new().unwrap();
606 let pkg_path = tmp.path().join("package.py");
607 let mut file = File::create(&pkg_path).unwrap();
608 writeln!(file, "name = 'mypackage'").unwrap();
609 writeln!(file, "version = '1.0.0'").unwrap();
610 file.flush().unwrap();
611
612 let pkg = Package::from_path(pkg_path.to_str().unwrap()).unwrap();
613 assert_eq!(pkg.name, "mypackage");
614 assert_eq!(pkg.version.as_ref().unwrap().as_str(), "1.0.0");
615 assert_eq!(pkg.filepath, Some(pkg_path.to_string_lossy().to_string()));
616 assert_eq!(pkg.is_dev_package, Some(true));
617 }
618
619 #[test]
620 fn test_from_path_with_directory() {
621 use std::fs::File;
622 use std::io::Write;
623 use tempfile::TempDir;
624
625 let tmp = TempDir::new().unwrap();
626 let pkg_path = tmp.path().join("package.py");
627 let mut file = File::create(&pkg_path).unwrap();
628 writeln!(file, "name = 'dirpkg'").unwrap();
629 writeln!(file, "version = '2.0.0'").unwrap();
630 file.flush().unwrap();
631
632 let pkg = Package::from_path(tmp.path().to_str().unwrap()).unwrap();
634 assert_eq!(pkg.name, "dirpkg");
635 assert_eq!(pkg.filepath, Some(pkg_path.to_string_lossy().to_string()));
636 }
637
638 #[test]
639 fn test_root() {
640 use std::fs::File;
641 use std::io::Write;
642 use tempfile::TempDir;
643
644 let tmp = TempDir::new().unwrap();
645 let pkg_path = tmp.path().join("package.py");
646 let mut file = File::create(&pkg_path).unwrap();
647 writeln!(file, "name = 'rootpkg'").unwrap();
648 file.flush().unwrap();
649
650 let pkg = Package::from_path(pkg_path.to_str().unwrap()).unwrap();
651 let root = pkg.root();
652 assert_eq!(root, Some(tmp.path().to_string_lossy().to_string()));
653 }
654
655 #[test]
656 fn test_from_path_no_package_file() {
657 use tempfile::TempDir;
658 let tmp = TempDir::new().unwrap();
659 let result = Package::from_path(tmp.path().to_str().unwrap());
660 assert!(result.is_err());
661 }
662}