Skip to main content

rez_next_package/package/
methods.rs

1//! Package method implementations.
2
3use 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    /// Get the qualified name of the package (name-version)
56    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    /// Get the package as an exact requirement string
64    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    /// Check if this is a package (always true for Package)
72    pub fn is_package(&self) -> bool {
73        true
74    }
75
76    /// Check if this is a variant (always false for Package)
77    pub fn is_variant(&self) -> bool {
78        false
79    }
80
81    /// Get the number of variants
82    pub fn num_variants(&self) -> usize {
83        self.variants.len()
84    }
85
86    /// Set the package version
87    pub fn set_version(&mut self, version: Version) {
88        self.version = Some(version);
89    }
90
91    /// Set the package description
92    pub fn set_description(&mut self, description: String) {
93        self.description = Some(description);
94    }
95
96    /// Add an author
97    pub fn add_author(&mut self, author: String) {
98        self.authors.push(author);
99    }
100
101    /// Add a requirement
102    pub fn add_requirement(&mut self, requirement: String) {
103        self.requires.push(requirement);
104    }
105
106    /// Add a build requirement
107    pub fn add_build_requirement(&mut self, requirement: String) {
108        self.build_requires.push(requirement);
109    }
110
111    /// Add a private build requirement
112    pub fn add_private_build_requirement(&mut self, requirement: String) {
113        self.private_build_requires.push(requirement);
114    }
115
116    /// Add a variant
117    pub fn add_variant(&mut self, variant: Vec<String>) {
118        self.variants.push(variant);
119    }
120
121    /// Add a tool
122    pub fn add_tool(&mut self, tool: String) {
123        self.tools.push(tool);
124    }
125
126    /// Set commands
127    pub fn set_commands(&mut self, commands: String) {
128        self.commands = Some(commands);
129    }
130
131    /// Check if the package definition is valid
132    pub fn is_valid(&self) -> bool {
133        self.validate().is_ok()
134    }
135
136    /// Validate the package definition
137    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    /// Load a developer package from a path.
201    ///
202    /// This aligns with Rez's `DeveloperPackage.from_path()` interface.
203    /// Supports both file path (package.py/package.yaml) and directory path.
204    ///
205    /// # Arguments
206    /// * `path` - Directory containing package definition, or path to the file itself
207    ///
208    /// # Returns
209    /// * `Result<Package, RezCoreError>` - The loaded package
210    ///
211    /// # Example
212    /// ```
213    /// # use rez_next_package::package::types::Package;
214    /// # use std::path::PathBuf;
215    /// # use tempfile::TempDir;
216    /// # let dir = TempDir::new().unwrap();
217    /// # let pkg_path = dir.path().join("package.py");
218    /// # std::fs::write(&pkg_path, r#"name = "mypackage""#).unwrap();
219    /// let pkg = Package::from_path(pkg_path).unwrap();
220    /// assert_eq!(pkg.name, "mypackage");
221    /// ```
222    pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self, RezCoreError> {
223        let path = path.as_ref();
224
225        // Determine if path is a directory or file
226        let file_path = if path.is_dir() {
227            // Look for package.py or package.yaml in directory
228            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        // Load package from file
249        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        // Set filepath
253        pkg.filepath = Some(file_path.to_string_lossy().to_string());
254
255        // Set is_dev_package flag
256        pkg.is_dev_package = Some(true);
257
258        // TODO: Collect includes from SourceCode objects
259        // This requires parsing the package data to find all @include decorators
260        // For now, we leave includes as None
261
262        Ok(pkg)
263    }
264
265    /// Get the root directory of the package (parent of filepath).
266    ///
267    /// Aligns with Rez's `DeveloperPackage.root` property.
268    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    /// Serialize the package to package.py format string.
277    /// Compatible with rez package.py format.
278    pub fn to_package_py(&self) -> String {
279        let mut lines = Vec::new();
280
281        // name (required)
282        lines.push(format!("name = \"{}\"", self.name));
283
284        // version (optional)
285        if let Some(ref version) = self.version {
286            lines.push(format!("version = \"{}\"", version.as_str()));
287        }
288
289        // description (optional)
290        if let Some(ref desc) = self.description {
291            // Escape quotes in description
292            let desc_escaped = desc.replace("\"", "\\\"");
293            lines.push(format!("description = \"{}\"", desc_escaped));
294        }
295
296        // authors (optional)
297        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        // requires (optional)
308        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        // build_requires (optional)
319        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        // private_build_requires (optional)
330        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        // variants (optional)
341        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        // tools (optional)
355        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        // uuid (optional)
366        if let Some(ref uuid) = self.uuid {
367            lines.push(format!("uuid = \"{}\"", uuid));
368        }
369
370        // relocatable (optional)
371        if let Some(ref relocatable) = self.relocatable {
372            lines.push(format!("relocatable = {}", relocatable));
373        }
374
375        // cachable (optional)
376        if let Some(ref cachable) = self.cachable {
377            lines.push(format!("cachable = {}", cachable));
378        }
379
380        // commands function (optional)
381        if let Some(ref commands) = self.commands {
382            lines.push("\ndef commands():".to_string());
383            // Add commands function body with proper indentation
384            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        // pre_commands (optional)
394        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        // post_commands (optional)
406        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        // tests (optional) - simplified serialization
418        if !self.tests.is_empty() {
419            lines.push("\ntests = {".to_string());
420            for test_name in self.tests.keys() {
421                // Simplified: just add a comment, full serialization is complex
422                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        // Verify all fields are present
560        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        // Verify output can be parsed as valid Python (basic check)
584        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    // ── Phase N: from_path() and root() tests ─────────────────────────────
598
599    #[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        // Pass directory path
633        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}