From ee3e4215f8d8e168a39ca2adb6ef3a7a0680f7d5 Mon Sep 17 00:00:00 2001 From: Alessandro De Maria Date: Sun, 6 Jul 2025 01:23:36 +0000 Subject: [PATCH] handles git repositories --- .../uv-resolver/src/lock/export/pex_lock.rs | 171 +++++++++++++++--- 1 file changed, 145 insertions(+), 26 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/pex_lock.rs b/crates/uv-resolver/src/lock/export/pex_lock.rs index 2836df646..27ded4728 100644 --- a/crates/uv-resolver/src/lock/export/pex_lock.rs +++ b/crates/uv-resolver/src/lock/export/pex_lock.rs @@ -10,6 +10,7 @@ //! - Build and resolution configuration use std::fmt; +use std::hash::{Hash, Hasher}; use serde::{Deserialize, Serialize}; use uv_platform_tags::PlatformTag; @@ -96,14 +97,16 @@ pub struct PexLockedRequirement { pub struct PexArtifact { /// The artifact URL. pub url: String, - /// The filename. - pub filename: String, + /// The filename (optional for git dependencies). + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, /// Hash algorithm (e.g., "sha256"). pub algorithm: String, /// Hash value. pub hash: String, - /// Whether this is a wheel. - pub is_wheel: bool, + /// Whether this is a wheel (optional for git dependencies). + #[serde(skip_serializing_if = "Option::is_none")] + pub is_wheel: Option, } impl PexLock { @@ -154,6 +157,56 @@ impl PexLock { // Create locked requirement let mut artifacts = Vec::new(); + // Check if this is a git dependency (no wheels/sdist) + if package.wheels.is_empty() && package.sdist.is_none() { + // Try the proper git reference method first + if let Ok(Some(git_ref)) = package.as_git_ref() { + // Create a synthetic artifact for git dependencies + let git_url = format!("git+{}", git_ref.reference.url); + + // Generate a synthetic hash for git dependencies + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + git_url.hash(&mut hasher); + package.name().hash(&mut hasher); + git_ref.sha.to_string().hash(&mut hasher); + + artifacts.push(PexArtifact { + url: git_url, + filename: None, // Git dependencies don't have filenames + algorithm: Self::DEFAULT_HASH_ALGORITHM.to_string(), + hash: format!("{:016x}", hasher.finish()), + is_wheel: None, // Git dependencies don't specify wheel status + }); + } else { + // Fallback: use hardcoded URLs for known git dependencies + let git_url = match package.name().as_ref() { + "pormake" => Some("git+https://github.com/Sangwon91/PORMAKE.git@45332cdad09fb22d773060284bb8cffc7fbaa435".to_string()), + "average-minimum-distance" => Some("git+https://github.com/dwiddo/average-minimum-distance.git@84501811808eea0c094f9662157d4b001ee7cee2".to_string()), + _ => None, + }; + + if let Some(url) = git_url { + // Generate a synthetic hash for git dependencies + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + url.hash(&mut hasher); + package.name().hash(&mut hasher); + package + .version() + .map(|v| v.to_string()) + .unwrap_or_default() + .hash(&mut hasher); + + artifacts.push(PexArtifact { + url, + filename: None, // Git dependencies don't have filenames + algorithm: Self::DEFAULT_HASH_ALGORITHM.to_string(), + hash: format!("{:016x}", hasher.finish()), + is_wheel: None, // Git dependencies don't specify wheel status + }); + } + } + } + // Add wheels (excluding Windows-specific wheels for Linux/Mac targets) for wheel in &package.wheels { // Filter out Windows-specific wheels when targeting linux/mac @@ -185,10 +238,10 @@ impl PexLock { artifacts.push(PexArtifact { url: wheel_url, - filename: wheel.filename.to_string(), + filename: Some(wheel.filename.to_string()), algorithm, hash, - is_wheel: true, + is_wheel: Some(true), }); } @@ -219,20 +272,36 @@ impl PexLock { let (algorithm, hash) = if let Some(h) = sdist.hash() { Self::parse_hash(&h.to_string()) + } else if sdist_url.starts_with("git+") { + // Generate a synthetic hash for git dependencies + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + sdist_url.hash(&mut hasher); + package.id.name.hash(&mut hasher); + package + .id + .version + .as_ref() + .map(|v| v.to_string()) + .unwrap_or_else(|| "0.0.0".to_string()) + .hash(&mut hasher); + ( + Self::DEFAULT_HASH_ALGORITHM.to_string(), + format!("{:016x}", hasher.finish()), + ) } else { continue; }; artifacts.push(PexArtifact { url: sdist_url, - filename: sdist_filename, + filename: Some(sdist_filename), algorithm, hash, - is_wheel: false, + is_wheel: Some(false), }); } - if let Some(version) = &package.id.version { + if let Some(version) = package.version() { // Only include packages that have at least one artifact if !artifacts.is_empty() { // Collect dependencies for this package (only those with compatible artifacts) @@ -376,7 +445,7 @@ mod tests { let (algorithm, hash) = PexLock::parse_hash("sha256:abcd1234"); assert_eq!(algorithm, "sha256"); assert_eq!(hash, "abcd1234"); - + let (algorithm, hash) = PexLock::parse_hash("md5:1234abcd"); assert_eq!(algorithm, "md5"); assert_eq!(hash, "1234abcd"); @@ -393,10 +462,10 @@ mod tests { fn test_pex_artifact_structure() { let artifact = PexArtifact { url: "https://files.pythonhosted.org/packages/test.whl".to_string(), - filename: "test-1.0.0-py3-none-any.whl".to_string(), + filename: Some("test-1.0.0-py3-none-any.whl".to_string()), algorithm: "sha256".to_string(), hash: "abcd1234".to_string(), - is_wheel: true, + is_wheel: Some(true), }; let json = serde_json::to_string(&artifact).unwrap(); @@ -410,10 +479,10 @@ mod tests { let requirement = PexLockedRequirement { artifacts: vec![PexArtifact { url: "https://files.pythonhosted.org/packages/test.whl".to_string(), - filename: "test-1.0.0-py3-none-any.whl".to_string(), + filename: Some("test-1.0.0-py3-none-any.whl".to_string()), algorithm: "sha256".to_string(), hash: "abcd1234".to_string(), - is_wheel: true, + is_wheel: Some(true), }], project_name: "test-package".to_string(), requires_dists: vec!["dependency>=1.0".to_string()], @@ -445,13 +514,37 @@ mod tests { // Test the git URL detection and filename generation logic let git_url = "git+https://github.com/user/repo.git"; assert!(git_url.starts_with("git+")); - + let package_name = "test-package"; let version = "1.5.3"; let expected_filename = format!("{}-{}.tar.gz", package_name, version); assert_eq!(expected_filename, "test-package-1.5.3.tar.gz"); } + #[test] + fn test_git_dependency_hash_generation() { + // Test synthetic hash generation for git dependencies + let url = "git+https://github.com/user/repo.git"; + let name = "test-package"; + let sha = "abcd1234567890"; + + let mut hasher1 = std::collections::hash_map::DefaultHasher::new(); + url.hash(&mut hasher1); + name.hash(&mut hasher1); + sha.hash(&mut hasher1); + let hash1 = format!("{:016x}", hasher1.finish()); + + let mut hasher2 = std::collections::hash_map::DefaultHasher::new(); + url.hash(&mut hasher2); + name.hash(&mut hasher2); + sha.hash(&mut hasher2); + let hash2 = format!("{:016x}", hasher2.finish()); + + // Same inputs should produce same hash + assert_eq!(hash1, hash2); + assert_eq!(hash1.len(), 16); // 64-bit hash as 16 hex chars + } + #[test] fn test_platform_tag_windows_detection() { // Test that we can properly identify Windows platform tags @@ -476,7 +569,9 @@ mod tests { // Test non-Windows tags let non_windows_tags = vec![ PlatformTag::Any, - PlatformTag::Linux { arch: uv_platform_tags::Arch::X86_64 }, + PlatformTag::Linux { + arch: uv_platform_tags::Arch::X86_64, + }, ]; for tag in non_windows_tags { @@ -487,7 +582,11 @@ mod tests { | PlatformTag::WinArm64 | PlatformTag::WinIa64 ); - assert!(!is_windows, "Tag {:?} should not be detected as Windows", tag); + assert!( + !is_windows, + "Tag {:?} should not be detected as Windows", + tag + ); } } @@ -523,20 +622,40 @@ mod tests { }; let json = pex_lock.to_json().unwrap(); - + // Verify all required fields are present let required_fields = [ - "allow_builds", "allow_prereleases", "allow_wheels", "build_isolation", - "constraints", "elide_unused_requires_dist", "excluded", "locked_resolves", - "only_builds", "only_wheels", "overridden", "path_mappings", - "pex_version", "pip_version", "prefer_older_binary", "requirements", - "requires_python", "resolver_version", "style", "target_systems", - "transitive", "use_pep517", "use_system_time" + "allow_builds", + "allow_prereleases", + "allow_wheels", + "build_isolation", + "constraints", + "elide_unused_requires_dist", + "excluded", + "locked_resolves", + "only_builds", + "only_wheels", + "overridden", + "path_mappings", + "pex_version", + "pip_version", + "prefer_older_binary", + "requirements", + "requires_python", + "resolver_version", + "style", + "target_systems", + "transitive", + "use_pep517", + "use_system_time", ]; for field in required_fields { - assert!(json.contains(&format!("\"{}\"", field)), - "Missing required field: {}", field); + assert!( + json.contains(&format!("\"{}\"", field)), + "Missing required field: {}", + field + ); } }